event-sourced-collection 0.0.1 → 0.0.2
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/ARCHITECTURE.md +479 -0
- package/README.md +284 -69
- package/dist/browser.d.mts +27 -2
- package/dist/browser.d.mts.map +1 -1
- package/dist/browser.mjs +54 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/index.d.mts +13 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -584
- package/dist/lazy-singleton-DFrAsYTV.mjs +629 -0
- package/dist/lazy-singleton-DFrAsYTV.mjs.map +1 -0
- package/dist/react-native.d.mts +26 -2
- package/dist/react-native.d.mts.map +1 -1
- package/dist/react-native.mjs +34 -1
- package/dist/react-native.mjs.map +1 -1
- package/package.json +8 -3
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# event-sourced-collection
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
+
Design decisions: [ARCHITECTURE.md](./ARCHITECTURE.md)
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npm install
|
|
10
|
+
npm install event-sourced-collection @tanstack/db @tanstack/db-sqlite-persistence-core
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
Plus a platform package:
|
|
@@ -18,9 +20,47 @@ npm install @tanstack/browser-db-sqlite-persistence
|
|
|
18
20
|
npm install @tanstack/react-native-db-sqlite-persistence
|
|
19
21
|
```
|
|
20
22
|
|
|
23
|
+
## Project structure
|
|
24
|
+
|
|
25
|
+
A minimal browser app needs four pieces. Split them by concern so setup stays obvious:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
src/
|
|
29
|
+
├── data-access-layer/
|
|
30
|
+
│ ├── collections.ts # types, DB init, sync transport, `ensureDb()` + `db`
|
|
31
|
+
│ └── sync-events.ts # thin wrappers around `db.sync()` / `db.manualSync()`
|
|
32
|
+
├── hooks/common/
|
|
33
|
+
│ └── use-event-sourced-sync.ts # periodic background sync (optional)
|
|
34
|
+
└── routes/ or app shell
|
|
35
|
+
└── layout.tsx # call `ensureDb()` once before rendering data UI
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| File | Role |
|
|
39
|
+
| ---- | ---- |
|
|
40
|
+
| `collections.ts` | Single source of truth: row types, collection registry, push/pull handlers, one `createBrowserEventSourcedDB` call exporting `ensureDb` + `db` |
|
|
41
|
+
| `sync-events.ts` | Keeps sync calls out of React; import from buttons, jobs, or hooks |
|
|
42
|
+
| `use-event-sourced-sync.ts` | Polls `manualSync()` on an interval when the DB is ready |
|
|
43
|
+
| App shell | `await ensureDb()` before routes that read/write collections |
|
|
44
|
+
|
|
45
|
+
Flow:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
app mount → ensureDb() → load() + platform + engine (run once, deduped)
|
|
49
|
+
↓
|
|
50
|
+
db.collections.* available
|
|
51
|
+
↓
|
|
52
|
+
useLiveQuery / insert / update / delete
|
|
53
|
+
↓
|
|
54
|
+
db.sync() or useEventSourcedSync()
|
|
55
|
+
```
|
|
56
|
+
|
|
21
57
|
## Quick start
|
|
22
58
|
|
|
23
|
-
|
|
59
|
+
Copy the pattern below into `collections.ts`, then wire `ensureDb()` in your app shell.
|
|
60
|
+
|
|
61
|
+
### 1. Domain types
|
|
62
|
+
|
|
63
|
+
One type per collection you will register. These are your row shapes in SQLite.
|
|
24
64
|
|
|
25
65
|
```typescript
|
|
26
66
|
type User = {
|
|
@@ -46,65 +86,154 @@ type AppSettings = {
|
|
|
46
86
|
};
|
|
47
87
|
```
|
|
48
88
|
|
|
49
|
-
### 2.
|
|
89
|
+
### 2. Collection registry + typed DB handle
|
|
50
90
|
|
|
51
|
-
|
|
91
|
+
Keys here must match the `collections` option you pass in step 4. This gives you `db.collections.users`, etc. with full inference.
|
|
52
92
|
|
|
53
93
|
```typescript
|
|
54
|
-
import {
|
|
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
|
-
);
|
|
94
|
+
import type { EventSourcedDB } from "event-sourced-collection";
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
type AppCollectionDefs = {
|
|
97
|
+
users: { getKey: (user: User) => string };
|
|
98
|
+
todos: { getKey: (todo: Todo) => string };
|
|
99
|
+
settings: { getKey: (settings: AppSettings) => string };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type AppDb = EventSourcedDB<AppCollectionDefs>;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 3. Sync transport
|
|
106
|
+
|
|
107
|
+
Implement push/pull against your API. These run during `db.sync()` — upload local outbox rows, download server events.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import type { OutboundEvent, PullResponse, PushResponse } from "event-sourced-collection";
|
|
111
|
+
|
|
112
|
+
const getAccessToken = () => localStorage.getItem("accessToken") ?? "";
|
|
113
|
+
|
|
114
|
+
async function pushEvents(events: ReadonlyArray<OutboundEvent>): Promise<PushResponse> {
|
|
115
|
+
const response = await fetch("/api/sync/events", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
Authorization: `Bearer ${getAccessToken()}`,
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify(events),
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) throw new Error(`Push failed: ${response.status}`);
|
|
124
|
+
return response.json();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function pullEvents({ since }: { since: number }): Promise<PullResponse> {
|
|
128
|
+
const response = await fetch(`/api/sync/events?since=${since}`, {
|
|
129
|
+
headers: {
|
|
130
|
+
Accept: "application/json",
|
|
131
|
+
Authorization: `Bearer ${getAccessToken()}`,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
if (!response.ok) throw new Error(`Pull failed: ${response.status}`);
|
|
135
|
+
return response.json();
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 4. Create the database (one call)
|
|
140
|
+
|
|
141
|
+
`createBrowserEventSourcedDB` (from `event-sourced-collection/browser`) wires the SQLite platform, registers collections, and gives you a lazy singleton (`ensureDb`) plus a `db` proxy you can import anywhere. **Collections are created here** — there is no separate `createUsersCollection()` call.
|
|
142
|
+
|
|
143
|
+
The `load` callback is where you import the platform packages. Keeping the imports in *your* app (instead of inside the library) means the package never bundles a platform or a framework: you choose `@tanstack/react-db` vs `@tanstack/db`, and SSR bundles stay clean because `load` is dynamically imported on first `ensureDb()`.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { createBrowserEventSourcedDB } from "event-sourced-collection/browser";
|
|
147
|
+
|
|
148
|
+
const { ensureDb, db, close } = createBrowserEventSourcedDB<AppCollectionDefs>({
|
|
149
|
+
databaseName: "my-app.sqlite",
|
|
150
|
+
debug: import.meta.env.DEV,
|
|
81
151
|
collections: {
|
|
82
|
-
users: { getKey: (
|
|
83
|
-
todos: { getKey: (
|
|
84
|
-
settings: { getKey: (
|
|
152
|
+
users: { getKey: (user: User) => user.id },
|
|
153
|
+
todos: { getKey: (todo: Todo) => todo.id },
|
|
154
|
+
settings: { getKey: (settings: AppSettings) => settings.id },
|
|
155
|
+
},
|
|
156
|
+
sync: { pushEvents, pullEvents },
|
|
157
|
+
load: async () => {
|
|
158
|
+
const { createCollection } = await import("@tanstack/react-db");
|
|
159
|
+
const platform = await import("@tanstack/browser-db-sqlite-persistence");
|
|
160
|
+
return { ...platform, createCollection };
|
|
85
161
|
},
|
|
86
162
|
});
|
|
163
|
+
|
|
164
|
+
export { db, ensureDb, close };
|
|
87
165
|
```
|
|
88
166
|
|
|
167
|
+
The handle is `{ ensureDb, db, close }`:
|
|
168
|
+
|
|
169
|
+
- `ensureDb()` — runs `load` + setup once (deduped across concurrent calls), returns the ready DB. Throws outside a browser environment.
|
|
170
|
+
- `db` — a proxy that forwards to the instance after `ensureDb()` resolves, and throws if used before then.
|
|
171
|
+
- `close()` — disposes the engine and closes the SQLite platform; the next `ensureDb()` re-initializes.
|
|
172
|
+
|
|
89
173
|
After setup you get:
|
|
90
174
|
|
|
91
175
|
- `db.collections.users`
|
|
92
176
|
- `db.collections.todos`
|
|
93
177
|
- `db.collections.settings`
|
|
94
178
|
|
|
95
|
-
Plus two built-in collections
|
|
179
|
+
Plus two built-in collections (always present, even without sync):
|
|
180
|
+
|
|
181
|
+
- `db.collections.outbox` — local mutations waiting to be pushed
|
|
182
|
+
- `db.collections.inbox` — server events pulled to this device
|
|
183
|
+
|
|
184
|
+
Both are normal queryable collections for sync UI (see [Inspecting sync state](#inspecting-sync-state)). `outbox` and `inbox` are reserved ids.
|
|
185
|
+
|
|
186
|
+
### 5. Wire the app shell
|
|
187
|
+
|
|
188
|
+
Call `ensureDb()` once when the app mounts, before any collection reads:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { useEffect, useState } from "react";
|
|
192
|
+
import { ensureDb } from "./data-access-layer/collections";
|
|
193
|
+
|
|
194
|
+
function AppShell() {
|
|
195
|
+
const [ready, setReady] = useState(false);
|
|
96
196
|
|
|
97
|
-
|
|
98
|
-
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
void ensureDb().then(() => setReady(true));
|
|
199
|
+
}, []);
|
|
99
200
|
|
|
100
|
-
|
|
201
|
+
if (!ready) return <Loading />;
|
|
202
|
+
return <YourRoutes />;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Optional background sync (`sync-events.ts` + hook):
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// sync-events.ts
|
|
210
|
+
import { ensureDb } from "./collections";
|
|
211
|
+
|
|
212
|
+
export async function manualSyncEvents() {
|
|
213
|
+
const database = await ensureDb();
|
|
214
|
+
return database.manualSync();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// use-event-sourced-sync.ts
|
|
218
|
+
import { useQuery } from "@tanstack/react-query";
|
|
219
|
+
import { manualSyncEvents } from "@/data-access-layer/sync-events";
|
|
220
|
+
|
|
221
|
+
export function useEventSourcedSync(enabled: boolean) {
|
|
222
|
+
return useQuery({
|
|
223
|
+
queryKey: ["sync"],
|
|
224
|
+
queryFn: () => manualSyncEvents(),
|
|
225
|
+
enabled,
|
|
226
|
+
refetchInterval: 60_000,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
```
|
|
101
230
|
|
|
102
|
-
###
|
|
231
|
+
### 6. Write data
|
|
103
232
|
|
|
104
233
|
Use collections like normal TanStack DB. Mutations are logged to the event store automatically.
|
|
105
234
|
|
|
106
235
|
```typescript
|
|
107
|
-
import { db } from "./
|
|
236
|
+
import { db } from "./data-access-layer/collections";
|
|
108
237
|
|
|
109
238
|
const userId = crypto.randomUUID();
|
|
110
239
|
|
|
@@ -146,11 +275,11 @@ db.collections.settings.update("app", (draft) => {
|
|
|
146
275
|
});
|
|
147
276
|
```
|
|
148
277
|
|
|
149
|
-
###
|
|
278
|
+
### 7. Read data
|
|
150
279
|
|
|
151
280
|
```typescript
|
|
152
281
|
import { useLiveQuery } from "@tanstack/react-db";
|
|
153
|
-
import { db } from "./
|
|
282
|
+
import { db } from "./data-access-layer/collections";
|
|
154
283
|
|
|
155
284
|
function TodoList() {
|
|
156
285
|
const { data: todos = [] } = useLiveQuery((q) =>
|
|
@@ -177,14 +306,19 @@ function TodoList() {
|
|
|
177
306
|
}
|
|
178
307
|
```
|
|
179
308
|
|
|
180
|
-
###
|
|
309
|
+
### 8. Sync
|
|
181
310
|
|
|
182
311
|
```typescript
|
|
183
|
-
|
|
312
|
+
import { ensureDb } from "./data-access-layer/collections";
|
|
313
|
+
|
|
314
|
+
const db = await ensureDb();
|
|
315
|
+
await db.sync();
|
|
184
316
|
|
|
185
|
-
window.addEventListener("online", () => db.sync());
|
|
317
|
+
window.addEventListener("online", () => void db.sync());
|
|
186
318
|
```
|
|
187
319
|
|
|
320
|
+
Or use a periodic hook (see step 5).
|
|
321
|
+
|
|
188
322
|
## Inspecting sync state
|
|
189
323
|
|
|
190
324
|
`outbox` and `inbox` are queryable collections. Each row carries a `sync` flag:
|
|
@@ -195,7 +329,7 @@ window.addEventListener("online", () => db.sync());
|
|
|
195
329
|
```typescript
|
|
196
330
|
import { useLiveQuery } from "@tanstack/react-db";
|
|
197
331
|
import { eq } from "@tanstack/db";
|
|
198
|
-
import { db } from "./
|
|
332
|
+
import { db } from "./data-access-layer/collections";
|
|
199
333
|
|
|
200
334
|
function SyncStatus() {
|
|
201
335
|
const { data: unpushed = [] } = useLiveQuery((q) =>
|
|
@@ -218,7 +352,7 @@ function SyncStatus() {
|
|
|
218
352
|
|
|
219
353
|
| Step | What happens |
|
|
220
354
|
| ---- | ------------ |
|
|
221
|
-
| Register collections | `collections: { users, todos, settings }` in `createEventSourcedDB` |
|
|
355
|
+
| Register collections | `collections: { users, todos, settings }` in `createBrowserEventSourcedDB` (or low-level `createEventSourcedDB`) |
|
|
222
356
|
| Access them | `db.collections.users`, etc. |
|
|
223
357
|
| Write data | `.insert()`, `.update()`, `.delete()` |
|
|
224
358
|
| Log mutations | Each write appends a row to `db.collections.outbox` (`sync: false`) |
|
|
@@ -227,9 +361,17 @@ function SyncStatus() {
|
|
|
227
361
|
|
|
228
362
|
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
363
|
|
|
364
|
+
Step by step:
|
|
365
|
+
|
|
366
|
+
1. User mutates `db.collections.todos` → row appended to local **outbox** (`sync: false`).
|
|
367
|
+
2. `manualSync()` or background hook pushes outbox → `POST /api/sync/events`.
|
|
368
|
+
3. Server assigns `globalSeq` → outbox rows flip to `sync: true`.
|
|
369
|
+
4. Pull fetches newer events → `GET /api/sync/events?since=<cursor>`.
|
|
370
|
+
5. Remote events land in **inbox**, replay into collections, flip to `sync: true`.
|
|
371
|
+
|
|
230
372
|
## Server contract
|
|
231
373
|
|
|
232
|
-
### `POST /api/events`
|
|
374
|
+
### `POST /api/sync/events`
|
|
233
375
|
|
|
234
376
|
Request: array of outbound events (`eventId`, `collectionId`, `type`, `key`, `payload`, `timestamp`).
|
|
235
377
|
|
|
@@ -242,7 +384,7 @@ Response:
|
|
|
242
384
|
}
|
|
243
385
|
```
|
|
244
386
|
|
|
245
|
-
### `GET /api/events?since={globalSeq}`
|
|
387
|
+
### `GET /api/sync/events?since={globalSeq}`
|
|
246
388
|
|
|
247
389
|
Response:
|
|
248
390
|
|
|
@@ -270,6 +412,38 @@ CREATE TABLE events (
|
|
|
270
412
|
CREATE INDEX idx_events_global_seq ON events (global_seq);
|
|
271
413
|
```
|
|
272
414
|
|
|
415
|
+
## Simulate another device
|
|
416
|
+
|
|
417
|
+
POST a fake event directly to your sync server to mimic a push from another client. Use a unique `eventId` — it must not already exist in the receiving device's local outbox.
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
curl -X POST http://localhost:3000/api/sync/events \
|
|
421
|
+
-H "Content-Type: application/json" \
|
|
422
|
+
-d '[{
|
|
423
|
+
"eventId": "019c0000-0000-7000-8000-remote-demo",
|
|
424
|
+
"collectionId": "todos",
|
|
425
|
+
"type": "insert",
|
|
426
|
+
"key": "remote-todo-demo",
|
|
427
|
+
"payload": {
|
|
428
|
+
"id": "remote-todo-demo",
|
|
429
|
+
"userId": "remote-user",
|
|
430
|
+
"title": "From another device",
|
|
431
|
+
"status": "pending",
|
|
432
|
+
"createdAt": 1730000000000,
|
|
433
|
+
"updatedAt": 1730000000000
|
|
434
|
+
},
|
|
435
|
+
"timestamp": 1730000000000
|
|
436
|
+
}]'
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Then call `manualSync()` or `db.sync()` on the client. The event appears in inbox and the todo is replayed locally.
|
|
440
|
+
|
|
441
|
+
Check server state:
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
curl "http://localhost:3000/api/sync/events?since=0"
|
|
445
|
+
```
|
|
446
|
+
|
|
273
447
|
## Sync Options
|
|
274
448
|
|
|
275
449
|
You can run fully offline by omitting `sync`. In that mode local mutations still append to `outbox`, and `sync()` is a no-op.
|
|
@@ -277,7 +451,7 @@ You can run fully offline by omitting `sync`. In that mode local mutations still
|
|
|
277
451
|
Use typed functions when your app already has RPC/server-function clients:
|
|
278
452
|
|
|
279
453
|
```typescript
|
|
280
|
-
import type { PullResponse, PushResponse } from "
|
|
454
|
+
import type { PullResponse, PushResponse } from "event-sourced-collection";
|
|
281
455
|
|
|
282
456
|
const sync = {
|
|
283
457
|
pushEvents: async (events): Promise<PushResponse> => {
|
|
@@ -309,7 +483,7 @@ You can provide only `pushEvents`/`pushUrl` for upload-only sync or only `pullEv
|
|
|
309
483
|
For existing integrations, the legacy shapes still work:
|
|
310
484
|
|
|
311
485
|
```typescript
|
|
312
|
-
import type { SyncTransport } from "
|
|
486
|
+
import type { SyncTransport } from "event-sourced-collection";
|
|
313
487
|
|
|
314
488
|
const transport: SyncTransport = {
|
|
315
489
|
async push(events) { /* return confirmed event ids + globalSeq */ },
|
|
@@ -321,33 +495,48 @@ const db = await createEventSourcedDB({ sync: transport, /* ... */ });
|
|
|
321
495
|
|
|
322
496
|
## React Native
|
|
323
497
|
|
|
498
|
+
Same helper shape via `createReactNativeEventSourcedDB` (from `event-sourced-collection/react-native`). React Native can't auto-open the database, so `load` returns the `database` driver alongside the platform functions.
|
|
499
|
+
|
|
324
500
|
```typescript
|
|
325
|
-
import {
|
|
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
|
-
);
|
|
501
|
+
import { createReactNativeEventSourcedDB } from "event-sourced-collection/react-native";
|
|
335
502
|
|
|
336
|
-
|
|
337
|
-
persistence: platform.persistence,
|
|
338
|
-
createCollection,
|
|
339
|
-
persistedCollectionOptions,
|
|
503
|
+
const { ensureDb, db } = createReactNativeEventSourcedDB<AppCollectionDefs>({
|
|
340
504
|
collections: {
|
|
341
|
-
users: { getKey: (
|
|
342
|
-
todos: { getKey: (
|
|
505
|
+
users: { getKey: (user: User) => user.id },
|
|
506
|
+
todos: { getKey: (todo: Todo) => todo.id },
|
|
507
|
+
},
|
|
508
|
+
sync: { pushEvents, pullEvents },
|
|
509
|
+
load: async () => {
|
|
510
|
+
const { createCollection } = await import("@tanstack/react-native-db");
|
|
511
|
+
const { createReactNativeSQLitePersistence, persistedCollectionOptions } = await import(
|
|
512
|
+
"@tanstack/react-native-db-sqlite-persistence"
|
|
513
|
+
);
|
|
514
|
+
const { openDatabase } = await import("react-native-op-sqlite");
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
createReactNativeSQLitePersistence,
|
|
518
|
+
persistedCollectionOptions,
|
|
519
|
+
createCollection,
|
|
520
|
+
database: openDatabase({ name: "my-app.sqlite" }),
|
|
521
|
+
};
|
|
343
522
|
},
|
|
344
523
|
});
|
|
524
|
+
|
|
525
|
+
export { db, ensureDb };
|
|
345
526
|
```
|
|
346
527
|
|
|
347
528
|
Insert, update, delete, `useLiveQuery`, and `sync()` work the same as in the browser.
|
|
348
529
|
|
|
349
530
|
## Cleanup
|
|
350
531
|
|
|
532
|
+
Call the handle's `close()` to dispose the engine and release the platform:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
await close();
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
If you wired things up by hand with the low-level `createEventSourcedDB` + `createBrowserPlatform`, dispose them directly instead:
|
|
539
|
+
|
|
351
540
|
```typescript
|
|
352
541
|
db.dispose();
|
|
353
542
|
await platform.close();
|
|
@@ -355,8 +544,34 @@ await platform.close();
|
|
|
355
544
|
|
|
356
545
|
## API
|
|
357
546
|
|
|
547
|
+
### `createBrowserEventSourcedDB(config)` / `createReactNativeEventSourcedDB(config)`
|
|
548
|
+
|
|
549
|
+
The recommended entry points. Import from `event-sourced-collection/browser` or `event-sourced-collection/react-native`.
|
|
550
|
+
|
|
551
|
+
| Option | Required | Description |
|
|
552
|
+
| ------ | -------- | ----------- |
|
|
553
|
+
| `collections` | Yes | Collection definitions (`getKey`, optional `schemaVersion`) — must not use the reserved ids `outbox`/`inbox` |
|
|
554
|
+
| `load` | Yes | Async callback returning the platform modules + `createCollection` + `persistedCollectionOptions`. Keeps platform/framework imports in your app |
|
|
555
|
+
| `databaseName` | Browser only | SQLite database file name |
|
|
556
|
+
| `coordinatorDbName` | No (browser) | Multi-tab coordinator name; defaults to `databaseName` without the `.sqlite` suffix |
|
|
557
|
+
| `sync` | No | Offline by default. Same shapes as `createEventSourcedDB` |
|
|
558
|
+
| `schemaVersion` | No | Default `1` |
|
|
559
|
+
| `debug` | No | `boolean` or a custom logger |
|
|
560
|
+
|
|
561
|
+
Returns `{ ensureDb, db, close }`:
|
|
562
|
+
|
|
563
|
+
- `ensureDb()` — runs `load` + setup once (deduped), returns the ready DB. The browser helper throws outside a browser environment.
|
|
564
|
+
- `db` — proxy that forwards to the instance after init and throws if used before `ensureDb()`.
|
|
565
|
+
- `close()` — disposes the engine (and closes the SQLite platform on browser); the next `ensureDb()` re-initializes.
|
|
566
|
+
|
|
567
|
+
### `createLazySingleton(factory, options?)`
|
|
568
|
+
|
|
569
|
+
Dependency-free building block behind the helpers. Wraps an async `factory` into `{ ensure, proxy, reset }`: `ensure()` runs the factory once (deduped, retried after failure), `proxy` forwards to the instance and throws until initialized, `reset()` clears it. Accepts an optional `guard` and `notInitializedMessage`.
|
|
570
|
+
|
|
358
571
|
### `createEventSourcedDB(config)`
|
|
359
572
|
|
|
573
|
+
Low-level core for full control or non-standard platforms. The helpers above call this for you.
|
|
574
|
+
|
|
360
575
|
| Option | Required | Description |
|
|
361
576
|
| ------ | -------- | ----------- |
|
|
362
577
|
| `persistence` | Yes | TanStack DB persistence config |
|
|
@@ -370,8 +585,8 @@ Returns `{ collections, sync, manualSync, dispose }`, where `collections` includ
|
|
|
370
585
|
|
|
371
586
|
### `createBrowserPlatform(deps, config)`
|
|
372
587
|
|
|
373
|
-
Import from
|
|
588
|
+
Import from `event-sourced-collection/browser`. Sets up browser SQLite + multi-tab coordinator.
|
|
374
589
|
|
|
375
590
|
### `createReactNativePlatform(deps, config)`
|
|
376
591
|
|
|
377
|
-
Import from
|
|
592
|
+
Import from `event-sourced-collection/react-native`. Sets up React Native SQLite persistence.
|
package/dist/browser.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { x as SQLiteDriver } from "./types-CAT14Tbj.mjs";
|
|
1
|
+
import { A as PersistedCollectionOptionsFn, C as SyncHandlersConfig, D as EventSourcedLogger, E as SyncUrlConfig, T as SyncTransport, k as CreateCollectionFn, r as EventSourcedDB, x as SQLiteDriver } from "./types-CAT14Tbj.mjs";
|
|
2
2
|
import { PersistedCollectionCoordinator, PersistedCollectionPersistence } from "@tanstack/db-sqlite-persistence-core";
|
|
3
3
|
import { BrowserCollectionCoordinatorOptions, BrowserWASQLiteDatabase as BrowserWASQLiteDatabase$1, BrowserWASQLitePersistenceOptions, OpenBrowserWASQLiteOPFSDatabaseOptions } from "@tanstack/browser-db-sqlite-persistence";
|
|
4
4
|
|
|
@@ -30,5 +30,30 @@ type BrowserWASQLiteDatabase = {
|
|
|
30
30
|
//#region src/platforms/browser.d.ts
|
|
31
31
|
declare function createBrowserPlatform(deps: BrowserPlatformDeps, config: BrowserPlatformConfig): Promise<BrowserPlatformResult>;
|
|
32
32
|
//#endregion
|
|
33
|
-
|
|
33
|
+
//#region src/platforms/browser-event-sourced-db.d.ts
|
|
34
|
+
type CollectionDefConstraint = {
|
|
35
|
+
getKey: (state: never) => string | number;
|
|
36
|
+
schemaVersion?: number;
|
|
37
|
+
};
|
|
38
|
+
type BrowserEventSourcedModules = BrowserPlatformDeps & {
|
|
39
|
+
createCollection: CreateCollectionFn;
|
|
40
|
+
persistedCollectionOptions: PersistedCollectionOptionsFn;
|
|
41
|
+
};
|
|
42
|
+
type BrowserEventSourcedDBConfig<TDefs extends Record<string, CollectionDefConstraint>> = {
|
|
43
|
+
databaseName: string;
|
|
44
|
+
coordinatorDbName?: string;
|
|
45
|
+
collections: TDefs;
|
|
46
|
+
load: () => Promise<BrowserEventSourcedModules>;
|
|
47
|
+
sync?: SyncHandlersConfig | SyncUrlConfig | SyncTransport;
|
|
48
|
+
schemaVersion?: number;
|
|
49
|
+
debug?: boolean | EventSourcedLogger;
|
|
50
|
+
};
|
|
51
|
+
type BrowserEventSourcedDBHandle<TDefs extends Record<string, CollectionDefConstraint>> = {
|
|
52
|
+
ensureDb: () => Promise<EventSourcedDB<TDefs>>;
|
|
53
|
+
db: EventSourcedDB<TDefs>;
|
|
54
|
+
close: () => Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
declare function createBrowserEventSourcedDB<const TDefs extends Record<string, CollectionDefConstraint>>(config: BrowserEventSourcedDBConfig<TDefs>): BrowserEventSourcedDBHandle<TDefs>;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { type BrowserCoordinatorInstance, type BrowserEventSourcedDBConfig, type BrowserEventSourcedDBHandle, type BrowserEventSourcedModules, type BrowserPlatformConfig, type BrowserPlatformDeps, type BrowserPlatformResult, type BrowserWASQLiteDatabase, createBrowserEventSourcedDB, createBrowserPlatform };
|
|
34
59
|
//# sourceMappingURL=browser.d.mts.map
|
package/dist/browser.d.mts.map
CHANGED
|
@@ -1 +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"}
|
|
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","../src/platforms/browser-event-sourced-db.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;;;KCPN,uBAAA;EACH,MAAA,GAAS,KAAA;EACT,aAAA;AAAA;AAAA,KAGU,0BAAA,GAA6B,mBAAA;EACvC,gBAAA,EAAkB,kBAAA;EAClB,0BAAA,EAA4B,4BAAA;AAAA;AAAA,KAGlB,2BAAA,eAA0C,MAAA,SAAe,uBAAA;EACnE,YAAA;EACA,iBAAA;EACA,WAAA,EAAa,KAAA;EACb,IAAA,QAAY,OAAA,CAAQ,0BAAA;EACpB,IAAA,GAAO,kBAAA,GAAqB,aAAA,GAAgB,aAAA;EAC5C,aAAA;EACA,KAAA,aAAkB,kBAAA;AAAA;AAAA,KAGR,2BAAA,eAA0C,MAAA,SAAe,uBAAA;EACnE,QAAA,QAAgB,OAAA,CAAQ,cAAA,CAAe,KAAA;EACvC,EAAA,EAAI,cAAA,CAAe,KAAA;EACnB,KAAA,QAAa,OAAA;AAAA;AAAA,iBAGC,2BAAA,qBACM,MAAA,SAAe,uBAAA,GACnC,MAAA,EAAQ,2BAAA,CAA4B,KAAA,IAAS,2BAAA,CAA4B,KAAA"}
|
package/dist/browser.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as createEventSourcedDB, t as createLazySingleton } from "./lazy-singleton-DFrAsYTV.mjs";
|
|
1
2
|
//#region src/platforms/browser-wa-sqlite-driver.ts
|
|
2
3
|
function assertTransactionCallbackHasDriverArg(fn) {
|
|
3
4
|
if (fn.length > 0) return;
|
|
@@ -102,6 +103,58 @@ async function createBrowserPlatform(deps, config) {
|
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
//#endregion
|
|
105
|
-
|
|
106
|
+
//#region src/platforms/browser-event-sourced-db.ts
|
|
107
|
+
function createBrowserEventSourcedDB(config) {
|
|
108
|
+
let activeDb = null;
|
|
109
|
+
let closePlatform = null;
|
|
110
|
+
const factory = async () => {
|
|
111
|
+
const modules = await config.load();
|
|
112
|
+
const platform = await createBrowserPlatform({
|
|
113
|
+
openBrowserWASQLiteOPFSDatabase: modules.openBrowserWASQLiteOPFSDatabase,
|
|
114
|
+
createBrowserWASQLitePersistence: modules.createBrowserWASQLitePersistence,
|
|
115
|
+
BrowserCollectionCoordinator: modules.BrowserCollectionCoordinator
|
|
116
|
+
}, {
|
|
117
|
+
databaseName: config.databaseName,
|
|
118
|
+
coordinatorDbName: config.coordinatorDbName
|
|
119
|
+
});
|
|
120
|
+
closePlatform = platform.close;
|
|
121
|
+
const database = await createEventSourcedDB({
|
|
122
|
+
persistence: platform.persistence,
|
|
123
|
+
createCollection: modules.createCollection,
|
|
124
|
+
persistedCollectionOptions: modules.persistedCollectionOptions,
|
|
125
|
+
collections: config.collections,
|
|
126
|
+
sync: config.sync,
|
|
127
|
+
schemaVersion: config.schemaVersion,
|
|
128
|
+
debug: config.debug
|
|
129
|
+
});
|
|
130
|
+
activeDb = database;
|
|
131
|
+
return database;
|
|
132
|
+
};
|
|
133
|
+
const singleton = createLazySingleton(factory, {
|
|
134
|
+
guard: assertBrowserEnvironment,
|
|
135
|
+
notInitializedMessage: "Event-sourced DB is not initialized. Call ensureDb() before using db."
|
|
136
|
+
});
|
|
137
|
+
const close = async () => {
|
|
138
|
+
if (activeDb) {
|
|
139
|
+
activeDb.dispose();
|
|
140
|
+
activeDb = null;
|
|
141
|
+
}
|
|
142
|
+
if (closePlatform) {
|
|
143
|
+
await closePlatform();
|
|
144
|
+
closePlatform = null;
|
|
145
|
+
}
|
|
146
|
+
singleton.reset();
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
ensureDb: singleton.ensure,
|
|
150
|
+
db: singleton.proxy,
|
|
151
|
+
close
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function assertBrowserEnvironment() {
|
|
155
|
+
if (typeof window === "undefined") throw new Error("Event-sourced DB (browser) is only available in a browser environment.");
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
export { createBrowserEventSourcedDB, createBrowserPlatform };
|
|
106
159
|
|
|
107
160
|
//# sourceMappingURL=browser.mjs.map
|
package/dist/browser.mjs.map
CHANGED
|
@@ -1 +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":";AAUA,SAAS,sCACP,IACM;CACN,IAAI,GAAG,SAAS,GACd;CAGF,MAAM,IAAI,MACR,+EACF;AACF;AAEA,SAAS,oBACP,UAC6C;CAC7C,IAAI,OAAO,SAAS,YAAY,YAC9B,MAAM,IAAI,MAAM,sEAAsE;AAE1F;AAEA,IAAa,wBAAb,MAA2D;CACzD;CACA,QAA+B,QAAQ,QAAQ;CAC/C,kBAA0B;CAC1B,SAAiB;CAEjB,YAAY,UAAmC;EAC7C,oBAAoB,QAAQ;EAC5B,KAAK,WAAW;CAClB;CAEA,MAAM,KAAK,KAA4B;EACrC,MAAM,KAAK,QAAQ,YAAY;GAC7B,MAAM,KAAK,SAAS,QAAQ,GAAG;EACjC,CAAC;CACH;CAEA,MAAM,MAAS,KAAa,SAAiC,CAAC,GAA8B;EAC1F,OAAO,KAAK,cAAc,KAAK,SAAS,QAAW,KAAK,MAAM,CAAC;CACjE;CAEA,MAAM,IAAI,KAAa,SAAiC,CAAC,GAAkB;EACzE,MAAM,KAAK,QAAQ,YAAY;GAC7B,MAAM,KAAK,SAAS,QAAQ,KAAK,MAAM;EACzC,CAAC;CACH;CAEA,MAAM,YAAe,IAAiE;EACpF,sCAAsC,EAAE;EAExC,OAAO,KAAK,QAAQ,YAAY;GAC9B,MAAM,KAAK,SAAS,QAAQ,iBAAiB;GAC7C,IAAI;IACF,MAAM,SAAS,MAAM,GAAG,KAAK,wBAAwB,CAAC;IACtD,MAAM,KAAK,SAAS,QAAQ,QAAQ;IACpC,OAAO;GACT,SAAS,OAAO;IACd,IAAI;KACF,MAAM,KAAK,SAAS,QAAQ,UAAU;IACxC,QAAQ,CACR;IACA,MAAM;GACR;EACF,CAAC;CACH;CAEA,MAAM,sBACJ,IACY;EACZ,OAAO,KAAK,YAAY,EAAE;CAC5B;CAEA,MAAM,QAAuB;EAC3B,IAAI,KAAK,QACP;EAEF,KAAK,SAAS;EAEd,IAAI,OAAO,KAAK,SAAS,UAAU,YACjC,MAAM,QAAQ,QAAQ,KAAK,SAAS,MAAM,CAAC;CAE/C;CAEA,0BAAgD;EAC9C,OAAO;GACL,OAAO,QAAQ,KAAK,SAAS,QAAQ,GAAG,CAAC,CAAC,WAAW,KAAA,CAAS;GAC9D,QAAW,KAAa,SAAiC,CAAC,MACxD,KAAK,SAAS,QAAW,KAAK,MAAM;GACtC,MAAM,KAAa,SAAiC,CAAC,MACnD,KAAK,SAAS,QAAQ,KAAK,MAAM,CAAC,CAAC,WAAW,KAAA,CAAS;GACzD,cAAiB,OACf,KAAK,qBAAqB,EAAE;GAC9B,wBAA2B,OACzB,KAAK,qBAAqB,EAAE;EAChC;CACF;CAEA,MAAc,qBACZ,IACY;EACZ,sCAAsC,EAAE;EAExC,MAAM,gBAAgB,WAAW,KAAK;EACtC,KAAK;EACL,MAAM,KAAK,SAAS,QAAQ,aAAa,eAAe;EACxD,IAAI;GACF,MAAM,SAAS,MAAM,GAAG,KAAK,wBAAwB,CAAC;GACtD,MAAM,KAAK,SAAS,QAAQ,qBAAqB,eAAe;GAChE,OAAO;EACT,SAAS,OAAO;GACd,MAAM,KAAK,SAAS,QAAQ,yBAAyB,eAAe;GACpE,MAAM,KAAK,SAAS,QAAQ,qBAAqB,eAAe;GAChE,MAAM;EACR;CACF;CAEA,QAAmB,WAA6C;EAC9D,MAAM,kBAAkB,KAAK,MAAM,KAAK,WAAW,SAAS;EAC5D,KAAK,QAAQ,gBAAgB,WACrB,KAAA,SACA,KAAA,CACR;EACA,OAAO;CACT;AACF;;;ACrHA,eAAsB,sBACpB,MACA,QACgC;CAChC,MAAM,WAAW,MAAM,KAAK,gCAAgC,EAC1D,cAAc,OAAO,aACvB,CAAC;CAED,MAAM,cAAc,IAAI,KAAK,6BAA6B,EACxD,QAAQ,OAAO,qBAAqB,OAAO,aAAa,QAAQ,aAAa,EAAE,EACjF,CAAC;CAED,MAAM,cAAc,KAAK,iCAAiC;EACxD;EACA;CACF,CAAC;CAED,MAAM,SAAS,IAAI,sBAAsB,QAAQ;CAEjD,OAAO;EACL;EACA;EACA,OAAO,YAAY;GACjB,YAAY,QAAQ;GACpB,MAAM,OAAO,MAAM;EACrB;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"browser.mjs","names":[],"sources":["../src/platforms/browser-wa-sqlite-driver.ts","../src/platforms/browser.ts","../src/platforms/browser-event-sourced-db.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","import { createEventSourcedDB } from \"../create-event-sourced-db\";\nimport { createLazySingleton } from \"../lazy-singleton\";\nimport type { CreateCollectionFn, PersistedCollectionOptionsFn } from \"../persisted-collection\";\nimport type {\n EventSourcedDB,\n EventSourcedLogger,\n SyncHandlersConfig,\n SyncTransport,\n SyncUrlConfig,\n} from \"../types\";\nimport { createBrowserPlatform } from \"./browser\";\nimport type { BrowserPlatformDeps } from \"./browser-types\";\n\ntype CollectionDefConstraint = {\n getKey: (state: never) => string | number;\n schemaVersion?: number;\n};\n\nexport type BrowserEventSourcedModules = BrowserPlatformDeps & {\n createCollection: CreateCollectionFn;\n persistedCollectionOptions: PersistedCollectionOptionsFn;\n};\n\nexport type BrowserEventSourcedDBConfig<TDefs extends Record<string, CollectionDefConstraint>> = {\n databaseName: string;\n coordinatorDbName?: string;\n collections: TDefs;\n load: () => Promise<BrowserEventSourcedModules>;\n sync?: SyncHandlersConfig | SyncUrlConfig | SyncTransport;\n schemaVersion?: number;\n debug?: boolean | EventSourcedLogger;\n};\n\nexport type BrowserEventSourcedDBHandle<TDefs extends Record<string, CollectionDefConstraint>> = {\n ensureDb: () => Promise<EventSourcedDB<TDefs>>;\n db: EventSourcedDB<TDefs>;\n close: () => Promise<void>;\n};\n\nexport function createBrowserEventSourcedDB<\n const TDefs extends Record<string, CollectionDefConstraint>,\n>(config: BrowserEventSourcedDBConfig<TDefs>): BrowserEventSourcedDBHandle<TDefs> {\n let activeDb: EventSourcedDB<TDefs> | null = null;\n let closePlatform: (() => Promise<void>) | null = null;\n\n const factory = async (): Promise<EventSourcedDB<TDefs>> => {\n const modules = await config.load();\n\n const platform = await createBrowserPlatform(\n {\n openBrowserWASQLiteOPFSDatabase: modules.openBrowserWASQLiteOPFSDatabase,\n createBrowserWASQLitePersistence: modules.createBrowserWASQLitePersistence,\n BrowserCollectionCoordinator: modules.BrowserCollectionCoordinator,\n },\n {\n databaseName: config.databaseName,\n coordinatorDbName: config.coordinatorDbName,\n },\n );\n\n closePlatform = platform.close;\n\n const database = await createEventSourcedDB<TDefs>({\n persistence: platform.persistence,\n createCollection: modules.createCollection,\n persistedCollectionOptions: modules.persistedCollectionOptions,\n collections: config.collections,\n sync: config.sync,\n schemaVersion: config.schemaVersion,\n debug: config.debug,\n });\n\n activeDb = database;\n return database;\n };\n\n const singleton = createLazySingleton<EventSourcedDB<TDefs>>(factory, {\n guard: assertBrowserEnvironment,\n notInitializedMessage:\n \"Event-sourced DB is not initialized. Call ensureDb() before using db.\",\n });\n\n const close = async (): Promise<void> => {\n if (activeDb) {\n activeDb.dispose();\n activeDb = null;\n }\n if (closePlatform) {\n await closePlatform();\n closePlatform = null;\n }\n singleton.reset();\n };\n\n return {\n ensureDb: singleton.ensure,\n db: singleton.proxy,\n close,\n };\n}\n\nfunction assertBrowserEnvironment(): void {\n if (typeof window === \"undefined\") {\n throw new Error(\"Event-sourced DB (browser) is only available in a browser environment.\");\n }\n}\n"],"mappings":"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"}
|