@laboverwire/stitch 0.4.1 → 0.5.0

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +8 -5
  3. package/dist/index.d.ts +1 -8
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +0 -7
  6. package/dist/index.js.map +1 -1
  7. package/dist/internal-list-apply.js.map +1 -1
  8. package/dist/react/context.js.map +1 -1
  9. package/dist/react/hooks/useChildCounts.js.map +1 -1
  10. package/dist/react/hooks/useConnectionStatus.js.map +1 -1
  11. package/dist/react/hooks/useEntitySnapshot.js.map +1 -1
  12. package/dist/react/hooks/useRootEntityList.js.map +1 -1
  13. package/dist/react/hooks/useScopedEntities.js.map +1 -1
  14. package/dist/react/hooks/useSyncScope.js.map +1 -1
  15. package/dist/react/hooks/useTopLevelEntities.js.map +1 -1
  16. package/dist/react/index.js.map +1 -1
  17. package/dist/react/provider.d.ts +2 -2
  18. package/dist/react/provider.d.ts.map +1 -1
  19. package/dist/react/provider.js.map +1 -1
  20. package/dist/store.d.ts +2 -2
  21. package/dist/store.d.ts.map +1 -1
  22. package/dist/store.js +190 -724
  23. package/dist/store.js.map +1 -1
  24. package/dist/types.d.ts +23 -161
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/types.js +1 -6
  27. package/dist/types.js.map +1 -1
  28. package/dist/vue/StitchAuth.js.map +1 -1
  29. package/dist/vue/StoreRoot.js.map +1 -1
  30. package/dist/vue/composables/useChildCounts.js.map +1 -1
  31. package/dist/vue/composables/useConnectionStatus.js.map +1 -1
  32. package/dist/vue/composables/useEntitySnapshot.js.map +1 -1
  33. package/dist/vue/composables/useRootEntityList.js.map +1 -1
  34. package/dist/vue/composables/useScopedEntities.js.map +1 -1
  35. package/dist/vue/composables/useStore.js.map +1 -1
  36. package/dist/vue/composables/useSyncScope.js.map +1 -1
  37. package/dist/vue/composables/useTopLevelEntities.js.map +1 -1
  38. package/dist/vue/index.js.map +1 -1
  39. package/dist/vue/injection-key.js.map +1 -1
  40. package/package.json +17 -8
  41. package/dist/internal-utils.d.ts +0 -3
  42. package/dist/internal-utils.d.ts.map +0 -1
  43. package/dist/internal-utils.js +0 -15
  44. package/dist/internal-utils.js.map +0 -1
  45. package/dist/internal-wasm-error.d.ts +0 -9
  46. package/dist/internal-wasm-error.d.ts.map +0 -1
  47. package/dist/internal-wasm-error.js +0 -48
  48. package/dist/internal-wasm-error.js.map +0 -1
  49. package/dist/memory-store.d.ts +0 -3
  50. package/dist/memory-store.d.ts.map +0 -1
  51. package/dist/memory-store.js +0 -572
  52. package/dist/memory-store.js.map +0 -1
  53. package/dist/offline-queue.d.ts +0 -6
  54. package/dist/offline-queue.d.ts.map +0 -1
  55. package/dist/offline-queue.js +0 -361
  56. package/dist/offline-queue.js.map +0 -1
  57. package/dist/persistence-layer.d.ts +0 -7
  58. package/dist/persistence-layer.d.ts.map +0 -1
  59. package/dist/persistence-layer.js +0 -409
  60. package/dist/persistence-layer.js.map +0 -1
  61. package/dist/remote-sync-layer.d.ts +0 -8
  62. package/dist/remote-sync-layer.d.ts.map +0 -1
  63. package/dist/remote-sync-layer.js +0 -439
  64. package/dist/remote-sync-layer.js.map +0 -1
  65. package/dist/sync-engine.d.ts +0 -3
  66. package/dist/sync-engine.d.ts.map +0 -1
  67. package/dist/sync-engine.js +0 -643
  68. package/dist/sync-engine.js.map +0 -1
package/dist/store.js CHANGED
@@ -1,779 +1,245 @@
1
- import { OwnershipError } from "./types.js";
2
- import { createMemoryStore } from "./memory-store.js";
3
- import { createPersistenceLayer } from "./persistence-layer.js";
4
- import { createRemoteSyncLayer } from "./remote-sync-layer.js";
5
- import { createPersistentOfflineQueue, createInMemoryOfflineQueue } from "./offline-queue.js";
6
- import { stripNulls, isTransientSyncError } from "./internal-utils.js";
7
- class StoreImpl {
8
- config;
9
- memory;
10
- _persistence = null;
11
- _remote = null;
12
- _queue = null;
13
- options;
14
- _ready = false;
15
- _initPromise = null;
16
- _connectionStatus = 'offline';
17
- statusListeners = new Set();
18
- currentScopeId = null;
19
- initialSyncDone = false;
20
- _authenticatedUser = null;
21
- _earlySubscribers = [];
22
- constructor(config, options) {
23
- this.config = config;
24
- this.options = options;
25
- this.memory = createMemoryStore(config);
1
+ import { createStore as createWasmStore } from '@laboverwire/stitch-wasm';
2
+ const EMPTY_ARRAY = [];
3
+ const EMPTY_MAP = {};
4
+ function normalizeStatus(raw) {
5
+ switch (raw) {
6
+ case 'Connected':
7
+ return 'connected';
8
+ case 'Connecting':
9
+ return 'connecting';
10
+ case 'Disconnected':
11
+ return 'disconnected';
12
+ case 'Error':
13
+ return 'error';
14
+ default:
15
+ return 'offline';
26
16
  }
27
- get ready() {
28
- return this._ready;
29
- }
30
- get connectionStatus() {
31
- if (this._remote)
32
- return this._remote.connectionStatus;
33
- return this._connectionStatus;
17
+ }
18
+ function cacheKey(scopeId, entity) {
19
+ return `${scopeId} ${entity}`;
20
+ }
21
+ /**
22
+ * Wire a subscription now if the store is ready, otherwise defer wiring until
23
+ * `whenReady` resolves. Returns an unsubscribe that cancels the pending wire or
24
+ * the live subscription, whichever exists. `poke` fires once the deferred
25
+ * subscription attaches so `useSyncExternalStore`-style consumers re-read.
26
+ */
27
+ function deferrableSubscribe(isReady, whenReady, subscribeNow, poke) {
28
+ if (isReady())
29
+ return subscribeNow();
30
+ let live = null;
31
+ let cancelled = false;
32
+ void whenReady().then(() => {
33
+ if (cancelled)
34
+ return;
35
+ live = subscribeNow();
36
+ poke();
37
+ });
38
+ return () => {
39
+ cancelled = true;
40
+ if (live) {
41
+ live();
42
+ live = null;
43
+ }
44
+ };
45
+ }
46
+ class MemoryView {
47
+ #inner;
48
+ #whenReady;
49
+ #arrays = new Map();
50
+ #maps = new Map();
51
+ constructor(inner, whenReady) {
52
+ this.#inner = inner;
53
+ this.#whenReady = whenReady;
34
54
  }
35
- get isReconnecting() {
36
- return this._remote?.isReconnecting ?? false;
55
+ getSnapshot(entity, scopeId) {
56
+ if (!this.#inner.ready())
57
+ return EMPTY_ARRAY;
58
+ const version = this.#inner.getVersion(scopeId, entity);
59
+ const key = cacheKey(scopeId, entity);
60
+ const hit = this.#arrays.get(key);
61
+ if (hit && hit.version === version)
62
+ return hit.data;
63
+ const data = this.#inner.getSnapshot(entity, scopeId);
64
+ this.#arrays.set(key, { version, data });
65
+ return data;
37
66
  }
38
- get hasPersistence() {
39
- return this._persistence !== null;
67
+ getSnapshotAsMap(entity, scopeId) {
68
+ if (!this.#inner.ready())
69
+ return EMPTY_MAP;
70
+ const version = this.#inner.getVersion(scopeId, entity);
71
+ const key = cacheKey(scopeId, entity);
72
+ const hit = this.#maps.get(key);
73
+ if (hit && hit.version === version)
74
+ return hit.data;
75
+ const data = this.#inner.getSnapshotAsMap(entity, scopeId);
76
+ this.#maps.set(key, { version, data });
77
+ return data;
40
78
  }
41
- get hasRemote() {
42
- return this._remote !== null;
79
+ subscribeToScope(scopeId, entity, callback) {
80
+ return deferrableSubscribe(() => this.#inner.ready(), this.#whenReady, () => this.#inner.subscribeToScope(scopeId, entity, callback), callback);
43
81
  }
44
- async initialize() {
45
- if (this._ready)
46
- return;
47
- if (this._initPromise)
48
- return this._initPromise;
49
- this._initPromise = this.doInitialize().finally(() => {
50
- this._initPromise = null;
82
+ }
83
+ class StitchStore {
84
+ config;
85
+ memory;
86
+ #inner;
87
+ #memory;
88
+ #hasPersistence;
89
+ #hasRemote;
90
+ #reconnectValidator = null;
91
+ #readyPromise;
92
+ #resolveReady;
93
+ constructor(inner, config, options) {
94
+ this.#inner = inner;
95
+ this.config = config;
96
+ this.#hasPersistence = options.persistence != null;
97
+ this.#hasRemote = options.remote != null;
98
+ this.#readyPromise = new Promise((resolve) => {
99
+ this.#resolveReady = resolve;
51
100
  });
52
- return this._initPromise;
101
+ this.#memory = new MemoryView(inner, () => this.#readyPromise);
102
+ this.memory = this.#memory;
53
103
  }
54
- async doInitialize() {
55
- if (this._ready)
56
- return;
57
- await this.memory.ensureReady();
58
- if (this.options.persistence) {
59
- const persistence = createPersistenceLayer(this.config);
60
- this._persistence = persistence;
61
- await persistence.open(this.options.persistence.dbName);
62
- this.memory.onCorruption(() => {
63
- persistence.notifyCorruption();
64
- });
65
- for (const entry of this._earlySubscribers) {
66
- entry.persistenceUnsub = persistence.subscribe(entry.entity, entry.callback);
67
- }
68
- this._earlySubscribers = [];
69
- }
70
- if (this.options.remote) {
71
- const remote = createRemoteSyncLayer(this.config);
72
- this._remote = remote;
73
- if (this._authenticatedUser) {
74
- remote.setAuthenticatedUser(this._authenticatedUser);
75
- }
76
- if (this._persistence) {
77
- const queue = createPersistentOfflineQueue(this._persistence, this.config.scope.rootEntity);
78
- this._queue = queue;
79
- if (this._authenticatedUser && 'setAuthenticatedUser' in queue) {
80
- queue.setAuthenticatedUser(this._authenticatedUser);
81
- }
82
- }
83
- else {
84
- this._queue = createInMemoryOfflineQueue(this.config.scope.rootEntity);
85
- }
86
- remote.setMutationCallback((mutation) => {
87
- this.handleRemoteMutation(mutation);
88
- });
89
- remote.setConnectionChangeCallback((status) => {
90
- this._connectionStatus = status;
91
- for (const listener of this.statusListeners) {
92
- listener(status);
93
- }
94
- });
95
- remote.setOnConnectedCallback(() => {
96
- this.onConnected();
97
- });
98
- await remote.connect(this.options.remote.serverUrl, this.options.remote.getTicket);
99
- }
100
- else {
101
- this._connectionStatus = 'offline';
102
- this.initialSyncDone = true;
103
- }
104
- if (this._persistence) {
105
- this.setupPersistenceSubscriptions();
106
- }
107
- this._ready = true;
108
- if (this._persistence) {
109
- this._persistence.notifyAllEntitySubscribers();
110
- }
104
+ #afterReady(fn) {
105
+ return this.#inner.ready() ? fn() : this.#readyPromise.then(fn);
106
+ }
107
+ async initialize() {
108
+ await this.#inner.initialize();
109
+ this.#resolveReady();
111
110
  }
112
111
  destroy() {
113
- this._persistence?.close();
114
- this._remote?.disconnect();
115
- this._ready = false;
116
- this._initPromise = null;
112
+ return this.#inner.ready() ? this.#inner.destroy() : Promise.resolve();
113
+ }
114
+ get ready() {
115
+ return this.#inner.ready();
117
116
  }
118
117
  read(entity, id) {
119
- return this.memory.read(entity, id);
118
+ if (!this.#inner.ready())
119
+ return null;
120
+ return (this.#inner.read(entity, id) ?? null);
120
121
  }
121
122
  getSnapshot(entity, scopeId) {
122
- return this.memory.getSnapshot(entity, scopeId);
123
+ return this.#memory.getSnapshot(entity, scopeId);
123
124
  }
124
125
  getSnapshotAsMap(entity, scopeId) {
125
- return this.memory.getSnapshotAsMap(entity, scopeId);
126
- }
127
- async list(entity, filter) {
128
- if (this._persistence) {
129
- const { scopeField } = this.config.scope;
130
- const options = {};
131
- if (filter?.scopeId) {
132
- options.filters = [{ field: scopeField, op: 'eq', value: filter.scopeId }];
133
- }
134
- if (filter?.sort && filter.sort.length > 0) {
135
- options.sort = filter.sort;
136
- }
137
- if (filter?.projection && filter.projection.length > 0) {
138
- options.projection = filter.projection;
139
- }
140
- return this._persistence.list(entity, options);
141
- }
142
- const scopeId = filter?.scopeId ?? '';
143
- return Promise.resolve(this.memory.list(entity, scopeId));
144
- }
145
- async listRootEntities(sort) {
146
- if (!this.initialSyncDone && this._remote)
147
- return [];
148
- if (this._persistence) {
149
- const { rootEntity } = this.config.scope;
150
- const options = {};
151
- if (sort && sort.length > 0) {
152
- options.sort = sort;
153
- }
154
- return this._persistence.list(rootEntity, options);
155
- }
156
- return Promise.resolve(this.memory.list(this.config.scope.rootEntity, ''));
157
- }
158
- async getChildCount(entity, scopeId) {
159
- if (this._persistence) {
160
- const { scopeField } = this.config.scope;
161
- return this._persistence.count(entity, {
162
- filters: [{ field: scopeField, op: 'eq', value: scopeId }],
163
- });
164
- }
165
- return this.memory.list(entity, scopeId).length;
166
- }
167
- async create(entity, scopeId, data, tag) {
168
- const { rootEntity } = this.config.scope;
169
- const id = data.id || crypto.randomUUID();
170
- const record = stripNulls({ ...data, id });
171
- const effectiveScopeId = entity === rootEntity ? id : scopeId;
172
- this.memory.create(entity, effectiveScopeId, record, tag);
173
- if (this._persistence) {
174
- try {
175
- await this._persistence.create(entity, record);
176
- }
177
- catch (err) {
178
- console.error(`[Stitch] Persistence create ${entity} failed:`, err);
179
- }
180
- }
181
- if (this._queue && effectiveScopeId) {
182
- await this._queue.queue({
183
- op: 'insert',
184
- entity,
185
- id,
186
- scopeId: effectiveScopeId,
187
- data: record,
188
- });
189
- }
190
- if (this._remote && this._remote.connectionStatus === 'connected' && effectiveScopeId) {
191
- try {
192
- await this._remote.syncCreate(entity, effectiveScopeId, record);
193
- await this._queue?.remove(entity, id, effectiveScopeId, 'insert');
194
- }
195
- catch (err) {
196
- if (err instanceof OwnershipError) {
197
- await this._queue?.remove(entity, id, effectiveScopeId, 'insert');
198
- console.warn(`[Stitch] OwnershipError on create ${entity}/${id}:`, err.message);
199
- return id;
200
- }
201
- if (!isTransientSyncError(err)) {
202
- console.error(`[Stitch] Failed to sync create ${entity}:`, err);
203
- }
204
- }
205
- }
206
- return id;
207
- }
208
- async update(entity, id, fields, tag) {
209
- const { rootEntity, scopeField } = this.config.scope;
210
- const memExisting = this.memory.read(entity, id);
211
- let scopeId;
212
- if (memExisting) {
213
- scopeId = entity === rootEntity ? id : memExisting[scopeField];
214
- if (scopeId) {
215
- this.memory.update(entity, id, fields, tag);
216
- }
217
- }
218
- if (this._persistence) {
219
- if (!scopeId) {
220
- let existing;
221
- try {
222
- existing = await this._persistence.read(entity, id);
223
- }
224
- catch (readErr) {
225
- const msg = readErr instanceof Error ? readErr.message : String(readErr);
226
- if (/not found/i.test(msg))
227
- return;
228
- throw readErr;
229
- }
230
- scopeId = entity === rootEntity ? id : existing[scopeField];
231
- if (scopeId && !memExisting) {
232
- this.memory.update(entity, id, fields, tag);
233
- }
234
- }
235
- try {
236
- await this._persistence.update(entity, id, fields);
237
- }
238
- catch (updErr) {
239
- console.error(`[Stitch] Persistence update ${entity} failed:`, updErr);
240
- }
241
- }
242
- else if (!memExisting) {
243
- return;
244
- }
245
- if (this._queue && scopeId) {
246
- await this._queue.queue({ op: 'update', entity, id, scopeId, data: fields });
247
- }
248
- if (this._remote && this._remote.connectionStatus === 'connected' && scopeId) {
249
- try {
250
- await this._remote.syncUpdate(entity, scopeId, id, fields);
251
- await this._queue?.remove(entity, id, scopeId, 'update');
252
- }
253
- catch (err) {
254
- if (err instanceof OwnershipError) {
255
- await this._queue?.remove(entity, id, scopeId, 'update');
256
- console.warn(`[Stitch] OwnershipError on update ${entity}/${id}:`, err.message);
257
- return;
258
- }
259
- const isNotFound = err instanceof Error && /not found/i.test(err.message);
260
- if (isNotFound && entity === rootEntity) {
261
- try {
262
- if (this._persistence) {
263
- await this._persistence.delete(rootEntity, id);
264
- }
265
- this.memory.delete(rootEntity, id);
266
- }
267
- catch (cleanupErr) {
268
- console.error(`[Stitch] Cleanup delete ${rootEntity}/${id} failed:`, cleanupErr);
269
- }
270
- await this._queue?.remove(entity, id, scopeId, 'update');
271
- }
272
- else if (isNotFound) {
273
- try {
274
- let full;
275
- if (this._persistence) {
276
- full = await this._persistence.read(entity, id);
277
- }
278
- else {
279
- full = this.memory.read(entity, id) ?? {};
280
- }
281
- await this._remote.syncCreate(entity, scopeId, full);
282
- }
283
- catch (createErr) {
284
- if (createErr instanceof OwnershipError) {
285
- await this._queue?.remove(entity, id, scopeId, 'update');
286
- console.warn(`[Stitch] OwnershipError on upsert ${entity}/${id}:`, createErr.message);
287
- return;
288
- }
289
- if (!isTransientSyncError(createErr)) {
290
- console.error(`[Stitch] Failed to sync upsert ${entity}:`, createErr);
291
- }
292
- }
293
- }
294
- else if (!isTransientSyncError(err)) {
295
- console.error(`[Stitch] Failed to sync update ${entity}:`, err);
296
- }
297
- }
298
- }
126
+ return this.#memory.getSnapshotAsMap(entity, scopeId);
299
127
  }
300
- async delete(entity, id, tag) {
301
- const { rootEntity, scopeField } = this.config.scope;
302
- const memExisting = this.memory.read(entity, id);
303
- let scopeId;
304
- if (memExisting) {
305
- scopeId = entity === rootEntity ? id : memExisting[scopeField];
306
- if (scopeId) {
307
- this.memory.delete(entity, id, tag);
308
- }
309
- }
310
- if (this._persistence) {
311
- if (!scopeId) {
312
- let existing;
313
- try {
314
- existing = await this._persistence.read(entity, id);
315
- }
316
- catch (readErr) {
317
- const msg = readErr instanceof Error ? readErr.message : String(readErr);
318
- if (/not found/i.test(msg))
319
- return;
320
- throw readErr;
321
- }
322
- scopeId = entity === rootEntity ? id : existing[scopeField];
323
- if (scopeId && !memExisting) {
324
- this.memory.delete(entity, id, tag);
325
- }
326
- }
327
- try {
328
- await this._persistence.delete(entity, id);
329
- }
330
- catch (delErr) {
331
- const msg = delErr instanceof Error ? delErr.message : String(delErr);
332
- if (/not found/i.test(msg))
333
- return;
334
- console.error(`[Stitch] Persistence delete ${entity} failed:`, delErr);
335
- }
336
- }
337
- else if (!memExisting) {
338
- return;
339
- }
340
- if (this._queue && scopeId) {
341
- await this._queue.queue({ op: 'delete', entity, id, scopeId, data: null });
342
- }
343
- if (this._remote && this._remote.connectionStatus === 'connected' && scopeId) {
344
- try {
345
- await this._remote.syncDelete(entity, scopeId, id);
346
- await this._queue?.remove(entity, id, scopeId, 'delete');
347
- }
348
- catch (err) {
349
- if (err instanceof OwnershipError) {
350
- await this._queue?.remove(entity, id, scopeId, 'delete');
351
- console.warn(`[Stitch] OwnershipError on delete ${entity}/${id}:`, err.message);
352
- return;
353
- }
354
- const isNotFound = err instanceof Error && /not found/i.test(err.message);
355
- if (!isNotFound && !isTransientSyncError(err)) {
356
- console.error(`[Stitch] Failed to sync delete ${entity}:`, err);
357
- }
358
- }
359
- }
128
+ list(entity, filter) {
129
+ return this.#afterReady(() => this.#inner.list(entity, filter ?? {}));
130
+ }
131
+ listRootEntities(sort) {
132
+ return this.#afterReady(() => this.#inner.listRootEntities(sort ?? []));
133
+ }
134
+ getChildCount(entity, scopeId) {
135
+ if (!this.#inner.ready())
136
+ return 0;
137
+ return this.#inner.getChildCount(entity, scopeId);
138
+ }
139
+ getVersion(scopeId, entity) {
140
+ if (!this.#inner.ready())
141
+ return 0;
142
+ return this.#inner.getVersion(scopeId, entity);
143
+ }
144
+ create(entity, scopeId, data, tag) {
145
+ return this.#afterReady(() => this.#inner.create(entity, scopeId, data, tag ?? undefined));
146
+ }
147
+ update(entity, id, fields, tag) {
148
+ return this.#afterReady(() => this.#inner.update(entity, id, fields, tag ?? undefined));
149
+ }
150
+ delete(entity, id, tag) {
151
+ return this.#afterReady(() => this.#inner.delete(entity, id, tag ?? undefined));
360
152
  }
361
153
  subscribeToScope(scopeId, entity, callback) {
362
- return this.memory.subscribeToScope(scopeId, entity, callback);
154
+ return deferrableSubscribe(() => this.#inner.ready(), () => this.#readyPromise, () => this.#inner.subscribeToScope(scopeId, entity, callback), callback);
363
155
  }
364
156
  subscribeToEntity(entity, callback) {
365
- const memUnsub = this.memory.onMutation((event) => {
366
- if (event.entity !== entity)
367
- return;
368
- if (this._persistence && event.originTag !== 'load' && event.originTag !== 'clear')
369
- return;
370
- const op = event.operation === 'create' ? 'insert' : event.operation;
371
- callback(event.data, op);
372
- });
373
- const entry = {
374
- entity,
375
- callback: (data, op) => {
376
- callback((data ?? null), op);
377
- },
378
- memoryUnsub: memUnsub,
379
- persistenceUnsub: null,
380
- };
381
- if (this._persistence) {
382
- entry.persistenceUnsub = this._persistence.subscribe(entity, entry.callback);
383
- }
384
- else {
385
- this._earlySubscribers.push(entry);
386
- }
387
- return () => {
388
- entry.memoryUnsub();
389
- entry.persistenceUnsub?.();
390
- const idx = this._earlySubscribers.indexOf(entry);
391
- if (idx >= 0)
392
- this._earlySubscribers.splice(idx, 1);
393
- };
157
+ return deferrableSubscribe(() => this.#inner.ready(), () => this.#readyPromise, () => this.#inner.subscribeToEntity(entity, (data, op) => callback((data ?? null), op)), () => callback(null, 'update'));
394
158
  }
395
159
  beginBatch() {
396
- this.memory.beginBatch();
160
+ if (this.#inner.ready())
161
+ this.#inner.beginBatch();
397
162
  }
398
163
  endBatch() {
399
- this.memory.endBatch();
164
+ if (this.#inner.ready())
165
+ this.#inner.endBatch();
400
166
  }
401
- async replaceScope(scopeId) {
402
- if (this.currentScopeId === scopeId)
403
- return;
404
- const { rootEntity, childEntities } = this.config.scope;
405
- if (this.currentScopeId && this.currentScopeId !== scopeId) {
406
- const prevScope = this.currentScopeId;
407
- this.currentScopeId = scopeId;
408
- this.closeScope(prevScope).catch(() => { });
409
- }
410
- else {
411
- this.currentScopeId = scopeId;
412
- }
413
- if (this._remote && this._remote.connectionStatus === 'connected') {
414
- if (this._persistence) {
415
- this._persistence.setSuppressNotifications(true);
416
- }
417
- try {
418
- const state = await this._remote.openScope(scopeId);
419
- if (this._persistence) {
420
- const localAccessor = this.createLocalAccessor();
421
- const versionField = this.config.versionField ?? 'version';
422
- if (state.root && state.root.id) {
423
- if (state.root[versionField] != null && typeof state.root[versionField] !== 'number') {
424
- state.root[versionField] = Number(state.root[versionField]) || 1;
425
- }
426
- try {
427
- await localAccessor.read(rootEntity, state.root.id);
428
- await localAccessor.update(rootEntity, state.root.id, state.root);
429
- }
430
- catch {
431
- await localAccessor.create(rootEntity, state.root);
432
- }
433
- }
434
- else {
435
- try {
436
- await localAccessor.read(rootEntity, scopeId);
437
- }
438
- catch {
439
- return;
440
- }
441
- }
442
- for (const childEntity of childEntities) {
443
- const serverChildren = state.children[childEntity] ?? [];
444
- await this._remote.reconcileChildren(scopeId, childEntity, serverChildren, localAccessor, this._queue);
445
- }
446
- for (const mutation of state.bufferedMutations) {
447
- await this._remote.applyMutationToDb(mutation, localAccessor);
448
- }
449
- const bundle = await this.loadScopeFromPersistence(scopeId);
450
- if (bundle) {
451
- this.memory.loadScope(scopeId, bundle, 'load');
452
- }
453
- await this.loadRootIntoMemory(rootEntity, scopeId);
454
- }
455
- else {
456
- this.memory.loadScope(scopeId, state.children, 'load');
457
- if (state.root && state.root.id) {
458
- this.memory.create(rootEntity, scopeId, state.root, 'load');
459
- }
460
- }
461
- }
462
- catch (err) {
463
- console.error('[Stitch] Failed to open scope from server:', err);
464
- if (this._persistence) {
465
- const bundle = await this.loadScopeFromPersistence(scopeId);
466
- if (bundle) {
467
- this.memory.loadScope(scopeId, bundle, 'load');
468
- }
469
- await this.loadRootIntoMemory(rootEntity, scopeId);
470
- }
471
- }
472
- finally {
473
- if (this._persistence) {
474
- this._persistence.setSuppressNotifications(false);
475
- }
476
- }
477
- }
478
- else if (this._persistence) {
479
- const bundle = await this.loadScopeFromPersistence(scopeId);
480
- if (bundle) {
481
- this.memory.loadScope(scopeId, bundle, 'load');
482
- }
483
- await this.loadRootIntoMemory(rootEntity, scopeId);
484
- }
167
+ replaceScope(scopeId) {
168
+ return this.#afterReady(() => this.#inner.replaceScope(scopeId));
485
169
  }
486
- async closeScope(scopeId) {
487
- if (this._remote) {
488
- await this._remote.closeScope(scopeId);
489
- }
490
- if (this.currentScopeId === scopeId) {
491
- this.currentScopeId = null;
492
- }
170
+ closeScope(scopeId) {
171
+ return this.#afterReady(() => this.#inner.closeScope(scopeId));
493
172
  }
494
173
  loadScope(scopeId, data) {
495
- this.memory.loadScope(scopeId, data);
174
+ return this.#afterReady(() => this.#inner.loadScope(scopeId, data));
496
175
  }
497
176
  clearScope(scopeId) {
498
- this.memory.clearScope(scopeId);
177
+ return this.#afterReady(() => this.#inner.clearScope(scopeId));
178
+ }
179
+ get connectionStatus() {
180
+ if (!this.#inner.ready())
181
+ return 'offline';
182
+ return normalizeStatus(this.#inner.connectionStatus());
499
183
  }
500
184
  subscribeToConnectionStatus(cb) {
501
- if (this._remote) {
502
- return this._remote.subscribeToConnectionStatus(cb);
503
- }
504
- this.statusListeners.add(cb);
505
- cb(this._connectionStatus);
506
- return () => this.statusListeners.delete(cb);
185
+ return deferrableSubscribe(() => this.#inner.ready(), () => this.#readyPromise, () => this.#inner.subscribeToConnectionStatus((raw) => cb(normalizeStatus(raw))), () => cb(normalizeStatus(this.#inner.connectionStatus())));
507
186
  }
508
187
  disconnect() {
509
- this._remote?.disconnect();
510
- this._connectionStatus = 'offline';
188
+ return this.#inner.ready() ? this.#inner.disconnect() : Promise.resolve();
511
189
  }
512
190
  async reconnect(serverUrl, getTicket) {
513
- if (this._remote) {
514
- await this._remote.reconnect(serverUrl, getTicket);
515
- }
191
+ if (this.#reconnectValidator)
192
+ await this.#reconnectValidator();
193
+ const ticket = getTicket ? await getTicket() : undefined;
194
+ await this.#inner.reconnect(serverUrl, ticket ?? undefined);
195
+ }
196
+ get isReconnecting() {
197
+ if (!this.#inner.ready())
198
+ return false;
199
+ return this.#inner.isReconnecting();
516
200
  }
517
201
  setAuthenticatedUser(userId) {
518
- this._authenticatedUser = userId;
519
- this._remote?.setAuthenticatedUser(userId);
520
- if (this._queue && 'setAuthenticatedUser' in this._queue) {
521
- this._queue.setAuthenticatedUser(userId);
522
- }
202
+ if (this.#inner.ready())
203
+ this.#inner.setAuthenticatedUser(userId);
204
+ else
205
+ void this.#readyPromise.then(() => this.#inner.setAuthenticatedUser(userId));
523
206
  }
524
207
  setSessionInvalidHandler(handler) {
525
- this._remote?.setSessionInvalidHandler(handler);
208
+ if (this.#inner.ready())
209
+ this.#inner.setSessionInvalidHandler(handler);
210
+ else
211
+ void this.#readyPromise.then(() => this.#inner.setSessionInvalidHandler(handler));
526
212
  }
527
213
  setReconnectValidator(validator) {
528
- this._remote?.setReconnectValidator(validator);
214
+ this.#reconnectValidator = validator;
529
215
  }
530
216
  resetForLogout() {
531
- this._remote?.resetForLogout();
532
- this._persistence?.close();
533
- this._persistence = null;
534
- this._queue = null;
535
- this._ready = false;
536
- this._initPromise = null;
537
- this._earlySubscribers = [];
538
- this.currentScopeId = null;
539
- this._authenticatedUser = null;
540
- this.initialSyncDone = false;
541
- this._connectionStatus = 'offline';
542
- sessionStorage.removeItem('stitch_cached_user');
543
- sessionStorage.removeItem('stitch_pending_logout');
544
- }
545
- async request(topic, payload) {
546
- if (!this._remote)
547
- throw new Error('No remote connection');
548
- return this._remote.request(topic, payload);
549
- }
550
- async readLocalState(entity, id) {
551
- if (this._persistence) {
552
- return this._persistence.readLocalState(entity, id);
553
- }
554
- return this.memory.read(entity, id);
555
- }
556
- async updateLocalState(entity, id, fields) {
557
- if (this._persistence) {
558
- await this._persistence.updateLocalState(entity, id, fields);
559
- }
560
- else {
561
- const existing = this.memory.read(entity, id);
562
- if (existing) {
563
- this.memory.update(entity, id, fields);
564
- }
565
- }
217
+ return this.#inner.ready() ? this.#inner.resetForLogout() : Promise.resolve();
566
218
  }
567
- async getCachedUser() {
568
- try {
569
- const raw = sessionStorage.getItem('stitch_cached_user');
570
- if (!raw)
571
- return null;
572
- const envelope = JSON.parse(raw);
573
- if (typeof envelope !== 'object' || envelope === null)
574
- return null;
575
- const { user, cachedAt } = envelope;
576
- const MAX_AGE_MS = 15 * 60 * 1000;
577
- if (typeof cachedAt !== 'number' || Date.now() - cachedAt > MAX_AGE_MS) {
578
- sessionStorage.removeItem('stitch_cached_user');
579
- return null;
580
- }
581
- if (typeof user !== 'object' || user === null)
582
- return null;
583
- return user;
584
- }
585
- catch {
586
- return null;
587
- }
588
- }
589
- async setCachedUser(user) {
590
- sessionStorage.setItem('stitch_cached_user', JSON.stringify({ user, cachedAt: Date.now() }));
591
- sessionStorage.removeItem('stitch_pending_logout');
219
+ readLocalState(entity, id) {
220
+ return this.#afterReady(() => this.#inner
221
+ .readLocalState(entity, id)
222
+ .then((r) => (r ?? null)));
592
223
  }
593
- async clearCachedUser() {
594
- sessionStorage.removeItem('stitch_cached_user');
595
- }
596
- async hasPendingLogout() {
597
- return sessionStorage.getItem('stitch_pending_logout') === 'true';
598
- }
599
- async setPendingLogout(pending) {
600
- if (pending) {
601
- sessionStorage.setItem('stitch_pending_logout', 'true');
602
- }
603
- else {
604
- sessionStorage.removeItem('stitch_pending_logout');
605
- }
606
- }
607
- async flushPendingLogout(logoutFn) {
608
- const pending = await this.hasPendingLogout();
609
- if (!pending)
610
- return;
611
- try {
612
- await logoutFn();
613
- await this.setPendingLogout(false);
614
- }
615
- catch { }
616
- }
617
- async handleRemoteMutation(mutation) {
618
- const localAccessor = this.createLocalAccessor();
619
- if (this._remote) {
620
- await this._remote.applyMutationToDb(mutation, localAccessor);
621
- }
622
- if (!this._persistence) {
623
- const { scopeField } = this.config.scope;
624
- switch (mutation.op) {
625
- case 'insert':
626
- if (mutation.data) {
627
- const sid = mutation.data[scopeField];
628
- if (sid) {
629
- this.memory.create(mutation.entity, sid, { ...mutation.data, id: mutation.id }, 'remote');
630
- }
631
- }
632
- break;
633
- case 'update':
634
- if (mutation.data) {
635
- this.memory.update(mutation.entity, mutation.id, mutation.data, 'remote');
636
- }
637
- break;
638
- case 'delete':
639
- this.memory.delete(mutation.entity, mutation.id, 'remote');
640
- break;
641
- }
642
- }
224
+ updateLocalState(entity, id, fields) {
225
+ return this.#afterReady(() => this.#inner.updateLocalState(entity, id, fields));
643
226
  }
644
- async onConnected() {
645
- try {
646
- if (this._queue && this._remote) {
647
- const sender = this.createMutationSender();
648
- await this._queue.flush(sender);
649
- await this._queue.flush(sender);
650
- }
651
- if (this._remote) {
652
- const localAccessor = this.createLocalAccessor();
653
- this.initialSyncDone = false;
654
- await this._remote.syncRootEntityList(localAccessor, this._queue);
655
- this.initialSyncDone = true;
656
- }
657
- this._persistence?.notifyAllEntitySubscribers();
658
- }
659
- catch (err) {
660
- void err;
661
- this.initialSyncDone = true;
662
- }
227
+ pendingMutationCount(scopeId) {
228
+ return this.#afterReady(() => this.#inner.pendingMutationCount(scopeId));
663
229
  }
664
- createLocalAccessor() {
665
- if (this._persistence) {
666
- return {
667
- read: (entity, id) => this._persistence.read(entity, id),
668
- list: (entity, options) => this._persistence.list(entity, options),
669
- create: (entity, data) => this._persistence.create(entity, data),
670
- update: (entity, id, data) => this._persistence.update(entity, id, data),
671
- delete: (entity, id) => this._persistence.delete(entity, id),
672
- };
673
- }
674
- return {
675
- read: async (entity, id) => {
676
- const result = this.memory.read(entity, id);
677
- if (!result)
678
- throw new Error(`${entity} ${id} not found`);
679
- return result;
680
- },
681
- list: async (entity, options) => {
682
- const filters = options.filters;
683
- if (filters && filters.length > 0) {
684
- const scopeFilter = filters.find((f) => f.field === this.config.scope.scopeField);
685
- if (scopeFilter) {
686
- return this.memory.list(entity, scopeFilter.value);
687
- }
688
- }
689
- return this.memory.list(entity, '');
690
- },
691
- create: async (entity, data) => {
692
- const { scopeField } = this.config.scope;
693
- const scopeId = data[scopeField];
694
- this.memory.create(entity, scopeId ?? '', data, 'remote');
695
- },
696
- update: async (entity, id, data) => {
697
- this.memory.update(entity, id, data, 'remote');
698
- },
699
- delete: async (entity, id) => {
700
- this.memory.delete(entity, id, 'remote');
701
- },
702
- };
703
- }
704
- createMutationSender() {
705
- return {
706
- syncCreate: (entity, scopeId, data) => this._remote.syncCreate(entity, scopeId, data),
707
- syncUpdate: (entity, scopeId, id, data) => this._remote.syncUpdate(entity, scopeId, id, data),
708
- syncDelete: (entity, scopeId, id) => this._remote.syncDelete(entity, scopeId, id),
709
- readEntity: async (entity, id) => {
710
- if (this._persistence) {
711
- return this._persistence.read(entity, id);
712
- }
713
- const result = this.memory.read(entity, id);
714
- if (!result)
715
- throw new Error(`${entity} ${id} not found`);
716
- return result;
717
- },
718
- deleteEntity: async (entity, id) => {
719
- if (this._persistence) {
720
- await this._persistence.delete(entity, id);
721
- }
722
- this.memory.delete(entity, id);
723
- },
724
- };
725
- }
726
- async loadRootIntoMemory(rootEntity, scopeId) {
727
- if (!this._persistence)
728
- return;
729
- try {
730
- const rootRecord = await this._persistence.read(rootEntity, scopeId);
731
- this.memory.create(rootEntity, scopeId, rootRecord, 'load');
732
- }
733
- catch (readErr) {
734
- console.debug(`[Stitch] Root entity ${rootEntity}/${scopeId} not in persistence:`, readErr);
735
- }
230
+ request(topic, payload) {
231
+ return this.#afterReady(() => this.#inner.request(topic, payload));
736
232
  }
737
- async loadScopeFromPersistence(scopeId) {
738
- if (!this._persistence)
739
- return null;
740
- const { childEntities, scopeField } = this.config.scope;
741
- const children = {};
742
- for (const entity of childEntities) {
743
- children[entity] = await this._persistence.list(entity, {
744
- filters: [{ field: scopeField, op: 'eq', value: scopeId }],
745
- });
746
- }
747
- return children;
233
+ get hasPersistence() {
234
+ return this.#hasPersistence;
748
235
  }
749
- setupPersistenceSubscriptions() {
750
- if (!this._persistence)
751
- return;
752
- const { childEntities, scopeField } = this.config.scope;
753
- for (const entity of childEntities) {
754
- this._persistence.subscribe(entity, (entityData, op) => {
755
- if (entityData === null || typeof entityData !== 'object')
756
- return;
757
- const record = entityData;
758
- const sid = record[scopeField];
759
- if (!sid || sid !== this.currentScopeId)
760
- return;
761
- switch (op) {
762
- case 'insert':
763
- this.memory.create(entity, sid, record, 'remote');
764
- break;
765
- case 'update':
766
- this.memory.update(entity, record.id, record, 'remote');
767
- break;
768
- case 'delete':
769
- this.memory.delete(entity, record.id, 'remote');
770
- break;
771
- }
772
- });
773
- }
236
+ get hasRemote() {
237
+ return this.#hasRemote;
774
238
  }
775
239
  }
776
240
  export function createStore(config, options) {
777
- return new StoreImpl(config, options ?? {});
241
+ const resolvedOptions = options ?? {};
242
+ const inner = createWasmStore(config, resolvedOptions);
243
+ return new StitchStore(inner, config, resolvedOptions);
778
244
  }
779
245
  //# sourceMappingURL=store.js.map