For AI agents: the complete documentation index is available at https://silt-db.dev/llms.txt, the full documentation bundle is available at https://silt-db.dev/llms-full.txt, and this page is available as Markdown at https://silt-db.dev/advanced/teamplay.md.

Teamplay integration

This advanced integration layers Teamplay on the ShareDB adapter. The connection helpers below are repository examples, not exports from an npm package. Start from integration/teamplay and copy or adapt its bootstrap helpers into your application. The examples assume the same root-relative file layout as that checkout.

The same Teamplay object-tree application module can run against a local ShareDB backend or a remote WebSocket connection. integration/teamplay/app.js is executed unchanged in both modes. It uses the actual Teamplay $, sub(), signal writes, and named aggregation APIs.

Only startup chooses the connection. This does not synchronize a local offline database with a remote database or switch an already-running singleton connection. Teamplay currently has one active connection per JavaScript runtime.

Local startup

import { openDatabase } from '@silt-db/sqlite';
import { createSiltBackend, connectLocal } from './integration/teamplay/connect.js';
import { aggregates } from './integration/teamplay/app.js';

// Node selects its SQLite adapter; Expo selects expo-sqlite via package conditions.
const database = await openDatabase('app.sqlite');
const backend = createSiltBackend({ database, aggregates });
connectLocal({ backend, idFields: ['id', '_id'] });

// Normal application code:
const { subscribeApp } = await import('./integration/teamplay/app.js');
const app = await subscribeApp();

The backend reads documents and executes queries directly in SQLite. It does not hydrate a separate Mingo database at startup. Applications must await database initialization before attaching Teamplay. The example helper returns a backend so its lifecycle remains explicit.

In the validated Teamplay revision, teamplay/connect-offline hardcodes @startupjs/sharedb-mingo-memory and its storage hydration; it has no database injection option. Its teamplay/server backend factory also selects its own database and does not accept a db override. The supplied helper uses the public Teamplay setConnection() hook with an actual ShareDB backend and the Silt adapter. Upstream Teamplay source is not patched.

Remote startup

On the Node server:

import { openDatabase } from '@silt-db/sqlite/node';
import createChannel from '@teamplay/channel/server';
import { createSiltBackend } from './integration/teamplay/connect.js';
import { aggregates } from './integration/teamplay/app.js';

const database = await openDatabase('server.sqlite', { adapter: 'better-sqlite3' });
const backend = createSiltBackend({ database, aggregates });
const channel = createChannel(backend, { path: '/channel' });
httpServer.on('upgrade', channel.upgrade);

On the app:

import { connectRemote } from './integration/teamplay/connect-remote.js';

connectRemote({
  socket: new WebSocket('wss://your-server.example/channel'),
  idFields: ['id', '_id'],
});

const { subscribeApp } = await import('./integration/teamplay/app.js');
const app = await subscribeApp();

The dedicated remote entry imports the ShareDB client without importing a local backend. The Node server database uses WAL by default. A supplied database is borrowed: closing the ShareDB backend does not close it; the owner subsequently calls await database.close().

For local asynchronous worker databases, call the example helper's await closeSiltBackend(backend) before closing the database. It stops server agents and drains their outstanding adapter calls. ShareDB's query-unsubscribe callback does not acknowledge server teardown, so awaiting Teamplay unsub() alone is insufficient to close a SQLite worker immediately after a mutation.

Browser tabs sharing one offline database

Expo's browser OPFS implementation holds an origin-wide pool of file handles. Opening independent SQLite workers from multiple tabs can fail even when filenames differ. The optional integration/teamplay/browser-owner.js connector instead elects one owner tab with Web Locks and carries real ShareDB protocol over BroadcastChannel. Only that owner calls the database factory; every tab continues using the same Teamplay app API. Messages retain ShareDB's JSON wire encoding: directly structured-cloning query diff objects loses the operation types supplied by their toJSON() methods.

import { openDatabase } from '@silt-db/sqlite';
import { connectBrowserOwner } from './integration/teamplay/browser-owner.js';
import { aggregates } from './integration/teamplay/app.js';

const owner = connectBrowserOwner({
  name: 'my-app',
  createDatabase: () => openDatabase('app.sqlite'),
  aggregates,
});
await owner.ready;
// Teamplay $, sub(), and signal writes now use this connection.

This browser-only example requires a secure context with Web Locks and BroadcastChannel. Use one owner room for the origin, and do not independently open other Expo SQLite workers there. When the owner page closes, another connected tab acquires the lock, opens the persisted database, and announces a fresh owner generation; existing ShareDB clients reconnect and resubscribe. The owner lease deliberately lasts until page destruction because closing a database does not necessarily terminate Expo's worker and release its OPFS pool. disconnect() closes that tab's client connection; an owner continues serving peers until its page closes.

This is a bounded application bootstrap example. It does not provide remote synchronization, a background service independent of browser tabs, or a tested guarantee for crashes during an unacknowledged write. Same-origin tabs are trusted peers. integration/teamplay/owner-validation.js exposes the shared application operations to the browser test harness.

The actual Chromium/Expo WASM test passed: the first tab opened one database, a follower opened none, query reordering and foreign lookup updates crossed tabs, closing the owner elected the follower and resumed its subscriptions, and a third tab read subsequent persisted writes without opening another SQLite worker. The same run completed the direct Teamplay write/reload/reopen scenarios without console or subscription errors. Results are recorded in integration/expo/artifacts/web-results.json under teamplayWrite, teamplayReopen, and teamplayCrossTab; the broader numeric-compiler probes have their own gate.

Named aggregations and live lookup updates

Named aggregation definitions remain in Teamplay's @teamplay/server-aggregate middleware. The helper installs this middleware, calls its backend.addAggregate() API, and then installs Silt's installQueryDependencies(backend). This order matters: the middleware first resolves the named query into a pipeline, then Silt discovers foreign collections that require live query invalidation.

The test registers orders.withCustomer and subscribes using a real Teamplay aggregation header. Changing a customers document updates an existing orders aggregation subscription without an orders mutation. Silt does not reimplement named-query authorization or model registration. The helper's explicit registration list is intended as a small bootstrap example, not a replacement for Teamplay model loading, schema validation, or access control.

Verification

npm ci
node scripts/teamplay-test.mjs --setup
# Later runs reuse the checkout/dependencies:
npm run test:teamplay

The setup checks out upstream startupjs/teamplay revision 0552d14f3b2681eee9e00e7d22702dda359e78e7 (package version 0.5.10), installs its runtime dependencies in a separate research checkout, and links that dependency directory into this integration harness. TEAMPLAY_CHECKOUT can point to an existing checkout at the same revision. Source exports are selected with Node's -C teamplay-ts condition. Teamplay's missing AJV peer under --legacy-peer-deps is installed explicitly; native upstream sqlite3 installation scripts are unnecessary because Silt owns persistence.

The default integration matrix contains 12 tests. Each runs both a write scenario and a fresh-process reopen scenario:

DimensionTested values
Actual Teamplay runtimePinned 0.5.10 source
ConnectionLocal backend.connect(); real loopback WebSocket through @teamplay/channel
ShareDB server5.2.2; 6.0.2
SQLite adapterExisting synchronous Node; asynchronous Node; asynchronous better-sqlite3

Assertions cover create/read/update/delete, document subscriptions updated by a second connection, query sort/limit reordering, named aggregation execution, foreign $lookup updates, configured identity fields with colon-containing IDs, identity stripping from stored snapshots, operation history, tombstones, reopen and continued writes, WAL mode, and borrowed-database ownership. The WebSocket client is Teamplay's pinned ShareDB 5.2.2 client in both server-version cases.

These are Node integration tests, including the local-backend topology intended for offline clients. The additional portable integration/teamplay/browser-scenario.js runs the same app module over a supplied Expo database, with separate write/reopen phases and live document/query/lookup assertions. See Expo validation for measured browser and native bundle coverage. Neither suite by itself proves Android/iOS execution or a React useSub() UI. The Node harness fails when prerequisites are missing rather than silently skipping. TEAMPLAY_DRIVERS=better-sqlite3 can narrow a diagnostic run.

Two upstream behavior details are reflected in the application test: modifying a nested field requires a loaded document and deleting an unloaded document is a no-op, so the example subscribes before renaming a customer or deleting an order; after installing @teamplay/channel, an additional server-side ShareDB connection supplies an empty request object (backend.connect(null, {})) because its connect middleware expects one.