@nexus_js/sync 0.7.0 → 0.7.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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Nexus Local-First Sync Engine.
3
+ *
4
+ * Inspired by Replicache / ElectricSQL. Uses IndexedDB as the local store.
5
+ *
6
+ * Guarantees:
7
+ * - Writes are immediate (IndexedDB) — zero perceived latency for the user.
8
+ * - A "pending ops" queue is maintained. When online, ops are flushed to the
9
+ * server. When offline, they accumulate and are retried on reconnect.
10
+ * - Conflict resolution is pluggable via onConflict hook.
11
+ * - All pending ops survive page refreshes (persisted in IDB, not memory).
12
+ */
13
+ export type SyncStatus = 'synced' | 'pending' | 'syncing' | 'error' | 'offline';
14
+ export interface SyncOp<T = unknown> {
15
+ id: string;
16
+ store: string;
17
+ type: 'put' | 'delete';
18
+ key: string;
19
+ data?: T;
20
+ ts: number;
21
+ retries: number;
22
+ }
23
+ export interface ConflictInfo<T> {
24
+ local: T;
25
+ remote: T;
26
+ op: SyncOp<T>;
27
+ }
28
+ export interface SyncCollectionOpts<T> {
29
+ /** Server endpoint to flush ops to (POST). Receives `{ ops: SyncOp[] }`. */
30
+ endpoint: string;
31
+ /**
32
+ * Called when the server returns a conflict for an op.
33
+ * Return the version that should win. Default: local wins.
34
+ */
35
+ onConflict?: (info: ConflictInfo<T>) => T | Promise<T>;
36
+ /** Max retries before an op is moved to "dead letter" (logged, dropped). */
37
+ maxRetries?: number;
38
+ }
39
+ export declare class NexusSyncEngine {
40
+ #private;
41
+ private db;
42
+ private flushing;
43
+ private listeners;
44
+ status: SyncStatus;
45
+ init(): Promise<void>;
46
+ private ensureDB;
47
+ /** Read the local (IndexedDB) value for a key in a collection. */
48
+ get<T>(collection: string, key: string): Promise<T | undefined>;
49
+ /** Read all values in a collection. */
50
+ getAll<T>(collection: string): Promise<T[]>;
51
+ /**
52
+ * Write a value locally (instant) and enqueue a server sync op.
53
+ * The UI is updated immediately — the server sync happens in the background.
54
+ */
55
+ put<T>(collection: string, key: string, value: T, endpoint: string): Promise<void>;
56
+ /** Delete locally and enqueue a delete op. */
57
+ delete(collection: string, key: string, endpoint: string): Promise<void>;
58
+ /**
59
+ * Flush pending ops to the server. Ops are sent in timestamp order.
60
+ * On conflict, `onConflict` is called and the local store is updated.
61
+ */
62
+ flush(endpoint?: string, opts?: Partial<SyncCollectionOpts<unknown>>): Promise<void>;
63
+ /** Returns the number of pending (unsynced) ops. */
64
+ pendingCount(): Promise<number>;
65
+ /** Subscribe to status changes (useful for UI indicators). */
66
+ subscribe(cb: () => void): () => void;
67
+ }
68
+ declare global {
69
+ interface Window {
70
+ __NEXUS_DEV__?: boolean;
71
+ }
72
+ }
73
+ /** Singleton engine instance (shared across all $sync calls on a page). */
74
+ export declare const syncEngine: NexusSyncEngine;
75
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAEhF,MAAM,WAAW,MAAM,CAAC,CAAC,GAAG,OAAO;IACjC,EAAE,EAAS,MAAM,CAAC;IAClB,KAAK,EAAM,MAAM,CAAC;IAClB,IAAI,EAAO,KAAK,GAAG,QAAQ,CAAC;IAC5B,GAAG,EAAQ,MAAM,CAAC;IAClB,IAAI,CAAC,EAAM,CAAC,CAAC;IACb,EAAE,EAAS,MAAM,CAAC;IAClB,OAAO,EAAI,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,KAAK,EAAK,CAAC,CAAC;IACZ,MAAM,EAAI,CAAC,CAAC;IACZ,EAAE,EAAQ,MAAM,CAAC,CAAC,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,EAAK,MAAM,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACvD,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA4DD,qBAAa,eAAe;;IAC1B,OAAO,CAAC,EAAE,CAAmC;IAC7C,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,SAAS,CAAiC;IAClD,MAAM,EAAE,UAAU,CAAY;IAExB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B,OAAO,CAAC,QAAQ;IAKhB,kEAAkE;IAC5D,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAIrE,uCAAuC;IACjC,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IASjD;;;OAGG;IACG,GAAG,CAAC,CAAC,EACT,UAAU,EAAE,MAAM,EAClB,GAAG,EAAS,MAAM,EAClB,KAAK,EAAO,CAAC,EACb,QAAQ,EAAI,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAqBhB,8CAA8C;IACxC,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB9E;;;OAGG;IACG,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA2E1F,oDAAoD;IAC9C,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAKrC,8DAA8D;IAC9D,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;CA4BtC;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QAAG,aAAa,CAAC,EAAE,OAAO,CAAC;KAAE;CAC9C;AAED,2EAA2E;AAC3E,eAAO,MAAM,UAAU,iBAAwB,CAAC"}
package/dist/engine.js ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Nexus Local-First Sync Engine.
3
+ *
4
+ * Inspired by Replicache / ElectricSQL. Uses IndexedDB as the local store.
5
+ *
6
+ * Guarantees:
7
+ * - Writes are immediate (IndexedDB) — zero perceived latency for the user.
8
+ * - A "pending ops" queue is maintained. When online, ops are flushed to the
9
+ * server. When offline, they accumulate and are retried on reconnect.
10
+ * - Conflict resolution is pluggable via onConflict hook.
11
+ * - All pending ops survive page refreshes (persisted in IDB, not memory).
12
+ */
13
+ const DB_NAME = 'nexus_sync';
14
+ const DB_VERSION = 1;
15
+ const STORES = { data: 'data', ops: 'pending_ops' };
16
+ // ── IDB helpers ───────────────────────────────────────────────────────────────
17
+ function openDB() {
18
+ return new Promise((resolve, reject) => {
19
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
20
+ req.onupgradeneeded = () => {
21
+ const db = req.result;
22
+ if (!db.objectStoreNames.contains(STORES.data)) {
23
+ db.createObjectStore(STORES.data);
24
+ }
25
+ if (!db.objectStoreNames.contains(STORES.ops)) {
26
+ const ops = db.createObjectStore(STORES.ops, { keyPath: 'id' });
27
+ ops.createIndex('store', 'store');
28
+ }
29
+ };
30
+ req.onsuccess = () => resolve(req.result);
31
+ req.onerror = () => reject(req.error);
32
+ });
33
+ }
34
+ function idbGet(db, storeName, key) {
35
+ return new Promise((resolve, reject) => {
36
+ const tx = db.transaction(storeName, 'readonly');
37
+ const req = tx.objectStore(storeName).get(key);
38
+ req.onsuccess = () => resolve(req.result);
39
+ req.onerror = () => reject(req.error);
40
+ });
41
+ }
42
+ function idbPut(db, storeName, key, value) {
43
+ return new Promise((resolve, reject) => {
44
+ const tx = db.transaction(storeName, 'readwrite');
45
+ const req = tx.objectStore(storeName).put(value, key);
46
+ req.onsuccess = () => resolve();
47
+ req.onerror = () => reject(req.error);
48
+ });
49
+ }
50
+ function idbDelete(db, storeName, key) {
51
+ return new Promise((resolve, reject) => {
52
+ const tx = db.transaction(storeName, 'readwrite');
53
+ const req = tx.objectStore(storeName).delete(key);
54
+ req.onsuccess = () => resolve();
55
+ req.onerror = () => reject(req.error);
56
+ });
57
+ }
58
+ function idbGetAll(db, storeName) {
59
+ return new Promise((resolve, reject) => {
60
+ const tx = db.transaction(storeName, 'readonly');
61
+ const req = tx.objectStore(storeName).getAll();
62
+ req.onsuccess = () => resolve(req.result);
63
+ req.onerror = () => reject(req.error);
64
+ });
65
+ }
66
+ // ── Sync Engine ───────────────────────────────────────────────────────────────
67
+ export class NexusSyncEngine {
68
+ db = null;
69
+ flushing = false;
70
+ listeners = new Set();
71
+ status = 'synced';
72
+ async init() {
73
+ if (this.db)
74
+ return;
75
+ this.db = await openDB();
76
+ this.#startNetworkWatcher();
77
+ // Flush any ops that survived a page refresh
78
+ if (navigator.onLine)
79
+ void this.flush();
80
+ }
81
+ ensureDB() {
82
+ if (!this.db)
83
+ throw new Error('[Nexus Sync] Engine not initialized. Call await sync.init()');
84
+ return this.db;
85
+ }
86
+ /** Read the local (IndexedDB) value for a key in a collection. */
87
+ async get(collection, key) {
88
+ return idbGet(this.ensureDB(), STORES.data, `${collection}:${key}`);
89
+ }
90
+ /** Read all values in a collection. */
91
+ async getAll(collection) {
92
+ const db = this.ensureDB();
93
+ const all = await idbGetAll(db, STORES.data);
94
+ const prefix = `${collection}:`;
95
+ return all
96
+ .filter((r) => r._idbKey?.startsWith(prefix) ?? false)
97
+ .map((r) => r.value);
98
+ }
99
+ /**
100
+ * Write a value locally (instant) and enqueue a server sync op.
101
+ * The UI is updated immediately — the server sync happens in the background.
102
+ */
103
+ async put(collection, key, value, endpoint) {
104
+ const db = this.ensureDB();
105
+ // 1. Persist locally
106
+ await idbPut(db, STORES.data, `${collection}:${key}`, value);
107
+ // 2. Enqueue sync op
108
+ const op = {
109
+ id: crypto.randomUUID(),
110
+ store: collection,
111
+ type: 'put',
112
+ key,
113
+ data: value,
114
+ ts: Date.now(),
115
+ retries: 0,
116
+ };
117
+ await idbPut(db, STORES.ops, op.id, op);
118
+ this.#setStatus('pending');
119
+ this.#notify();
120
+ // 3. Try to flush immediately if online
121
+ if (navigator.onLine)
122
+ void this.flush(endpoint);
123
+ }
124
+ /** Delete locally and enqueue a delete op. */
125
+ async delete(collection, key, endpoint) {
126
+ const db = this.ensureDB();
127
+ await idbDelete(db, STORES.data, `${collection}:${key}`);
128
+ const op = {
129
+ id: crypto.randomUUID(),
130
+ store: collection,
131
+ type: 'delete',
132
+ key,
133
+ ts: Date.now(),
134
+ retries: 0,
135
+ };
136
+ await idbPut(db, STORES.ops, op.id, op);
137
+ this.#setStatus('pending');
138
+ this.#notify();
139
+ if (navigator.onLine)
140
+ void this.flush(endpoint);
141
+ }
142
+ /**
143
+ * Flush pending ops to the server. Ops are sent in timestamp order.
144
+ * On conflict, `onConflict` is called and the local store is updated.
145
+ */
146
+ async flush(endpoint, opts) {
147
+ if (this.flushing || !navigator.onLine)
148
+ return;
149
+ const db = this.ensureDB();
150
+ const ops = (await idbGetAll(db, STORES.ops))
151
+ .sort((a, b) => a.ts - b.ts);
152
+ if (ops.length === 0) {
153
+ this.#setStatus('synced');
154
+ return;
155
+ }
156
+ this.flushing = true;
157
+ this.#setStatus('syncing');
158
+ const maxRetries = opts?.maxRetries ?? 5;
159
+ const url = endpoint ?? ops[0]?.store ?? '';
160
+ try {
161
+ const res = await fetch(url, {
162
+ method: 'POST',
163
+ headers: { 'content-type': 'application/json', 'x-nexus-sync': '1' },
164
+ body: JSON.stringify({ ops }),
165
+ });
166
+ if (!res.ok)
167
+ throw new Error(`Server returned ${res.status}`);
168
+ const body = (await res.json());
169
+ // Acknowledge successful ops
170
+ for (const id of body.acked ?? []) {
171
+ await idbDelete(db, STORES.ops, id);
172
+ }
173
+ // Handle conflicts
174
+ for (const conflict of body.conflicts ?? []) {
175
+ const op = ops.find((o) => o.id === conflict.opId);
176
+ if (!op)
177
+ continue;
178
+ if (opts?.onConflict) {
179
+ const localValue = await this.get(op.store, op.key);
180
+ const winner = await opts.onConflict({
181
+ local: localValue,
182
+ remote: conflict.serverValue,
183
+ op,
184
+ });
185
+ await idbPut(db, STORES.data, `${op.store}:${op.key}`, winner);
186
+ }
187
+ else {
188
+ // Default: local wins — re-enqueue with fresh timestamp
189
+ const updated = { ...op, ts: Date.now() };
190
+ await idbPut(db, STORES.ops, updated.id, updated);
191
+ }
192
+ }
193
+ this.#setStatus('synced');
194
+ }
195
+ catch {
196
+ // Increment retries on all remaining ops
197
+ for (const op of ops) {
198
+ const updated = { ...op, retries: op.retries + 1 };
199
+ if (updated.retries >= maxRetries) {
200
+ // Dead letter — drop and warn
201
+ console.warn(`[Nexus Sync] Op ${op.id} exceeded max retries, dropping.`, op);
202
+ await idbDelete(db, STORES.ops, op.id);
203
+ }
204
+ else {
205
+ await idbPut(db, STORES.ops, op.id, updated);
206
+ }
207
+ }
208
+ this.#setStatus(navigator.onLine ? 'error' : 'offline');
209
+ }
210
+ finally {
211
+ this.flushing = false;
212
+ this.#notify();
213
+ }
214
+ }
215
+ /** Returns the number of pending (unsynced) ops. */
216
+ async pendingCount() {
217
+ const ops = await idbGetAll(this.ensureDB(), STORES.ops);
218
+ return ops.length;
219
+ }
220
+ /** Subscribe to status changes (useful for UI indicators). */
221
+ subscribe(cb) {
222
+ this.listeners.add(cb);
223
+ return () => this.listeners.delete(cb);
224
+ }
225
+ #setStatus(s) {
226
+ this.status = s;
227
+ }
228
+ #notify() {
229
+ for (const cb of this.listeners)
230
+ cb();
231
+ }
232
+ #startNetworkWatcher() {
233
+ window.addEventListener('online', () => {
234
+ if (window.__NEXUS_DEV__) {
235
+ console.log('%c[Nexus Sync]%c 🟢 Back online — flushing pending ops...', 'color:#818cf8;font-weight:bold', 'color:#a3e635');
236
+ }
237
+ void this.flush();
238
+ });
239
+ window.addEventListener('offline', () => {
240
+ this.#setStatus('offline');
241
+ this.#notify();
242
+ if (window.__NEXUS_DEV__) {
243
+ console.log('%c[Nexus Sync]%c 🔴 Offline — writes queued in IndexedDB', 'color:#818cf8;font-weight:bold', 'color:#f87171');
244
+ }
245
+ });
246
+ }
247
+ }
248
+ /** Singleton engine instance (shared across all $sync calls on a page). */
249
+ export const syncEngine = new NexusSyncEngine();
250
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -4,22 +4,12 @@
4
4
  * Provides offline-first data persistence and background synchronization
5
5
  * using IndexedDB as the local store and Server Actions as the sync target.
6
6
  */
7
-
8
7
  export { syncEngine, NexusSyncEngine } from './engine.js';
9
8
  export type { SyncOp, SyncStatus, SyncCollectionOpts, ConflictInfo } from './engine.js';
10
-
11
9
  export { $localSync } from './rune.js';
12
10
  export type { LocalSyncState, LocalSyncOpts } from './rune.js';
13
-
14
11
  /** Convenience: check if the browser is currently online. */
15
- export const isOnline = (): boolean =>
16
- typeof navigator !== 'undefined' ? navigator.onLine : true;
17
-
12
+ export declare const isOnline: () => boolean;
18
13
  /** Returns a promise that resolves when the browser comes back online. */
19
- export function waitForOnline(): Promise<void> {
20
- if (isOnline()) return Promise.resolve();
21
- return new Promise<void>((resolve) => {
22
- const handler = () => { window.removeEventListener('online', handler); resolve(); };
23
- window.addEventListener('online', handler);
24
- });
25
- }
14
+ export declare function waitForOnline(): Promise<void>;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAExF,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/D,6DAA6D;AAC7D,eAAO,MAAM,QAAQ,QAAO,OACgC,CAAC;AAE7D,0EAA0E;AAC1E,wBAAgB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAM7C"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @nexus_js/sync — Local-First Sync Engine
3
+ *
4
+ * Provides offline-first data persistence and background synchronization
5
+ * using IndexedDB as the local store and Server Actions as the sync target.
6
+ */
7
+ export { syncEngine, NexusSyncEngine } from './engine.js';
8
+ export { $localSync } from './rune.js';
9
+ /** Convenience: check if the browser is currently online. */
10
+ export const isOnline = () => typeof navigator !== 'undefined' ? navigator.onLine : true;
11
+ /** Returns a promise that resolves when the browser comes back online. */
12
+ export function waitForOnline() {
13
+ if (isOnline())
14
+ return Promise.resolve();
15
+ return new Promise((resolve) => {
16
+ const handler = () => { window.removeEventListener('online', handler); resolve(); };
17
+ window.addEventListener('online', handler);
18
+ });
19
+ }
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG1D,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC,6DAA6D;AAC7D,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAY,EAAE,CACpC,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAE7D,0EAA0E;AAC1E,MAAM,UAAU,aAAa;IAC3B,IAAI,QAAQ,EAAE;QAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IACzC,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACnC,MAAM,OAAO,GAAG,GAAG,EAAE,GAAG,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACpF,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC"}
package/dist/rune.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * $localSync — the Local-First reactive rune.
3
+ *
4
+ * Usage (inside a .nx island):
5
+ *
6
+ * ```ts
7
+ * import { $localSync } from '@nexus_js/sync';
8
+ *
9
+ * const captures = $localSync<string[]>('my-captures', {
10
+ * default: [],
11
+ * endpoint: '/_nexus/action/sync-captures',
12
+ * onConflict: ({ local, remote }) => [...new Set([...local, ...remote])],
13
+ * });
14
+ *
15
+ * // Read — always reflects local IndexedDB state
16
+ * console.log(captures.value);
17
+ *
18
+ * // Mutate — instant UI update, background server sync
19
+ * await captures.add('pikachu');
20
+ * await captures.remove('pikachu');
21
+ * ```
22
+ *
23
+ * The framework automatically:
24
+ * - Persists every mutation to IndexedDB (works offline).
25
+ * - Syncs to the server when online.
26
+ * - Retries failed syncs on reconnect.
27
+ * - Calls onConflict if the server disagrees.
28
+ */
29
+ import { type SyncCollectionOpts, type SyncStatus } from './engine.js';
30
+ export interface LocalSyncState<T> {
31
+ /** Current value (IndexedDB, reflects all local mutations immediately). */
32
+ readonly value: T;
33
+ /** Current sync status. */
34
+ readonly status: SyncStatus;
35
+ /** Pending op count. */
36
+ readonly pending: number;
37
+ /** Overwrite entire value and enqueue a sync op. */
38
+ set(next: T): Promise<void>;
39
+ /**
40
+ * Array helpers — only valid when T extends unknown[].
41
+ * push/remove mutate the local array atomically.
42
+ */
43
+ push(item: T extends (infer I)[] ? I : never): Promise<void>;
44
+ remove(predicate: T extends (infer I)[] ? (item: I) => boolean : never): Promise<void>;
45
+ /** Force an immediate flush attempt. */
46
+ flush(): Promise<void>;
47
+ /** Subscribe to value/status changes (returns unsubscribe fn). */
48
+ subscribe(cb: (state: LocalSyncState<T>) => void): () => void;
49
+ }
50
+ export interface LocalSyncOpts<T> extends Partial<SyncCollectionOpts<T>> {
51
+ /** Default value used before IndexedDB is ready. */
52
+ default: T;
53
+ /** Server endpoint to sync ops to. */
54
+ endpoint: string;
55
+ /**
56
+ * Unique key for this value within the collection.
57
+ * Defaults to 'default'. Useful to scope per-user data.
58
+ */
59
+ key?: string;
60
+ }
61
+ /**
62
+ * Creates a Local-First reactive state container.
63
+ * The returned object is reactive — UI should re-render on .value access
64
+ * (integration with Svelte 5 Runes happens at the compiler layer).
65
+ */
66
+ export declare function $localSync<T>(collection: string, opts: LocalSyncOpts<T>): LocalSyncState<T>;
67
+ //# sourceMappingURL=rune.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rune.d.ts","sourceRoot":"","sources":["../src/rune.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAc,KAAK,kBAAkB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AASnF,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,2BAA2B;IAC3B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,wBAAwB;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,MAAM,CAAC,SAAS,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,wCAAwC;IACxC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,kEAAkE;IAClE,SAAS,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC/D;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,CAAE,SAAQ,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;IACtE,oDAAoD;IACpD,OAAO,EAAE,CAAC,CAAC;IACX,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAC1B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAQ,aAAa,CAAC,CAAC,CAAC,GAC3B,cAAc,CAAC,CAAC,CAAC,CA4DnB"}
package/dist/rune.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * $localSync — the Local-First reactive rune.
3
+ *
4
+ * Usage (inside a .nx island):
5
+ *
6
+ * ```ts
7
+ * import { $localSync } from '@nexus_js/sync';
8
+ *
9
+ * const captures = $localSync<string[]>('my-captures', {
10
+ * default: [],
11
+ * endpoint: '/_nexus/action/sync-captures',
12
+ * onConflict: ({ local, remote }) => [...new Set([...local, ...remote])],
13
+ * });
14
+ *
15
+ * // Read — always reflects local IndexedDB state
16
+ * console.log(captures.value);
17
+ *
18
+ * // Mutate — instant UI update, background server sync
19
+ * await captures.add('pikachu');
20
+ * await captures.remove('pikachu');
21
+ * ```
22
+ *
23
+ * The framework automatically:
24
+ * - Persists every mutation to IndexedDB (works offline).
25
+ * - Syncs to the server when online.
26
+ * - Retries failed syncs on reconnect.
27
+ * - Calls onConflict if the server disagrees.
28
+ */
29
+ import { syncEngine } from './engine.js';
30
+ let engineReady = null;
31
+ function ensureEngine() {
32
+ if (!engineReady)
33
+ engineReady = syncEngine.init();
34
+ return engineReady;
35
+ }
36
+ /**
37
+ * Creates a Local-First reactive state container.
38
+ * The returned object is reactive — UI should re-render on .value access
39
+ * (integration with Svelte 5 Runes happens at the compiler layer).
40
+ */
41
+ export function $localSync(collection, opts) {
42
+ const storeKey = opts.key ?? 'default';
43
+ let current = opts.default;
44
+ let status = 'synced';
45
+ let pending = 0;
46
+ const subs = new Set();
47
+ function notify() {
48
+ for (const cb of subs)
49
+ cb(state);
50
+ }
51
+ // Load initial value from IndexedDB
52
+ ensureEngine().then(async () => {
53
+ const stored = await syncEngine.get(collection, storeKey);
54
+ if (stored !== undefined) {
55
+ current = stored;
56
+ notify();
57
+ }
58
+ // Track engine status
59
+ syncEngine.subscribe(async () => {
60
+ status = syncEngine.status;
61
+ pending = await syncEngine.pendingCount();
62
+ notify();
63
+ });
64
+ });
65
+ const state = {
66
+ get value() { return current; },
67
+ get status() { return status; },
68
+ get pending() { return pending; },
69
+ async set(next) {
70
+ current = next;
71
+ notify();
72
+ await ensureEngine();
73
+ await syncEngine.put(collection, storeKey, next, opts.endpoint);
74
+ },
75
+ async push(item) {
76
+ if (!Array.isArray(current))
77
+ throw new Error('[Nexus Sync] .push() requires an array value');
78
+ await state.set([...current, item]);
79
+ },
80
+ async remove(predicate) {
81
+ if (!Array.isArray(current))
82
+ throw new Error('[Nexus Sync] .remove() requires an array value');
83
+ await state.set(current.filter((i) => !predicate(i)));
84
+ },
85
+ async flush() {
86
+ await ensureEngine();
87
+ await syncEngine.flush(opts.endpoint, opts);
88
+ },
89
+ subscribe(cb) {
90
+ subs.add(cb);
91
+ return () => subs.delete(cb);
92
+ },
93
+ };
94
+ return state;
95
+ }
96
+ //# sourceMappingURL=rune.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rune.js","sourceRoot":"","sources":["../src/rune.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@nexus_js/sync",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Nexus Local-First Sync Engine — IndexedDB-backed optimistic mutations with conflict resolution",
5
5
  "type": "module",
6
- "main": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
7
8
  "exports": {
8
- ".": "./src/index.ts"
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
9
13
  },
10
14
  "keywords": [
11
15
  "nexus",
@@ -31,12 +35,21 @@
31
35
  "bugs": {
32
36
  "url": "https://github.com/bierfor/nexus/issues"
33
37
  },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "typescript": "^5.5.0"
41
+ },
34
42
  "files": [
35
- "src",
43
+ "dist",
36
44
  "README.md"
37
45
  ],
38
46
  "publishConfig": {
39
47
  "access": "public",
40
48
  "registry": "https://registry.npmjs.org/"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.json",
52
+ "dev": "tsc -p tsconfig.json --watch",
53
+ "clean": "rm -rf dist"
41
54
  }
42
55
  }
package/src/engine.ts DELETED
@@ -1,309 +0,0 @@
1
- /**
2
- * Nexus Local-First Sync Engine.
3
- *
4
- * Inspired by Replicache / ElectricSQL. Uses IndexedDB as the local store.
5
- *
6
- * Guarantees:
7
- * - Writes are immediate (IndexedDB) — zero perceived latency for the user.
8
- * - A "pending ops" queue is maintained. When online, ops are flushed to the
9
- * server. When offline, they accumulate and are retried on reconnect.
10
- * - Conflict resolution is pluggable via onConflict hook.
11
- * - All pending ops survive page refreshes (persisted in IDB, not memory).
12
- */
13
-
14
- const DB_NAME = 'nexus_sync';
15
- const DB_VERSION = 1;
16
- const STORES = { data: 'data', ops: 'pending_ops' } as const;
17
-
18
- export type SyncStatus = 'synced' | 'pending' | 'syncing' | 'error' | 'offline';
19
-
20
- export interface SyncOp<T = unknown> {
21
- id: string; // uuid for idempotency
22
- store: string; // logical collection name
23
- type: 'put' | 'delete'; // operation type
24
- key: string; // record key within collection
25
- data?: T; // payload (undefined for deletes)
26
- ts: number; // client timestamp (for ordering)
27
- retries: number; // how many sync attempts have failed
28
- }
29
-
30
- export interface ConflictInfo<T> {
31
- local: T; // what the client has
32
- remote: T; // what the server returned
33
- op: SyncOp<T>; // the pending op that caused the conflict
34
- }
35
-
36
- export interface SyncCollectionOpts<T> {
37
- /** Server endpoint to flush ops to (POST). Receives `{ ops: SyncOp[] }`. */
38
- endpoint: string;
39
- /**
40
- * Called when the server returns a conflict for an op.
41
- * Return the version that should win. Default: local wins.
42
- */
43
- onConflict?: (info: ConflictInfo<T>) => T | Promise<T>;
44
- /** Max retries before an op is moved to "dead letter" (logged, dropped). */
45
- maxRetries?: number;
46
- }
47
-
48
- // ── IDB helpers ───────────────────────────────────────────────────────────────
49
-
50
- function openDB(): Promise<IDBDatabase> {
51
- return new Promise((resolve, reject) => {
52
- const req = indexedDB.open(DB_NAME, DB_VERSION);
53
- req.onupgradeneeded = () => {
54
- const db = req.result;
55
- if (!db.objectStoreNames.contains(STORES.data)) {
56
- db.createObjectStore(STORES.data);
57
- }
58
- if (!db.objectStoreNames.contains(STORES.ops)) {
59
- const ops = db.createObjectStore(STORES.ops, { keyPath: 'id' });
60
- ops.createIndex('store', 'store');
61
- }
62
- };
63
- req.onsuccess = () => resolve(req.result);
64
- req.onerror = () => reject(req.error);
65
- });
66
- }
67
-
68
- function idbGet<T>(db: IDBDatabase, storeName: string, key: string): Promise<T | undefined> {
69
- return new Promise((resolve, reject) => {
70
- const tx = db.transaction(storeName, 'readonly');
71
- const req = tx.objectStore(storeName).get(key);
72
- req.onsuccess = () => resolve(req.result as T | undefined);
73
- req.onerror = () => reject(req.error);
74
- });
75
- }
76
-
77
- function idbPut(db: IDBDatabase, storeName: string, key: string, value: unknown): Promise<void> {
78
- return new Promise((resolve, reject) => {
79
- const tx = db.transaction(storeName, 'readwrite');
80
- const req = tx.objectStore(storeName).put(value, key);
81
- req.onsuccess = () => resolve();
82
- req.onerror = () => reject(req.error);
83
- });
84
- }
85
-
86
- function idbDelete(db: IDBDatabase, storeName: string, key: string): Promise<void> {
87
- return new Promise((resolve, reject) => {
88
- const tx = db.transaction(storeName, 'readwrite');
89
- const req = tx.objectStore(storeName).delete(key);
90
- req.onsuccess = () => resolve();
91
- req.onerror = () => reject(req.error);
92
- });
93
- }
94
-
95
- function idbGetAll<T>(db: IDBDatabase, storeName: string): Promise<T[]> {
96
- return new Promise((resolve, reject) => {
97
- const tx = db.transaction(storeName, 'readonly');
98
- const req = tx.objectStore(storeName).getAll();
99
- req.onsuccess = () => resolve(req.result as T[]);
100
- req.onerror = () => reject(req.error);
101
- });
102
- }
103
-
104
- // ── Sync Engine ───────────────────────────────────────────────────────────────
105
-
106
- export class NexusSyncEngine {
107
- private db: IDBDatabase | null = null;
108
- private flushing: boolean = false;
109
- private listeners: Set<() => void> = new Set();
110
- status: SyncStatus = 'synced';
111
-
112
- async init(): Promise<void> {
113
- if (this.db) return;
114
- this.db = await openDB();
115
- this.#startNetworkWatcher();
116
- // Flush any ops that survived a page refresh
117
- if (navigator.onLine) void this.flush();
118
- }
119
-
120
- private ensureDB(): IDBDatabase {
121
- if (!this.db) throw new Error('[Nexus Sync] Engine not initialized. Call await sync.init()');
122
- return this.db;
123
- }
124
-
125
- /** Read the local (IndexedDB) value for a key in a collection. */
126
- async get<T>(collection: string, key: string): Promise<T | undefined> {
127
- return idbGet<T>(this.ensureDB(), STORES.data, `${collection}:${key}`);
128
- }
129
-
130
- /** Read all values in a collection. */
131
- async getAll<T>(collection: string): Promise<T[]> {
132
- const db = this.ensureDB();
133
- const all = await idbGetAll<{ key: string; value: T }>(db, STORES.data);
134
- const prefix = `${collection}:`;
135
- return all
136
- .filter((r) => (r as unknown as { _idbKey?: string })._idbKey?.startsWith(prefix) ?? false)
137
- .map((r) => r.value);
138
- }
139
-
140
- /**
141
- * Write a value locally (instant) and enqueue a server sync op.
142
- * The UI is updated immediately — the server sync happens in the background.
143
- */
144
- async put<T>(
145
- collection: string,
146
- key: string,
147
- value: T,
148
- endpoint: string,
149
- ): Promise<void> {
150
- const db = this.ensureDB();
151
- // 1. Persist locally
152
- await idbPut(db, STORES.data, `${collection}:${key}`, value);
153
- // 2. Enqueue sync op
154
- const op: SyncOp<T> = {
155
- id: crypto.randomUUID(),
156
- store: collection,
157
- type: 'put',
158
- key,
159
- data: value,
160
- ts: Date.now(),
161
- retries: 0,
162
- };
163
- await idbPut(db, STORES.ops, op.id, op);
164
- this.#setStatus('pending');
165
- this.#notify();
166
- // 3. Try to flush immediately if online
167
- if (navigator.onLine) void this.flush(endpoint);
168
- }
169
-
170
- /** Delete locally and enqueue a delete op. */
171
- async delete(collection: string, key: string, endpoint: string): Promise<void> {
172
- const db = this.ensureDB();
173
- await idbDelete(db, STORES.data, `${collection}:${key}`);
174
- const op: SyncOp = {
175
- id: crypto.randomUUID(),
176
- store: collection,
177
- type: 'delete',
178
- key,
179
- ts: Date.now(),
180
- retries: 0,
181
- };
182
- await idbPut(db, STORES.ops, op.id, op);
183
- this.#setStatus('pending');
184
- this.#notify();
185
- if (navigator.onLine) void this.flush(endpoint);
186
- }
187
-
188
- /**
189
- * Flush pending ops to the server. Ops are sent in timestamp order.
190
- * On conflict, `onConflict` is called and the local store is updated.
191
- */
192
- async flush(endpoint?: string, opts?: Partial<SyncCollectionOpts<unknown>>): Promise<void> {
193
- if (this.flushing || !navigator.onLine) return;
194
- const db = this.ensureDB();
195
- const ops = (await idbGetAll<SyncOp>(db, STORES.ops))
196
- .sort((a, b) => a.ts - b.ts);
197
-
198
- if (ops.length === 0) {
199
- this.#setStatus('synced');
200
- return;
201
- }
202
-
203
- this.flushing = true;
204
- this.#setStatus('syncing');
205
-
206
- const maxRetries = opts?.maxRetries ?? 5;
207
- const url = endpoint ?? ops[0]?.store ?? '';
208
-
209
- try {
210
- const res = await fetch(url, {
211
- method: 'POST',
212
- headers: { 'content-type': 'application/json', 'x-nexus-sync': '1' },
213
- body: JSON.stringify({ ops }),
214
- });
215
-
216
- if (!res.ok) throw new Error(`Server returned ${res.status}`);
217
-
218
- const body = (await res.json()) as {
219
- acked: string[];
220
- conflicts: Array<{ opId: string; serverValue: unknown }>;
221
- };
222
-
223
- // Acknowledge successful ops
224
- for (const id of body.acked ?? []) {
225
- await idbDelete(db, STORES.ops, id);
226
- }
227
-
228
- // Handle conflicts
229
- for (const conflict of body.conflicts ?? []) {
230
- const op = ops.find((o) => o.id === conflict.opId);
231
- if (!op) continue;
232
- if (opts?.onConflict) {
233
- const localValue = await this.get(op.store, op.key);
234
- const winner = await opts.onConflict({
235
- local: localValue as unknown,
236
- remote: conflict.serverValue,
237
- op,
238
- });
239
- await idbPut(db, STORES.data, `${op.store}:${op.key}`, winner);
240
- } else {
241
- // Default: local wins — re-enqueue with fresh timestamp
242
- const updated: SyncOp = { ...op, ts: Date.now() };
243
- await idbPut(db, STORES.ops, updated.id, updated);
244
- }
245
- }
246
-
247
- this.#setStatus('synced');
248
- } catch {
249
- // Increment retries on all remaining ops
250
- for (const op of ops) {
251
- const updated = { ...op, retries: op.retries + 1 };
252
- if (updated.retries >= maxRetries) {
253
- // Dead letter — drop and warn
254
- console.warn(`[Nexus Sync] Op ${op.id} exceeded max retries, dropping.`, op);
255
- await idbDelete(db, STORES.ops, op.id);
256
- } else {
257
- await idbPut(db, STORES.ops, op.id, updated);
258
- }
259
- }
260
- this.#setStatus(navigator.onLine ? 'error' : 'offline');
261
- } finally {
262
- this.flushing = false;
263
- this.#notify();
264
- }
265
- }
266
-
267
- /** Returns the number of pending (unsynced) ops. */
268
- async pendingCount(): Promise<number> {
269
- const ops = await idbGetAll<SyncOp>(this.ensureDB(), STORES.ops);
270
- return ops.length;
271
- }
272
-
273
- /** Subscribe to status changes (useful for UI indicators). */
274
- subscribe(cb: () => void): () => void {
275
- this.listeners.add(cb);
276
- return () => this.listeners.delete(cb);
277
- }
278
-
279
- #setStatus(s: SyncStatus): void {
280
- this.status = s;
281
- }
282
-
283
- #notify(): void {
284
- for (const cb of this.listeners) cb();
285
- }
286
-
287
- #startNetworkWatcher(): void {
288
- window.addEventListener('online', () => {
289
- if (window.__NEXUS_DEV__) {
290
- console.log('%c[Nexus Sync]%c 🟢 Back online — flushing pending ops...', 'color:#818cf8;font-weight:bold', 'color:#a3e635');
291
- }
292
- void this.flush();
293
- });
294
- window.addEventListener('offline', () => {
295
- this.#setStatus('offline');
296
- this.#notify();
297
- if (window.__NEXUS_DEV__) {
298
- console.log('%c[Nexus Sync]%c 🔴 Offline — writes queued in IndexedDB', 'color:#818cf8;font-weight:bold', 'color:#f87171');
299
- }
300
- });
301
- }
302
- }
303
-
304
- declare global {
305
- interface Window { __NEXUS_DEV__?: boolean; }
306
- }
307
-
308
- /** Singleton engine instance (shared across all $sync calls on a page). */
309
- export const syncEngine = new NexusSyncEngine();
package/src/rune.ts DELETED
@@ -1,140 +0,0 @@
1
- /**
2
- * $localSync — the Local-First reactive rune.
3
- *
4
- * Usage (inside a .nx island):
5
- *
6
- * ```ts
7
- * import { $localSync } from '@nexus_js/sync';
8
- *
9
- * const captures = $localSync<string[]>('my-captures', {
10
- * default: [],
11
- * endpoint: '/_nexus/action/sync-captures',
12
- * onConflict: ({ local, remote }) => [...new Set([...local, ...remote])],
13
- * });
14
- *
15
- * // Read — always reflects local IndexedDB state
16
- * console.log(captures.value);
17
- *
18
- * // Mutate — instant UI update, background server sync
19
- * await captures.add('pikachu');
20
- * await captures.remove('pikachu');
21
- * ```
22
- *
23
- * The framework automatically:
24
- * - Persists every mutation to IndexedDB (works offline).
25
- * - Syncs to the server when online.
26
- * - Retries failed syncs on reconnect.
27
- * - Calls onConflict if the server disagrees.
28
- */
29
-
30
- import { syncEngine, type SyncCollectionOpts, type SyncStatus } from './engine.js';
31
-
32
- let engineReady: Promise<void> | null = null;
33
-
34
- function ensureEngine(): Promise<void> {
35
- if (!engineReady) engineReady = syncEngine.init();
36
- return engineReady;
37
- }
38
-
39
- export interface LocalSyncState<T> {
40
- /** Current value (IndexedDB, reflects all local mutations immediately). */
41
- readonly value: T;
42
- /** Current sync status. */
43
- readonly status: SyncStatus;
44
- /** Pending op count. */
45
- readonly pending: number;
46
- /** Overwrite entire value and enqueue a sync op. */
47
- set(next: T): Promise<void>;
48
- /**
49
- * Array helpers — only valid when T extends unknown[].
50
- * push/remove mutate the local array atomically.
51
- */
52
- push(item: T extends (infer I)[] ? I : never): Promise<void>;
53
- remove(predicate: T extends (infer I)[] ? (item: I) => boolean : never): Promise<void>;
54
- /** Force an immediate flush attempt. */
55
- flush(): Promise<void>;
56
- /** Subscribe to value/status changes (returns unsubscribe fn). */
57
- subscribe(cb: (state: LocalSyncState<T>) => void): () => void;
58
- }
59
-
60
- export interface LocalSyncOpts<T> extends Partial<SyncCollectionOpts<T>> {
61
- /** Default value used before IndexedDB is ready. */
62
- default: T;
63
- /** Server endpoint to sync ops to. */
64
- endpoint: string;
65
- /**
66
- * Unique key for this value within the collection.
67
- * Defaults to 'default'. Useful to scope per-user data.
68
- */
69
- key?: string;
70
- }
71
-
72
- /**
73
- * Creates a Local-First reactive state container.
74
- * The returned object is reactive — UI should re-render on .value access
75
- * (integration with Svelte 5 Runes happens at the compiler layer).
76
- */
77
- export function $localSync<T>(
78
- collection: string,
79
- opts: LocalSyncOpts<T>,
80
- ): LocalSyncState<T> {
81
- const storeKey = opts.key ?? 'default';
82
- let current = opts.default;
83
- let status: SyncStatus = 'synced';
84
- let pending = 0;
85
- const subs = new Set<(s: LocalSyncState<T>) => void>();
86
-
87
- function notify(): void {
88
- for (const cb of subs) cb(state);
89
- }
90
-
91
- // Load initial value from IndexedDB
92
- ensureEngine().then(async () => {
93
- const stored = await syncEngine.get<T>(collection, storeKey);
94
- if (stored !== undefined) {
95
- current = stored;
96
- notify();
97
- }
98
- // Track engine status
99
- syncEngine.subscribe(async () => {
100
- status = syncEngine.status;
101
- pending = await syncEngine.pendingCount();
102
- notify();
103
- });
104
- });
105
-
106
- const state: LocalSyncState<T> = {
107
- get value() { return current; },
108
- get status() { return status; },
109
- get pending() { return pending; },
110
-
111
- async set(next: T): Promise<void> {
112
- current = next;
113
- notify();
114
- await ensureEngine();
115
- await syncEngine.put(collection, storeKey, next, opts.endpoint);
116
- },
117
-
118
- async push(item): Promise<void> {
119
- if (!Array.isArray(current)) throw new Error('[Nexus Sync] .push() requires an array value');
120
- await state.set([...current, item] as T);
121
- },
122
-
123
- async remove(predicate): Promise<void> {
124
- if (!Array.isArray(current)) throw new Error('[Nexus Sync] .remove() requires an array value');
125
- await state.set((current as unknown[]).filter((i) => !(predicate as (x: unknown) => boolean)(i)) as T);
126
- },
127
-
128
- async flush(): Promise<void> {
129
- await ensureEngine();
130
- await syncEngine.flush(opts.endpoint, opts as Partial<SyncCollectionOpts<unknown>>);
131
- },
132
-
133
- subscribe(cb): () => void {
134
- subs.add(cb);
135
- return () => subs.delete(cb);
136
- },
137
- };
138
-
139
- return state;
140
- }