TinyBase v9.3 lets multiple independently synchronized MergeableStores share one WebSocket connection. It also includes broad reliability and hardening work across core data, synchronization, persistence, UI integrations, packaging, and performance.
Multiple Stores Over One WebSocket
Multiple WebSocket Synchronizers can now share a single physical WebSocket connection (#177). This is useful when an application has several independently synchronized MergeableStore instances but needs to limit its connection count.
Create the WebSocket with the tinybase subprotocol and provide a channel Id as the third argument to each createWsSynchronizer call:
import {createMergeableStore} from 'tinybase';
import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client';
import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server';
import {WebSocket, WebSocketServer} from 'ws';
const multistoreServer = createWsServer(new WebSocketServer({port: 8052}));
const multistoreWebSocket = new WebSocket(
'ws://localhost:8052/petShop',
'tinybase',
);
const petsSynchronizer = await createWsSynchronizer(
createMergeableStore(),
multistoreWebSocket,
'pets',
);
const employeesSynchronizer = await createWsSynchronizer(
createMergeableStore(),
multistoreWebSocket,
'employees',
);
console.log(petsSynchronizer.getWebSocket() == multistoreWebSocket);
// -> true
console.log(employeesSynchronizer.getWebSocket() == multistoreWebSocket);
// -> true
await petsSynchronizer.destroy();
console.log(multistoreWebSocket.readyState == WebSocket.OPEN);
// -> true
await employeesSynchronizer.destroy();
await multistoreServer.destroy();Each channel extends the base URL path, so the example uses the logical paths petShop/pets and petShop/employees. Legacy clients can connect directly to those full paths, while omitting the channel Id retains the existing signature and wire protocol. Multiplexing is supported by WsServer and WsServerSimple; WsServerDurableObject continues to use one URL path and Durable Object per WebSocket.
Channel Ids are not an authorization boundary: a client accepted on a base path can subscribe to any valid descendant channel. Authenticate and isolate untrusted clients by base path, and do not treat client Ids derived from Sec-WebSocket-Key as authenticated identities.
Reliability And Hardening
Breaking-ish Changes
- Errors use compact numeric codes.
- Failed transactions now rollback.
- Certain previously accepted/reserved or unserializable values are rejected.
- Client-only Solid and Svelte exports now correctly fail server resolution.
- Solid's UndoOrRedoInformation changed from values to accessors.
- Over-eager runtime exports disappeared from the Svelte package.
The full explanations for these and many other changes are below:
Core Data And APIs
- Arbitrary Ids such as proto, constructor, and toString are now safe throughout Store, MergeableStore, synchronization, and persistence.
- Errors from transaction actions and pre-commit callbacks now roll back content, schemas, MergeableStore stamps and hashes, and temporary state across Store, MergeableStore, and Checkpoints; nested failures roll back the shared outer transaction, while post-commit listener failures no longer strand the Store.
- Query definitions are staged before commit, and new Index, Metric, Query, and Relationship definitions are discarded if their Ids listener throws.
- Deleting a Query definition now releases cached pre- and result Stores once nothing still references them.
- MergeableStore now atomically rejects HLCs that are not exactly 16 characters or are over five minutes in the future, carries overflowing 24-bit counters into wall-clock time, and verifies local stamps without mutating rejected caller payloads.
- String Cells, Values, and schema defaults using TinyBase's reserved leading U+FFFD or exact U+FFFC encodings are rejected, while invalid or schema-incompatible persisted encodings are ignored safely.
- Object and array Cells and Values now have an explicit JSON-compatible content contract, reject unserializable data, and no longer modify caller-owned or frozen containers during bulk writes.
- Queries and Indexes now group, sort, and index equivalent object and array values consistently without stale results; direct rich Index keys remain distinct from custom-function arrays that select multiple Slices.
- TablesSchema and ValuesSchema objects are cloned before normalization, making frozen schemas reusable and preserving the previous schema after an invalid replacement; schema JSON getters also preserve object and array defaults.
- Middleware receives cloned object and array values in public JavaScript form, with callback results validated and encoded only at the Store boundary.
- Middleware and Checkpoints now clean up Store registrations safely, avoid duplicate listeners after recreation, and skip phantom checkpoints for structurally unchanged rich content.
Synchronization
- WsServer and WsServerSimple now share multiplex negotiation, decoding, channel-lifecycle, and cleanup behavior, and retain safe error listeners for their lifetimes.
- Synchronizers reject pending requests on transport failure, remove built-in listeners when destroyed, and expose transport failures to custom register callbacks.
- BroadcastChannelSynchronizer validates message envelopes, while LocalSynchronizer snapshots scheduled recipients and cancels deliveries when destroyed.
- WebSocket fragments now split by UTF-8 byte size at code-point boundaries and use at most 1,000 fragments.
- Malformed WebSocket traffic reports error 14 and closes the offending peer with status 1007 before relay; complete messages and multiplex envelopes are limited to 16 MiB, with oversize input closing only its sender.
- Offline and startup queues, pending requests, incomplete fragments, and socket buffering are bounded; queued traffic expires or coalesces, while overload reports error 15, closes peers with status 1013 where appropriate, and stops accepting new requests when the pending map is full.
- Multiplexed channels keep independent timeout and error handling, clear failed subscriptions for retry, and settle immediately around closing or closed sockets; reconnect handshakes and queued replays cannot consume replacement connection state.
- WsServer setup, teardown, Persister retries, path resubscription, and destruction now clean up deterministically; subscriptions are acknowledged before persisted startup, destroy closes clients and awaits the WebSocketServer, and stale path cleanup cannot remove replacements.
- All path and client Id listeners still run when one throws, listener and ignored-error failures no longer strand server state, and WsServerSimple applies the same outbound safety checks as WsServer.
- WsServerDurableObject reports an empty path before its first connection, correctly types synchronous errors and asynchronous namespace responses, and lets subclasses observe malformed traffic.
- PartyKit now persists and broadcasts only authorized changes, leaves rooms uninitialized after fully rejected first writes, serializes writes, validates incoming content and stored keys, and accepts empty HTTP responses on the client.
Persistence
- Remote Persisters preserve ETags until successful downloads, reject failed responses, serialize polling, and abort active polls when stopped or destroyed.
- Persister operations now await queued work; shared schedulers attribute errors to the correct owner and survive failures in ignored-error handlers.
- Repeated destruction shares one completion Promise, waits for active work, restores idle status despite cleanup failures, and releases shared scheduler state.
- Concurrent automatic lifecycle calls support falsey handles, retain only the latest registration, wait for setup and cleanup during stop or destroy, and stop both auto-load and auto-save even if one cleanup fails.
- Auto-load startup closes missed-change windows and preserves notification ordering; steady-state inline changes apply synchronously without redundant saves, while no-content notifications during a save coalesce into a trailing load.
- Database transaction failures roll back; remote libSQL uses one transaction session while local file clients retain their connection, and tabular Persisters replace every table-name placeholder, enforce collision-safe Row Id uniqueness, and reject configurations that cannot preserve merge metadata.
- PostgreSQL auto-load shares notification resources until the final owner stops and releases reserved clients after setup failure.
- SQLite auto-load establishes its baseline before use, defers update-hook work until the triggering write completes, drains pending work on stop, and allows asynchronous change-listener cleanup.
- IndexedDB waits for transaction completion, reports aborts and blocked opens, uses read-only loads, and serializes and drains changed-content polling.
- Durable Object KV persistence batches limit-safe writes transactionally and ignores unrelated or malformed keys; Durable Object SQL preserves empty Ids and safely quotes generated table names.
- React Native MMKV preserves deletion tombstones and contains malformed content and listener failures; Automerge and Yjs validate TinyBase document roots before loading or applying changes, while Automerge still allows unrelated root metadata.
- Browser storage, IndexedDB, PowerSync, SQLite, PostgreSQL, Automerge, and Yjs notifications route parsing and listener failures through ignored-error handling.
- File persistence atomically replaces data while preserving modes and symbolic links, and its auto-load follows replacements without duplicate self-loads; OPFS always closes successful writes and aborts failed ones.
UI And Lifecycle
- Solid primitives no longer register Store listeners during server rendering.
- React and Solid asynchronous resource hooks destroy stale or post-unmount results, hide resources during replacement, await destruction, contain failures, and serialize Solid replacements behind previous cleanup.
- Solid and Svelte DOM and Inspector packages are explicitly client-only, so server builds fail cleanly during resolution.
- Solid DOM table wrappers keep table, query, and slice metadata reactive, while Solid Inspector actions and custom Cell renderers react to editability and Cell Id changes.
- Solid useUndoInformation and useRedoInformation now return Accessors for availability, checkpoint Id, and label values.
- Object and array editors mark JSON with the wrong container type invalid, and React, Solid, and Svelte paginators normalize boundary offsets and clamp previous navigation to zero.
- React and Solid paginators defer offset corrections until after rendering, and Provider registrations in all three UI modules preserve the correct resource when duplicate Id owners are removed.
Packaging And Performance
- TinyBase errors now use compact numeric codes documented in the Error Codes guide.
- The build now classifies Svelte modules correctly and verifies generated JavaScript exports against declared public values.
- Accidental React, Solid, DOM, and IndexedDB helper exports have been removed, while createDurableObjectSqlStoragePersister is exported from the omni module as declared.
- The getTransactionMergeableChanges declaration now distinguishes its default unhashed result from explicitly requested hashed changes.
- Internal iteration paths now avoid unnecessary allocations, while invalid Store JSON setters complete synchronously without creating Promises or microtasks.
- Metric extrema and Cartesian chart summaries now handle large datasets without variadic argument limits or quadratic category de-duplication.