@laboverwire/stitch 0.4.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.
Files changed (136) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/LICENSE +201 -0
  3. package/README.md +39 -0
  4. package/dist/index.d.ts +10 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +9 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/internal-list-apply.d.ts +5 -0
  9. package/dist/internal-list-apply.d.ts.map +1 -0
  10. package/dist/internal-list-apply.js +30 -0
  11. package/dist/internal-list-apply.js.map +1 -0
  12. package/dist/internal-utils.d.ts +3 -0
  13. package/dist/internal-utils.d.ts.map +1 -0
  14. package/dist/internal-utils.js +15 -0
  15. package/dist/internal-utils.js.map +1 -0
  16. package/dist/internal-wasm-error.d.ts +9 -0
  17. package/dist/internal-wasm-error.d.ts.map +1 -0
  18. package/dist/internal-wasm-error.js +48 -0
  19. package/dist/internal-wasm-error.js.map +1 -0
  20. package/dist/memory-store.d.ts +3 -0
  21. package/dist/memory-store.d.ts.map +1 -0
  22. package/dist/memory-store.js +572 -0
  23. package/dist/memory-store.js.map +1 -0
  24. package/dist/offline-queue.d.ts +6 -0
  25. package/dist/offline-queue.d.ts.map +1 -0
  26. package/dist/offline-queue.js +361 -0
  27. package/dist/offline-queue.js.map +1 -0
  28. package/dist/persistence-layer.d.ts +7 -0
  29. package/dist/persistence-layer.d.ts.map +1 -0
  30. package/dist/persistence-layer.js +409 -0
  31. package/dist/persistence-layer.js.map +1 -0
  32. package/dist/react/context.d.ts +10 -0
  33. package/dist/react/context.d.ts.map +1 -0
  34. package/dist/react/context.js +9 -0
  35. package/dist/react/context.js.map +1 -0
  36. package/dist/react/hooks/useChildCounts.d.ts +3 -0
  37. package/dist/react/hooks/useChildCounts.d.ts.map +1 -0
  38. package/dist/react/hooks/useChildCounts.js +85 -0
  39. package/dist/react/hooks/useChildCounts.js.map +1 -0
  40. package/dist/react/hooks/useConnectionStatus.d.ts +3 -0
  41. package/dist/react/hooks/useConnectionStatus.d.ts.map +1 -0
  42. package/dist/react/hooks/useConnectionStatus.js +9 -0
  43. package/dist/react/hooks/useConnectionStatus.js.map +1 -0
  44. package/dist/react/hooks/useEntitySnapshot.d.ts +4 -0
  45. package/dist/react/hooks/useEntitySnapshot.d.ts.map +1 -0
  46. package/dist/react/hooks/useEntitySnapshot.js +33 -0
  47. package/dist/react/hooks/useEntitySnapshot.js.map +1 -0
  48. package/dist/react/hooks/useRootEntityList.d.ts +9 -0
  49. package/dist/react/hooks/useRootEntityList.d.ts.map +1 -0
  50. package/dist/react/hooks/useRootEntityList.js +69 -0
  51. package/dist/react/hooks/useRootEntityList.js.map +1 -0
  52. package/dist/react/hooks/useScopedEntities.d.ts +9 -0
  53. package/dist/react/hooks/useScopedEntities.d.ts.map +1 -0
  54. package/dist/react/hooks/useScopedEntities.js +82 -0
  55. package/dist/react/hooks/useScopedEntities.js.map +1 -0
  56. package/dist/react/hooks/useSyncScope.d.ts +8 -0
  57. package/dist/react/hooks/useSyncScope.d.ts.map +1 -0
  58. package/dist/react/hooks/useSyncScope.js +49 -0
  59. package/dist/react/hooks/useSyncScope.js.map +1 -0
  60. package/dist/react/hooks/useTopLevelEntities.d.ts +7 -0
  61. package/dist/react/hooks/useTopLevelEntities.d.ts.map +1 -0
  62. package/dist/react/hooks/useTopLevelEntities.js +52 -0
  63. package/dist/react/hooks/useTopLevelEntities.js.map +1 -0
  64. package/dist/react/index.d.ts +11 -0
  65. package/dist/react/index.d.ts.map +1 -0
  66. package/dist/react/index.js +10 -0
  67. package/dist/react/index.js.map +1 -0
  68. package/dist/react/provider.d.ts +32 -0
  69. package/dist/react/provider.d.ts.map +1 -0
  70. package/dist/react/provider.js +94 -0
  71. package/dist/react/provider.js.map +1 -0
  72. package/dist/remote-sync-layer.d.ts +8 -0
  73. package/dist/remote-sync-layer.d.ts.map +1 -0
  74. package/dist/remote-sync-layer.js +439 -0
  75. package/dist/remote-sync-layer.js.map +1 -0
  76. package/dist/store.d.ts +3 -0
  77. package/dist/store.d.ts.map +1 -0
  78. package/dist/store.js +779 -0
  79. package/dist/store.js.map +1 -0
  80. package/dist/sync-engine.d.ts +3 -0
  81. package/dist/sync-engine.d.ts.map +1 -0
  82. package/dist/sync-engine.js +643 -0
  83. package/dist/sync-engine.js.map +1 -0
  84. package/dist/types.d.ts +263 -0
  85. package/dist/types.d.ts.map +1 -0
  86. package/dist/types.js +7 -0
  87. package/dist/types.js.map +1 -0
  88. package/dist/vue/StitchAuth.d.ts +58 -0
  89. package/dist/vue/StitchAuth.d.ts.map +1 -0
  90. package/dist/vue/StitchAuth.js +39 -0
  91. package/dist/vue/StitchAuth.js.map +1 -0
  92. package/dist/vue/StoreRoot.d.ts +41 -0
  93. package/dist/vue/StoreRoot.d.ts.map +1 -0
  94. package/dist/vue/StoreRoot.js +94 -0
  95. package/dist/vue/StoreRoot.js.map +1 -0
  96. package/dist/vue/composables/useChildCounts.d.ts +4 -0
  97. package/dist/vue/composables/useChildCounts.d.ts.map +1 -0
  98. package/dist/vue/composables/useChildCounts.js +85 -0
  99. package/dist/vue/composables/useChildCounts.js.map +1 -0
  100. package/dist/vue/composables/useConnectionStatus.d.ts +4 -0
  101. package/dist/vue/composables/useConnectionStatus.d.ts.map +1 -0
  102. package/dist/vue/composables/useConnectionStatus.js +12 -0
  103. package/dist/vue/composables/useConnectionStatus.js.map +1 -0
  104. package/dist/vue/composables/useEntitySnapshot.d.ts +5 -0
  105. package/dist/vue/composables/useEntitySnapshot.d.ts.map +1 -0
  106. package/dist/vue/composables/useEntitySnapshot.js +41 -0
  107. package/dist/vue/composables/useEntitySnapshot.js.map +1 -0
  108. package/dist/vue/composables/useRootEntityList.d.ts +10 -0
  109. package/dist/vue/composables/useRootEntityList.d.ts.map +1 -0
  110. package/dist/vue/composables/useRootEntityList.js +76 -0
  111. package/dist/vue/composables/useRootEntityList.js.map +1 -0
  112. package/dist/vue/composables/useScopedEntities.d.ts +10 -0
  113. package/dist/vue/composables/useScopedEntities.d.ts.map +1 -0
  114. package/dist/vue/composables/useScopedEntities.js +91 -0
  115. package/dist/vue/composables/useScopedEntities.js.map +1 -0
  116. package/dist/vue/composables/useStore.d.ts +3 -0
  117. package/dist/vue/composables/useStore.d.ts.map +1 -0
  118. package/dist/vue/composables/useStore.js +9 -0
  119. package/dist/vue/composables/useStore.js.map +1 -0
  120. package/dist/vue/composables/useSyncScope.d.ts +9 -0
  121. package/dist/vue/composables/useSyncScope.d.ts.map +1 -0
  122. package/dist/vue/composables/useSyncScope.js +50 -0
  123. package/dist/vue/composables/useSyncScope.js.map +1 -0
  124. package/dist/vue/composables/useTopLevelEntities.d.ts +8 -0
  125. package/dist/vue/composables/useTopLevelEntities.d.ts.map +1 -0
  126. package/dist/vue/composables/useTopLevelEntities.js +58 -0
  127. package/dist/vue/composables/useTopLevelEntities.js.map +1 -0
  128. package/dist/vue/index.d.ts +13 -0
  129. package/dist/vue/index.d.ts.map +1 -0
  130. package/dist/vue/index.js +12 -0
  131. package/dist/vue/index.js.map +1 -0
  132. package/dist/vue/injection-key.d.ts +10 -0
  133. package/dist/vue/injection-key.d.ts.map +1 -0
  134. package/dist/vue/injection-key.js +2 -0
  135. package/dist/vue/injection-key.js.map +1 -0
  136. package/package.json +111 -0
package/dist/store.js ADDED
@@ -0,0 +1,779 @@
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);
26
+ }
27
+ get ready() {
28
+ return this._ready;
29
+ }
30
+ get connectionStatus() {
31
+ if (this._remote)
32
+ return this._remote.connectionStatus;
33
+ return this._connectionStatus;
34
+ }
35
+ get isReconnecting() {
36
+ return this._remote?.isReconnecting ?? false;
37
+ }
38
+ get hasPersistence() {
39
+ return this._persistence !== null;
40
+ }
41
+ get hasRemote() {
42
+ return this._remote !== null;
43
+ }
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;
51
+ });
52
+ return this._initPromise;
53
+ }
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
+ }
111
+ }
112
+ destroy() {
113
+ this._persistence?.close();
114
+ this._remote?.disconnect();
115
+ this._ready = false;
116
+ this._initPromise = null;
117
+ }
118
+ read(entity, id) {
119
+ return this.memory.read(entity, id);
120
+ }
121
+ getSnapshot(entity, scopeId) {
122
+ return this.memory.getSnapshot(entity, scopeId);
123
+ }
124
+ 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
+ }
299
+ }
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
+ }
360
+ }
361
+ subscribeToScope(scopeId, entity, callback) {
362
+ return this.memory.subscribeToScope(scopeId, entity, callback);
363
+ }
364
+ 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
+ };
394
+ }
395
+ beginBatch() {
396
+ this.memory.beginBatch();
397
+ }
398
+ endBatch() {
399
+ this.memory.endBatch();
400
+ }
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
+ }
485
+ }
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
+ }
493
+ }
494
+ loadScope(scopeId, data) {
495
+ this.memory.loadScope(scopeId, data);
496
+ }
497
+ clearScope(scopeId) {
498
+ this.memory.clearScope(scopeId);
499
+ }
500
+ 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);
507
+ }
508
+ disconnect() {
509
+ this._remote?.disconnect();
510
+ this._connectionStatus = 'offline';
511
+ }
512
+ async reconnect(serverUrl, getTicket) {
513
+ if (this._remote) {
514
+ await this._remote.reconnect(serverUrl, getTicket);
515
+ }
516
+ }
517
+ 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
+ }
523
+ }
524
+ setSessionInvalidHandler(handler) {
525
+ this._remote?.setSessionInvalidHandler(handler);
526
+ }
527
+ setReconnectValidator(validator) {
528
+ this._remote?.setReconnectValidator(validator);
529
+ }
530
+ 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
+ }
566
+ }
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');
592
+ }
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
+ }
643
+ }
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
+ }
663
+ }
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
+ }
736
+ }
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;
748
+ }
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
+ }
774
+ }
775
+ }
776
+ export function createStore(config, options) {
777
+ return new StoreImpl(config, options ?? {});
778
+ }
779
+ //# sourceMappingURL=store.js.map