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/core

v0.4.0

Published

doync shared types and synced-schema definition

Readme

@doync/core

Shared types and the synced-schema definition layer for doync. This is the isomorphic data layer: one module of tables, migrations, queries, and mutations that every client (web / mobile) and the server (Origin / Mirror) consume. It does not open sockets, hold SQLite storage, or talk to Durable Objects — those live in @doync/web, @doync/mobile, and @doync/server.

Install

pnpm add @doync/core

Optional peers you will usually add next:

pnpm add @doync/web @doync/react     # browser
# or
pnpm add @doync/mobile @doync/react  # React Native
pnpm add @doync/server               # Cloudflare Worker + Durable Objects
pnpm add zod                         # or any standard-schema validator

The Drizzle flavor of the same definition API lives in @doync/drizzle (createDoyncDrizzle → bound defineQuery / defineMutation). This README documents the plain @doync/core SQL surface.

Define once, share everywhere

Author one module through createDoync — schema, queries, mutations — and import it from every process that participates in the same Database name:

// shared/data.ts  — imported by web, mobile, and the Worker
import { createDoync } from '@doync/core'
import { z } from 'zod'

export const ctxValidationSchema = z.object({
  userId: z.string(),
  role: z.string(),
})

export const {
  schema,
  defineQuery,
  defineMutation,
  defineQueries,
  defineMutations,
} = createDoync({
  schema: {
    migrations: [
      {
        id: '0001_init',
        sql: `
          CREATE TABLE task (
            id TEXT NOT NULL PRIMARY KEY,
            title TEXT NOT NULL,
            done INTEGER NOT NULL DEFAULT 0,
            owner_id TEXT NOT NULL
          );
          CREATE INDEX task_owner ON task (owner_id);
        `,
      },
    ],
    tables: [
      {
        name: 'task',
        columns: [
          { name: 'id', type: 'text', pk: true, notNull: true },
          { name: 'title', type: 'text', notNull: true },
          { name: 'done', type: 'integer', notNull: true },
          { name: 'owner_id', type: 'text', notNull: true },
        ],
      },
    ],
  },
  ctxValidationSchema,
})

export const queries = defineQueries({
  tasks: {
    all: defineQuery(({ sql }) => sql`SELECT * FROM task ORDER BY id`),
    byOwner: defineQuery(
      z.object({ ownerId: z.string() }),
      ({ args, sql }) =>
        sql`SELECT * FROM task WHERE owner_id = ${args.ownerId} ORDER BY id`,
    ),
    mine: defineQuery(({ ctx, sql }) => {
      if (ctx === null) return sql`SELECT * FROM task WHERE 0`
      return sql`SELECT * FROM task WHERE owner_id = ${ctx.userId} ORDER BY id`
    }),
  },
})

export const mutations = defineMutations({
  tasks: {
    create: defineMutation(
      z.object({ id: z.string(), title: z.string(), ownerId: z.string() }),
      ({ args, sql }) => {
        sql.exec(
          'INSERT INTO task (id, title, owner_id) VALUES (?, ?, ?)',
          args.id,
          args.title,
          args.ownerId,
        )
      },
    ),
    complete: defineMutation(z.object({ id: z.string() }), ({ args, sql }) => {
      sql.exec('UPDATE task SET done = 1 WHERE id = ?', args.id)
    }),
  },
})

| Consumer | Imports | | ------------------- | -------------------------------- | | Web / mobile client | schema, queries, mutations | | Origin DO | schema, mutations | | Mirror DO | schema, queries |

Clients call registered query leaves to produce a Bound query (queries.tasks.byOwner({ ownerId })) and pass that into client.subscribe / useQuery. The wire protocol never carries client SQL — only a name + args the Mirror resolves under the verified auth context.

Schema — DoyncSchema

import type { DoyncSchema, SchemaMigration, TableSchema } from '@doync/core'

Pass a plain DoyncSchema object into createDoync({ schema, ctxValidationSchema }) (or import one emitted by @doync/schema-codegen). The factory validates it. Capture triggers, replication, and convergence checks are derived from this single definition.

  • migrations — append-only history from empty DB → current shape. Order is the schema version; entries are never edited or reordered after ship. Each SchemaMigration is { id, sql }. sql may freely mix DDL and backfill DML; the engine runs the whole migration as one Schema commit (DDL recorded for replay, backfill captured as ordinary row images).
  • tables — the current shape for capture/apply: each TableSchema is { name, columns } where columns lists every column (name, type affinity, optional pk / notNull). The primary key is the pk-flagged columns in declaration order. Secondary indexes, triggers, FKs, and CHECKs are ordinary migration DDL — not fields on TableSchema. Table / column names must be plain SQL identifiers; the __doync_ prefix is reserved.
const schema = {
  migrations: [
    { id: '0001_init', sql: `CREATE TABLE task (…);` },
    {
      id: '0002_priority',
      sql: `
        ALTER TABLE task ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
        UPDATE task SET priority = 2 WHERE done = 0;
      `,
    },
  ],
  tables: [
    {
      name: 'task',
      columns: [
        { name: 'id', type: 'text', pk: true, notNull: true },
        { name: 'title', type: 'text', notNull: true },
        { name: 'done', type: 'integer', notNull: true },
        { name: 'priority', type: 'integer', notNull: true },
      ],
    },
  ],
} satisfies DoyncSchema

Number precision

Numbers are float64; anything occupying SQLite's INTEGER class beyond ±(2^53−1) is rejected loudly everywhere; finite REALs of any size are fine; use TEXT or BLOB keys for snowflake-sized ids.

Queries — defineQuery / defineQueries / sql

import {
  sql,
  type BoundQuery,
  type QueryDefinition,
  type QueryTree,
  type RegisteredQuery,
  type ResolvedQuery,
  type ResolvedStatement,
  type SqlTag,
} from '@doync/core'

defineQuery(argsValidationSchema?, ({ args, ctx, sql }) => statement)

  • Optional args validation schema is any Standard Schema validator; args are validated at the wire boundary.
  • ctx is AuthContext (null ⇔ anonymous).
  • The callback is deterministic and pure with respect to the database state it reads through the compiled statement — same args + ctx ⇒ same statement on client and Mirror.
  • Return a ResolvedStatement via the injected sql tag.
const openTasks = defineQuery(
  ({ sql }) => sql`SELECT * FROM task WHERE done = ${0} ORDER BY id`,
)

const byId = defineQuery(
  z.object({ id: z.string() }),
  ({ args, sql }) => sql.one`SELECT * FROM task WHERE id = ${args.id}`,
)

sql interpolations become bound ? parameters — never string-spliced. sql.one`…` marks a one-row query (one: true is presentation metadata; it does not change Query-instance identity). Type-level one-ness flows into useQuery as [Row | undefined, status] vs a row array. One-ness is static per definition: a resolver must use sql.one on every return path or on none — mixing the two forms is a compile error.

Filters derived from ctx become part of the statement identity: different roles resolve to different Query instances — structural row-level security.

defineQueries(tree) — nested registry, Bound queries

const queries = defineQueries({
  tasks: {
    open: openTasks,
    byId,
  },
})

// Call surface for clients:
const bound: BoundQuery = queries.tasks.byId({ id: 't1' })
// bound.kind === 'bound-query'
// bound.query.name === 'tasks.byId'

defineQueries stamps each leaf with its dotted name (tasks.byId) and turns it into a RegisteredQuery callable. Binding is pure (no validation / resolve at call time) so it is safe during render. Public client surfaces accept a BoundQuery only.

Pass the tree to every surface that needs it (createWorker, DoyncMirrorConfig.queries, …).

Mutations — defineMutation / defineMutations

import type {
  MutationBody,
  MutationDefinition,
  MutationExec,
  MutationTree,
} from '@doync/core'

defineMutation(argsValidationSchema?, ({ args, ctx, sql }) => void | Promise<void>)

  • Shared bodies are deterministic and DB-only. They may be async (e.g. crypto.subtle.digest over args), but any external I/O that would re-fire on rebase belongs in a server override.
  • sql is a MutationExec: exec<T>(query, …params): T[] — run one parameterized statement and read its rows back. Read-then-decide (asserts, branching) runs identically optimistic on the client Replica and authoritative at the Origin.
  • Ids and timestamps must be client-generated and passed through args — the rebase replays the body on every poke.
const create = defineMutation(
  z.object({ id: z.string(), title: z.string() }),
  ({ args, ctx, sql }) => {
    if (ctx === null) throw new Error('auth required')
    sql.exec(
      'INSERT INTO task (id, title, owner_id) VALUES (?, ?, ?)',
      args.id,
      args.title,
      ctx.userId,
    )
  },
)

defineMutations(shared, serverOverrides?) — shared tree + server-only overrides

Compose a shared (client-safe) mutation tree with optional server-only replacements for the same dotted names — external I/O and secrets stay out of the client bundle (the prior-art shape popularized by Zero’s .server.ts split).

// shared/mutations.ts — imported by clients AND the server
export const sharedMutations = defineMutations({
  tasks: { create, complete },
})

// server/mutations.ts — Worker-only entry
import { defineMutation, defineMutations } from '../shared/data'
import { sharedMutations } from '../shared/mutations'

export const mutations = defineMutations(sharedMutations, {
  tasks: {
    // Same dotted name ⇒ replaces the shared body on the server only.
    create: defineMutation(createArgs, async (args, ctx, tx) => {
      await notifySlack(args) // external I/O OK here
      // …authoritative write…
    }),
  },
})
  • Client entry: defineMutations(shared) only — override modules never enter the client bundle.
  • Server entry: defineMutations(shared, overrides) — each override replaces a same-named shared body. An override naming a mutation the shared tree does not hold is a loud config error (a server-only mutation has no optimistic counterpart).

Pass the resulting tree to every surface that needs it (createClient / createWorker, DoyncOriginConfig.mutations, …). Each consumer flattens (and the Origin ports the wire-boundary runners) internally; there is no consumer-facing flatten step.

Args value-space

Args leaf types are null | boolean | number | string | ArrayBuffer (TypedArray / DataView accepted at the boundary and canonicalized to ArrayBuffer), nested in plain arrays / objects.

  • The __doync_blob key is reserved — present in consumer data, validation rejects (never rewrites).
  • Number rule: see Schema above.
  • Encoding runs at the client boundary; decoding runs server-side before standard-schema validation so resolvers always see live ArrayBuffers.
  • Booleans also bind directly in SQL — sql`… WHERE done = ${args.done}` and sql.exec('…', args.done) canonicalize true/false to 1/0 before the statement is built (SQLite has no boolean affinity; rows read back as 0/1).

Types you will import

| Symbol | Role | | --- | --- | | createDoync, CreateDoyncOptions, CreateDoyncResult | Factory front door | | AuthContext, CtxValidationSchema, AuthData | Auth-context shape, its Standard Schema type, and the client identity | | AuthData | Client identity { userId, token?, ctx } | | DoyncSchema, SchemaMigration, TableSchema | Schema shape | | QueryDefinition, QueryCallbackInput, QueryResolver, QueryTree | Query authoring | | RegisteredQuery, RegisteredQueryCallable, RegisteredQueryTree, BoundQuery | Registration / client call surface | | ResolvedStatement, ResolvedQuery, SqlTag, RowDecoder | Statement / identity pieces | | MutationDefinition, MutationTree, MutationBody, MutationExec | Mutation authoring | | SqlRow, SqlValue, SqlBindable | Row / cell types on the SQL surface |

Internal surface

The main entry (. / @doync/core) is the semver-governed public API this README documents.

Anything imported from @doync/core/internal may change in any release, including patches, with no notice. Sibling @doync/* packages use that subpath; application code should not.