@arcote.tech/arc 0.7.15 → 0.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -119,6 +119,56 @@ class AuthAdapter {
119
119
  this.scopes.clear();
120
120
  }
121
121
  }
122
+ // src/adapters/module-sync-coordinator.ts
123
+ var DEFAULT_TIMEOUT_MS = 8000;
124
+ var provider = null;
125
+ var latestSync = null;
126
+ var syncSeq = 0;
127
+ function registerModuleSyncProvider(fn) {
128
+ provider = fn;
129
+ return () => {
130
+ if (provider === fn)
131
+ provider = null;
132
+ };
133
+ }
134
+ function hasModuleSyncProvider() {
135
+ return provider !== null;
136
+ }
137
+ function triggerModuleSync(scope, timeoutMs = DEFAULT_TIMEOUT_MS) {
138
+ if (!provider) {
139
+ latestSync = Promise.resolve();
140
+ return latestSync;
141
+ }
142
+ const seq = ++syncSeq;
143
+ const run = (async () => {
144
+ try {
145
+ await provider(scope);
146
+ } catch (err) {
147
+ console.warn("[arc] module sync failed during setToken:", err);
148
+ }
149
+ })();
150
+ const guarded = new Promise((resolve) => {
151
+ let settled = false;
152
+ const done = () => {
153
+ if (settled)
154
+ return;
155
+ settled = true;
156
+ resolve();
157
+ };
158
+ const timer = setTimeout(() => {
159
+ console.warn(`[arc] module sync did not complete within ${timeoutMs}ms; proceeding anyway.`);
160
+ done();
161
+ }, timeoutMs);
162
+ timer?.unref?.();
163
+ run.then(done, done).finally(() => clearTimeout(timer));
164
+ });
165
+ if (seq === syncSeq)
166
+ latestSync = guarded;
167
+ return guarded;
168
+ }
169
+ function awaitModuleSync() {
170
+ return latestSync ?? Promise.resolve();
171
+ }
122
172
  // src/adapters/wire.ts
123
173
  class Wire {
124
174
  baseUrl;
@@ -211,7 +261,8 @@ class EventWire {
211
261
  onSyncedCallback;
212
262
  reconnectTimeout;
213
263
  syncRequested = false;
214
- viewSubscriptions = new Map;
264
+ querySubscriptions = new Map;
265
+ querySubCounter = 0;
215
266
  enableEventSync;
216
267
  constructor(baseUrl, options) {
217
268
  this.baseUrl = baseUrl;
@@ -266,7 +317,7 @@ class EventWire {
266
317
  this.requestSync();
267
318
  }
268
319
  this.flushPendingEvents();
269
- this.sendAllViewSubscriptions();
320
+ this.sendAllQuerySubscriptions();
270
321
  } else {
271
322
  console.log(`[EventWire] onopen called but ws is not OPEN, readyState:`, this.ws?.readyState);
272
323
  }
@@ -342,24 +393,29 @@ class EventWire {
342
393
  onSynced(callback) {
343
394
  this.onSyncedCallback = callback;
344
395
  }
345
- subscribeView(element, scope, callbacks) {
346
- const key = `${scope}:${element}`;
347
- this.viewSubscriptions.set(key, callbacks);
396
+ subscribeQuery(descriptor, scope, callbacks) {
397
+ const subscriptionId = `qs_${this.instanceId}_${++this.querySubCounter}`;
398
+ this.querySubscriptions.set(subscriptionId, {
399
+ descriptor,
400
+ scope,
401
+ callbacks
402
+ });
348
403
  if (this.state === "connected" && this.ws) {
349
404
  this.ws.send(JSON.stringify({
350
- type: "subscribe-view",
351
- element,
405
+ type: "subscribe-query",
406
+ subscriptionId,
407
+ descriptor,
352
408
  scope
353
409
  }));
354
410
  }
411
+ return subscriptionId;
355
412
  }
356
- unsubscribeView(element, scope) {
357
- this.viewSubscriptions.delete(`${scope}:${element}`);
413
+ unsubscribeQuery(subscriptionId) {
414
+ this.querySubscriptions.delete(subscriptionId);
358
415
  if (this.state === "connected" && this.ws) {
359
416
  this.ws.send(JSON.stringify({
360
- type: "unsubscribe-view",
361
- element,
362
- scope
417
+ type: "unsubscribe-query",
418
+ subscriptionId
363
419
  }));
364
420
  }
365
421
  }
@@ -394,17 +450,17 @@ class EventWire {
394
450
  this.lastHostEventId = message.lastHostEventId;
395
451
  }
396
452
  break;
397
- case "view-snapshot": {
398
- const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
453
+ case "query-snapshot": {
454
+ const sub = this.querySubscriptions.get(message.subscriptionId);
399
455
  if (sub) {
400
- sub.onSnapshot(message.items ?? []);
456
+ sub.callbacks.onSnapshot(message.result ?? null);
401
457
  }
402
458
  break;
403
459
  }
404
- case "view-changes": {
405
- const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
460
+ case "query-changes": {
461
+ const sub = this.querySubscriptions.get(message.subscriptionId);
406
462
  if (sub && Array.isArray(message.changes)) {
407
- sub.onChanges(message.changes);
463
+ sub.callbacks.onChanges(message.changes);
408
464
  }
409
465
  break;
410
466
  }
@@ -432,17 +488,15 @@ class EventWire {
432
488
  this.pendingEvents = [];
433
489
  }
434
490
  }
435
- sendAllViewSubscriptions() {
491
+ sendAllQuerySubscriptions() {
436
492
  if (!this.ws || this.state !== "connected")
437
493
  return;
438
- for (const key of this.viewSubscriptions.keys()) {
439
- const sepIdx = key.indexOf(":");
440
- const scope = key.slice(0, sepIdx);
441
- const element = key.slice(sepIdx + 1);
494
+ for (const [subscriptionId, sub] of this.querySubscriptions) {
442
495
  this.ws.send(JSON.stringify({
443
- type: "subscribe-view",
444
- element,
445
- scope
496
+ type: "subscribe-query",
497
+ subscriptionId,
498
+ descriptor: sub.descriptor,
499
+ scope: sub.scope
446
500
  }));
447
501
  }
448
502
  }
@@ -484,19 +538,12 @@ class LocalEventPublisher {
484
538
  views = [];
485
539
  syncCallback;
486
540
  subscribers = new Map;
487
- viewChangesCallbacks = new Set;
488
541
  constructor(dataStorage) {
489
542
  this.dataStorage = dataStorage;
490
543
  }
491
544
  onPublish(callback) {
492
545
  this.syncCallback = callback;
493
546
  }
494
- onViewChanges(callback) {
495
- this.viewChangesCallbacks.add(callback);
496
- return () => {
497
- this.viewChangesCallbacks.delete(callback);
498
- };
499
- }
500
547
  registerViews(views) {
501
548
  this.views = views;
502
549
  }
@@ -564,19 +611,7 @@ class LocalEventPublisher {
564
611
  });
565
612
  const viewChanges = await this.collectViewChanges(event);
566
613
  allChanges.push(...viewChanges);
567
- const viewStoreNames = new Set(viewChanges.map((c) => c.store));
568
- const committed = await this.dataStorage.commitChanges(allChanges, {
569
- captureRowsFor: viewStoreNames
570
- });
571
- if (committed.length > 0 && this.viewChangesCallbacks.size > 0) {
572
- for (const callback of this.viewChangesCallbacks) {
573
- try {
574
- callback(committed);
575
- } catch (error) {
576
- console.error(`[EventPublisher] onViewChanges callback error:`, error);
577
- }
578
- }
579
- }
614
+ await this.dataStorage.commitChanges(allChanges);
580
615
  await this.notifySubscribers(event);
581
616
  if (this.syncCallback) {
582
617
  this.syncCallback(event);
@@ -1262,9 +1297,8 @@ function object(element) {
1262
1297
 
1263
1298
  // src/data-storage/data-storage.abstract.ts
1264
1299
  class DataStorage {
1265
- async commitChanges(changes, _options) {
1300
+ async commitChanges(changes) {
1266
1301
  await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
1267
- return [];
1268
1302
  }
1269
1303
  }
1270
1304
 
@@ -2229,11 +2263,6 @@ class ArcAggregateElement extends ArcContextElement {
2229
2263
  }
2230
2264
  return adapters.dataStorage.getStore(viewName).find(options);
2231
2265
  }
2232
- if (adapters.streamingCache) {
2233
- const store = adapters.streamingCache.getStore(viewName, adapters.scope?.scopeName);
2234
- if (store.hasData())
2235
- return store.find(options);
2236
- }
2237
2266
  if (adapters.queryWire)
2238
2267
  return adapters.queryWire.query(viewName, options);
2239
2268
  return [];
@@ -2261,12 +2290,6 @@ class ArcAggregateElement extends ArcContextElement {
2261
2290
  }
2262
2291
  };
2263
2292
  }
2264
- getRestrictionsFor(adapters) {
2265
- return {
2266
- restrictions: this.getScopeRestrictions(adapters),
2267
- denied: this.isScopeDenied(adapters)
2268
- };
2269
- }
2270
2293
  isScopeDenied(adapters, protections = this._protections) {
2271
2294
  if (protections.length === 0)
2272
2295
  return false;
@@ -2912,13 +2935,6 @@ class ArcView extends ArcContextElement {
2912
2935
  }
2913
2936
  return adapters.dataStorage.getStore(viewName).find(options);
2914
2937
  }
2915
- if (adapters.streamingCache) {
2916
- const store = adapters.streamingCache.getStore(viewName, adapters.scope?.scopeName);
2917
- if (store.hasData()) {
2918
- const where = restrictions ? { ...options?.where || {}, ...restrictions } : options?.where;
2919
- return store.find({ ...options, where });
2920
- }
2921
- }
2922
2938
  if (adapters.queryWire) {
2923
2939
  const where = restrictions ? { ...options?.where || {}, ...restrictions } : options?.where;
2924
2940
  return adapters.queryWire.query(viewName, { ...options, where });
@@ -2937,13 +2953,6 @@ class ArcView extends ArcContextElement {
2937
2953
  const results = await adapters.dataStorage.getStore(viewName).find({ where });
2938
2954
  return results[0];
2939
2955
  }
2940
- if (adapters.streamingCache) {
2941
- const store = adapters.streamingCache.getStore(viewName, adapters.scope?.scopeName);
2942
- if (store.hasData()) {
2943
- const mergedWhere = restrictions ? { ...where || {}, ...restrictions } : where;
2944
- return store.findOne(mergedWhere);
2945
- }
2946
- }
2947
2956
  if (adapters.queryWire) {
2948
2957
  const mergedWhere = restrictions ? { ...where, ...restrictions } : where;
2949
2958
  const results = await adapters.queryWire.query(viewName, { where: mergedWhere });
@@ -2953,9 +2962,6 @@ class ArcView extends ArcContextElement {
2953
2962
  }
2954
2963
  };
2955
2964
  }
2956
- getRestrictionsFor(adapters) {
2957
- return this.resolveProtection(this.data.protections || [], adapters);
2958
- }
2959
2965
  resolveProtection(protections, adapters) {
2960
2966
  if (protections.length === 0)
2961
2967
  return { restrictions: null, denied: false };
@@ -3403,12 +3409,8 @@ class MasterStoreState extends StoreState {
3403
3409
  }
3404
3410
  return transaction.find(this.storeName, { where: { _id: id2 } }).then((results) => results[0]);
3405
3411
  }
3406
- async applyChangeAndReturnEvent(transaction, change, transactionCache, options) {
3412
+ async applyChangeAndReturnEvent(transaction, change, transactionCache) {
3407
3413
  if (change.type === "set") {
3408
- let existing;
3409
- if (options?.captureRows) {
3410
- existing = await this.readExisting(transaction, change.data._id, transactionCache);
3411
- }
3412
3414
  await transaction.set(this.storeName, change.data);
3413
3415
  const item = this.deserialize ? this.deserialize(change.data) : change.data;
3414
3416
  if (transactionCache) {
@@ -3421,16 +3423,10 @@ class MasterStoreState extends StoreState {
3421
3423
  type: "set",
3422
3424
  item: change.data,
3423
3425
  id: change.data._id
3424
- },
3425
- oldRow: existing ?? null,
3426
- newRow: change.data
3426
+ }
3427
3427
  };
3428
3428
  }
3429
3429
  if (change.type === "delete") {
3430
- let existing;
3431
- if (options?.captureRows) {
3432
- existing = await this.readExisting(transaction, change.id, transactionCache);
3433
- }
3434
3430
  await transaction.remove(this.storeName, change.id);
3435
3431
  if (transactionCache) {
3436
3432
  transactionCache.delete(`${this.storeName}:${change.id}`);
@@ -3442,9 +3438,7 @@ class MasterStoreState extends StoreState {
3442
3438
  type: "delete",
3443
3439
  item: null,
3444
3440
  id: change.id
3445
- },
3446
- oldRow: existing ?? null,
3447
- newRow: null
3441
+ }
3448
3442
  };
3449
3443
  }
3450
3444
  if (change.type === "modify") {
@@ -3462,9 +3456,7 @@ class MasterStoreState extends StoreState {
3462
3456
  type: "set",
3463
3457
  item,
3464
3458
  id: change.id
3465
- },
3466
- oldRow: existing ?? null,
3467
- newRow: updated
3459
+ }
3468
3460
  };
3469
3461
  }
3470
3462
  if (change.type === "mutate") {
@@ -3482,9 +3474,7 @@ class MasterStoreState extends StoreState {
3482
3474
  type: "set",
3483
3475
  item,
3484
3476
  id: change.id
3485
- },
3486
- oldRow: existing ?? null,
3487
- newRow: updated
3477
+ }
3488
3478
  };
3489
3479
  }
3490
3480
  throw new Error("Unknown change type");
@@ -3563,22 +3553,17 @@ class MasterDataStorage extends DataStorage {
3563
3553
  applySerializedChanges(changes) {
3564
3554
  return Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applySerializedChanges(changes2)));
3565
3555
  }
3566
- async commitChanges(changes, options) {
3556
+ async commitChanges(changes) {
3567
3557
  const transaction = await this.getReadWriteTransaction();
3568
3558
  const transactionCache = new Map;
3569
3559
  const eventsByStore = new Map;
3570
- const committed = [];
3571
3560
  for (const { store, changes: storeChanges } of changes) {
3572
3561
  const storeState = this.getStore(store);
3573
3562
  const storeEvents = [];
3574
- const capture = options?.captureRowsFor?.has(store) ?? false;
3575
3563
  for (const change of storeChanges) {
3576
- const { event: event3, oldRow, newRow } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache, { captureRows: capture });
3564
+ const { event: event3 } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache);
3577
3565
  if (event3)
3578
3566
  storeEvents.push(event3);
3579
- if (capture) {
3580
- committed.push({ store, id: event3.id, oldRow, newRow });
3581
- }
3582
3567
  }
3583
3568
  if (storeEvents.length > 0) {
3584
3569
  eventsByStore.set(store, storeEvents);
@@ -3589,7 +3574,6 @@ class MasterDataStorage extends DataStorage {
3589
3574
  const storeState = this.getStore(store);
3590
3575
  storeState.notifyListenersPublic(events);
3591
3576
  }
3592
- return committed;
3593
3577
  }
3594
3578
  fork() {
3595
3579
  return new ForkedDataStorage(this);
@@ -3706,8 +3690,8 @@ class ObservableDataStorage {
3706
3690
  getReadWriteTransaction() {
3707
3691
  return this.source.getReadWriteTransaction();
3708
3692
  }
3709
- commitChanges(changes, options) {
3710
- return this.source.commitChanges(changes, options);
3693
+ commitChanges(changes) {
3694
+ return this.source.commitChanges(changes);
3711
3695
  }
3712
3696
  trackQuery(storeName, options, result, listener4) {
3713
3697
  const key = this.getQueryKey(storeName, options);
@@ -3720,7 +3704,8 @@ class ObservableDataStorage {
3720
3704
  }
3721
3705
  handleStoreChange(storeName, events) {
3722
3706
  let hasChanges = false;
3723
- for (const query of this.trackedQueries.values()) {
3707
+ const staleKeys = [];
3708
+ for (const [key, query] of this.trackedQueries) {
3724
3709
  if (query.storeName !== storeName)
3725
3710
  continue;
3726
3711
  let currentResult = query.result;
@@ -3732,10 +3717,20 @@ class ObservableDataStorage {
3732
3717
  queryChanged = true;
3733
3718
  }
3734
3719
  }
3735
- if (queryChanged) {
3736
- query.result = currentResult;
3720
+ if (!queryChanged)
3721
+ continue;
3722
+ if (query.options.limit !== undefined && query.result.length === query.options.limit && currentResult.length < query.options.limit) {
3723
+ staleKeys.push(key);
3737
3724
  hasChanges = true;
3725
+ continue;
3738
3726
  }
3727
+ query.result = currentResult;
3728
+ hasChanges = true;
3729
+ }
3730
+ for (const key of staleKeys) {
3731
+ const query = this.trackedQueries.get(key);
3732
+ this.source.getStore(query.storeName).unsubscribe(query.listener);
3733
+ this.trackedQueries.delete(key);
3739
3734
  }
3740
3735
  if (hasChanges) {
3741
3736
  this.onChange();
@@ -4507,6 +4502,7 @@ class ScopedModel {
4507
4502
  for (const listener4 of this.tokenListeners) {
4508
4503
  listener4();
4509
4504
  }
4505
+ return triggerModuleSync(this.scopeName);
4510
4506
  }
4511
4507
  getToken() {
4512
4508
  return this.authAdapter.getToken();
@@ -4621,245 +4617,260 @@ function mutationExecutor(model) {
4621
4617
  }
4622
4618
  });
4623
4619
  }
4624
- // src/streaming/streaming-query-cache.ts
4625
- var DEFAULT_SCOPE = "default";
4626
-
4627
- class StreamingQueryCache {
4628
- stores = new Map;
4629
- views = [];
4630
- activeStreams = new Map;
4631
- pendingUnsubscribes = new Map;
4632
- static UNSUBSCRIBE_DELAY_MS = 5000;
4633
- storeKey(viewName, scope) {
4634
- return `${scope ?? DEFAULT_SCOPE}:${viewName}`;
4635
- }
4636
- registerViews(views) {
4637
- this.views = views;
4638
- }
4639
- getStore(viewName, scope) {
4640
- const key = this.storeKey(viewName, scope);
4641
- if (!this.stores.has(key)) {
4642
- this.stores.set(key, new StreamingStore);
4620
+ // src/model/live-query/diff.ts
4621
+ function isIdList(value) {
4622
+ return Array.isArray(value) && value.every((it) => it && typeof it === "object" && typeof it._id === "string");
4623
+ }
4624
+ function diffResults(prev, next) {
4625
+ if (!isIdList(prev) || !isIdList(next)) {
4626
+ return JSON.stringify(prev) === JSON.stringify(next) ? { kind: "none" } : { kind: "snapshot", result: next };
4627
+ }
4628
+ const json = (o) => JSON.stringify(o);
4629
+ const nextIds = new Set(next.map((it) => it._id));
4630
+ const changes = [];
4631
+ for (const it of prev) {
4632
+ if (!nextIds.has(it._id)) {
4633
+ changes.push({ type: "delete", id: it._id });
4634
+ }
4635
+ }
4636
+ const sim = prev.filter((it) => nextIds.has(it._id));
4637
+ for (let i = 0;i < next.length; i++) {
4638
+ const target = next[i];
4639
+ if (sim[i] && sim[i]._id === target._id && json(sim[i]) === json(target)) {
4640
+ continue;
4643
4641
  }
4644
- return this.stores.get(key);
4642
+ changes.push({ type: "set", id: target._id, item: target, index: i });
4643
+ const oldIdx = sim.findIndex((it) => it._id === target._id);
4644
+ if (oldIdx !== -1)
4645
+ sim.splice(oldIdx, 1);
4646
+ sim.splice(i, 0, target);
4645
4647
  }
4646
- hasData(viewName, scope) {
4647
- const store = this.stores.get(this.storeKey(viewName, scope));
4648
- return store ? store.hasData() : false;
4648
+ if (changes.length === 0)
4649
+ return { kind: "none" };
4650
+ if (changes.length > next.length) {
4651
+ return { kind: "snapshot", result: next };
4649
4652
  }
4650
- registerStream(key, createStream) {
4651
- const pending = this.pendingUnsubscribes.get(key);
4652
- if (pending) {
4653
- clearTimeout(pending);
4654
- this.pendingUnsubscribes.delete(key);
4655
- }
4656
- const existing = this.activeStreams.get(key);
4657
- if (existing) {
4658
- existing.refCount++;
4659
- return {
4660
- unsubscribe: () => this.unregisterStream(key),
4661
- wasReused: true
4662
- };
4663
- }
4664
- const streamConn = createStream();
4665
- this.activeStreams.set(key, {
4666
- unsubscribe: streamConn.unsubscribe,
4667
- refCount: 1
4668
- });
4669
- return {
4670
- unsubscribe: () => this.unregisterStream(key),
4671
- wasReused: false
4672
- };
4653
+ if (sim.length !== next.length || json(sim) !== json(next)) {
4654
+ return { kind: "snapshot", result: next };
4673
4655
  }
4674
- unregisterStream(key) {
4675
- const stream = this.activeStreams.get(key);
4676
- if (!stream)
4677
- return;
4678
- stream.refCount--;
4679
- if (stream.refCount <= 0) {
4680
- const timeout = setTimeout(() => {
4681
- this.pendingUnsubscribes.delete(key);
4682
- const current = this.activeStreams.get(key);
4683
- if (current && current.refCount <= 0) {
4684
- current.unsubscribe();
4685
- this.activeStreams.delete(key);
4686
- }
4687
- }, StreamingQueryCache.UNSUBSCRIBE_DELAY_MS);
4688
- this.pendingUnsubscribes.set(key, timeout);
4689
- }
4690
- }
4691
- subscribeView(viewName, eventWire, scope) {
4692
- const key = this.storeKey(viewName, scope);
4693
- const { unsubscribe } = this.registerStream(key, () => {
4694
- const store = this.stores.get(key) ?? new StreamingStore;
4695
- this.stores.set(key, store);
4696
- eventWire.subscribeView(viewName, scope ?? DEFAULT_SCOPE, {
4697
- onSnapshot: (items) => store.setAll(items),
4698
- onChanges: (changes) => store.applyChanges(changes)
4699
- });
4700
- return {
4701
- unsubscribe: () => eventWire.unsubscribeView(viewName, scope ?? DEFAULT_SCOPE)
4702
- };
4703
- });
4704
- return unsubscribe;
4705
- }
4706
- invalidateScope(scope) {
4707
- const prefix = `${scope}:`;
4708
- for (const [key, timeout] of this.pendingUnsubscribes) {
4709
- if (!key.startsWith(prefix))
4710
- continue;
4711
- clearTimeout(timeout);
4712
- this.pendingUnsubscribes.delete(key);
4713
- }
4714
- for (const [key, stream] of this.activeStreams) {
4715
- if (!key.startsWith(prefix))
4716
- continue;
4717
- try {
4718
- stream.unsubscribe();
4719
- } catch {}
4720
- this.activeStreams.delete(key);
4721
- }
4722
- for (const [key, store] of this.stores) {
4723
- if (!key.startsWith(prefix))
4724
- continue;
4725
- store.clear();
4726
- }
4727
- }
4728
- async applyEvent(event3) {
4729
- for (const view3 of this.views) {
4730
- const handlers = view3.getHandlers();
4731
- const handler = handlers[event3.type];
4732
- if (!handler)
4733
- continue;
4734
- const suffix = `:${view3.name}`;
4735
- for (const [key, store] of this.stores) {
4736
- if (!key.endsWith(suffix))
4737
- continue;
4738
- const ctx = {
4739
- set: async (id3, data) => {
4740
- store.set(String(id3), { _id: String(id3), ...data });
4741
- },
4742
- modify: async (id3, data) => {
4743
- store.modify(String(id3), data);
4744
- },
4745
- remove: async (id3) => {
4746
- store.remove(String(id3));
4747
- },
4748
- find: async (options) => {
4749
- return store.find(options);
4750
- },
4751
- findOne: async (where) => {
4752
- return store.findOne(where);
4753
- },
4754
- $auth: {}
4755
- };
4756
- await handler(ctx, event3);
4757
- }
4656
+ return { kind: "changes", changes };
4657
+ }
4658
+ function applyQueryChanges(result, changes) {
4659
+ const next = [...result];
4660
+ for (const change of changes) {
4661
+ if (change.type === "delete") {
4662
+ const idx = next.findIndex((it) => it._id === change.id);
4663
+ if (idx !== -1)
4664
+ next.splice(idx, 1);
4758
4665
  }
4759
4666
  }
4760
- clear() {
4761
- for (const stream of this.activeStreams.values()) {
4762
- stream.unsubscribe();
4763
- }
4764
- this.activeStreams.clear();
4765
- for (const timeout of this.pendingUnsubscribes.values()) {
4766
- clearTimeout(timeout);
4767
- }
4768
- this.pendingUnsubscribes.clear();
4769
- for (const store of this.stores.values()) {
4770
- store.clear();
4667
+ for (const change of changes) {
4668
+ if (change.type === "set") {
4669
+ const idx = next.findIndex((it) => it._id === change.id);
4670
+ if (idx !== -1)
4671
+ next.splice(idx, 1);
4672
+ next.splice(change.index, 0, change.item);
4771
4673
  }
4772
4674
  }
4675
+ return next;
4773
4676
  }
4774
4677
 
4775
- class StreamingStore {
4776
- data = new Map;
4777
- listeners = new Set;
4778
- initialized = false;
4779
- hasData() {
4780
- return this.initialized;
4678
+ // src/model/live-query/live-query-subscription.ts
4679
+ class LiveQuery {
4680
+ model;
4681
+ descriptor;
4682
+ scope;
4683
+ rawToken;
4684
+ onUpdate;
4685
+ observable = null;
4686
+ adapters = null;
4687
+ lastResult;
4688
+ scheduled = false;
4689
+ running = false;
4690
+ rerunRequested = false;
4691
+ stopped = false;
4692
+ constructor(model, descriptor, scope, rawToken, onUpdate) {
4693
+ this.model = model;
4694
+ this.descriptor = descriptor;
4695
+ this.scope = scope;
4696
+ this.rawToken = rawToken;
4697
+ this.onUpdate = onUpdate;
4698
+ }
4699
+ async start() {
4700
+ const scoped = new ScopedModel(this.model, this.scope);
4701
+ if (this.rawToken)
4702
+ scoped.setToken(this.rawToken);
4703
+ const baseAdapters = scoped.getAdapters();
4704
+ if (!baseAdapters.dataStorage) {
4705
+ throw new Error("LiveQuery requires a dataStorage adapter (server-side)");
4706
+ }
4707
+ this.observable = new ObservableDataStorage(baseAdapters.dataStorage, () => this.schedule());
4708
+ this.adapters = {
4709
+ ...baseAdapters,
4710
+ dataStorage: this.observable
4711
+ };
4712
+ this.lastResult = await executeDescriptor(this.descriptor, this.model.context, this.adapters, "queryContext", { fromWire: true });
4713
+ return this.lastResult;
4781
4714
  }
4782
- setAll(items) {
4783
- this.initialized = true;
4784
- this.data.clear();
4785
- for (const item of items) {
4786
- this.data.set(item._id, item);
4715
+ flush() {
4716
+ this.schedule();
4717
+ }
4718
+ stop() {
4719
+ this.stopped = true;
4720
+ this.observable?.clear();
4721
+ }
4722
+ schedule() {
4723
+ if (this.stopped)
4724
+ return;
4725
+ if (this.running) {
4726
+ this.rerunRequested = true;
4727
+ return;
4787
4728
  }
4788
- this.notifyListeners(null);
4729
+ if (this.scheduled)
4730
+ return;
4731
+ this.scheduled = true;
4732
+ queueMicrotask(() => {
4733
+ this.scheduled = false;
4734
+ this.run();
4735
+ });
4789
4736
  }
4790
- applyChanges(events) {
4791
- if (events.length === 0)
4737
+ async run() {
4738
+ if (this.stopped)
4792
4739
  return;
4793
- for (const event3 of events) {
4794
- if (event3.type === "set" && event3.item) {
4795
- this.data.set(event3.id, event3.item);
4796
- } else if (event3.type === "delete") {
4797
- this.data.delete(event3.id);
4740
+ this.running = true;
4741
+ try {
4742
+ const next = await executeDescriptor(this.descriptor, this.model.context, this.adapters, "queryContext", { fromWire: true });
4743
+ if (this.stopped)
4744
+ return;
4745
+ const diff = diffResults(this.lastResult, next);
4746
+ this.lastResult = next;
4747
+ if (diff.kind === "changes") {
4748
+ this.onUpdate({ type: "changes", changes: diff.changes });
4749
+ } else if (diff.kind === "snapshot") {
4750
+ this.onUpdate({ type: "snapshot", result: diff.result });
4751
+ }
4752
+ } catch (err) {
4753
+ console.error(`[Arc] LiveQuery re-execute error:`, err);
4754
+ } finally {
4755
+ this.running = false;
4756
+ if (this.rerunRequested) {
4757
+ this.rerunRequested = false;
4758
+ this.schedule();
4798
4759
  }
4799
4760
  }
4800
- this.notifyListeners(events);
4801
- }
4802
- set(id3, item) {
4803
- this.data.set(id3, item);
4804
- this.notifyListeners([{ type: "set", id: id3, item }]);
4805
4761
  }
4806
- modify(id3, updates) {
4807
- const existing = this.data.get(id3);
4808
- if (existing) {
4809
- const updated = { ...existing, ...updates };
4810
- this.data.set(id3, updated);
4811
- this.notifyListeners([{ type: "set", id: id3, item: updated }]);
4762
+ }
4763
+ // src/streaming/streaming-query-cache.ts
4764
+ class StreamingQueryCache {
4765
+ entries = new Map;
4766
+ static UNSUBSCRIBE_DELAY_MS = 5000;
4767
+ entryKey(descriptor, scope) {
4768
+ return `${scope ?? "default"}:${murmurHash(JSON.stringify(descriptor))}`;
4769
+ }
4770
+ subscribe(descriptor, scope, eventWire, onChange) {
4771
+ const key = this.entryKey(descriptor, scope);
4772
+ let entry = this.entries.get(key);
4773
+ if (entry) {
4774
+ if (entry.pendingUnsub) {
4775
+ clearTimeout(entry.pendingUnsub);
4776
+ entry.pendingUnsub = undefined;
4777
+ }
4778
+ entry.refCount++;
4779
+ } else {
4780
+ const newEntry = {
4781
+ result: undefined,
4782
+ hasResult: false,
4783
+ listeners: new Set,
4784
+ refCount: 1,
4785
+ subscriptionId: ""
4786
+ };
4787
+ newEntry.subscriptionId = eventWire.subscribeQuery(descriptor, scope, {
4788
+ onSnapshot: (result) => {
4789
+ newEntry.result = result;
4790
+ newEntry.hasResult = true;
4791
+ this.notify(newEntry);
4792
+ },
4793
+ onChanges: (changes) => {
4794
+ if (!newEntry.hasResult || !Array.isArray(newEntry.result)) {
4795
+ return;
4796
+ }
4797
+ newEntry.result = applyQueryChanges(newEntry.result, changes);
4798
+ this.notify(newEntry);
4799
+ }
4800
+ });
4801
+ this.entries.set(key, newEntry);
4802
+ entry = newEntry;
4812
4803
  }
4804
+ const subscribed = entry;
4805
+ subscribed.listeners.add(onChange);
4806
+ let active = true;
4807
+ return {
4808
+ read: () => ({
4809
+ result: subscribed.result,
4810
+ loading: !subscribed.hasResult
4811
+ }),
4812
+ unsubscribe: () => {
4813
+ if (!active)
4814
+ return;
4815
+ active = false;
4816
+ subscribed.listeners.delete(onChange);
4817
+ subscribed.refCount--;
4818
+ if (subscribed.refCount > 0)
4819
+ return;
4820
+ subscribed.pendingUnsub = setTimeout(() => {
4821
+ subscribed.pendingUnsub = undefined;
4822
+ if (subscribed.refCount > 0)
4823
+ return;
4824
+ eventWire.unsubscribeQuery(subscribed.subscriptionId);
4825
+ this.entries.delete(key);
4826
+ }, StreamingQueryCache.UNSUBSCRIBE_DELAY_MS);
4827
+ }
4828
+ };
4813
4829
  }
4814
- remove(id3) {
4815
- if (this.data.delete(id3)) {
4816
- this.notifyListeners([{ type: "delete", id: id3, item: null }]);
4830
+ invalidateScope(scope, eventWire) {
4831
+ const prefix = `${scope}:`;
4832
+ for (const [key, entry] of this.entries) {
4833
+ if (!key.startsWith(prefix))
4834
+ continue;
4835
+ if (entry.pendingUnsub)
4836
+ clearTimeout(entry.pendingUnsub);
4837
+ eventWire?.unsubscribeQuery(entry.subscriptionId);
4838
+ this.entries.delete(key);
4839
+ entry.result = undefined;
4840
+ entry.hasResult = false;
4841
+ this.notify(entry);
4817
4842
  }
4818
4843
  }
4819
- clear() {
4820
- this.initialized = false;
4821
- this.data.clear();
4822
- this.notifyListeners(null);
4823
- }
4824
- find(options = {}) {
4825
- let results = Array.from(this.data.values());
4826
- if (options.where) {
4827
- results = results.filter((item) => checkItemMatchesWhere(item, options.where));
4844
+ clear(eventWire) {
4845
+ for (const entry of this.entries.values()) {
4846
+ if (entry.pendingUnsub)
4847
+ clearTimeout(entry.pendingUnsub);
4848
+ eventWire?.unsubscribeQuery(entry.subscriptionId);
4828
4849
  }
4829
- return applyOrderByAndLimit(results, options);
4830
- }
4831
- findOne(where) {
4832
- const results = this.find({ where });
4833
- return results[0];
4850
+ this.entries.clear();
4834
4851
  }
4835
- subscribe(listener4) {
4836
- this.listeners.add(listener4);
4837
- return () => {
4838
- this.listeners.delete(listener4);
4839
- };
4840
- }
4841
- notifyListeners(events) {
4842
- for (const listener4 of this.listeners) {
4843
- listener4(events);
4852
+ notify(entry) {
4853
+ for (const listener4 of entry.listeners) {
4854
+ try {
4855
+ listener4();
4856
+ } catch (err) {
4857
+ console.error(`[Arc] Query cache listener error:`, err);
4858
+ }
4844
4859
  }
4845
4860
  }
4846
4861
  }
4847
4862
  // src/streaming/streaming-event-publisher.ts
4848
4863
  class StreamingEventPublisher {
4849
- cache;
4850
4864
  eventWire;
4851
4865
  views = [];
4852
4866
  subscribers = new Map;
4853
- constructor(cache, eventWire) {
4854
- this.cache = cache;
4867
+ constructor(eventWire) {
4855
4868
  this.eventWire = eventWire;
4856
4869
  }
4857
4870
  registerViews(views) {
4858
4871
  this.views = views;
4859
- this.cache.registerViews(views);
4860
4872
  }
4861
4873
  async publish(event3) {
4862
- await this.cache.applyEvent(event3);
4863
4874
  await this.notifySubscribers(event3);
4864
4875
  this.eventWire.syncEvents([
4865
4876
  {
@@ -5290,6 +5301,7 @@ class TokenCache {
5290
5301
  }
5291
5302
  export {
5292
5303
  view,
5304
+ triggerModuleSync,
5293
5305
  token,
5294
5306
  stringEnum,
5295
5307
  string,
@@ -5297,6 +5309,7 @@ export {
5297
5309
  secureDataStorage,
5298
5310
  route,
5299
5311
  resolveQueryChange,
5312
+ registerModuleSyncProvider,
5300
5313
  record,
5301
5314
  or,
5302
5315
  observeQueries,
@@ -5307,10 +5320,12 @@ export {
5307
5320
  mergeUnsafe,
5308
5321
  listener,
5309
5322
  id,
5323
+ hasModuleSyncProvider,
5310
5324
  file,
5311
5325
  extractDatabaseAgnosticSchema,
5312
5326
  executeDescriptor,
5313
5327
  event,
5328
+ diffResults,
5314
5329
  defaultFunctionData,
5315
5330
  deepMerge,
5316
5331
  date,
@@ -5324,9 +5339,11 @@ export {
5324
5339
  buildContextAccessor,
5325
5340
  boolean,
5326
5341
  blob,
5342
+ awaitModuleSync,
5327
5343
  array,
5328
5344
  arcFunctionWithCtx,
5329
5345
  arcFunction,
5346
+ applyQueryChanges,
5330
5347
  applyOrderByAndLimit,
5331
5348
  any,
5332
5349
  aggregate,
@@ -5347,6 +5364,7 @@ export {
5347
5364
  MasterStoreState,
5348
5365
  MasterDataStorage,
5349
5366
  LocalEventPublisher,
5367
+ LiveQuery,
5350
5368
  ForkedStoreState,
5351
5369
  ForkedDataStorage,
5352
5370
  EventWire,