event-sourced-collection 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 my-tanstack-db-collections contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,377 @@
1
+ # @tanstack-db-collections/event-sourced
2
+
3
+ Event-sourced local-first database on top of TanStack DB persistence. Every `insert`, `update`, and `delete` is logged to a SQLite event table and synced to your backend when online.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @tanstack-db-collections/event-sourced @tanstack/db @tanstack/db-sqlite-persistence-core
9
+ ```
10
+
11
+ Plus a platform package:
12
+
13
+ ```bash
14
+ # Browser
15
+ npm install @tanstack/browser-db-sqlite-persistence
16
+
17
+ # React Native
18
+ npm install @tanstack/react-native-db-sqlite-persistence
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ### 1. Define types
24
+
25
+ ```typescript
26
+ type User = {
27
+ id: string;
28
+ name: string;
29
+ email: string;
30
+ createdAt: number;
31
+ };
32
+
33
+ type Todo = {
34
+ id: string;
35
+ userId: string;
36
+ title: string;
37
+ status: "pending" | "complete";
38
+ createdAt: number;
39
+ updatedAt: number;
40
+ };
41
+
42
+ type AppSettings = {
43
+ id: string;
44
+ theme: "light" | "dark";
45
+ language: string;
46
+ };
47
+ ```
48
+
49
+ ### 2. Create the database and collections
50
+
51
+ Register collections in the `collections` option — that is how they are created. There is no separate `createUsersCollection()` call.
52
+
53
+ ```typescript
54
+ import { createCollection } from "@tanstack/react-db";
55
+ import {
56
+ BrowserCollectionCoordinator,
57
+ createBrowserWASQLitePersistence,
58
+ openBrowserWASQLiteOPFSDatabase,
59
+ persistedCollectionOptions,
60
+ } from "@tanstack/browser-db-sqlite-persistence";
61
+ import { createEventSourcedDB } from "@tanstack-db-collections/event-sourced";
62
+ import { createBrowserPlatform } from "@tanstack-db-collections/event-sourced/browser";
63
+
64
+ const platform = await createBrowserPlatform(
65
+ {
66
+ openBrowserWASQLiteOPFSDatabase,
67
+ createBrowserWASQLitePersistence,
68
+ BrowserCollectionCoordinator,
69
+ },
70
+ { databaseName: "my-app.sqlite" },
71
+ );
72
+
73
+ export const db = await createEventSourcedDB({
74
+ persistence: platform.persistence,
75
+ createCollection,
76
+ persistedCollectionOptions,
77
+ sync: {
78
+ pushEvents: (events) => typedRpc.events.push({ events }),
79
+ pullEvents: ({ since }) => typedRpc.events.pull({ since }),
80
+ },
81
+ collections: {
82
+ users: { getKey: (u: User) => u.id },
83
+ todos: { getKey: (t: Todo) => t.id },
84
+ settings: { getKey: (s: AppSettings) => s.id },
85
+ },
86
+ });
87
+ ```
88
+
89
+ After setup you get:
90
+
91
+ - `db.collections.users`
92
+ - `db.collections.todos`
93
+ - `db.collections.settings`
94
+
95
+ Plus two built-in collections that are always available, even with no sync configured:
96
+
97
+ - `db.collections.outbox` — local mutations waiting to be pushed to the server
98
+ - `db.collections.inbox` — server events that have been pulled to this device
99
+
100
+ Both are normal queryable collections, so you can visualize sync state directly in your UI (see [Inspecting sync state](#inspecting-sync-state)). `outbox` and `inbox` are reserved ids — defining a collection with either name throws.
101
+
102
+ ### 3. Write data
103
+
104
+ Use collections like normal TanStack DB. Mutations are logged to the event store automatically.
105
+
106
+ ```typescript
107
+ import { db } from "./db";
108
+
109
+ const userId = crypto.randomUUID();
110
+
111
+ db.collections.users.insert({
112
+ id: userId,
113
+ name: "Alice",
114
+ email: "alice@example.com",
115
+ createdAt: Date.now(),
116
+ });
117
+
118
+ db.collections.todos.insert({
119
+ id: crypto.randomUUID(),
120
+ userId,
121
+ title: "Buy groceries",
122
+ status: "pending",
123
+ createdAt: Date.now(),
124
+ updatedAt: Date.now(),
125
+ });
126
+
127
+ db.collections.settings.insert({
128
+ id: "app",
129
+ theme: "dark",
130
+ language: "en",
131
+ });
132
+
133
+ db.collections.todos.update("todo-id", (draft) => {
134
+ draft.status = "complete";
135
+ draft.updatedAt = Date.now();
136
+ });
137
+
138
+ db.collections.todos.delete("todo-id");
139
+ ```
140
+
141
+ For `settings`, use a fixed id (e.g. `"app"`) as a singleton row:
142
+
143
+ ```typescript
144
+ db.collections.settings.update("app", (draft) => {
145
+ draft.theme = "light";
146
+ });
147
+ ```
148
+
149
+ ### 4. Read data
150
+
151
+ ```typescript
152
+ import { useLiveQuery } from "@tanstack/react-db";
153
+ import { db } from "./db";
154
+
155
+ function TodoList() {
156
+ const { data: todos = [] } = useLiveQuery((q) =>
157
+ q.from({ todo: db.collections.todos }),
158
+ );
159
+
160
+ const { data: users = [] } = useLiveQuery((q) =>
161
+ q.from({ user: db.collections.users }),
162
+ );
163
+
164
+ const { data: settings = [] } = useLiveQuery((q) =>
165
+ q.from({ setting: db.collections.settings }),
166
+ );
167
+
168
+ return (
169
+ <ul>
170
+ {todos.map((todo) => (
171
+ <li key={todo.id}>
172
+ {todo.title} — {users.find((u) => u.id === todo.userId)?.name}
173
+ </li>
174
+ ))}
175
+ </ul>
176
+ );
177
+ }
178
+ ```
179
+
180
+ ### 5. Sync
181
+
182
+ ```typescript
183
+ const result = await db.sync();
184
+
185
+ window.addEventListener("online", () => db.sync());
186
+ ```
187
+
188
+ ## Inspecting sync state
189
+
190
+ `outbox` and `inbox` are queryable collections. Each row carries a `sync` flag:
191
+
192
+ - **outbox** — `sync: false` until the event has been pushed to the server successfully. `sync: true` means the server accepted it (and assigned a `globalSeq`). Outbox rows also include `syncStatus`, `attemptCount`, `lastAttemptAt`, `lastError`, `lastErrorCode`, and `retryable` for push diagnostics.
193
+ - **inbox** — `sync: false` until the event has been replayed on top of local data. `sync: true` means it has been applied to the relevant collection.
194
+
195
+ ```typescript
196
+ import { useLiveQuery } from "@tanstack/react-db";
197
+ import { eq } from "@tanstack/db";
198
+ import { db } from "./db";
199
+
200
+ function SyncStatus() {
201
+ const { data: unpushed = [] } = useLiveQuery((q) =>
202
+ q.from({ e: db.collections.outbox }).where(({ e }) => eq(e.sync, false)),
203
+ );
204
+
205
+ const { data: incoming = [] } = useLiveQuery((q) =>
206
+ q.from({ e: db.collections.inbox }),
207
+ );
208
+
209
+ return (
210
+ <p>
211
+ {unpushed.length} pending upload · {incoming.length} received
212
+ </p>
213
+ );
214
+ }
215
+ ```
216
+
217
+ ## How it works
218
+
219
+ | Step | What happens |
220
+ | ---- | ------------ |
221
+ | Register collections | `collections: { users, todos, settings }` in `createEventSourcedDB` |
222
+ | Access them | `db.collections.users`, etc. |
223
+ | Write data | `.insert()`, `.update()`, `.delete()` |
224
+ | Log mutations | Each write appends a row to `db.collections.outbox` (`sync: false`) |
225
+ | Read data | `useLiveQuery` against those collections |
226
+ | Sync | `await db.sync()` pushes the outbox and pulls into the inbox |
227
+
228
+ On push, confirmed outbox rows flip to `sync: true` with their server `globalSeq`. On pull, new server events are written to `inbox` (`sync: false`), replayed into the target collection via `acceptMutations`, then flipped to `sync: true`. The client's own events come back on pull and are written to `inbox` as already applied. The pull cursor is derived from the highest synced `globalSeq` in `inbox` — there is no separate cursor table.
229
+
230
+ ## Server contract
231
+
232
+ ### `POST /api/events`
233
+
234
+ Request: array of outbound events (`eventId`, `collectionId`, `type`, `key`, `payload`, `timestamp`).
235
+
236
+ Response:
237
+
238
+ ```json
239
+ {
240
+ "confirmed": [{ "eventId": "...", "globalSeq": 100 }],
241
+ "failed": [{ "eventId": "...", "message": "Validation failed", "code": "VALIDATION_ERROR", "retryable": false }]
242
+ }
243
+ ```
244
+
245
+ ### `GET /api/events?since={globalSeq}`
246
+
247
+ Response:
248
+
249
+ ```json
250
+ {
251
+ "events": [{ "globalSeq": 102, "eventId": "...", "collectionId": "todos", "type": "insert", "key": "...", "payload": {}, "timestamp": 0, "cursor": "102" }],
252
+ "cursor": "102",
253
+ "hasMore": false
254
+ }
255
+ ```
256
+
257
+ Minimal PostgreSQL schema:
258
+
259
+ ```sql
260
+ CREATE TABLE events (
261
+ global_seq BIGSERIAL PRIMARY KEY,
262
+ event_id TEXT NOT NULL UNIQUE,
263
+ collection_id TEXT NOT NULL,
264
+ type TEXT NOT NULL CHECK (type IN ('insert', 'update', 'delete')),
265
+ key TEXT NOT NULL,
266
+ payload JSONB NOT NULL,
267
+ client_timestamp BIGINT NOT NULL
268
+ );
269
+
270
+ CREATE INDEX idx_events_global_seq ON events (global_seq);
271
+ ```
272
+
273
+ ## Sync Options
274
+
275
+ You can run fully offline by omitting `sync`. In that mode local mutations still append to `outbox`, and `sync()` is a no-op.
276
+
277
+ Use typed functions when your app already has RPC/server-function clients:
278
+
279
+ ```typescript
280
+ import type { PullResponse, PushResponse } from "@tanstack-db-collections/event-sourced";
281
+
282
+ const sync = {
283
+ pushEvents: async (events): Promise<PushResponse> => {
284
+ return typedRpc.events.push({ events });
285
+ },
286
+ pullEvents: async ({ since }): Promise<PullResponse> => {
287
+ return typedRpc.events.pull({ since });
288
+ },
289
+ };
290
+
291
+ const db = await createEventSourcedDB({ sync, /* ... */ });
292
+ ```
293
+
294
+ Use URLs when you want the built-in HTTP adapter:
295
+
296
+ ```typescript
297
+ const db = await createEventSourcedDB({
298
+ sync: {
299
+ pushUrl: "/api/events",
300
+ pullUrl: "/api/events",
301
+ headers: () => ({ Authorization: `Bearer ${getAccessToken()}` }),
302
+ },
303
+ /* ... */
304
+ });
305
+ ```
306
+
307
+ You can provide only `pushEvents`/`pushUrl` for upload-only sync or only `pullEvents`/`pullUrl` for download-only sync. If both a function and URL are provided for the same direction, the function is used.
308
+
309
+ For existing integrations, the legacy shapes still work:
310
+
311
+ ```typescript
312
+ import type { SyncTransport } from "@tanstack-db-collections/event-sourced";
313
+
314
+ const transport: SyncTransport = {
315
+ async push(events) { /* return confirmed event ids + globalSeq */ },
316
+ async pull(since) { /* return { events, cursor, hasMore } */ },
317
+ };
318
+
319
+ const db = await createEventSourcedDB({ sync: transport, /* ... */ });
320
+ ```
321
+
322
+ ## React Native
323
+
324
+ ```typescript
325
+ import { createCollection } from "@tanstack/react-native-db";
326
+ import { createReactNativeSQLitePersistence, persistedCollectionOptions } from "@tanstack/react-native-db-sqlite-persistence";
327
+ import { createEventSourcedDB } from "@tanstack-db-collections/event-sourced";
328
+ import { createReactNativePlatform } from "@tanstack-db-collections/event-sourced/react-native";
329
+ import { openDatabase } from "react-native-op-sqlite";
330
+
331
+ const platform = createReactNativePlatform(
332
+ { createReactNativeSQLitePersistence },
333
+ { database: openDatabase({ name: "my-app.sqlite" }) },
334
+ );
335
+
336
+ export const db = await createEventSourcedDB({
337
+ persistence: platform.persistence,
338
+ createCollection,
339
+ persistedCollectionOptions,
340
+ collections: {
341
+ users: { getKey: (u: User) => u.id },
342
+ todos: { getKey: (t: Todo) => t.id },
343
+ },
344
+ });
345
+ ```
346
+
347
+ Insert, update, delete, `useLiveQuery`, and `sync()` work the same as in the browser.
348
+
349
+ ## Cleanup
350
+
351
+ ```typescript
352
+ db.dispose();
353
+ await platform.close();
354
+ ```
355
+
356
+ ## API
357
+
358
+ ### `createEventSourcedDB(config)`
359
+
360
+ | Option | Required | Description |
361
+ | ------ | -------- | ----------- |
362
+ | `persistence` | Yes | TanStack DB persistence config |
363
+ | `createCollection` | Yes | TanStack DB `createCollection` |
364
+ | `persistedCollectionOptions` | Yes | From your platform package |
365
+ | `collections` | Yes | Collection definitions (`getKey`, optional `schemaVersion`) — must not use the reserved ids `outbox`/`inbox` |
366
+ | `sync` | No | Offline by default. Accepts typed `pushEvents`/`pullEvents`, URL `pushUrl`/`pullUrl`, legacy URL `push`/`pull`, or legacy `SyncTransport` |
367
+ | `schemaVersion` | No | Default `1` |
368
+
369
+ Returns `{ collections, sync, manualSync, dispose }`, where `collections` includes your registered collections plus the built-in `outbox` and `inbox`.
370
+
371
+ ### `createBrowserPlatform(deps, config)`
372
+
373
+ Import from `@tanstack-db-collections/event-sourced/browser`. Sets up browser SQLite + multi-tab coordinator.
374
+
375
+ ### `createReactNativePlatform(deps, config)`
376
+
377
+ Import from `@tanstack-db-collections/event-sourced/react-native`. Sets up React Native SQLite persistence.
@@ -0,0 +1,34 @@
1
+ import { x as SQLiteDriver } from "./types-CAT14Tbj.mjs";
2
+ import { PersistedCollectionCoordinator, PersistedCollectionPersistence } from "@tanstack/db-sqlite-persistence-core";
3
+ import { BrowserCollectionCoordinatorOptions, BrowserWASQLiteDatabase as BrowserWASQLiteDatabase$1, BrowserWASQLitePersistenceOptions, OpenBrowserWASQLiteOPFSDatabaseOptions } from "@tanstack/browser-db-sqlite-persistence";
4
+
5
+ //#region src/platforms/browser-types.d.ts
6
+ type BrowserCoordinatorInstance = PersistedCollectionCoordinator & {
7
+ dispose: () => void;
8
+ };
9
+ type BrowserPlatformDeps = {
10
+ openBrowserWASQLiteOPFSDatabase: (options: OpenBrowserWASQLiteOPFSDatabaseOptions) => Promise<BrowserWASQLiteDatabase$1>;
11
+ createBrowserWASQLitePersistence: (options: BrowserWASQLitePersistenceOptions) => PersistedCollectionPersistence;
12
+ BrowserCollectionCoordinator: new (options: BrowserCollectionCoordinatorOptions) => BrowserCoordinatorInstance;
13
+ };
14
+ type BrowserPlatformConfig = {
15
+ databaseName: string;
16
+ coordinatorDbName?: string;
17
+ };
18
+ type BrowserPlatformResult = {
19
+ driver: SQLiteDriver;
20
+ persistence: PersistedCollectionPersistence;
21
+ close: () => Promise<void>;
22
+ };
23
+ //#endregion
24
+ //#region src/platforms/browser-wa-sqlite-driver.d.ts
25
+ type BrowserWASQLiteDatabase = {
26
+ execute: <TRow = unknown>(sql: string, params?: ReadonlyArray<unknown>) => Promise<ReadonlyArray<TRow>>;
27
+ close?: () => Promise<void> | void;
28
+ };
29
+ //#endregion
30
+ //#region src/platforms/browser.d.ts
31
+ declare function createBrowserPlatform(deps: BrowserPlatformDeps, config: BrowserPlatformConfig): Promise<BrowserPlatformResult>;
32
+ //#endregion
33
+ export { type BrowserCoordinatorInstance, type BrowserPlatformConfig, type BrowserPlatformDeps, type BrowserPlatformResult, type BrowserWASQLiteDatabase, createBrowserPlatform };
34
+ //# sourceMappingURL=browser.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.mts","names":[],"sources":["../src/platforms/browser-types.ts","../src/platforms/browser-wa-sqlite-driver.ts","../src/platforms/browser.ts"],"mappings":";;;;;KAQY,0BAAA,GAA6B,8BAA8B;EACrE,OAAO;AAAA;AAAA,KAGG,mBAAA;EACV,+BAAA,GACE,OAAA,EAAS,sCAAA,KACN,OAAA,CAAQ,yBAAA;EACb,gCAAA,GACE,OAAA,EAAS,iCAAA,KACN,8BAAA;EACL,4BAAA,OACE,OAAA,EAAS,mCAAA,KACN,0BAAA;AAAA;AAAA,KAGK,qBAAA;EACV,YAAA;EACA,iBAAiB;AAAA;AAAA,KAGP,qBAAA;EACV,MAAA,EAD+B,YAAA;EAE/B,WAAA,EAAa,8BAAA;EACb,KAAA,QAAa,OAAA;AAAA;;;KC9BH,uBAAA;EACV,OAAA,mBACE,GAAA,UACA,MAAA,GAAS,aAAA,cACN,OAAA,CAAQ,aAAA,CAAc,IAAA;EAC3B,KAAA,SAAc,OAAA;AAAA;;;iBCUM,qBAAA,CACpB,IAAA,EAAM,mBAAA,EACN,MAAA,EAAQ,qBAAA,GACP,OAAA,CAAQ,qBAAA"}
@@ -0,0 +1,107 @@
1
+ //#region src/platforms/browser-wa-sqlite-driver.ts
2
+ function assertTransactionCallbackHasDriverArg(fn) {
3
+ if (fn.length > 0) return;
4
+ throw new Error("SQLiteDriver.transaction callback must accept the transaction driver argument");
5
+ }
6
+ function assertDatabaseShape(database) {
7
+ if (typeof database.execute !== "function") throw new Error("Browser wa-sqlite database handle must provide execute(sql, params?)");
8
+ }
9
+ var BrowserWASQLiteDriver = class {
10
+ database;
11
+ queue = Promise.resolve();
12
+ nextSavepointId = 1;
13
+ closed = false;
14
+ constructor(database) {
15
+ assertDatabaseShape(database);
16
+ this.database = database;
17
+ }
18
+ async exec(sql) {
19
+ await this.enqueue(async () => {
20
+ await this.database.execute(sql);
21
+ });
22
+ }
23
+ async query(sql, params = []) {
24
+ return this.enqueue(() => this.database.execute(sql, params));
25
+ }
26
+ async run(sql, params = []) {
27
+ await this.enqueue(async () => {
28
+ await this.database.execute(sql, params);
29
+ });
30
+ }
31
+ async transaction(fn) {
32
+ assertTransactionCallbackHasDriverArg(fn);
33
+ return this.enqueue(async () => {
34
+ await this.database.execute(`BEGIN IMMEDIATE`);
35
+ try {
36
+ const result = await fn(this.createTransactionDriver());
37
+ await this.database.execute(`COMMIT`);
38
+ return result;
39
+ } catch (error) {
40
+ try {
41
+ await this.database.execute(`ROLLBACK`);
42
+ } catch {}
43
+ throw error;
44
+ }
45
+ });
46
+ }
47
+ async transactionWithDriver(fn) {
48
+ return this.transaction(fn);
49
+ }
50
+ async close() {
51
+ if (this.closed) return;
52
+ this.closed = true;
53
+ if (typeof this.database.close === "function") await Promise.resolve(this.database.close());
54
+ }
55
+ createTransactionDriver() {
56
+ return {
57
+ exec: (sql) => this.database.execute(sql).then(() => void 0),
58
+ query: (sql, params = []) => this.database.execute(sql, params),
59
+ run: (sql, params = []) => this.database.execute(sql, params).then(() => void 0),
60
+ transaction: (fn) => this.runNestedTransaction(fn),
61
+ transactionWithDriver: (fn) => this.runNestedTransaction(fn)
62
+ };
63
+ }
64
+ async runNestedTransaction(fn) {
65
+ assertTransactionCallbackHasDriverArg(fn);
66
+ const savepointName = `tsdb_sp_${this.nextSavepointId}`;
67
+ this.nextSavepointId++;
68
+ await this.database.execute(`SAVEPOINT ${savepointName}`);
69
+ try {
70
+ const result = await fn(this.createTransactionDriver());
71
+ await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`);
72
+ return result;
73
+ } catch (error) {
74
+ await this.database.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`);
75
+ await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`);
76
+ throw error;
77
+ }
78
+ }
79
+ enqueue(operation) {
80
+ const queuedOperation = this.queue.then(operation, operation);
81
+ this.queue = queuedOperation.then(() => void 0, () => void 0);
82
+ return queuedOperation;
83
+ }
84
+ };
85
+ //#endregion
86
+ //#region src/platforms/browser.ts
87
+ async function createBrowserPlatform(deps, config) {
88
+ const database = await deps.openBrowserWASQLiteOPFSDatabase({ databaseName: config.databaseName });
89
+ const coordinator = new deps.BrowserCollectionCoordinator({ dbName: config.coordinatorDbName ?? config.databaseName.replace(/\.sqlite$/, "") });
90
+ const persistence = deps.createBrowserWASQLitePersistence({
91
+ database,
92
+ coordinator
93
+ });
94
+ const driver = new BrowserWASQLiteDriver(database);
95
+ return {
96
+ driver,
97
+ persistence,
98
+ close: async () => {
99
+ coordinator.dispose();
100
+ await driver.close();
101
+ }
102
+ };
103
+ }
104
+ //#endregion
105
+ export { createBrowserPlatform };
106
+
107
+ //# sourceMappingURL=browser.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.mjs","names":[],"sources":["../src/platforms/browser-wa-sqlite-driver.ts","../src/platforms/browser.ts"],"sourcesContent":["import type { SQLiteDriver } from \"../types\";\n\nexport type BrowserWASQLiteDatabase = {\n execute: <TRow = unknown>(\n sql: string,\n params?: ReadonlyArray<unknown>,\n ) => Promise<ReadonlyArray<TRow>>;\n close?: () => Promise<void> | void;\n};\n\nfunction assertTransactionCallbackHasDriverArg(\n fn: (transactionDriver: SQLiteDriver) => Promise<unknown>,\n): void {\n if (fn.length > 0) {\n return;\n }\n\n throw new Error(\n \"SQLiteDriver.transaction callback must accept the transaction driver argument\",\n );\n}\n\nfunction assertDatabaseShape(\n database: BrowserWASQLiteDatabase,\n): asserts database is BrowserWASQLiteDatabase {\n if (typeof database.execute !== \"function\") {\n throw new Error(\"Browser wa-sqlite database handle must provide execute(sql, params?)\");\n }\n}\n\nexport class BrowserWASQLiteDriver implements SQLiteDriver {\n private readonly database: BrowserWASQLiteDatabase;\n private queue: Promise<void> = Promise.resolve();\n private nextSavepointId = 1;\n private closed = false;\n\n constructor(database: BrowserWASQLiteDatabase) {\n assertDatabaseShape(database);\n this.database = database;\n }\n\n async exec(sql: string): Promise<void> {\n await this.enqueue(async () => {\n await this.database.execute(sql);\n });\n }\n\n async query<T>(sql: string, params: ReadonlyArray<unknown> = []): Promise<ReadonlyArray<T>> {\n return this.enqueue(() => this.database.execute<T>(sql, params));\n }\n\n async run(sql: string, params: ReadonlyArray<unknown> = []): Promise<void> {\n await this.enqueue(async () => {\n await this.database.execute(sql, params);\n });\n }\n\n async transaction<T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn);\n\n return this.enqueue(async () => {\n await this.database.execute(`BEGIN IMMEDIATE`);\n try {\n const result = await fn(this.createTransactionDriver());\n await this.database.execute(`COMMIT`);\n return result;\n } catch (error) {\n try {\n await this.database.execute(`ROLLBACK`);\n } catch {\n }\n throw error;\n }\n });\n }\n\n async transactionWithDriver<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n return this.transaction(fn);\n }\n\n async close(): Promise<void> {\n if (this.closed) {\n return;\n }\n this.closed = true;\n\n if (typeof this.database.close === \"function\") {\n await Promise.resolve(this.database.close());\n }\n }\n\n private createTransactionDriver(): SQLiteDriver {\n return {\n exec: (sql) => this.database.execute(sql).then(() => undefined),\n query: <T>(sql: string, params: ReadonlyArray<unknown> = []) =>\n this.database.execute<T>(sql, params),\n run: (sql: string, params: ReadonlyArray<unknown> = []) =>\n this.database.execute(sql, params).then(() => undefined),\n transaction: <T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>) =>\n this.runNestedTransaction(fn),\n transactionWithDriver: <T>(fn: (transactionDriver: SQLiteDriver) => Promise<T>) =>\n this.runNestedTransaction(fn),\n };\n }\n\n private async runNestedTransaction<T>(\n fn: (transactionDriver: SQLiteDriver) => Promise<T>,\n ): Promise<T> {\n assertTransactionCallbackHasDriverArg(fn);\n\n const savepointName = `tsdb_sp_${this.nextSavepointId}`;\n this.nextSavepointId++;\n await this.database.execute(`SAVEPOINT ${savepointName}`);\n try {\n const result = await fn(this.createTransactionDriver());\n await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`);\n return result;\n } catch (error) {\n await this.database.execute(`ROLLBACK TO SAVEPOINT ${savepointName}`);\n await this.database.execute(`RELEASE SAVEPOINT ${savepointName}`);\n throw error;\n }\n }\n\n private enqueue<T>(operation: () => Promise<T> | T): Promise<T> {\n const queuedOperation = this.queue.then(operation, operation);\n this.queue = queuedOperation.then(\n () => undefined,\n () => undefined,\n );\n return queuedOperation;\n }\n}\n","import type { PersistedCollectionPersistence } from \"../types\";\nimport { BrowserWASQLiteDriver } from \"./browser-wa-sqlite-driver\";\nimport type {\n BrowserPlatformConfig,\n BrowserPlatformDeps,\n BrowserPlatformResult,\n} from \"./browser-types\";\n\nexport type {\n BrowserCoordinatorInstance,\n BrowserPlatformConfig,\n BrowserPlatformDeps,\n BrowserPlatformResult,\n} from \"./browser-types\";\n\nexport type { BrowserWASQLiteDatabase } from \"./browser-wa-sqlite-driver\";\n\nexport async function createBrowserPlatform(\n deps: BrowserPlatformDeps,\n config: BrowserPlatformConfig,\n): Promise<BrowserPlatformResult> {\n const database = await deps.openBrowserWASQLiteOPFSDatabase({\n databaseName: config.databaseName,\n });\n\n const coordinator = new deps.BrowserCollectionCoordinator({\n dbName: config.coordinatorDbName ?? config.databaseName.replace(/\\.sqlite$/, \"\"),\n });\n\n const persistence = deps.createBrowserWASQLitePersistence({\n database,\n coordinator,\n });\n\n const driver = new BrowserWASQLiteDriver(database);\n\n return {\n driver,\n persistence,\n close: async () => {\n coordinator.dispose();\n await driver.close();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { A as PersistedCollectionOptionsFn, C as SyncHandlersConfig, D as EventSourcedLogger, E as SyncUrlConfig, O as createEventSourcedLogger, S as ServerEvent, T as SyncTransport, _ as PushEventsFn, a as InboxEntry, b as ReservedCollections, c as ManualSyncResult, d as OutboxEntry, f as OutboxSyncStatus, g as PushConfirmation, h as PullResponse, i as EventSourcedDBConfig, k as CreateCollectionFn, l as MutationType, m as PullEventsFn, n as CollectionMap, o as InferKey, p as PersistedCollectionPersistence, r as EventSourcedDB, s as InferState, t as CollectionDef, u as OutboundEvent, v as PushFailure, w as SyncResult, x as SQLiteDriver, y as PushResponse } from "./types-CAT14Tbj.mjs";
2
+ import { uuidv7 as generateEventId } from "uuidv7";
3
+
4
+ //#region src/create-event-sourced-db.d.ts
5
+ type CollectionDefConstraint = {
6
+ getKey: (state: never) => string | number;
7
+ schemaVersion?: number;
8
+ };
9
+ declare function createEventSourcedDB<const TDefs extends Record<string, CollectionDefConstraint>>(config: EventSourcedDBConfig<TDefs>): Promise<EventSourcedDB<TDefs>>;
10
+ //#endregion
11
+ //#region src/sync.d.ts
12
+ declare function createHttpTransport(config: SyncUrlConfig): SyncTransport;
13
+ declare class SyncPushError extends Error {
14
+ readonly status: number;
15
+ readonly body: string;
16
+ constructor(status: number, body: string);
17
+ }
18
+ declare class SyncPullError extends Error {
19
+ readonly status: number;
20
+ readonly body: string;
21
+ constructor(status: number, body: string);
22
+ }
23
+ //#endregion
24
+ export { type CollectionDef, type CollectionMap, type CreateCollectionFn, type EventSourcedDB, type EventSourcedDBConfig, type EventSourcedLogger, type InboxEntry, type InferKey, type InferState, type ManualSyncResult, type MutationType, type OutboundEvent, type OutboxEntry, type OutboxSyncStatus, type PersistedCollectionOptionsFn, type PersistedCollectionPersistence, type PullEventsFn, type PullResponse, type PushConfirmation, type PushEventsFn, type PushFailure, type PushResponse, type ReservedCollections, type SQLiteDriver, type ServerEvent, type SyncHandlersConfig, SyncPullError, SyncPushError, type SyncResult, type SyncTransport, type SyncUrlConfig, createEventSourcedDB, createEventSourcedLogger, createHttpTransport, generateEventId };
25
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/create-event-sourced-db.ts","../src/sync.ts"],"mappings":";;;;KAuBK,uBAAA;EACH,MAAA,GAAS,KAAA;EACT,aAAA;AAAA;AAAA,iBAsCoB,oBAAA,qBACA,MAAA,SAAe,uBAAA,GACnC,MAAA,EAAQ,oBAAA,CAAqB,KAAA,IAAS,OAAA,CAAQ,cAAA,CAAe,KAAA;;;iBC9C/C,mBAAA,CAAoB,MAAA,EAAQ,aAAA,GAAgB,aAAa;AAAA,cAwI5D,aAAA,SAAsB,KAAK;EAAA,SAEpB,MAAA;EAAA,SACA,IAAA;cADA,MAAA,UACA,IAAA;AAAA;AAAA,cAOP,aAAA,SAAsB,KAAK;EAAA,SAEpB,MAAA;EAAA,SACA,IAAA;cADA,MAAA,UACA,IAAA;AAAA"}