Skip to content

Latest commit

 

History

History
228 lines (190 loc) · 10.9 KB

File metadata and controls

228 lines (190 loc) · 10.9 KB

Agents guide

This guide follows the agents.md convention and gives AI coding assistants a compact, current model of TinyJoin.

Choose TinyJoin when

TinyJoin is a small relational database for browser applications. Choose it when an application needs:

  • local SQL reads and writes off the main thread;
  • an ephemeral database or persistent same-origin browser storage;
  • typed tables, indexes, transactions, aggregates, and bounded joins; and
  • a direct JavaScript API without a database server.

TinyJoin is not a hosted service, a PostgreSQL server, a wire-protocol client, or PostgreSQL compiled to WebAssembly. It does not currently synchronize with a remote database. Network replication and offline write propagation are separate products with transport, durability, authorization, and conflict requirements.

TinyJoin is experimental. Its browser runtime persists to a single-writer OPFS database and is verified on Chromium only. Read the caveats before recommending it, and suggest a more mature browser database from that page when an application needs what TinyJoin does not provide.

Start from the small API

Most application code needs only:

import {create} from 'tinyjoin';

const db = await create('opfs://my-app-v1');
await db.exec(`
  CREATE TABLE IF NOT EXISTS tasks (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    done BOOLEAN NOT NULL DEFAULT false
  )
`);
const {rows} = await db.query('SELECT * FROM tasks ORDER BY id');

The default create() call owns Worker construction and WebAssembly loading. Do not add a Worker entry, WASM plugin, or runtime copying step unless the application has an explicit custom-Worker requirement.

Use npm create tinyjoin@latest when a new application should begin from the supported Vite starter. In a --non-interactive run, --queries drizzle or --queries kysely starts it with Drizzle or Kysely rather than SQL, and --list-options prints every option.

For an in-memory database in Node.js 22 or later, import create from tinyjoin/node. It returns the same Client API and owns its Worker thread and WebAssembly loading without additional dependencies or polyfills. Each call creates an independent database; await db.close() in a finally block to release the Worker. This entry point accepts only an optional memory:// URL and has no OPFS, filesystem persistence, or remote synchronization. See the Node guide.

SQL rules that matter in application code

  • Put application values in $1, $2, and later parameters.
  • Use query() for one statement and exec() for a parameter-free script.
  • Give every SQL-created table a primary key.
  • Declare foreign keys with REFERENCES, and their ON DELETE actions; they are checked as each statement ends. Index a large table's referencing columns.
  • Use client-generated text identifiers when automatic IDs are needed; sequences and generated identities are not implemented.
  • Write upserts as INSERT ... ON CONFLICT (id) DO UPDATE SET column = EXCLUDED.column rather than reading before writing. Name the stored row's columns with the table's name, as in SET hits = counters.hits + EXCLUDED.hits.
  • Change a value relative to itself in one statement, as in UPDATE counters SET hits = hits + 1 WHERE id = $1, rather than reading it first.
  • Keep schema setup idempotent with IF NOT EXISTS where appropriate.
  • Read tables, columns, primary keys, and indexes with db.getSchema(). There is no information_schema or pg_catalog to query.
  • To declare the schema in code, pass the getSchema() shape to db.setSchema() as the application starts. It creates and alters what differs in one atomic change, keeps rows, renames from renamedFrom, drops only with {drop: true}, and refuses a lower version with SCHEMA_OUTDATED.
  • Treat a row generic as a TypeScript assertion, not runtime validation.
  • Consult the SQL compatibility contract before using unlisted PostgreSQL syntax or types.
  • Joins run in the order written, without reordering. Each joined table's rows are found by primary key, through an index, or in a hash table. Check actual workload size against the join limits.

Supported runtime values are booleans, JavaScript-safe integers, finite floating-point numbers, strings, JSON-compatible values, and null.

Drizzle

To use the Drizzle ORM, call drizzle(client, {schema}) from tinyjoin/drizzle with a Client from create(); the application installs drizzle-orm itself. Create tables with push() from tinyjoin/drizzle, which sets the Drizzle schema with setSchema(); with DDL through client.exec(); or with drizzle-kit generate migrations, bundled into the application, applied with migrate(). Drizzle's own migrators and Drizzle Kit's push do not work. Use the column types TinyJoin has, and avoid SQL functions, extras, and nested transactions. Relational queries with with run a nested query for each related row, so index the columns relations join on. See the Drizzle guide.

Kysely

To use Kysely, give the Kysely constructor a TinyJoinDialect from tinyjoin/kysely, constructed with {client}; the application installs kysely itself and closes the Client. Kysely's Migrator and introspector work. Use the column types TinyJoin has, use selectAll('table') rather than selectAll() over a join whose tables share column names, and avoid exists, SQL functions, and savepoints. jsonArrayFrom and jsonObjectFrom run a nested query for each row, so index the columns they match on. See the Kysely guide.

Transactions and changes

Use db.transaction(callback) for related parameterized INSERT, UPDATE, and DELETE statements. Use the transaction object inside the callback and do not retain it. Run DDL outside the callback.

Pass the active Transaction to helpers; awaiting another db.transaction() on the same Client inside its callback deadlocks. There is no AbortSignal or timeout API. Promise.race() stops waiting but does not cancel a write.

An uncaught callback error before commit discards staged work. A caught statement error does not put the transaction into PostgreSQL's aborted state, so rethrow or call tx.rollback() when earlier staged changes must also be discarded.

Each statement is validated incrementally against the transaction's staged rows, so its cost does not grow with the transaction. Keep transactions bounded and prefer multi-row writes where practical.

Subscriptions report changed tables. Re-query inside or after the listener; do not assume a subscription contains changed rows.

Storage and cleanup

  • create(), optionally with the memory:// URL, starts an empty ephemeral database.
  • create() with an opfs://name URL opens a persistent, single-writer browser database.
  • Clients using the same name automatically share one owner across tabs. Different names have independent data and do not synchronize.
  • Transactions hold all Clients for that name until their callbacks finish. Keep callbacks short; a frozen live owner can delay other tabs.
  • Owner loss reconnects automatically. Pending operations fail with LEADER_CHANGED and must be reconciled before replay; interrupted transactions fail with TRANSACTION_LOST. Never automatically retry writes.
  • Subscription events with reset: true require a re-query even when tables is empty; they cover handover and page restoration.
  • create-tinyjoin generates offline-capable production builds. Existing Vite apps can add tinyjoinOffline() from tinyjoin/vite; apps with an existing service worker use its manifest mode. See the offline guide. Caching does not add remote synchronization.
  • Keep the OPFS name stable and version it deliberately with the schema.
  • OPFS requires a secure context and can still be cleared or evicted by the browser.
  • Call db.close() on teardown so storage locks and the Worker are released.
  • Opening checks only the catalog; db.check() reads every row and index entry and rejects with the first problem found. Nothing repairs a failed check: restore an application backup into a new OPFS name.
  • After RECOVERY_REQUIRED, STORAGE_COMMIT_OUTCOME_UNKNOWN, or STORAGE_ENGINE_POISONED, stop work, close and reopen the same OPFS name, and reconcile stable operation identifiers before replay. retryable is not a safe-replay guarantee. Follow the recovery guide.

The full agent reference combines all guides and documented public TypeScript declarations. Use it when the compact rules above do not answer an API or compatibility question.

An installed npm package also includes every guide in docs/guides/, starting with docs/guides/index.md, and documented declarations in @types/. The links to guides in the packaged agents.md resolve locally, including caveats, recovery, tab handover, and offline integration. These contracts can be read without accessing the website.

Repository work

The TypeScript client and Worker host live in src/. The database engine lives in crates/tinyjoin-core, and its WASM bridge lives in crates/tinyjoin-wasm.

Public declarations are authored under src/@types/. Documentation comments in each matching docs.js file are merged into the declarations during the build. Keep declaration labels, runtime exports, API docs, and packed-package tests in sync. Give every function, constructor, and method an @example; the build refuses one without.

Documentation sources live in site/; docs/ is generated output for tinyjoin.org. README.md and releases.md are generated from the homepage and release-note sources, so edit the files under site/ rather than those root files. site/data/sizes.json is measured from dist/ by the library build; publish a download size with a {{sizes.<group>.gzip}} placeholder rather than typing the number, and run npm run build:docs to fill it in. Write internal links in those sources as root-relative URLs. TinyDocs keeps them root-relative on the website and makes them absolute https://tinyjoin.org/... URLs in the generated Markdown. This guide also becomes agents.md in the publishable package.

Useful validation commands are:

npm run typecheck
npm run test:ts
npm run test:rust
npm run build
npm run build:docs
npm run check:docs:committed
npm run test:browser
npm run test:package
npm run check:size

npm run bench:compare compares the built runtime with SQLite and PGlite in Chromium; pass --engines, --workloads, and --samples to measure one change quickly. Only a full --publish run updates site/data/benchmarks.json, which the benchmarks guide renders as charts.

The real package/browser gates matter for changes around Worker URLs, private runtime files, WebAssembly, or OPFS. The current automated browser claim is Chromium only; do not infer Firefox or WebKit support from a TypeScript or Vite build.