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 +21 -0
- package/README.md +377 -0
- package/dist/browser.d.mts +34 -0
- package/dist/browser.d.mts.map +1 -0
- package/dist/browser.mjs +107 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +584 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react-native.d.mts +19 -0
- package/dist/react-native.d.mts.map +1 -0
- package/dist/react-native.mjs +12 -0
- package/dist/react-native.mjs.map +1 -0
- package/dist/types-CAT14Tbj.d.mts +148 -0
- package/dist/types-CAT14Tbj.d.mts.map +1 -0
- package/package.json +81 -0
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"}
|
package/dist/browser.mjs
ADDED
|
@@ -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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"}
|
package/dist/index.d.mts
ADDED
|
@@ -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"}
|