@absolutejs/sync 1.7.9 → 1.8.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/README.md CHANGED
@@ -217,6 +217,30 @@ await orders.mutate({
217
217
  });
218
218
  ```
219
219
 
220
+ TanStack DB can own the client-side collection graph while Absolute Sync handles
221
+ the live transport:
222
+
223
+ ```ts
224
+ import { createCollection } from '@tanstack/db';
225
+ import { createSyncTanStackCollectionOptions } from '@absolutejs/sync/tanstack-db';
226
+
227
+ type Order = { id: string; total: number; status: string };
228
+
229
+ const orders = createCollection(
230
+ createSyncTanStackCollectionOptions<Order>({
231
+ id: 'orders',
232
+ url: 'ws://localhost:3000/sync/ws',
233
+ collection: 'orders',
234
+ getKey: (order) => order.id,
235
+ mutations: {
236
+ insert: 'createOrder',
237
+ update: 'updateOrder',
238
+ delete: 'deleteOrder'
239
+ }
240
+ })
241
+ );
242
+ ```
243
+
220
244
  - **Incremental vs refetch.** A single-table filtered collection is matched
221
245
  incrementally (only the changed rows move). Joins/aggregations and filters the
222
246
  matcher can't evaluate fall back to a correct re-hydrate. `createAggregate`
@@ -0,0 +1,40 @@
1
+ import type { CollectionConfig, PendingMutation } from '@tanstack/db';
2
+ import { type CollectionCache, type MutationStorage, type SyncCollection } from '../../client/syncCollection';
3
+ import type { RowKey } from '../../engine/types';
4
+ export type TanStackRowKey = Extract<RowKey, string | number>;
5
+ export type TanStackMutationCall = {
6
+ name: string;
7
+ args?: unknown;
8
+ };
9
+ export type TanStackMutationMapper<T extends object, TOperation extends 'insert' | 'update' | 'delete'> = string | ((mutation: PendingMutation<T, TOperation>) => TanStackMutationCall | undefined);
10
+ export type SyncTanStackMutations<T extends object> = {
11
+ insert?: TanStackMutationMapper<T, 'insert'>;
12
+ update?: TanStackMutationMapper<T, 'update'>;
13
+ delete?: TanStackMutationMapper<T, 'delete'>;
14
+ };
15
+ export type SyncTanStackCollectionOptions<T extends object, TKey extends TanStackRowKey = TanStackRowKey> = Omit<CollectionConfig<T, TKey>, 'sync' | 'getKey' | 'onInsert' | 'onUpdate' | 'onDelete'> & {
16
+ /** WebSocket URL of the Absolute Sync endpoint. */
17
+ url: string;
18
+ /** Registered Absolute Sync collection name. */
19
+ collection: string;
20
+ /** Query params forwarded to the server collection hydrate/match/authorize hooks. */
21
+ params?: unknown;
22
+ /** Row identity shared by TanStack DB and Absolute Sync. */
23
+ getKey: (row: T) => TKey;
24
+ webSocketImpl?: typeof WebSocket;
25
+ reconnectMs?: number;
26
+ maxReconnectMs?: number;
27
+ storage?: MutationStorage;
28
+ cache?: CollectionCache<T>;
29
+ onError?: (error: unknown) => void;
30
+ /**
31
+ * Optional mapping from TanStack mutations to registered Absolute Sync
32
+ * mutation names. TanStack already applies optimistic writes, so forwarded
33
+ * sync mutations intentionally do not add another optimistic overlay.
34
+ */
35
+ mutations?: SyncTanStackMutations<T>;
36
+ /** Optional prebuilt Absolute Sync collection, useful when sharing lifecycle externally. */
37
+ syncCollection?: SyncCollection<T>;
38
+ };
39
+ export declare const createSyncTanStackCollectionOptions: <T extends object, TKey extends TanStackRowKey = TanStackRowKey>(options: SyncTanStackCollectionOptions<T, TKey>) => CollectionConfig<T, TKey>;
40
+ export { createSyncTanStackCollectionOptions as syncTanStackCollectionOptions };
@@ -0,0 +1,523 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __name = (target, name) => {
5
+ Object.defineProperty(target, "name", {
6
+ value: name,
7
+ enumerable: false,
8
+ configurable: true
9
+ });
10
+ return target;
11
+ };
12
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
13
+ var __typeError = (msg) => {
14
+ throw TypeError(msg);
15
+ };
16
+ var __defNormalProp = (obj, key, value) => (key in obj) ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
17
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
18
+ var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
19
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
20
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
21
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
22
+ var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
23
+ var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
24
+ var __expectFn = (fn) => fn !== undefined && typeof fn !== "function" ? __typeError("Function expected") : fn;
25
+ var __decoratorContext = (kind, name, done, metadata, fns) => ({
26
+ kind: __decoratorStrings[kind],
27
+ name,
28
+ metadata,
29
+ addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null))
30
+ });
31
+ var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
32
+ var __runInitializers = (array, flags, self, value) => {
33
+ for (var i = 0, fns = array[flags >> 1], n = fns && fns.length;i < n; i++)
34
+ flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
35
+ return value;
36
+ };
37
+ var __decorateElement = (array, flags, name, decorators, target, extra) => {
38
+ var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
39
+ var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
40
+ var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
41
+ var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : {
42
+ get [name]() {
43
+ return __privateGet(this, extra);
44
+ },
45
+ set [name](x) {
46
+ __privateSet(this, extra, x);
47
+ }
48
+ }, name));
49
+ k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
50
+ for (var i = decorators.length - 1;i >= 0; i--) {
51
+ ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
52
+ if (k) {
53
+ ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => (name in x) };
54
+ if (k ^ 3)
55
+ access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
56
+ if (k > 2)
57
+ access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
58
+ }
59
+ it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? undefined : { get: desc.get, set: desc.set } : target, ctx);
60
+ done._ = 1;
61
+ if (k ^ 4 || it === undefined)
62
+ __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
63
+ else if (typeof it !== "object" || it === null)
64
+ __typeError("Object expected");
65
+ else
66
+ __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
67
+ }
68
+ return k || __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
69
+ };
70
+
71
+ // src/client/syncCollection.ts
72
+ var localStorageMutationStorage = (key) => ({
73
+ load: () => {
74
+ const raw = globalThis.localStorage?.getItem(key);
75
+ return raw ? JSON.parse(raw) : [];
76
+ },
77
+ save: (records) => {
78
+ globalThis.localStorage?.setItem(key, JSON.stringify(records));
79
+ }
80
+ });
81
+ var localStorageCollectionCache = (key) => ({
82
+ load: () => {
83
+ const raw = globalThis.localStorage?.getItem(key);
84
+ return raw ? JSON.parse(raw) : undefined;
85
+ },
86
+ save: (snapshot) => {
87
+ globalThis.localStorage?.setItem(key, JSON.stringify(snapshot));
88
+ },
89
+ clear: () => {
90
+ globalThis.localStorage?.removeItem(key);
91
+ }
92
+ });
93
+ var openIndexedDb = (databaseName, storeName) => new Promise((resolve, reject) => {
94
+ const request = globalThis.indexedDB.open(databaseName, 1);
95
+ request.onupgradeneeded = () => {
96
+ request.result.createObjectStore(storeName);
97
+ };
98
+ request.onsuccess = () => resolve(request.result);
99
+ request.onerror = () => reject(request.error);
100
+ });
101
+ var indexedDbCollectionCache = ({
102
+ key,
103
+ databaseName = "absolutejs-sync",
104
+ storeName = "collections"
105
+ }) => {
106
+ let handle;
107
+ const database = () => {
108
+ handle ??= openIndexedDb(databaseName, storeName);
109
+ return handle;
110
+ };
111
+ const withStore = async (mode, run) => {
112
+ if (globalThis.indexedDB === undefined) {
113
+ return;
114
+ }
115
+ const db = await database();
116
+ return new Promise((resolve, reject) => {
117
+ const request = run(db.transaction(storeName, mode).objectStore(storeName));
118
+ request.onsuccess = () => resolve(request.result);
119
+ request.onerror = () => reject(request.error);
120
+ });
121
+ };
122
+ return {
123
+ load: () => withStore("readonly", (store) => store.get(key)),
124
+ save: async (snapshot) => {
125
+ await withStore("readwrite", (store) => store.put(snapshot, key));
126
+ },
127
+ clear: async () => {
128
+ await withStore("readwrite", (store) => store.delete(key));
129
+ }
130
+ };
131
+ };
132
+ var SUBSCRIPTION_ID = "s";
133
+ var createSyncCollection = (options) => {
134
+ const key = options.key ?? ((row) => row.id);
135
+ const reconnectMs = options.reconnectMs ?? 500;
136
+ const maxReconnectMs = options.maxReconnectMs ?? 1e4;
137
+ const Impl = options.webSocketImpl ?? globalThis.WebSocket;
138
+ if (!Impl) {
139
+ throw new Error("createSyncCollection requires WebSocket. Run in a browser or pass webSocketImpl.");
140
+ }
141
+ const confirmed = new Map;
142
+ const pending = [];
143
+ let mutationSeq = 0;
144
+ let state = {
145
+ data: [],
146
+ status: "connecting",
147
+ error: undefined
148
+ };
149
+ const listeners = new Set;
150
+ const setState = (patch) => {
151
+ state = { ...state, ...patch };
152
+ for (const listener of listeners) {
153
+ listener(state);
154
+ }
155
+ };
156
+ const recompute = (patch = {}) => {
157
+ const working = new Map(confirmed);
158
+ const draft = {
159
+ set: (row) => working.set(key(row), row),
160
+ delete: (rowKey) => working.delete(rowKey)
161
+ };
162
+ for (const mutation of pending) {
163
+ mutation.optimistic?.(draft);
164
+ }
165
+ setState({ ...patch, data: [...working.values()] });
166
+ };
167
+ let socket;
168
+ let connected = false;
169
+ let closed = false;
170
+ let attempt = 0;
171
+ let reconnectTimer;
172
+ let appliedVersion = 0;
173
+ const persist = () => {
174
+ options.storage?.save(pending.map((mutation) => ({
175
+ mutationId: mutation.mutationId,
176
+ name: mutation.name,
177
+ args: mutation.args
178
+ })));
179
+ };
180
+ let cacheScheduled = false;
181
+ const persistCache = () => {
182
+ if (options.cache === undefined || cacheScheduled) {
183
+ return;
184
+ }
185
+ cacheScheduled = true;
186
+ queueMicrotask(() => {
187
+ cacheScheduled = false;
188
+ options.cache?.save({
189
+ rows: [...confirmed.values()],
190
+ version: appliedVersion
191
+ });
192
+ });
193
+ };
194
+ const settlePending = (mutationId) => {
195
+ const index = pending.findIndex((mutation2) => mutation2.mutationId === mutationId);
196
+ if (index === -1) {
197
+ return;
198
+ }
199
+ const [mutation] = pending.splice(index, 1);
200
+ persist();
201
+ return mutation;
202
+ };
203
+ const applyFrame = (frame) => {
204
+ if (frame.type === "snapshot") {
205
+ confirmed.clear();
206
+ for (const row of frame.rows) {
207
+ confirmed.set(key(row), row);
208
+ }
209
+ if (frame.version !== undefined) {
210
+ appliedVersion = frame.version;
211
+ }
212
+ persistCache();
213
+ recompute({ status: "ready", error: undefined });
214
+ } else if (frame.type === "diff") {
215
+ for (const row of frame.removed) {
216
+ confirmed.delete(key(row));
217
+ }
218
+ for (const row of frame.added) {
219
+ confirmed.set(key(row), row);
220
+ }
221
+ for (const row of frame.changed) {
222
+ confirmed.set(key(row), row);
223
+ }
224
+ if (frame.version !== undefined) {
225
+ appliedVersion = Math.max(appliedVersion, frame.version);
226
+ }
227
+ persistCache();
228
+ recompute({ status: "ready", error: undefined });
229
+ } else if (frame.type === "error") {
230
+ setState({ error: frame.message });
231
+ options.onError?.(frame.message);
232
+ } else if (frame.type === "ack") {
233
+ const mutation = settlePending(frame.mutationId);
234
+ if (mutation !== undefined) {
235
+ recompute();
236
+ mutation.resolve(frame.result);
237
+ }
238
+ } else if (frame.type === "reject") {
239
+ const mutation = settlePending(frame.mutationId);
240
+ if (mutation !== undefined) {
241
+ recompute();
242
+ mutation.reject(new Error(String(frame.message)));
243
+ }
244
+ }
245
+ };
246
+ const sendMutate = (mutation) => {
247
+ if (connected) {
248
+ socket?.send(JSON.stringify({
249
+ type: "mutate",
250
+ mutationId: mutation.mutationId,
251
+ name: mutation.name,
252
+ args: mutation.args
253
+ }));
254
+ }
255
+ };
256
+ const connect = () => {
257
+ if (closed) {
258
+ return;
259
+ }
260
+ setState({ status: "connecting" });
261
+ const ws = new Impl(options.url);
262
+ socket = ws;
263
+ ws.onopen = () => {
264
+ attempt = 0;
265
+ connected = true;
266
+ ws.send(JSON.stringify({
267
+ type: "subscribe",
268
+ id: SUBSCRIPTION_ID,
269
+ collection: options.collection,
270
+ params: options.params,
271
+ since: appliedVersion > 0 ? appliedVersion : undefined
272
+ }));
273
+ for (const mutation of pending) {
274
+ sendMutate(mutation);
275
+ }
276
+ };
277
+ ws.onmessage = (event) => {
278
+ try {
279
+ applyFrame(JSON.parse(event.data));
280
+ } catch {}
281
+ };
282
+ ws.onclose = () => {
283
+ connected = false;
284
+ if (closed || reconnectMs <= 0) {
285
+ return;
286
+ }
287
+ const delay = Math.min(reconnectMs * 2 ** attempt, maxReconnectMs);
288
+ attempt += 1;
289
+ reconnectTimer = setTimeout(connect, delay);
290
+ };
291
+ };
292
+ const hydratePersisted = async () => {
293
+ if (options.storage === undefined) {
294
+ return;
295
+ }
296
+ const records = await options.storage.load();
297
+ for (const record of records) {
298
+ if (pending.some((m) => m.mutationId === record.mutationId)) {
299
+ continue;
300
+ }
301
+ pending.push({
302
+ mutationId: record.mutationId,
303
+ name: record.name,
304
+ args: record.args,
305
+ resolve: () => {},
306
+ reject: () => {}
307
+ });
308
+ mutationSeq = Math.max(mutationSeq, record.mutationId);
309
+ }
310
+ if (connected) {
311
+ for (const mutation of pending) {
312
+ sendMutate(mutation);
313
+ }
314
+ }
315
+ };
316
+ const hydrateCache = async () => {
317
+ if (options.cache === undefined) {
318
+ return;
319
+ }
320
+ let snapshot;
321
+ try {
322
+ snapshot = await options.cache.load();
323
+ } catch {
324
+ return;
325
+ }
326
+ if (snapshot === undefined || appliedVersion > 0) {
327
+ return;
328
+ }
329
+ for (const row of snapshot.rows) {
330
+ confirmed.set(key(row), row);
331
+ }
332
+ appliedVersion = snapshot.version;
333
+ recompute();
334
+ };
335
+ if (options.cache === undefined) {
336
+ connect();
337
+ hydratePersisted();
338
+ } else {
339
+ (async () => {
340
+ await hydrateCache();
341
+ await hydratePersisted();
342
+ connect();
343
+ })();
344
+ }
345
+ return {
346
+ get: () => state,
347
+ subscribe: (listener) => {
348
+ listeners.add(listener);
349
+ return () => {
350
+ listeners.delete(listener);
351
+ };
352
+ },
353
+ mutate: (mutateOptions) => new Promise((resolve, reject) => {
354
+ const mutation = {
355
+ mutationId: mutationSeq += 1,
356
+ name: mutateOptions.name,
357
+ args: mutateOptions.args,
358
+ optimistic: mutateOptions.optimistic,
359
+ resolve: (result) => resolve(result),
360
+ reject
361
+ };
362
+ pending.push(mutation);
363
+ persist();
364
+ recompute();
365
+ sendMutate(mutation);
366
+ }),
367
+ disconnect: () => {
368
+ if (closed || socket === undefined) {
369
+ return;
370
+ }
371
+ try {
372
+ socket.close();
373
+ } catch {}
374
+ },
375
+ close: () => {
376
+ if (closed) {
377
+ return;
378
+ }
379
+ closed = true;
380
+ connected = false;
381
+ if (reconnectTimer !== undefined) {
382
+ clearTimeout(reconnectTimer);
383
+ }
384
+ try {
385
+ socket?.send(JSON.stringify({ type: "unsubscribe", id: SUBSCRIPTION_ID }));
386
+ socket?.close();
387
+ } catch {}
388
+ for (const mutation of pending.splice(0)) {
389
+ mutation.reject(new Error("sync collection closed"));
390
+ }
391
+ persist();
392
+ setState({ status: "closed" });
393
+ listeners.clear();
394
+ }
395
+ };
396
+ };
397
+
398
+ // src/adapters/tanstack-db/index.ts
399
+ var toMutationCall = (mapper, mutation) => {
400
+ if (typeof mapper === "function") {
401
+ return mapper(mutation);
402
+ }
403
+ if (mutation.type === "insert") {
404
+ return {
405
+ name: mapper,
406
+ args: { row: mutation.modified, metadata: mutation.metadata }
407
+ };
408
+ }
409
+ if (mutation.type === "update") {
410
+ return {
411
+ name: mapper,
412
+ args: {
413
+ key: mutation.key,
414
+ row: mutation.modified,
415
+ changes: mutation.changes,
416
+ metadata: mutation.metadata
417
+ }
418
+ };
419
+ }
420
+ return {
421
+ name: mapper,
422
+ args: {
423
+ key: mutation.key,
424
+ row: mutation.original,
425
+ metadata: mutation.metadata
426
+ }
427
+ };
428
+ };
429
+ var createMutationHandler = (sync, mapper) => async ({
430
+ transaction
431
+ }) => {
432
+ if (mapper === undefined) {
433
+ return;
434
+ }
435
+ await Promise.all(transaction.mutations.map((mutation) => {
436
+ const call = toMutationCall(mapper, mutation);
437
+ return call === undefined ? Promise.resolve() : sync.mutate({ name: call.name, args: call.args });
438
+ }));
439
+ };
440
+ var createSyncConfig = (sync, getKey) => ({
441
+ rowUpdateMode: "full",
442
+ sync: ({ begin, write, commit, markReady }) => {
443
+ let previous = new Map;
444
+ let markedReady = false;
445
+ const flush = () => {
446
+ const state = sync.get();
447
+ const next = new Map;
448
+ for (const row of state.data) {
449
+ next.set(getKey(row), row);
450
+ }
451
+ begin();
452
+ for (const [key, row] of next) {
453
+ const old = previous.get(key);
454
+ if (old === undefined) {
455
+ write({ type: "insert", value: row });
456
+ } else if (!Object.is(old, row)) {
457
+ write({ type: "update", value: row, previousValue: old });
458
+ }
459
+ }
460
+ for (const key of previous.keys()) {
461
+ if (!next.has(key)) {
462
+ write({ type: "delete", key });
463
+ }
464
+ }
465
+ previous = next;
466
+ commit();
467
+ if (state.status === "ready" && !markedReady) {
468
+ markedReady = true;
469
+ markReady();
470
+ }
471
+ };
472
+ flush();
473
+ const unsubscribe = sync.subscribe(flush);
474
+ return () => {
475
+ unsubscribe();
476
+ sync.close();
477
+ };
478
+ }
479
+ });
480
+ var createSyncTanStackCollectionOptions = (options) => {
481
+ const {
482
+ url,
483
+ collection,
484
+ params,
485
+ getKey,
486
+ webSocketImpl,
487
+ reconnectMs,
488
+ maxReconnectMs,
489
+ storage,
490
+ cache,
491
+ onError,
492
+ mutations,
493
+ syncCollection,
494
+ ...collectionOptions
495
+ } = options;
496
+ const sync = syncCollection ?? createSyncCollection({
497
+ url,
498
+ collection,
499
+ params,
500
+ key: getKey,
501
+ webSocketImpl,
502
+ reconnectMs,
503
+ maxReconnectMs,
504
+ storage,
505
+ cache,
506
+ onError
507
+ });
508
+ return {
509
+ ...collectionOptions,
510
+ getKey,
511
+ sync: createSyncConfig(sync, getKey),
512
+ onInsert: createMutationHandler(sync, mutations?.insert),
513
+ onUpdate: createMutationHandler(sync, mutations?.update),
514
+ onDelete: createMutationHandler(sync, mutations?.delete)
515
+ };
516
+ };
517
+ export {
518
+ createSyncTanStackCollectionOptions as syncTanStackCollectionOptions,
519
+ createSyncTanStackCollectionOptions
520
+ };
521
+
522
+ //# debugId=87BE1F94279A349864756E2164756E21
523
+ //# sourceMappingURL=index.js.map