@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
@@ -1,643 +0,0 @@
1
- import { OwnershipError } from "./types.js";
2
- const FAST_RECONNECT_LIMIT = 5;
3
- const SLOW_RECONNECT_INTERVAL = 15_000;
4
- function applyTicketAuth(connectOpts, ticket) {
5
- if (!ticket)
6
- return;
7
- connectOpts.authenticationMethod = 'JWT';
8
- connectOpts.authenticationData = new TextEncoder().encode(ticket);
9
- }
10
- class SyncEngineImpl {
11
- client = null;
12
- clientId;
13
- config;
14
- prefix;
15
- responsePrefix;
16
- SubscribeOptions = null;
17
- PublishOptions = null;
18
- ConnectOptions = null;
19
- desiredScopes = new Set();
20
- subscribedScopes = new Set();
21
- appliedVersion = new Map();
22
- buffered = new Map();
23
- awaitingState = new Set();
24
- pendingRequests = new Map();
25
- topLevelSubscribed = false;
26
- onMutation = null;
27
- onConnectionStatus = null;
28
- onSessionInvalid = null;
29
- getTicket = null;
30
- active = false;
31
- reconnectTimer = null;
32
- reconnectAttempt = 0;
33
- reconnecting = false;
34
- slowReconnecting = false;
35
- serverUrl = null;
36
- constructor(clientId, config) {
37
- if (/[+#/]/.test(clientId)) {
38
- throw new Error('clientId must not contain MQTT special characters (+, #, /)');
39
- }
40
- this.clientId = clientId;
41
- this.config = config;
42
- this.prefix = config.syncTopicPrefix ?? '$DB';
43
- this.responsePrefix = config.responseTopicPrefix ?? '$SYS/responses';
44
- }
45
- async connect(serverUrl, wasmModule, getTicket) {
46
- this.onConnectionStatus?.('connecting');
47
- this.active = true;
48
- this.serverUrl = serverUrl;
49
- if (getTicket) {
50
- this.getTicket = getTicket;
51
- }
52
- if (!this.client) {
53
- const mod = wasmModule;
54
- const { default: initMqttWasm, MqttClient, ConnectOptions, SubscribeOptions, PublishOptions, } = mod;
55
- await initMqttWasm();
56
- this.client = new MqttClient(this.clientId);
57
- this.SubscribeOptions = SubscribeOptions;
58
- this.PublishOptions = PublishOptions;
59
- this.ConnectOptions = ConnectOptions;
60
- this.client.onConnect(() => {
61
- if (!this.active)
62
- return;
63
- this.clearReconnectTimer();
64
- this.reconnectAttempt = 0;
65
- this.slowReconnecting = false;
66
- this.topLevelSubscribed = false;
67
- Promise.all([this.subscribeToResponseTopic(), this.subscribeToTopLevel()])
68
- .then(() => {
69
- if (this.active)
70
- this.onConnectionStatus?.('connected');
71
- for (const scopeId of this.desiredScopes) {
72
- this.subscribeToScope(scopeId)
73
- .then(() => this.subscribedScopes.add(scopeId))
74
- .catch((err) => console.error(`[SyncEngine] Re-subscribe ${scopeId} failed:`, err));
75
- }
76
- })
77
- .catch((err) => {
78
- console.error('[SyncEngine] Post-connect subscribe failed:', err);
79
- this.onConnectionStatus?.('error');
80
- this.scheduleReconnect();
81
- });
82
- });
83
- this.client.onDisconnect(() => {
84
- if (!this.active)
85
- return;
86
- if (this.reconnecting)
87
- return;
88
- this.onConnectionStatus?.('disconnected');
89
- this.cleanupOnDisconnect();
90
- if (this.client?.isBrowserOnline()) {
91
- this.scheduleReconnect();
92
- }
93
- });
94
- this.client.onError((error) => {
95
- if (!this.active)
96
- return;
97
- if (/closed by peer|connection reset/i.test(error))
98
- return;
99
- if (/not.?authorized|bad.?user|bad.?password|authentication|0x8[567]/i.test(error)) {
100
- this.cancelReconnect();
101
- this.onSessionInvalid?.();
102
- return;
103
- }
104
- console.error('[SyncEngine] MQTT error:', error);
105
- this.onConnectionStatus?.('error');
106
- });
107
- this.client.onConnectivityChange((online) => {
108
- if (!this.active)
109
- return;
110
- if (!online) {
111
- this.cancelReconnect();
112
- this.onConnectionStatus?.('offline');
113
- this.cleanupOnDisconnect();
114
- }
115
- else {
116
- this.scheduleReconnect(0);
117
- }
118
- });
119
- }
120
- try {
121
- const connectOpts = new this.ConnectOptions();
122
- connectOpts.cleanStart = true;
123
- connectOpts.keepAlive = 60;
124
- if (this.getTicket) {
125
- applyTicketAuth(connectOpts, await this.getTicket());
126
- }
127
- await this.client.connectWithOptions(serverUrl, connectOpts);
128
- }
129
- catch (err) {
130
- this.onConnectionStatus?.('disconnected');
131
- if (this.client?.isBrowserOnline()) {
132
- this.scheduleReconnect();
133
- }
134
- throw err;
135
- }
136
- }
137
- async reconnect(serverUrl, getTicket) {
138
- if (!this.client || !this.ConnectOptions) {
139
- throw new Error('Cannot reconnect before initial connect');
140
- }
141
- this.clearReconnectTimer();
142
- this.reconnecting = true;
143
- this.active = true;
144
- this.serverUrl = serverUrl;
145
- if (!this.slowReconnecting) {
146
- this.onConnectionStatus?.('connecting');
147
- }
148
- if (getTicket) {
149
- this.getTicket = getTicket;
150
- }
151
- try {
152
- this.client.disconnect();
153
- }
154
- catch {
155
- // WASM client may throw if already disconnected
156
- }
157
- let failed = false;
158
- try {
159
- const connectOpts = new this.ConnectOptions();
160
- connectOpts.cleanStart = true;
161
- connectOpts.keepAlive = 60;
162
- if (this.getTicket) {
163
- const ticket = await Promise.race([
164
- this.getTicket(),
165
- new Promise((_, reject) => setTimeout(() => reject(new Error('getTicket timeout')), 10_000)),
166
- ]);
167
- applyTicketAuth(connectOpts, ticket);
168
- }
169
- await this.client.connectWithOptions(serverUrl, connectOpts);
170
- }
171
- catch (err) {
172
- if (err instanceof Error && err.name === 'SessionExpiredError') {
173
- this.cancelReconnect();
174
- this.onSessionInvalid?.();
175
- return;
176
- }
177
- failed = true;
178
- throw err;
179
- }
180
- finally {
181
- this.reconnecting = false;
182
- if (failed && this.active && this.client?.isBrowserOnline()) {
183
- this.scheduleReconnect();
184
- }
185
- }
186
- }
187
- cleanupOnDisconnect() {
188
- this.subscribedScopes.clear();
189
- this.appliedVersion.clear();
190
- this.buffered.clear();
191
- this.awaitingState.clear();
192
- this.topLevelSubscribed = false;
193
- for (const [, { reject }] of this.pendingRequests) {
194
- reject(new Error('Disconnected'));
195
- }
196
- this.pendingRequests.clear();
197
- }
198
- scheduleReconnect(delayOverride) {
199
- if (this.reconnectTimer || this.reconnecting || !this.active || !this.serverUrl)
200
- return;
201
- if (!this.slowReconnecting && this.reconnectAttempt >= FAST_RECONNECT_LIMIT) {
202
- this.slowReconnecting = true;
203
- this.onConnectionStatus?.('disconnected');
204
- }
205
- const baseDelay = this.slowReconnecting
206
- ? SLOW_RECONNECT_INTERVAL
207
- : (delayOverride ?? Math.min(1000 * 2 ** this.reconnectAttempt, 30000));
208
- const jitter = Math.random() * (baseDelay / 4);
209
- const delay = Math.round(baseDelay + jitter);
210
- this.reconnectTimer = setTimeout(async () => {
211
- this.reconnectTimer = null;
212
- if (!this.active || !this.serverUrl)
213
- return;
214
- this.reconnectAttempt++;
215
- try {
216
- await this.reconnect(this.serverUrl, this.getTicket ?? undefined);
217
- }
218
- catch {
219
- if (this.active && this.client?.isBrowserOnline()) {
220
- this.scheduleReconnect();
221
- }
222
- }
223
- }, delay);
224
- }
225
- clearReconnectTimer() {
226
- if (this.reconnectTimer) {
227
- clearTimeout(this.reconnectTimer);
228
- this.reconnectTimer = null;
229
- }
230
- }
231
- cancelReconnect() {
232
- this.clearReconnectTimer();
233
- this.reconnectAttempt = 0;
234
- this.slowReconnecting = false;
235
- }
236
- disconnect() {
237
- this.active = false;
238
- this.reconnecting = false;
239
- this.cancelReconnect();
240
- this.cleanupOnDisconnect();
241
- try {
242
- this.client?.disconnect();
243
- }
244
- catch {
245
- // mqtt5-wasm may throw on disconnect
246
- }
247
- }
248
- setMutationHandler(handler) {
249
- this.onMutation = handler;
250
- }
251
- setConnectionStatusHandler(handler) {
252
- this.onConnectionStatus = handler;
253
- }
254
- setSessionInvalidHandler(handler) {
255
- this.onSessionInvalid = handler;
256
- }
257
- async subscribeToResponseTopic() {
258
- if (!this.client || !this.SubscribeOptions)
259
- return;
260
- const responseTopic = `${this.responsePrefix}/${this.clientId}/#`;
261
- const subOpts = new this.SubscribeOptions();
262
- subOpts.qos = 1;
263
- await this.client.subscribeWithOptions(responseTopic, (topic, payload) => {
264
- this.handleResponseMessage(topic, payload);
265
- }, subOpts);
266
- }
267
- async subscribeToTopLevel() {
268
- if (!this.client || !this.SubscribeOptions || this.topLevelSubscribed)
269
- return;
270
- const { rootEntity } = this.config.scope;
271
- const subOpts = new this.SubscribeOptions();
272
- subOpts.qos = 1;
273
- await this.client.subscribeWithOptions(`${this.prefix}/${rootEntity}/+/events/#`, (topic, payload, properties) => {
274
- this.handleRootEntityEvent(topic, payload, properties);
275
- }, subOpts);
276
- if (this.config.topLevelEntities) {
277
- for (const topLevel of this.config.topLevelEntities) {
278
- const tSubOpts = new this.SubscribeOptions();
279
- tSubOpts.qos = 1;
280
- await this.client.subscribeWithOptions(topLevel.subscriptionPattern, (topic, payload, properties) => {
281
- this.handleTopLevelEntityEvent(topLevel.entity, topic, payload, properties);
282
- }, tSubOpts);
283
- }
284
- }
285
- this.topLevelSubscribed = true;
286
- }
287
- handleRootEntityEvent(topic, payload, properties) {
288
- const parsed = this.parseScopedTopic(topic);
289
- if (!parsed || parsed.entity !== this.config.scope.rootEntity)
290
- return;
291
- if (this.subscribedScopes.has(parsed.scopeId))
292
- return;
293
- let event;
294
- try {
295
- const parsedPayload = JSON.parse(new TextDecoder().decode(payload));
296
- if (!SyncEngineImpl.isValidChangeEvent(parsedPayload))
297
- return;
298
- event = parsedPayload;
299
- }
300
- catch {
301
- return;
302
- }
303
- if (this.isOwnMutation(properties, event.sender))
304
- return;
305
- const opMap = {
306
- created: 'Create',
307
- updated: 'Update',
308
- deleted: 'Delete',
309
- };
310
- event.operation = opMap[parsed.eventType] ?? event.operation;
311
- const mutation = SyncEngineImpl.mapChangeEvent(this.config.scope.rootEntity, event);
312
- this.applyMutation('__global__', mutation);
313
- }
314
- handleTopLevelEntityEvent(entityName, topic, payload, properties) {
315
- const match = topic.match(new RegExp(`^\\${this.prefix}/${entityName}/events/(.+)$`));
316
- if (!match)
317
- return;
318
- let event;
319
- try {
320
- const parsedPayload = JSON.parse(new TextDecoder().decode(payload));
321
- if (!SyncEngineImpl.isValidChangeEvent(parsedPayload))
322
- return;
323
- event = parsedPayload;
324
- }
325
- catch {
326
- return;
327
- }
328
- if (this.isOwnMutation(properties, event.sender))
329
- return;
330
- const mutation = SyncEngineImpl.mapChangeEvent(entityName, event);
331
- this.applyMutation('__global__', mutation);
332
- }
333
- handleResponseMessage(topic, payload) {
334
- const match = topic.match(new RegExp(`^\\${this.responsePrefix}/[^/]+/(.+)$`));
335
- if (!match)
336
- return;
337
- const requestId = match[1];
338
- const pending = this.pendingRequests.get(requestId);
339
- if (pending) {
340
- this.pendingRequests.delete(requestId);
341
- try {
342
- const response = JSON.parse(new TextDecoder().decode(payload));
343
- pending.resolve(response);
344
- }
345
- catch {
346
- pending.reject(new Error('Invalid response format'));
347
- }
348
- }
349
- }
350
- async subscribeToScope(scopeId) {
351
- if (!this.client || !this.SubscribeOptions)
352
- return;
353
- const { rootEntity } = this.config.scope;
354
- const subOpts = new this.SubscribeOptions();
355
- subOpts.qos = 1;
356
- await this.client.subscribeWithOptions(`${this.prefix}/${rootEntity}/${scopeId}/#`, (topic, payload, properties) => {
357
- this.handleWatchMessage(topic, payload, properties);
358
- }, subOpts);
359
- }
360
- async unsubscribeFromScope(scopeId) {
361
- if (!this.client)
362
- return;
363
- const { rootEntity } = this.config.scope;
364
- await this.client.unsubscribe(`${this.prefix}/${rootEntity}/${scopeId}/#`);
365
- }
366
- static isValidChangeEvent(value) {
367
- if (typeof value !== 'object' || value === null)
368
- return false;
369
- const e = value;
370
- return (typeof e.id === 'string' &&
371
- e.id.length > 0 &&
372
- typeof e.operation === 'string' &&
373
- ['Create', 'Update', 'Delete'].includes(e.operation) &&
374
- (e.data === undefined || e.data === null || typeof e.data === 'object'));
375
- }
376
- static mapChangeEvent(entity, event) {
377
- const opMap = {
378
- Create: 'insert',
379
- Update: 'update',
380
- Delete: 'delete',
381
- };
382
- return {
383
- op: opMap[event.operation] ?? 'update',
384
- entity,
385
- id: event.id,
386
- data: event.data ?? null,
387
- operationId: event.operation_id ?? null,
388
- };
389
- }
390
- parseScopedTopic(topic) {
391
- const { rootEntity } = this.config.scope;
392
- const rootOwn = topic.match(new RegExp(`^\\${this.prefix}/${rootEntity}/([^/]+)/events/(created|updated|deleted)$`));
393
- if (rootOwn) {
394
- return { scopeId: rootOwn[1], entity: rootEntity, eventType: rootOwn[2] };
395
- }
396
- const childEntity = topic.match(new RegExp(`^\\${this.prefix}/${rootEntity}/([^/]+)/(\\w+)/events/(created|updated|deleted)$`));
397
- if (childEntity) {
398
- return { scopeId: childEntity[1], entity: childEntity[2], eventType: childEntity[3] };
399
- }
400
- return null;
401
- }
402
- static extractOriginClientId(properties) {
403
- if (!properties)
404
- return null;
405
- const userProps = properties.getUserProperties();
406
- for (let i = 0; i < userProps.length; i++) {
407
- const [key, value] = userProps[i];
408
- if (key === 'x-origin-client-id')
409
- return value;
410
- }
411
- return null;
412
- }
413
- isOwnMutation(properties, eventSender) {
414
- if (eventSender === this.clientId)
415
- return true;
416
- const origin = SyncEngineImpl.extractOriginClientId(properties);
417
- return origin === this.clientId;
418
- }
419
- handleWatchMessage(topic, payload, properties) {
420
- const parsed = this.parseScopedTopic(topic);
421
- if (!parsed)
422
- return;
423
- const { scopeId, entity, eventType } = parsed;
424
- let event;
425
- try {
426
- const parsedPayload = JSON.parse(new TextDecoder().decode(payload));
427
- if (!SyncEngineImpl.isValidChangeEvent(parsedPayload))
428
- return;
429
- event = parsedPayload;
430
- }
431
- catch {
432
- return;
433
- }
434
- const opMap = {
435
- created: 'Create',
436
- updated: 'Update',
437
- deleted: 'Delete',
438
- };
439
- event.operation = opMap[eventType] ?? event.operation;
440
- if (this.isOwnMutation(properties, event.sender))
441
- return;
442
- const mutation = SyncEngineImpl.mapChangeEvent(entity, event);
443
- if (this.awaitingState.has(scopeId)) {
444
- this.buffered.get(scopeId)?.push(mutation);
445
- return;
446
- }
447
- this.applyMutation(scopeId, mutation);
448
- }
449
- applyMutation(scopeId, mutation) {
450
- this.onMutation?.(scopeId, mutation);
451
- }
452
- async openScope(scopeId) {
453
- if (!this.client)
454
- throw new Error('Not connected');
455
- const { rootEntity, childEntities, scopeField } = this.config.scope;
456
- this.desiredScopes.add(scopeId);
457
- this.awaitingState.add(scopeId);
458
- this.buffered.set(scopeId, []);
459
- this.subscribedScopes.add(scopeId);
460
- try {
461
- await this.subscribeToScope(scopeId);
462
- const fetchPromises = childEntities.map((entity) => this.fetchList(entity, scopeId));
463
- const [rootRecord, ...childResults] = await Promise.all([
464
- this.fetchOne(rootEntity, scopeId),
465
- ...fetchPromises,
466
- ]);
467
- const root = rootRecord ?? {};
468
- const children = {};
469
- for (let i = 0; i < childEntities.length; i++) {
470
- children[childEntities[i]] = childResults[i] ?? [];
471
- }
472
- const versionField = this.config.versionField ?? 'version';
473
- const version = root[versionField] || 0;
474
- this.appliedVersion.set(scopeId, version);
475
- const buffer = this.buffered.get(scopeId) || [];
476
- const bufferedMutations = [];
477
- for (const mutation of buffer) {
478
- const belongsToScope = mutation.entity === rootEntity
479
- ? mutation.id === scopeId
480
- : mutation.data?.[scopeField] === scopeId;
481
- if (belongsToScope) {
482
- bufferedMutations.push(mutation);
483
- }
484
- }
485
- this.buffered.delete(scopeId);
486
- this.awaitingState.delete(scopeId);
487
- return { root, children, version, bufferedMutations };
488
- }
489
- catch (err) {
490
- this.buffered.delete(scopeId);
491
- this.awaitingState.delete(scopeId);
492
- this.subscribedScopes.delete(scopeId);
493
- throw err;
494
- }
495
- }
496
- async closeScope(scopeId) {
497
- this.desiredScopes.delete(scopeId);
498
- this.subscribedScopes.delete(scopeId);
499
- this.buffered.delete(scopeId);
500
- this.awaitingState.delete(scopeId);
501
- this.appliedVersion.delete(scopeId);
502
- await this.unsubscribeFromScope(scopeId);
503
- }
504
- checkResponseAndAuth(response) {
505
- if (response.status === 'error') {
506
- const msg = response.message || 'Operation failed';
507
- if (response.code === 401) {
508
- this.onSessionInvalid?.();
509
- throw new Error(msg);
510
- }
511
- if (response.code === 403) {
512
- throw new OwnershipError(msg);
513
- }
514
- throw new Error(msg);
515
- }
516
- }
517
- extractResponseId(response) {
518
- const data = response.data;
519
- if (typeof data !== 'object' || data === null) {
520
- throw new Error('Response missing data payload');
521
- }
522
- const id = data.id;
523
- if (typeof id !== 'string' || id.length === 0) {
524
- throw new Error('Response missing valid id');
525
- }
526
- return id;
527
- }
528
- async createEntity(entity, scopeId, data) {
529
- const { scopeField, childEntities } = this.config.scope;
530
- const isChild = childEntities.includes(entity);
531
- const payload = isChild && scopeId ? { ...data, [scopeField]: scopeId } : data;
532
- const response = await this.request(`${this.prefix}/${entity}/create`, payload);
533
- this.checkResponseAndAuth(response);
534
- const id = this.extractResponseId(response);
535
- if (isChild && scopeId) {
536
- await this.bumpScopeVersion(scopeId);
537
- }
538
- return id;
539
- }
540
- async updateEntity(entity, scopeId, id, data) {
541
- const response = await this.request(`${this.prefix}/${entity}/${id}/update`, data);
542
- this.checkResponseAndAuth(response);
543
- const { childEntities } = this.config.scope;
544
- if (childEntities.includes(entity) && scopeId) {
545
- await this.bumpScopeVersion(scopeId);
546
- }
547
- }
548
- async deleteEntity(entity, scopeId, id) {
549
- const response = await this.request(`${this.prefix}/${entity}/${id}/delete`, {});
550
- this.checkResponseAndAuth(response);
551
- const { childEntities } = this.config.scope;
552
- if (childEntities.includes(entity) && scopeId) {
553
- await this.bumpScopeVersion(scopeId);
554
- }
555
- }
556
- async bumpScopeVersion(scopeId) {
557
- const { rootEntity } = this.config.scope;
558
- const versionField = this.config.versionField ?? 'version';
559
- const updatedAtField = this.config.updatedAtField ?? 'updatedAt';
560
- const now = Date.now();
561
- const response = await this.request(`${this.prefix}/${rootEntity}/${scopeId}/update`, {
562
- [versionField]: now,
563
- [updatedAtField]: now,
564
- });
565
- this.checkResponseAndAuth(response);
566
- this.appliedVersion.set(scopeId, now);
567
- }
568
- async fetchOne(entity, id) {
569
- try {
570
- const response = await this.request(`${this.prefix}/${entity}/${id}`, {});
571
- this.checkResponseAndAuth(response);
572
- return response.data;
573
- }
574
- catch {
575
- return null;
576
- }
577
- }
578
- async fetchList(entity, scopeId, sort) {
579
- try {
580
- const payload = {};
581
- if (scopeId) {
582
- const { scopeField } = this.config.scope;
583
- payload.filters = [{ field: scopeField, op: 'eq', value: scopeId }];
584
- }
585
- if (sort && sort.length > 0) {
586
- payload.sort = sort;
587
- }
588
- const response = await this.request(`${this.prefix}/${entity}/list`, payload);
589
- this.checkResponseAndAuth(response);
590
- return response.data || [];
591
- }
592
- catch {
593
- return null;
594
- }
595
- }
596
- async request(topic, payload) {
597
- if (!this.client || !this.PublishOptions)
598
- throw new Error('Not connected');
599
- const requestId = crypto.randomUUID();
600
- const responseTopic = `${this.responsePrefix}/${this.clientId}/${requestId}`;
601
- return new Promise((resolve, reject) => {
602
- const timeout = setTimeout(() => {
603
- this.pendingRequests.delete(requestId);
604
- reject(new Error('Request timeout'));
605
- }, 10000);
606
- this.pendingRequests.set(requestId, {
607
- resolve: (value) => {
608
- clearTimeout(timeout);
609
- resolve(value);
610
- },
611
- reject: (error) => {
612
- clearTimeout(timeout);
613
- reject(error);
614
- },
615
- });
616
- const encoder = new TextEncoder();
617
- const pubOpts = new this.PublishOptions();
618
- pubOpts.qos = 1;
619
- pubOpts.retain = false;
620
- pubOpts.responseTopic = responseTopic;
621
- pubOpts.correlationData = encoder.encode(requestId);
622
- const body = JSON.stringify(payload);
623
- this.client.publishWithOptions(topic, encoder.encode(body), pubOpts).catch((err) => {
624
- this.pendingRequests.delete(requestId);
625
- clearTimeout(timeout);
626
- reject(err);
627
- });
628
- });
629
- }
630
- get isReconnecting() {
631
- return this.reconnecting || this.reconnectTimer !== null;
632
- }
633
- isSubscribedTo(scopeId) {
634
- return this.subscribedScopes.has(scopeId);
635
- }
636
- getAppliedVersion(scopeId) {
637
- return this.appliedVersion.get(scopeId) || 0;
638
- }
639
- }
640
- export function createSyncEngine(clientId, config) {
641
- return new SyncEngineImpl(clientId, config);
642
- }
643
- //# sourceMappingURL=sync-engine.js.map