@arcote.tech/arc 0.7.15 → 0.7.16

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
@@ -211,7 +211,8 @@ class EventWire {
211
211
  onSyncedCallback;
212
212
  reconnectTimeout;
213
213
  syncRequested = false;
214
- viewSubscriptions = new Map;
214
+ querySubscriptions = new Map;
215
+ querySubCounter = 0;
215
216
  enableEventSync;
216
217
  constructor(baseUrl, options) {
217
218
  this.baseUrl = baseUrl;
@@ -266,7 +267,7 @@ class EventWire {
266
267
  this.requestSync();
267
268
  }
268
269
  this.flushPendingEvents();
269
- this.sendAllViewSubscriptions();
270
+ this.sendAllQuerySubscriptions();
270
271
  } else {
271
272
  console.log(`[EventWire] onopen called but ws is not OPEN, readyState:`, this.ws?.readyState);
272
273
  }
@@ -342,24 +343,29 @@ class EventWire {
342
343
  onSynced(callback) {
343
344
  this.onSyncedCallback = callback;
344
345
  }
345
- subscribeView(element, scope, callbacks) {
346
- const key = `${scope}:${element}`;
347
- this.viewSubscriptions.set(key, callbacks);
346
+ subscribeQuery(descriptor, scope, callbacks) {
347
+ const subscriptionId = `qs_${this.instanceId}_${++this.querySubCounter}`;
348
+ this.querySubscriptions.set(subscriptionId, {
349
+ descriptor,
350
+ scope,
351
+ callbacks
352
+ });
348
353
  if (this.state === "connected" && this.ws) {
349
354
  this.ws.send(JSON.stringify({
350
- type: "subscribe-view",
351
- element,
355
+ type: "subscribe-query",
356
+ subscriptionId,
357
+ descriptor,
352
358
  scope
353
359
  }));
354
360
  }
361
+ return subscriptionId;
355
362
  }
356
- unsubscribeView(element, scope) {
357
- this.viewSubscriptions.delete(`${scope}:${element}`);
363
+ unsubscribeQuery(subscriptionId) {
364
+ this.querySubscriptions.delete(subscriptionId);
358
365
  if (this.state === "connected" && this.ws) {
359
366
  this.ws.send(JSON.stringify({
360
- type: "unsubscribe-view",
361
- element,
362
- scope
367
+ type: "unsubscribe-query",
368
+ subscriptionId
363
369
  }));
364
370
  }
365
371
  }
@@ -394,17 +400,17 @@ class EventWire {
394
400
  this.lastHostEventId = message.lastHostEventId;
395
401
  }
396
402
  break;
397
- case "view-snapshot": {
398
- const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
403
+ case "query-snapshot": {
404
+ const sub = this.querySubscriptions.get(message.subscriptionId);
399
405
  if (sub) {
400
- sub.onSnapshot(message.items ?? []);
406
+ sub.callbacks.onSnapshot(message.result ?? null);
401
407
  }
402
408
  break;
403
409
  }
404
- case "view-changes": {
405
- const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
410
+ case "query-changes": {
411
+ const sub = this.querySubscriptions.get(message.subscriptionId);
406
412
  if (sub && Array.isArray(message.changes)) {
407
- sub.onChanges(message.changes);
413
+ sub.callbacks.onChanges(message.changes);
408
414
  }
409
415
  break;
410
416
  }
@@ -432,17 +438,15 @@ class EventWire {
432
438
  this.pendingEvents = [];
433
439
  }
434
440
  }
435
- sendAllViewSubscriptions() {
441
+ sendAllQuerySubscriptions() {
436
442
  if (!this.ws || this.state !== "connected")
437
443
  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);
444
+ for (const [subscriptionId, sub] of this.querySubscriptions) {
442
445
  this.ws.send(JSON.stringify({
443
- type: "subscribe-view",
444
- element,
445
- scope
446
+ type: "subscribe-query",
447
+ subscriptionId,
448
+ descriptor: sub.descriptor,
449
+ scope: sub.scope
446
450
  }));
447
451
  }
448
452
  }
@@ -484,19 +488,12 @@ class LocalEventPublisher {
484
488
  views = [];
485
489
  syncCallback;
486
490
  subscribers = new Map;
487
- viewChangesCallbacks = new Set;
488
491
  constructor(dataStorage) {
489
492
  this.dataStorage = dataStorage;
490
493
  }
491
494
  onPublish(callback) {
492
495
  this.syncCallback = callback;
493
496
  }
494
- onViewChanges(callback) {
495
- this.viewChangesCallbacks.add(callback);
496
- return () => {
497
- this.viewChangesCallbacks.delete(callback);
498
- };
499
- }
500
497
  registerViews(views) {
501
498
  this.views = views;
502
499
  }
@@ -564,19 +561,7 @@ class LocalEventPublisher {
564
561
  });
565
562
  const viewChanges = await this.collectViewChanges(event);
566
563
  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
- }
564
+ await this.dataStorage.commitChanges(allChanges);
580
565
  await this.notifySubscribers(event);
581
566
  if (this.syncCallback) {
582
567
  this.syncCallback(event);
@@ -1262,9 +1247,8 @@ function object(element) {
1262
1247
 
1263
1248
  // src/data-storage/data-storage.abstract.ts
1264
1249
  class DataStorage {
1265
- async commitChanges(changes, _options) {
1250
+ async commitChanges(changes) {
1266
1251
  await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
1267
- return [];
1268
1252
  }
1269
1253
  }
1270
1254
 
@@ -2229,11 +2213,6 @@ class ArcAggregateElement extends ArcContextElement {
2229
2213
  }
2230
2214
  return adapters.dataStorage.getStore(viewName).find(options);
2231
2215
  }
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
2216
  if (adapters.queryWire)
2238
2217
  return adapters.queryWire.query(viewName, options);
2239
2218
  return [];
@@ -2261,12 +2240,6 @@ class ArcAggregateElement extends ArcContextElement {
2261
2240
  }
2262
2241
  };
2263
2242
  }
2264
- getRestrictionsFor(adapters) {
2265
- return {
2266
- restrictions: this.getScopeRestrictions(adapters),
2267
- denied: this.isScopeDenied(adapters)
2268
- };
2269
- }
2270
2243
  isScopeDenied(adapters, protections = this._protections) {
2271
2244
  if (protections.length === 0)
2272
2245
  return false;
@@ -2912,13 +2885,6 @@ class ArcView extends ArcContextElement {
2912
2885
  }
2913
2886
  return adapters.dataStorage.getStore(viewName).find(options);
2914
2887
  }
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
2888
  if (adapters.queryWire) {
2923
2889
  const where = restrictions ? { ...options?.where || {}, ...restrictions } : options?.where;
2924
2890
  return adapters.queryWire.query(viewName, { ...options, where });
@@ -2937,13 +2903,6 @@ class ArcView extends ArcContextElement {
2937
2903
  const results = await adapters.dataStorage.getStore(viewName).find({ where });
2938
2904
  return results[0];
2939
2905
  }
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
2906
  if (adapters.queryWire) {
2948
2907
  const mergedWhere = restrictions ? { ...where, ...restrictions } : where;
2949
2908
  const results = await adapters.queryWire.query(viewName, { where: mergedWhere });
@@ -2953,9 +2912,6 @@ class ArcView extends ArcContextElement {
2953
2912
  }
2954
2913
  };
2955
2914
  }
2956
- getRestrictionsFor(adapters) {
2957
- return this.resolveProtection(this.data.protections || [], adapters);
2958
- }
2959
2915
  resolveProtection(protections, adapters) {
2960
2916
  if (protections.length === 0)
2961
2917
  return { restrictions: null, denied: false };
@@ -3403,12 +3359,8 @@ class MasterStoreState extends StoreState {
3403
3359
  }
3404
3360
  return transaction.find(this.storeName, { where: { _id: id2 } }).then((results) => results[0]);
3405
3361
  }
3406
- async applyChangeAndReturnEvent(transaction, change, transactionCache, options) {
3362
+ async applyChangeAndReturnEvent(transaction, change, transactionCache) {
3407
3363
  if (change.type === "set") {
3408
- let existing;
3409
- if (options?.captureRows) {
3410
- existing = await this.readExisting(transaction, change.data._id, transactionCache);
3411
- }
3412
3364
  await transaction.set(this.storeName, change.data);
3413
3365
  const item = this.deserialize ? this.deserialize(change.data) : change.data;
3414
3366
  if (transactionCache) {
@@ -3421,16 +3373,10 @@ class MasterStoreState extends StoreState {
3421
3373
  type: "set",
3422
3374
  item: change.data,
3423
3375
  id: change.data._id
3424
- },
3425
- oldRow: existing ?? null,
3426
- newRow: change.data
3376
+ }
3427
3377
  };
3428
3378
  }
3429
3379
  if (change.type === "delete") {
3430
- let existing;
3431
- if (options?.captureRows) {
3432
- existing = await this.readExisting(transaction, change.id, transactionCache);
3433
- }
3434
3380
  await transaction.remove(this.storeName, change.id);
3435
3381
  if (transactionCache) {
3436
3382
  transactionCache.delete(`${this.storeName}:${change.id}`);
@@ -3442,9 +3388,7 @@ class MasterStoreState extends StoreState {
3442
3388
  type: "delete",
3443
3389
  item: null,
3444
3390
  id: change.id
3445
- },
3446
- oldRow: existing ?? null,
3447
- newRow: null
3391
+ }
3448
3392
  };
3449
3393
  }
3450
3394
  if (change.type === "modify") {
@@ -3462,9 +3406,7 @@ class MasterStoreState extends StoreState {
3462
3406
  type: "set",
3463
3407
  item,
3464
3408
  id: change.id
3465
- },
3466
- oldRow: existing ?? null,
3467
- newRow: updated
3409
+ }
3468
3410
  };
3469
3411
  }
3470
3412
  if (change.type === "mutate") {
@@ -3482,9 +3424,7 @@ class MasterStoreState extends StoreState {
3482
3424
  type: "set",
3483
3425
  item,
3484
3426
  id: change.id
3485
- },
3486
- oldRow: existing ?? null,
3487
- newRow: updated
3427
+ }
3488
3428
  };
3489
3429
  }
3490
3430
  throw new Error("Unknown change type");
@@ -3563,22 +3503,17 @@ class MasterDataStorage extends DataStorage {
3563
3503
  applySerializedChanges(changes) {
3564
3504
  return Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applySerializedChanges(changes2)));
3565
3505
  }
3566
- async commitChanges(changes, options) {
3506
+ async commitChanges(changes) {
3567
3507
  const transaction = await this.getReadWriteTransaction();
3568
3508
  const transactionCache = new Map;
3569
3509
  const eventsByStore = new Map;
3570
- const committed = [];
3571
3510
  for (const { store, changes: storeChanges } of changes) {
3572
3511
  const storeState = this.getStore(store);
3573
3512
  const storeEvents = [];
3574
- const capture = options?.captureRowsFor?.has(store) ?? false;
3575
3513
  for (const change of storeChanges) {
3576
- const { event: event3, oldRow, newRow } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache, { captureRows: capture });
3514
+ const { event: event3 } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache);
3577
3515
  if (event3)
3578
3516
  storeEvents.push(event3);
3579
- if (capture) {
3580
- committed.push({ store, id: event3.id, oldRow, newRow });
3581
- }
3582
3517
  }
3583
3518
  if (storeEvents.length > 0) {
3584
3519
  eventsByStore.set(store, storeEvents);
@@ -3589,7 +3524,6 @@ class MasterDataStorage extends DataStorage {
3589
3524
  const storeState = this.getStore(store);
3590
3525
  storeState.notifyListenersPublic(events);
3591
3526
  }
3592
- return committed;
3593
3527
  }
3594
3528
  fork() {
3595
3529
  return new ForkedDataStorage(this);
@@ -3706,8 +3640,8 @@ class ObservableDataStorage {
3706
3640
  getReadWriteTransaction() {
3707
3641
  return this.source.getReadWriteTransaction();
3708
3642
  }
3709
- commitChanges(changes, options) {
3710
- return this.source.commitChanges(changes, options);
3643
+ commitChanges(changes) {
3644
+ return this.source.commitChanges(changes);
3711
3645
  }
3712
3646
  trackQuery(storeName, options, result, listener4) {
3713
3647
  const key = this.getQueryKey(storeName, options);
@@ -3720,7 +3654,8 @@ class ObservableDataStorage {
3720
3654
  }
3721
3655
  handleStoreChange(storeName, events) {
3722
3656
  let hasChanges = false;
3723
- for (const query of this.trackedQueries.values()) {
3657
+ const staleKeys = [];
3658
+ for (const [key, query] of this.trackedQueries) {
3724
3659
  if (query.storeName !== storeName)
3725
3660
  continue;
3726
3661
  let currentResult = query.result;
@@ -3732,10 +3667,20 @@ class ObservableDataStorage {
3732
3667
  queryChanged = true;
3733
3668
  }
3734
3669
  }
3735
- if (queryChanged) {
3736
- query.result = currentResult;
3670
+ if (!queryChanged)
3671
+ continue;
3672
+ if (query.options.limit !== undefined && query.result.length === query.options.limit && currentResult.length < query.options.limit) {
3673
+ staleKeys.push(key);
3737
3674
  hasChanges = true;
3675
+ continue;
3738
3676
  }
3677
+ query.result = currentResult;
3678
+ hasChanges = true;
3679
+ }
3680
+ for (const key of staleKeys) {
3681
+ const query = this.trackedQueries.get(key);
3682
+ this.source.getStore(query.storeName).unsubscribe(query.listener);
3683
+ this.trackedQueries.delete(key);
3739
3684
  }
3740
3685
  if (hasChanges) {
3741
3686
  this.onChange();
@@ -4621,245 +4566,260 @@ function mutationExecutor(model) {
4621
4566
  }
4622
4567
  });
4623
4568
  }
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);
4569
+ // src/model/live-query/diff.ts
4570
+ function isIdList(value) {
4571
+ return Array.isArray(value) && value.every((it) => it && typeof it === "object" && typeof it._id === "string");
4572
+ }
4573
+ function diffResults(prev, next) {
4574
+ if (!isIdList(prev) || !isIdList(next)) {
4575
+ return JSON.stringify(prev) === JSON.stringify(next) ? { kind: "none" } : { kind: "snapshot", result: next };
4576
+ }
4577
+ const json = (o) => JSON.stringify(o);
4578
+ const nextIds = new Set(next.map((it) => it._id));
4579
+ const changes = [];
4580
+ for (const it of prev) {
4581
+ if (!nextIds.has(it._id)) {
4582
+ changes.push({ type: "delete", id: it._id });
4583
+ }
4584
+ }
4585
+ const sim = prev.filter((it) => nextIds.has(it._id));
4586
+ for (let i = 0;i < next.length; i++) {
4587
+ const target = next[i];
4588
+ if (sim[i] && sim[i]._id === target._id && json(sim[i]) === json(target)) {
4589
+ continue;
4643
4590
  }
4644
- return this.stores.get(key);
4591
+ changes.push({ type: "set", id: target._id, item: target, index: i });
4592
+ const oldIdx = sim.findIndex((it) => it._id === target._id);
4593
+ if (oldIdx !== -1)
4594
+ sim.splice(oldIdx, 1);
4595
+ sim.splice(i, 0, target);
4645
4596
  }
4646
- hasData(viewName, scope) {
4647
- const store = this.stores.get(this.storeKey(viewName, scope));
4648
- return store ? store.hasData() : false;
4597
+ if (changes.length === 0)
4598
+ return { kind: "none" };
4599
+ if (changes.length > next.length) {
4600
+ return { kind: "snapshot", result: next };
4649
4601
  }
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
- };
4602
+ if (sim.length !== next.length || json(sim) !== json(next)) {
4603
+ return { kind: "snapshot", result: next };
4673
4604
  }
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
- }
4605
+ return { kind: "changes", changes };
4606
+ }
4607
+ function applyQueryChanges(result, changes) {
4608
+ const next = [...result];
4609
+ for (const change of changes) {
4610
+ if (change.type === "delete") {
4611
+ const idx = next.findIndex((it) => it._id === change.id);
4612
+ if (idx !== -1)
4613
+ next.splice(idx, 1);
4758
4614
  }
4759
4615
  }
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();
4616
+ for (const change of changes) {
4617
+ if (change.type === "set") {
4618
+ const idx = next.findIndex((it) => it._id === change.id);
4619
+ if (idx !== -1)
4620
+ next.splice(idx, 1);
4621
+ next.splice(change.index, 0, change.item);
4771
4622
  }
4772
4623
  }
4624
+ return next;
4773
4625
  }
4774
4626
 
4775
- class StreamingStore {
4776
- data = new Map;
4777
- listeners = new Set;
4778
- initialized = false;
4779
- hasData() {
4780
- return this.initialized;
4627
+ // src/model/live-query/live-query-subscription.ts
4628
+ class LiveQuery {
4629
+ model;
4630
+ descriptor;
4631
+ scope;
4632
+ rawToken;
4633
+ onUpdate;
4634
+ observable = null;
4635
+ adapters = null;
4636
+ lastResult;
4637
+ scheduled = false;
4638
+ running = false;
4639
+ rerunRequested = false;
4640
+ stopped = false;
4641
+ constructor(model, descriptor, scope, rawToken, onUpdate) {
4642
+ this.model = model;
4643
+ this.descriptor = descriptor;
4644
+ this.scope = scope;
4645
+ this.rawToken = rawToken;
4646
+ this.onUpdate = onUpdate;
4647
+ }
4648
+ async start() {
4649
+ const scoped = new ScopedModel(this.model, this.scope);
4650
+ if (this.rawToken)
4651
+ scoped.setToken(this.rawToken);
4652
+ const baseAdapters = scoped.getAdapters();
4653
+ if (!baseAdapters.dataStorage) {
4654
+ throw new Error("LiveQuery requires a dataStorage adapter (server-side)");
4655
+ }
4656
+ this.observable = new ObservableDataStorage(baseAdapters.dataStorage, () => this.schedule());
4657
+ this.adapters = {
4658
+ ...baseAdapters,
4659
+ dataStorage: this.observable
4660
+ };
4661
+ this.lastResult = await executeDescriptor(this.descriptor, this.model.context, this.adapters, "queryContext", { fromWire: true });
4662
+ return this.lastResult;
4781
4663
  }
4782
- setAll(items) {
4783
- this.initialized = true;
4784
- this.data.clear();
4785
- for (const item of items) {
4786
- this.data.set(item._id, item);
4664
+ flush() {
4665
+ this.schedule();
4666
+ }
4667
+ stop() {
4668
+ this.stopped = true;
4669
+ this.observable?.clear();
4670
+ }
4671
+ schedule() {
4672
+ if (this.stopped)
4673
+ return;
4674
+ if (this.running) {
4675
+ this.rerunRequested = true;
4676
+ return;
4787
4677
  }
4788
- this.notifyListeners(null);
4678
+ if (this.scheduled)
4679
+ return;
4680
+ this.scheduled = true;
4681
+ queueMicrotask(() => {
4682
+ this.scheduled = false;
4683
+ this.run();
4684
+ });
4789
4685
  }
4790
- applyChanges(events) {
4791
- if (events.length === 0)
4686
+ async run() {
4687
+ if (this.stopped)
4792
4688
  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);
4689
+ this.running = true;
4690
+ try {
4691
+ const next = await executeDescriptor(this.descriptor, this.model.context, this.adapters, "queryContext", { fromWire: true });
4692
+ if (this.stopped)
4693
+ return;
4694
+ const diff = diffResults(this.lastResult, next);
4695
+ this.lastResult = next;
4696
+ if (diff.kind === "changes") {
4697
+ this.onUpdate({ type: "changes", changes: diff.changes });
4698
+ } else if (diff.kind === "snapshot") {
4699
+ this.onUpdate({ type: "snapshot", result: diff.result });
4700
+ }
4701
+ } catch (err) {
4702
+ console.error(`[Arc] LiveQuery re-execute error:`, err);
4703
+ } finally {
4704
+ this.running = false;
4705
+ if (this.rerunRequested) {
4706
+ this.rerunRequested = false;
4707
+ this.schedule();
4798
4708
  }
4799
4709
  }
4800
- this.notifyListeners(events);
4801
- }
4802
- set(id3, item) {
4803
- this.data.set(id3, item);
4804
- this.notifyListeners([{ type: "set", id: id3, item }]);
4805
4710
  }
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 }]);
4711
+ }
4712
+ // src/streaming/streaming-query-cache.ts
4713
+ class StreamingQueryCache {
4714
+ entries = new Map;
4715
+ static UNSUBSCRIBE_DELAY_MS = 5000;
4716
+ entryKey(descriptor, scope) {
4717
+ return `${scope ?? "default"}:${murmurHash(JSON.stringify(descriptor))}`;
4718
+ }
4719
+ subscribe(descriptor, scope, eventWire, onChange) {
4720
+ const key = this.entryKey(descriptor, scope);
4721
+ let entry = this.entries.get(key);
4722
+ if (entry) {
4723
+ if (entry.pendingUnsub) {
4724
+ clearTimeout(entry.pendingUnsub);
4725
+ entry.pendingUnsub = undefined;
4726
+ }
4727
+ entry.refCount++;
4728
+ } else {
4729
+ const newEntry = {
4730
+ result: undefined,
4731
+ hasResult: false,
4732
+ listeners: new Set,
4733
+ refCount: 1,
4734
+ subscriptionId: ""
4735
+ };
4736
+ newEntry.subscriptionId = eventWire.subscribeQuery(descriptor, scope, {
4737
+ onSnapshot: (result) => {
4738
+ newEntry.result = result;
4739
+ newEntry.hasResult = true;
4740
+ this.notify(newEntry);
4741
+ },
4742
+ onChanges: (changes) => {
4743
+ if (!newEntry.hasResult || !Array.isArray(newEntry.result)) {
4744
+ return;
4745
+ }
4746
+ newEntry.result = applyQueryChanges(newEntry.result, changes);
4747
+ this.notify(newEntry);
4748
+ }
4749
+ });
4750
+ this.entries.set(key, newEntry);
4751
+ entry = newEntry;
4812
4752
  }
4753
+ const subscribed = entry;
4754
+ subscribed.listeners.add(onChange);
4755
+ let active = true;
4756
+ return {
4757
+ read: () => ({
4758
+ result: subscribed.result,
4759
+ loading: !subscribed.hasResult
4760
+ }),
4761
+ unsubscribe: () => {
4762
+ if (!active)
4763
+ return;
4764
+ active = false;
4765
+ subscribed.listeners.delete(onChange);
4766
+ subscribed.refCount--;
4767
+ if (subscribed.refCount > 0)
4768
+ return;
4769
+ subscribed.pendingUnsub = setTimeout(() => {
4770
+ subscribed.pendingUnsub = undefined;
4771
+ if (subscribed.refCount > 0)
4772
+ return;
4773
+ eventWire.unsubscribeQuery(subscribed.subscriptionId);
4774
+ this.entries.delete(key);
4775
+ }, StreamingQueryCache.UNSUBSCRIBE_DELAY_MS);
4776
+ }
4777
+ };
4813
4778
  }
4814
- remove(id3) {
4815
- if (this.data.delete(id3)) {
4816
- this.notifyListeners([{ type: "delete", id: id3, item: null }]);
4779
+ invalidateScope(scope, eventWire) {
4780
+ const prefix = `${scope}:`;
4781
+ for (const [key, entry] of this.entries) {
4782
+ if (!key.startsWith(prefix))
4783
+ continue;
4784
+ if (entry.pendingUnsub)
4785
+ clearTimeout(entry.pendingUnsub);
4786
+ eventWire?.unsubscribeQuery(entry.subscriptionId);
4787
+ this.entries.delete(key);
4788
+ entry.result = undefined;
4789
+ entry.hasResult = false;
4790
+ this.notify(entry);
4817
4791
  }
4818
4792
  }
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));
4793
+ clear(eventWire) {
4794
+ for (const entry of this.entries.values()) {
4795
+ if (entry.pendingUnsub)
4796
+ clearTimeout(entry.pendingUnsub);
4797
+ eventWire?.unsubscribeQuery(entry.subscriptionId);
4828
4798
  }
4829
- return applyOrderByAndLimit(results, options);
4830
- }
4831
- findOne(where) {
4832
- const results = this.find({ where });
4833
- return results[0];
4834
- }
4835
- subscribe(listener4) {
4836
- this.listeners.add(listener4);
4837
- return () => {
4838
- this.listeners.delete(listener4);
4839
- };
4799
+ this.entries.clear();
4840
4800
  }
4841
- notifyListeners(events) {
4842
- for (const listener4 of this.listeners) {
4843
- listener4(events);
4801
+ notify(entry) {
4802
+ for (const listener4 of entry.listeners) {
4803
+ try {
4804
+ listener4();
4805
+ } catch (err) {
4806
+ console.error(`[Arc] Query cache listener error:`, err);
4807
+ }
4844
4808
  }
4845
4809
  }
4846
4810
  }
4847
4811
  // src/streaming/streaming-event-publisher.ts
4848
4812
  class StreamingEventPublisher {
4849
- cache;
4850
4813
  eventWire;
4851
4814
  views = [];
4852
4815
  subscribers = new Map;
4853
- constructor(cache, eventWire) {
4854
- this.cache = cache;
4816
+ constructor(eventWire) {
4855
4817
  this.eventWire = eventWire;
4856
4818
  }
4857
4819
  registerViews(views) {
4858
4820
  this.views = views;
4859
- this.cache.registerViews(views);
4860
4821
  }
4861
4822
  async publish(event3) {
4862
- await this.cache.applyEvent(event3);
4863
4823
  await this.notifySubscribers(event3);
4864
4824
  this.eventWire.syncEvents([
4865
4825
  {
@@ -5311,6 +5271,7 @@ export {
5311
5271
  extractDatabaseAgnosticSchema,
5312
5272
  executeDescriptor,
5313
5273
  event,
5274
+ diffResults,
5314
5275
  defaultFunctionData,
5315
5276
  deepMerge,
5316
5277
  date,
@@ -5327,6 +5288,7 @@ export {
5327
5288
  array,
5328
5289
  arcFunctionWithCtx,
5329
5290
  arcFunction,
5291
+ applyQueryChanges,
5330
5292
  applyOrderByAndLimit,
5331
5293
  any,
5332
5294
  aggregate,
@@ -5347,6 +5309,7 @@ export {
5347
5309
  MasterStoreState,
5348
5310
  MasterDataStorage,
5349
5311
  LocalEventPublisher,
5312
+ LiveQuery,
5350
5313
  ForkedStoreState,
5351
5314
  ForkedDataStorage,
5352
5315
  EventWire,