npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@doync/client

v0.4.0

Published

doync client engine: optimistic Layer-1 savepoint/rebase over a synchronous local-DB port

Readme

@doync/client

Building an app? You almost certainly want one of the platform / framework packages, not this one:

  • @doync/web — browser client (SharedWorker + DB worker + OPFS)
  • @doync/mobile — single-process React Native client
  • @doync/reactDoyncProvider, useQuery, useMutation, and friends

Those packages own the host seams (workers, sockets, native SQLite) and hand your UI a ready DoyncClient. This package is the client engine they wrap: View handles, the optimistic Layer 1 (savepoint/rebase), the subscription desire map, mutate/push, schema events, reconnect coordination. App code should not import it directly unless you are writing a custom adapter.

The package splits its public surface by audience (same lockstep semver — the distinction is not stability):

| Entry | Audience | Contents | | --- | --- | --- | | @doync/client (.) | Apps via platform/framework packages | App-facing call-surface types only — nothing callable | | @doync/client/adapter | Third-party adapter authors | Host seams, reconnect coordinator, createClient | | @doync/client/internal | Sibling @doync/* packages | Engine construction; may break anytime |

What this package is

@doync/client is the engine half of every doync client:

  • DoyncClient + View handles — the call surface adapters wrap (subscribe / once / local / mutate, connection + schema status, preload, warmup); shared reactive reads with retain / release ownership.
  • OnceView — per-call cache-and-network reads.
  • Mutation invocation typesMutationResult / MutationOptions; definitions themselves come from @doync/core and are the registry entries on both ends.
  • Host seams + reconnect + createClient — on @doync/client/adapter; environment packages implement LocalDb / SyncSocket and construct clients there.

Schema, queries, and mutations themselves are defined in @doync/core. The concrete engine class and construction helpers live under @doync/client/internal.

What's on the main entry

The main entry exports types only — the symbols apps meet through @doync/web / @doync/mobile / @doync/react. Nothing callable remains here (createClient moved to /adapter).

| Symbol | Role | | --- | --- | | DoyncClient | The call surface adapters wrap | | View / OnceView / ViewStatus / QueryStatus | Reactive + once handle types | | SubscribeOptions / PreloadHandle / PreloadOptions / WarmupHandle / WarmupOptions / FalsyQuery | Subscribe / preload / warmup options | | MutationResult / MutationOptions | mutate() return + options | | ConnectionStatus / SchemaEvent / SchemaEventKind | Connection + schema status | | LogoutBehavior | 'keep' \| 'forget' durable logout policy (here only — /adapter references it, no double-listing) |

Writing a host / framework adapter

Host adapters (@doync/web, @doync/mobile, a community Electron or Node one) implement the engine's ports and construct a client. Framework adapters (@doync/react, a community Svelte/Vue/Solid one) are a thin layer over the DoyncClient call surface.

Import seams, reconnect, and construction from @doync/client/adapter. App-facing types stay on @doync/client.

import type { DoyncClient, View, OnceView, LogoutBehavior } from '@doync/client'
import {
  createClient,
  createReconnectingSocket,
  type CreateClientOptions,
  type LocalDb,
  type LocalRow,
  type SyncSocket,
  type SyncSocketHandlers,
  type SeamStatus,
  type ReconnectSocket,
  type ReconnectingSocket,
  type ReconnectingSocketConfig,
  type SocketCloseInfo,
} from '@doync/client/adapter'

Our own platform packages use createClientEngine from /internal rather than createClient — the latter is the construction entry a third-party adapter author reaches for.

The surface adapters wrap

import type { DoyncClient, View, OnceView } from '@doync/client'

interface DoyncClient {
  // Bound form only: pass queries.issues.open(args), or falsy to skip.
  subscribe(bound | FalsyQuery, options?): View // reactive; SHARED
  local(sql, ...params): View //                  reactive; SHARED
  once(bound | FalsyQuery): OnceView //           cache-and-network; PER-CALL
  mutate(mutation, args, options?): MutationResult // { client, server }

  preload(bound | FalsyQuery, options?): PreloadHandle
  warmup(bound | FalsyQuery, options?): WarmupHandle // held ahead of a mount

  readonly userId: string | null
  readonly connectionStatus: ConnectionStatus
  onConnectionChange(listener): () => void
  readonly schemaStatus: SchemaEvent | null
  onSchemaChange(listener): () => void
}

Lifecycle for framework adapters lives here: the adapter mints handles, retains for a mount, releases on unmount. Everything else — sharing, the Warm pool, wire frames, once's deferred dispose — is the client's job.

The rule: render may compute, only commit may own. Creation is pure: a handle you never retain owns nothing and is GC'd like any object. retain() is the ownership act; release() is its cleanup.

Query-taking methods accept a Bound queryqueries.issues.open(args) — or falsy (false | null | undefined) to mean "no query". Binding is pure; validation and resolve happen inside the engine. options.skip remains supported.

Host seams on /adapter

import type {
  LocalDb,
  LocalRow,
  SyncSocket,
  SyncSocketHandlers,
  SeamStatus,
} from '@doync/client/adapter'

// LocalDb — synchronous local-DB port (wa-sqlite / op-sqlite / node:sqlite)
// SyncSocket — Mirror socket seam the engine drives (send / setHandlers / reconnect)
// SeamStatus — 'connecting' | 'error' channel for truthful connectionStatus

Reconnect coordinator on /adapter

import {
  createReconnectingSocket,
  type ReconnectSocket,
  type ReconnectingSocket,
  type ReconnectingSocketConfig,
  type SocketCloseInfo,
} from '@doync/client/adapter'

Shared reconnect machinery (backoff, buffer-until-open outbox, re-handshake hook, keepalive). Wrap a transport-shaped ReconnectSocket and hand the resulting transcript to the engine as a SyncSocket.

createClient on /adapter

import { createClient, type CreateClientOptions } from '@doync/client/adapter'
import type { DoyncClient, LogoutBehavior } from '@doync/client'

const client: DoyncClient = createClient({
  db, // LocalDb
  socket, // SyncSocket
  schema,
  mutations,
  token,
  ctx,
  userId,
  logoutBehavior, // optional LogoutBehavior ('keep' | 'forget')
})

View — shared handle

interface View<Row> {
  current(): readonly Row[] // last decoded rows; STABLE until they move
  status(): ViewStatus //       { status: 'unknown' | 'complete' | 'error', error? }
  onChange(listener): () => void
  readonly one?: boolean //     runtime one-ness (set when known)
  retain(): void //             commit/mount — the only ownership act
  release(): void //            cleanup — idempotent; last release parks Warm pool
}

Lifecycle:

  1. Create in your setup/state slot. Pure: resolve, local SQL, decode. A discarded setup result owns nothing.
  2. retain() at mount / after the host commits the component. Never during render.
  3. release() at unmount / cleanup. The last release of a key fires the wire unsubscribe immediately (the Warm pool is a client paint budget, not a server cost) and parks the read for one policy tick.
  4. Key change → create the new handle, retain it, release the old.

current() stays stale-but-stable through any backing teardown (Warm-pool window, remount, server error status): it keeps answering the last snapshot until a new recompute notifies. Server rejection is status-only — last rows keep serving with status: 'error' alongside; the component decides.

Sharing is per statement identity at the engine desire map. Decode and one-ness are per-handle. Don't re-count, don't re-decode for sharing, don't invent a second store above DoyncClient.

OnceView — commit-owned, not shared

interface OnceView<Row> {
  current(): readonly Row[]
  onChange(listener): () => void
  dispose(): void // deferred one Warm-pool tick; re-use cancels
  readonly server: Promise<readonly Row[]> // starts the network half
}

once() is per-call: promise identity, no sharing, no Warm pool of rows. Create in setup; the network half starts on first onChange / server access (so a discarded render never dials). Cleanup calls dispose(). Cache-phase rows paint immediately (stale-but-stable).

mutate

const { client, server } = doync.mutate(mutations.issues.create, args)
await client // optimistic body applied on the local replica
await server // Origin ack (or reject → rebase)

Pass the registered mutation definition object and its args. The engine resolves the body from its own registry; the object is a typed reference only.

connectionStatus — five states

| state | meaning | | -------------- | ------------------------------------------------------ | | connecting | a (re)connect attempt is dialing or backing off | | connected | the socket is open — sync is live | | disconnected | dropped and backing off (offline) | | error | a transport-level error | | needs-auth | the Mirror rejected auth — a token refresh is required |

Subscribe with onConnectionChange and read connectionStatus. Never throw on a status — a degraded answer beats a crash.

Schema events

schemaStatus (SchemaEvent | null) covers disruption classes. Subscribe with onSchemaChange (fires on a new state and on a silent clear) and re-read schemaStatus. Kinds: reload (terminal — reload the app), server-behind (transient), resync / forget (transient recovery notices — surface as dismissible banners so a heal never looks like silent data loss).

/adapter surface

| Symbol | Role | | --- | --- | | createClient / CreateClientOptions | Construct a DoyncClient over a LocalDb + SyncSocket | | LocalDb / LocalRow | Synchronous local-DB port | | SyncSocket / SyncSocketHandlers / SeamStatus | Mirror socket seam | | createReconnectingSocket (+ reconnect types) | Shared reconnect coordinator |

LogoutBehavior is not re-exported here — import it from @doync/client.