@doync/web
v0.4.0
Published
doync web topology: tab SharedWorker + DB-worker (wa-sqlite/OPFS) browser surface
Maintainers
Readme
@doync/web
Browser client surface for doync. One call boots the tab client (SharedWorker + OPFS-backed DB worker under the hood) and returns a WebClient that @doync/react — or your own adapter — consumes.
Install
pnpm add @doync/web @doync/client @doync/core
# typically also:
pnpm add @doync/reactBoot the client
The tab entry creates the client once and keeps it for the life of the tab. Login, logout, and token refresh ride updateAuth — do not rebuild the client on every auth change.
// tab entry (e.g. zero-init.tsx)
import { createWebClient } from '@doync/web'
const client = createWebClient({
// MUST hold the inline `new Worker(new URL(...))` literal so the bundler
// emits the app-bound DB-worker chunk.
worker: () =>
new Worker(new URL('./db-worker.ts', import.meta.url), {
type: 'module',
}),
// Optional. Default derives `wss://host/sync` from the page origin.
url: `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/sync/ws`,
name: 'tasks', // Database name; default 'doync'
authData: { userId, token, ctx }, // or null for anonymous
ctxValidationSchema, // shared-module; validates authData.ctx
// logoutBehavior: 'keep', // optional; omit leaves a stored choice alone
})Pass the same stable client into <DoyncProvider> (from @doync/react) for the life of the tab.
DB-worker entry (@doync/web/worker)
The worker factory must point at an entry that calls createWorker with the same shared definition module the rest of the app uses:
// db-worker.ts
import { createWorker } from '@doync/web/worker'
import { schema, queries, mutations } from './shared/data'
createWorker({ schema, queries, mutations /* , name: 'tasks' */ })name on createWorker must match name on createWebClient when you set either (default 'doync'). One Database name = one Client; an app with several databases runs several clients with distinct names.
Options
| Option | Required | Notes |
| --- | --- | --- |
| worker | yes* | () => new Worker(new URL('./db-worker.ts', import.meta.url), { type: 'module' }). The inline new Worker(new URL(…)) form is required so the bundler emits the worker chunk. |
| authData | yes | AuthData \| null — { userId, token?, ctx } when authenticated, or null anonymous. |
| ctxValidationSchema | yes | Standard Schema for your auth-context shape (your shared-module binding); validates authData.ctx at construction / updateAuth. |
| url | no | Mirror WebSocket URL. Default derives wss://host/sync from the page origin. |
| name | no | Database name; default 'doync'. |
| logoutBehavior | no | Initial 'keep' | 'forget' for this identity. Omit leaves a previously stored choice alone across reloads. |
*Required on the real browser path.
Auth
Client identity is one value — authData: AuthData | null. Drive login / logout / refresh through updateAuth on the live client (shared semantics with @doync/client / @doync/mobile):
client.updateAuth({ userId: nextUserId, token: nextToken, ctx: nextCtx })
client.updateAuth(null) // logout- Same
userId(including bothnull) → in-band auth update. A routine JWT rotation is invisible to mounts. - Changed
userId→ the topology swaps the per-identity Replica and reconnects as that identity. The client object stays stable; React mounts do not remount.
Logout retention (LogoutBehavior)
Per identity, stored durably with that identity (same LogoutBehavior policy as @doync/client / @doync/mobile):
'keep'(default) — on auserIdchange the outgoing Replica and its pending queue stay on disk. Logging back in boots warm; unsynced writes re-push.'forget'— on auserIdchange the outgoing Replica is deleted. Use on a shared device where returning local data is not wanted.
const client = createWebClient({
/* … */,
logoutBehavior: 'forget', // initial stamp for this identity
})
// Or flip later — written into the live identity, honored at the next swap.
client.setLogoutBehavior('keep')Subscribe and mutate
WebClient is a full doync client call surface. Prefer @doync/react hooks in UI code; the imperative form is the same shape the hooks wrap:
import { schema, queries, mutations } from './shared/data'
// Subscription — retain while mounted, release on cleanup.
const view = client.subscribe(queries.issues.open({ projectId }))
view.retain()
view.onChange(() => {
console.log(view.current()) // rows; stable array ref until they move
console.log(view.status()) // { status: 'unknown' | 'complete' | 'error', … }
})
// later: view.release()
// Once — cache-and-network; dispose when done.
const once = client.once(queries.issueById({ id }))
console.log(once.current()) // local answer immediately
await once.server // network half
once.dispose()
// Local read — arbitrary SQL over the Replica; never registered upstream.
const local = client.local<{ n: number }>(
'select count(*) as n from issue where open = 1',
)
local.retain()
// later: local.release()
// Mutation — optimistic apply + server confirmation.
const { client: applied, server } = client.mutate(mutations.issue.create, {
id,
title,
})
await applied // local apply settled (throws if the local body rejects)
await server // Mirror confirmed (throws if the server rejects)Recovery verbs on the same object (same resync / forget contract as @doync/client):
client.resync() // wipe Replica / Memberships / cookie; drop pendings; mint fresh clientId
client.forget() // erase the active identity's local data
client.forget('other-user-id') // erase another identity's file onlyWhen you believe the network is back, connect your own navigator.onLine logic or a manual "Reconnect" button to client.reconnect(). It is safe to call anytime.
Connection and schema status for banners:
client.connectionStatus // 'connecting' | 'connected' | 'disconnected' | 'error' | 'needs-auth'
client.onConnectionChange(() => {
/* re-read client.connectionStatus */
})
client.schemaStatus // null when nominal, else { kind, message }
client.onSchemaChange(() => {
/* re-read client.schemaStatus */
})Public surface
| Export | Role |
| --- | --- |
| createWebClient / CreateWebClientOptions | Boot the tab client |
| WebClient | Returned client (subscribe / warmup / once / local / mutate + auth / reconnect / recovery) |
| authAction / AuthAction / AuthIdentity | Pure same-user vs identity-change classifier |
| createWorker / CreateWorkerOptions | DB-worker entry (@doync/web/worker) |
| DoyncClient / View / OnceView / ViewStatus / QueryStatus / ConnectionStatus / SchemaEvent / SchemaEventKind / FalsyQuery / MutationOptions / MutationResult / LogoutBehavior / SubscribeOptions / PreloadOptions / PreloadHandle / WarmupOptions / WarmupHandle | Client call-surface family (re-exported from @doync/client) |
Internal API
The main entry (.) and the DB Worker (./worker) are the semver-governed public surfaces documented here. Anything imported from @doync/web/internal may change in any release, including patches, without notice — use it only if you accept that risk.
