@sync-subscribe/client 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 fromkeith
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,430 @@
1
+ # @sync-subscribe/client
2
+
3
+ Framework-agnostic sync client for `sync-subscribe`. Manages subscriptions locally, maintains a local store, and runs pull/push cycles against any HTTP transport you provide.
4
+
5
+ ## Concepts
6
+
7
+ | Term | Description |
8
+ |---|---|
9
+ | `SyncRecord` | Every synced record must have `recordId`, `createdAt`, `updatedAt`, `revisionCount` |
10
+ | `SyncTransport` | Your HTTP adapter — implement `pull`, `push`, and optionally `stream` |
11
+ | `SyncClient` | Orchestrates subscriptions, local state, pull/push, and reactive queries |
12
+ | `ILocalStore` | Async interface for local storage — both built-in stores implement it |
13
+ | `LocalStore` | In-memory store (default); fast but data is lost on page reload |
14
+ | `IdbLocalStore` | IndexedDB-backed store; data persists across page reloads |
15
+ | `ClientSubscription` | Tracks a subscription's `subscriptionId`, `filter`, and `syncToken` |
16
+ | `SyncQuery<T>` | A reactive handle that follows the store contract: `{ data: T[], loading: boolean }` |
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @sync-subscribe/client @sync-subscribe/core
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ### 1. Define your record type
27
+
28
+ ```ts
29
+ import type { SyncRecord } from "@sync-subscribe/core";
30
+
31
+ interface NoteRecord extends SyncRecord {
32
+ title: string;
33
+ contents: string;
34
+ isDeleted: boolean;
35
+ }
36
+ ```
37
+
38
+ ### 2. Create a transport
39
+
40
+ Use the built-in `createFetchTransport` for standard fetch-based HTTP:
41
+
42
+ ```ts
43
+ import { createFetchTransport } from "@sync-subscribe/client";
44
+
45
+ const transport = createFetchTransport({
46
+ baseUrl: "/api",
47
+ headers: () => ({ Authorization: `Bearer ${getToken()}` }),
48
+ });
49
+ ```
50
+
51
+ Or implement `SyncTransport` yourself for full control:
52
+
53
+ ```ts
54
+ import type { SyncTransport } from "@sync-subscribe/client";
55
+
56
+ const transport: SyncTransport = {
57
+ async pull(subscriptions) {
58
+ const res = await fetch("/api/sync/pull", {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({ subscriptions }), // [{ key, filter, syncToken }]
62
+ });
63
+ return res.json(); // { patches, syncTokens }
64
+ },
65
+
66
+ async push(records) {
67
+ const res = await fetch("/api/sync/push", {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify({ records }),
71
+ });
72
+ return res.json(); // { ok: true } or { conflict: true, serverRecord }
73
+ },
74
+ };
75
+ ```
76
+
77
+ ### 3. Create a client
78
+
79
+ ```ts
80
+ import { SyncClient } from "@sync-subscribe/client";
81
+
82
+ const client = new SyncClient<NoteRecord>(transport);
83
+ ```
84
+
85
+ #### Persistent storage with IndexedDB
86
+
87
+ Pass an `IdbLocalStore` as the second argument to survive page reloads:
88
+
89
+ ```ts
90
+ import { SyncClient, IdbLocalStore } from "@sync-subscribe/client";
91
+
92
+ const client = new SyncClient<NoteRecord>(
93
+ transport,
94
+ new IdbLocalStore("notes-db"),
95
+ );
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Three ways to use data
101
+
102
+ There is a deliberate separation between **syncing** data (keeping the local store current) and **querying** data (reading from the local store into memory). This lets you sync more data than you display at any one time.
103
+
104
+ ### 1. Sync-only — keep data current in the store, nothing in memory
105
+
106
+ Use this when you want data available locally (for fast queries, offline use) but don't need it loaded into JS memory right now.
107
+
108
+ ```ts
109
+ // Keeps last 30 days synced. Automatically starts pull + SSE stream.
110
+ const sub = await client.subscribe({
111
+ filter: { createdAt: { $gte: Date.now() - 30 * 24 * 60 * 60 * 1000 } },
112
+ name: "last-30-days",
113
+ });
114
+
115
+ // Later, remove it
116
+ await client.unsubscribe(sub.subscriptionId);
117
+ ```
118
+
119
+ `subscribe()` handles pull scheduling and SSE stream management automatically. Multiple rapid `subscribe()` calls are debounced into a single stream reconnect.
120
+
121
+ ### 2. Query — read from the local store reactively
122
+
123
+ Use this when data is already being synced (by a separate `subscribe()`) but you want a filtered, reactive, in-memory view of it. No additional sync subscription is registered.
124
+
125
+ ```ts
126
+ // client.query() returns a SyncQuery<T> — a store-contract object.
127
+ // Nothing happens until you call .subscribe() on it.
128
+ const todayQuery = client.query({
129
+ filter: { createdAt: { $gte: startOfToday } },
130
+ });
131
+
132
+ // Follow the store contract: subscribe(run) => unsubscribe
133
+ const unsub = todayQuery.subscribe(({ data, loading }) => {
134
+ if (loading) return;
135
+ console.log("today's notes:", data);
136
+ });
137
+
138
+ unsub(); // stop listening
139
+ ```
140
+
141
+ `query()` is a sub-filter of whatever is already synced. It reads from the local store and re-runs whenever the store changes (pull patches or mutations). Loading starts `true` and becomes `false` after the first local read.
142
+
143
+ ### 3. Live query — sync + query combined (common case)
144
+
145
+ Use this when the sync filter and the query filter are the same, or when you want the query to manage its own subscription lifecycle.
146
+
147
+ ```ts
148
+ // client.liveQuery() registers a sync subscription when the first
149
+ // subscriber attaches, and removes it when the last subscriber detaches.
150
+ const notesQuery = client.liveQuery({
151
+ filter: { isDeleted: false },
152
+ name: "active-notes",
153
+ });
154
+
155
+ const unsub = notesQuery.subscribe(({ data, loading }) => {
156
+ if (loading) return;
157
+ renderNotes(data);
158
+ });
159
+
160
+ unsub(); // stops listening AND removes the sync subscription (if last subscriber)
161
+ ```
162
+
163
+ ### Pattern: large sync window, narrow display window
164
+
165
+ ```ts
166
+ // Sync 30 days in the background — data lives in local store, not in memory.
167
+ // This runs once at app startup (or in a root component).
168
+ await client.subscribe({
169
+ filter: { createdAt: { $gte: thirtyDaysAgo } },
170
+ name: "background-30d",
171
+ });
172
+
173
+ // In a component: query only today's slice — fast, no extra network request.
174
+ // The data is already in the local store from the background subscription.
175
+ const todayQuery = client.query({ filter: { createdAt: { $gte: startOfToday } } });
176
+
177
+ todayQuery.subscribe(({ data, loading }) => {
178
+ renderNotes(data); // instant — no loading spinner needed
179
+ });
180
+ ```
181
+
182
+ ---
183
+
184
+ ## The store contract
185
+
186
+ `SyncQuery<T>` follows the [Svelte store contract](https://svelte.dev/docs/svelte/stores#Store-contract), making it usable directly in Svelte templates, with `useSyncExternalStore` in React, or with any store-aware utility.
187
+
188
+ ```ts
189
+ interface SyncQuery<T extends SyncRecord> {
190
+ subscribe(
191
+ run: (value: { data: T[]; loading: boolean }) => void,
192
+ invalidate?: () => void,
193
+ ): () => void; // returns unsubscribe
194
+ }
195
+ ```
196
+
197
+ **In Svelte:**
198
+ ```svelte
199
+ <script>
200
+ const notes = client.liveQuery({ filter: { isDeleted: false } });
201
+ </script>
202
+
203
+ {#if $notes.loading}
204
+ <p>Loading…</p>
205
+ {:else}
206
+ {#each $notes.data as note}
207
+ <NoteCard {note} />
208
+ {/each}
209
+ {/if}
210
+ ```
211
+
212
+ **In React (proposed `useQuery` hook from `@sync-subscribe/client-react`):**
213
+ ```tsx
214
+ const { data, loading } = useQuery(client.query({ filter: { isDeleted: false } }));
215
+ // or the combined version:
216
+ const { data, loading } = useQuery(client.liveQuery({ filter: { isDeleted: false } }));
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Subscriptions
222
+
223
+ `subscribe` registers a filter locally and returns a `ClientSubscription`. The filter is sent to the server on every pull cycle or SSE stream request. Multiple overlapping subscriptions are fine — records are stored only once in the local store.
224
+
225
+ ```ts
226
+ const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
227
+
228
+ const sub1 = await client.subscribe({ filter: { createdAt: { $gte: thirtyDaysAgo } } });
229
+ const sub2 = await client.subscribe({ filter: { color: "blue" } });
230
+
231
+ // Both subscriptions are batched into a single pull request.
232
+ // The SSE stream (if transport supports it) is restarted once to include both.
233
+ ```
234
+
235
+ Available filter operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`, `$or`, `$and`, `$nor`.
236
+
237
+ ### Named subscriptions
238
+
239
+ Pass `name` to persist and restore subscription state across sessions (requires `IdbLocalStore`):
240
+
241
+ ```ts
242
+ await client.subscribe({ filter: { isDeleted: false }, name: "active-notes" });
243
+ ```
244
+
245
+ On the next session, `subscribe` with the same `name` and same filter reuses the stored `syncToken`, enabling incremental sync instead of a full re-fetch.
246
+
247
+ ### Updating a subscription
248
+
249
+ ```ts
250
+ await client.updateSubscription(sub.subscriptionId, { color: "red" });
251
+ ```
252
+
253
+ The client runs gap and eviction analysis locally: it detects whether any records matching the new filter are not yet cached (gap), fetches them, and evicts records that only the old filter needed.
254
+
255
+ ### Removing a subscription
256
+
257
+ ```ts
258
+ await client.unsubscribe(sub.subscriptionId);
259
+ ```
260
+
261
+ Removes the subscription from the local store and restarts the SSE stream without it. If no subscriptions remain, the stream is stopped.
262
+
263
+ ---
264
+
265
+ ## Mutating records
266
+
267
+ `mutate` writes the record locally immediately (read-your-own-writes), then pushes to the server. `updatedAt` and `revisionCount` are stamped automatically — do not set them yourself.
268
+
269
+ Returns `true` on success, `false` if the server detected a conflict (server record wins).
270
+
271
+ ```ts
272
+ // Create — provide recordId and createdAt; mutate() handles the rest
273
+ await client.mutate({
274
+ recordId: crypto.randomUUID(),
275
+ createdAt: Date.now(),
276
+ title: "Hello",
277
+ contents: "World",
278
+ isDeleted: false,
279
+ } as NoteRecord);
280
+
281
+ // Update — just spread and change fields; mutate() increments revisionCount
282
+ await client.mutate({ ...note, contents: "Updated" });
283
+
284
+ // Soft-delete
285
+ await client.mutate({ ...note, isDeleted: true });
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Listening for patches
291
+
292
+ `onPatches` fires whenever the local store changes from an incoming pull, stream event, or conflict resolution. Returns an unsubscribe function.
293
+
294
+ ```ts
295
+ const unsub = client.onPatches((patches) => {
296
+ for (const patch of patches) {
297
+ if (patch.op === "upsert") console.log("upserted", patch.record.recordId);
298
+ if (patch.op === "delete") console.log("deleted", patch.recordId);
299
+ }
300
+ });
301
+
302
+ unsub(); // stop listening
303
+ ```
304
+
305
+ This is the low-level primitive that `query()` and `liveQuery()` build on. Prefer those for UI code.
306
+
307
+ ---
308
+
309
+ ## SSE streaming
310
+
311
+ If your transport implements `stream`, the client opens a persistent SSE connection automatically when you call `subscribe()`. You do not manage the stream directly — it is started, restarted (when subscriptions change), and stopped (when all subscriptions are removed) internally.
312
+
313
+ ```ts
314
+ // Transport with SSE support
315
+ const transport: SyncTransport = {
316
+ // pull, push as before...
317
+
318
+ stream(subscriptions, onMessage, onError) {
319
+ const es = new EventSource("/api/sync/stream");
320
+ es.onmessage = (e) => onMessage(JSON.parse(e.data));
321
+ es.onerror = (e) => onError?.(new Error("SSE error"));
322
+ return () => es.close(); // cleanup
323
+ },
324
+ };
325
+
326
+ // SSE starts automatically when subscribe() is called.
327
+ // It restarts whenever subscriptions change, debounced 20 ms.
328
+ await client.subscribe({ filter: { isDeleted: false } });
329
+ ```
330
+
331
+ ---
332
+
333
+ ## Polling
334
+
335
+ There is no built-in polling timer. Set one up yourself:
336
+
337
+ ```ts
338
+ const timer = setInterval(async () => {
339
+ try { await client.pull(); } catch { /* retry next tick */ }
340
+ }, 5000);
341
+
342
+ clearInterval(timer); // on teardown
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Resetting state
348
+
349
+ Call `reset()` on logout or account switch — it stops the SSE stream, clears all subscriptions, and empties the local store.
350
+
351
+ ```ts
352
+ await client.reset();
353
+ ```
354
+
355
+ ---
356
+
357
+ ## API reference
358
+
359
+ ### `SyncClient<T>`
360
+
361
+ | Method | Returns | Description |
362
+ |---|---|---|
363
+ | `subscribe(options)` | `Promise<ClientSubscription>` | Sync a filter to local store; auto-starts pull + stream |
364
+ | `unsubscribe(id)` | `Promise<void>` | Remove a subscription; restarts stream without it |
365
+ | `updateSubscription(id, filter)` | `Promise<ClientSubscription>` | Replace a subscription's filter; handles gap/eviction |
366
+ | `query(options)` | `SyncQuery<T>` | Reactive local-store query; no sync subscription |
367
+ | `liveQuery(options)` | `SyncQuery<T>` | Reactive query that manages its own sync subscription |
368
+ | `mutate(record)` | `Promise<boolean>` | Write locally + push to server; stamps `updatedAt`/`revisionCount` |
369
+ | `pull()` | `Promise<void>` | Fetch pending patches for all active subscriptions |
370
+ | `schedulePull(delayMs?)` | `Promise<void>` | Debounced pull — collapses rapid calls into one request |
371
+ | `onPatches(listener)` | `() => void` | Low-level patch listener; returns unsubscribe |
372
+ | `getSubscription(key)` | `ClientSubscription \| undefined` | Look up by `subscriptionId` or `name` |
373
+ | `reset()` | `Promise<void>` | Stop stream, clear subscriptions and local store |
374
+ | `store` | `ILocalStore<T>` | Direct access to the local store |
375
+
376
+ ### `SyncQuery<T>` — store contract
377
+
378
+ ```ts
379
+ interface SyncQuery<T extends SyncRecord> {
380
+ subscribe(
381
+ run: (value: { data: T[]; loading: boolean }) => void,
382
+ invalidate?: () => void,
383
+ ): () => void;
384
+ }
385
+ ```
386
+
387
+ `liveQuery` vs `query`:
388
+
389
+ | | `query()` | `liveQuery()` |
390
+ |---|---|---|
391
+ | Registers a sync subscription | No | Yes (on first subscriber) |
392
+ | Removes sync subscription on cleanup | No | Yes (on last unsubscribe) |
393
+ | Reads from local store | Yes | Yes |
394
+ | Reacts to pull/stream patches | Yes | Yes |
395
+ | Use when | Data already synced elsewhere | Query filter === sync filter |
396
+
397
+ ### `SyncTransport`
398
+
399
+ ```ts
400
+ interface SyncTransport {
401
+ pull(subscriptions: { key: string; filter: SubscriptionFilter; syncToken: SyncToken }[]): Promise<{
402
+ patches: SyncPatch<SyncRecord>[];
403
+ syncTokens: Record<string, SyncToken>;
404
+ }>;
405
+
406
+ push(records: SyncRecord[]): Promise<
407
+ | { ok: true; serverUpdatedAt: number }
408
+ | { conflict: true; serverRecord: SyncRecord }
409
+ >;
410
+
411
+ stream?(
412
+ subscriptions: { key: string; filter: SubscriptionFilter; syncToken: SyncToken }[],
413
+ onMessage: (event: { patches: SyncPatch<SyncRecord>[]; syncTokens: Record<string, SyncToken> }) => void,
414
+ onError?: (err: Error) => void,
415
+ ): () => void;
416
+ }
417
+ ```
418
+
419
+ ### `ILocalStore<T>` — implemented by `LocalStore` and `IdbLocalStore`
420
+
421
+ | Method | Description |
422
+ |---|---|
423
+ | `getAll()` | Return all records |
424
+ | `getById(recordId)` | Return a single record or `undefined` |
425
+ | `query(filter)` | Return records matching a filter |
426
+ | `applyPatches(patches)` | Apply server patches; returns only patches that changed local state |
427
+ | `write(record)` | Write a record locally (used by `mutate`) |
428
+ | `evict(filter)` | Remove records matching a filter without deleting them from the server |
429
+ | `reconstructSyncToken(filter)` | Build a sync token from the latest locally-cached record matching `filter` |
430
+ | `clear()` | Remove all records |
@@ -0,0 +1,27 @@
1
+ import type { SyncTransport } from "./types.js";
2
+ export interface FetchTransportOptions {
3
+ /** Base URL of the sync server, e.g. "/api" or "https://api.example.com". */
4
+ baseUrl: string;
5
+ /**
6
+ * Called before every request to supply additional headers (e.g. Authorization).
7
+ * Return an empty object if no extra headers are needed.
8
+ */
9
+ headers?: () => Record<string, string>;
10
+ }
11
+ /**
12
+ * A fetch-based SyncTransport that works in any modern browser.
13
+ *
14
+ * Endpoints:
15
+ * - pull → POST {baseUrl}/sync/pull
16
+ * - push → POST {baseUrl}/sync/push
17
+ * - stream → POST {baseUrl}/sync/stream (fetch-based SSE)
18
+ *
19
+ * @example
20
+ * const transport = createFetchTransport({
21
+ * baseUrl: "/api",
22
+ * headers: () => ({ Authorization: `Bearer ${getToken()}` }),
23
+ * });
24
+ * const client = new SyncClient(transport);
25
+ */
26
+ export declare function createFetchTransport(options: FetchTransportOptions): SyncTransport;
27
+ //# sourceMappingURL=fetchTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchTransport.d.ts","sourceRoot":"","sources":["../src/fetchTransport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAA2B,MAAM,YAAY,CAAC;AAEzE,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,aAAa,CAiFlF"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A fetch-based SyncTransport that works in any modern browser.
3
+ *
4
+ * Endpoints:
5
+ * - pull → POST {baseUrl}/sync/pull
6
+ * - push → POST {baseUrl}/sync/push
7
+ * - stream → POST {baseUrl}/sync/stream (fetch-based SSE)
8
+ *
9
+ * @example
10
+ * const transport = createFetchTransport({
11
+ * baseUrl: "/api",
12
+ * headers: () => ({ Authorization: `Bearer ${getToken()}` }),
13
+ * });
14
+ * const client = new SyncClient(transport);
15
+ */
16
+ export function createFetchTransport(options) {
17
+ const { baseUrl, headers = () => ({}) } = options;
18
+ function jsonHeaders() {
19
+ return { "Content-Type": "application/json", ...headers() };
20
+ }
21
+ return {
22
+ async pull(subscriptions) {
23
+ const res = await fetch(`${baseUrl}/sync/pull`, {
24
+ method: "POST",
25
+ headers: jsonHeaders(),
26
+ body: JSON.stringify({ subscriptions }),
27
+ });
28
+ if (!res.ok)
29
+ throw new Error(`Pull failed: ${res.status}`);
30
+ return res.json();
31
+ },
32
+ async push(records) {
33
+ const res = await fetch(`${baseUrl}/sync/push`, {
34
+ method: "POST",
35
+ headers: jsonHeaders(),
36
+ body: JSON.stringify({ records }),
37
+ });
38
+ if (!res.ok)
39
+ throw new Error(`Push failed: ${res.status}`);
40
+ return res.json();
41
+ },
42
+ stream(subscriptions, onMessage, onError) {
43
+ const controller = new AbortController();
44
+ fetch(`${baseUrl}/sync/stream`, {
45
+ method: "POST",
46
+ headers: jsonHeaders(),
47
+ body: JSON.stringify({ subscriptions }),
48
+ signal: controller.signal,
49
+ }).then(async (res) => {
50
+ if (!res.ok) {
51
+ onError?.(new Error(`Stream failed: ${res.status}`));
52
+ return;
53
+ }
54
+ if (!res.body) {
55
+ onError?.(new Error("No response body"));
56
+ return;
57
+ }
58
+ const reader = res.body.getReader();
59
+ const decoder = new TextDecoder();
60
+ let buffer = "";
61
+ while (true) {
62
+ const { done, value } = await reader.read();
63
+ if (done)
64
+ break;
65
+ buffer += decoder.decode(value, { stream: true });
66
+ const lines = buffer.split("\n");
67
+ buffer = lines.pop() ?? "";
68
+ for (const line of lines) {
69
+ if (line.startsWith("data: ")) {
70
+ try {
71
+ onMessage(JSON.parse(line.slice(6)));
72
+ }
73
+ catch (err) {
74
+ onError?.(err instanceof Error ? err : new Error(String(err)));
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }).catch((err) => {
80
+ if (err?.name !== "AbortError") {
81
+ onError?.(err instanceof Error ? err : new Error(String(err)));
82
+ }
83
+ });
84
+ return () => controller.abort();
85
+ },
86
+ };
87
+ }
88
+ //# sourceMappingURL=fetchTransport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetchTransport.js","sourceRoot":"","sources":["../src/fetchTransport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { SyncRecord, SyncPatch, SyncToken, SubscriptionFilter } from "@sync-subscribe/core";
2
+ import type { ILocalStore, PersistedSubscription } from "./types.js";
3
+ /**
4
+ * IndexedDB-backed local store. Records survive page reloads.
5
+ *
6
+ * Pass a unique `dbName` per logical collection (or per user if you need
7
+ * data isolation on the same origin).
8
+ *
9
+ * Usage:
10
+ * const store = new IdbLocalStore<NoteRecord>("notes-db");
11
+ * const client = new SyncClient(transport, store);
12
+ */
13
+ export declare class IdbLocalStore<T extends SyncRecord> implements ILocalStore<T> {
14
+ private readonly dbName;
15
+ private readonly storeName;
16
+ private db;
17
+ constructor(dbName: string, storeName?: string);
18
+ private getDb;
19
+ /**
20
+ * Apply a batch of patches inside a single readwrite transaction.
21
+ * Copies `record.updatedAt` into `record.serverUpdatedAt` on upsert (server clock is authoritative).
22
+ * Conflict resolution mirrors InMemoryStore: server patch wins only when its
23
+ * revisionCount is higher (or equal with an older updatedAt).
24
+ */
25
+ applyPatches(patches: SyncPatch<T>[]): Promise<SyncPatch<T>[]>;
26
+ write(record: T): Promise<void>;
27
+ getAll(): Promise<T[]>;
28
+ query(filter: SubscriptionFilter): Promise<T[]>;
29
+ count(filter: SubscriptionFilter): Promise<number>;
30
+ getById(recordId: string): Promise<T | undefined>;
31
+ clear(): Promise<void>;
32
+ delete(filter: SubscriptionFilter): Promise<void>;
33
+ evict(evictFilter: SubscriptionFilter): Promise<void>;
34
+ reconstructSyncToken(filter: SubscriptionFilter<T>): Promise<SyncToken>;
35
+ setServerUpdatedAt(recordId: string, serverUpdatedAt: number): Promise<void>;
36
+ setSyncToken(subscriptionId: string, token: SyncToken): Promise<void>;
37
+ getSyncToken(subscriptionId: string): Promise<SyncToken | undefined>;
38
+ setSubscription(name: string, sub: PersistedSubscription): Promise<void>;
39
+ getSubscription(name: string): Promise<PersistedSubscription | undefined>;
40
+ getSubscriptionById(id: string): Promise<PersistedSubscription | undefined>;
41
+ listSubscriptions(): Promise<PersistedSubscription[]>;
42
+ removeSubscription(name: string): Promise<void>;
43
+ clearSubscriptions(): Promise<void>;
44
+ }
45
+ //# sourceMappingURL=idbLocalStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idbLocalStore.d.ts","sourceRoot":"","sources":["../src/idbLocalStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,SAAS,EACT,SAAS,EACT,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAErE;;;;;;;;;GASG;AACH,qBAAa,aAAa,CAAC,CAAC,SAAS,UAAU,CAAE,YAAW,WAAW,CAAC,CAAC,CAAC;IAItE,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAJ5B,OAAO,CAAC,EAAE,CAA4B;gBAGnB,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,MAAkB;IAGhD,OAAO,CAAC,KAAK;IAyBb;;;;;OAKG;IACG,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IAgD9D,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/B,MAAM,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;IAWtB,KAAK,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAO/C,KAAK,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC;IAIlD,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAWjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAWtB,MAAM,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BjD,KAAK,CAAC,WAAW,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrD,oBAAoB,CACxB,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC5B,OAAO,CAAC,SAAS,CAAC;IA8Bf,kBAAkB,CACtB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC;IAOV,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAUpE,eAAe,CACnB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,qBAAqB,GACzB,OAAO,CAAC,IAAI,CAAC;IAgBV,eAAe,CACnB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC;IAWvC,mBAAmB,CACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,qBAAqB,GAAG,SAAS,CAAC;IAKvC,iBAAiB,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAuBrD,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;CAS1C"}