@arcote.tech/arc-cli 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.
Files changed (2) hide show
  1. package/dist/index.js +444 -439
  2. package/package.json +9 -9
package/dist/index.js CHANGED
@@ -14018,11 +14018,11 @@ class EventWire {
14018
14018
  reconnectTimeout;
14019
14019
  syncRequested = false;
14020
14020
  viewSubscriptions = new Map;
14021
- viewSubCounter = 0;
14022
- pendingViewSubs = [];
14023
- constructor(baseUrl) {
14021
+ enableEventSync;
14022
+ constructor(baseUrl, options) {
14024
14023
  this.baseUrl = baseUrl;
14025
14024
  this.instanceId = ++eventWireInstanceCounter;
14025
+ this.enableEventSync = options?.enableEventSync ?? true;
14026
14026
  }
14027
14027
  setScopeToken(scope, token) {
14028
14028
  if (token === null) {
@@ -14068,9 +14068,11 @@ class EventWire {
14068
14068
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
14069
14069
  this.state = "connected";
14070
14070
  this.sendAllScopeTokens();
14071
- this.requestSync();
14071
+ if (this.enableEventSync) {
14072
+ this.requestSync();
14073
+ }
14072
14074
  this.flushPendingEvents();
14073
- this.flushPendingViewSubs();
14075
+ this.sendAllViewSubscriptions();
14074
14076
  } else {
14075
14077
  console.log(`[EventWire] onopen called but ws is not OPEN, readyState:`, this.ws?.readyState);
14076
14078
  }
@@ -14146,30 +14148,26 @@ class EventWire {
14146
14148
  onSynced(callback) {
14147
14149
  this.onSyncedCallback = callback;
14148
14150
  }
14149
- subscribeQuery(descriptor, callback, scope) {
14150
- const subscriptionId = `qs_${++this.viewSubCounter}_${Date.now()}`;
14151
- this.viewSubscriptions.set(subscriptionId, callback);
14151
+ subscribeView(element, scope, callbacks) {
14152
+ const key = `${scope}:${element}`;
14153
+ this.viewSubscriptions.set(key, callbacks);
14152
14154
  if (this.state === "connected" && this.ws) {
14153
14155
  this.ws.send(JSON.stringify({
14154
- type: "subscribe-query",
14155
- subscriptionId,
14156
- descriptor,
14156
+ type: "subscribe-view",
14157
+ element,
14157
14158
  scope
14158
14159
  }));
14159
- } else {
14160
- this.pendingViewSubs.push({ subscriptionId, descriptor, scope });
14161
14160
  }
14162
- return subscriptionId;
14163
14161
  }
14164
- unsubscribeQuery(subscriptionId) {
14165
- this.viewSubscriptions.delete(subscriptionId);
14162
+ unsubscribeView(element, scope) {
14163
+ this.viewSubscriptions.delete(`${scope}:${element}`);
14166
14164
  if (this.state === "connected" && this.ws) {
14167
14165
  this.ws.send(JSON.stringify({
14168
- type: "unsubscribe-query",
14169
- subscriptionId
14166
+ type: "unsubscribe-view",
14167
+ element,
14168
+ scope
14170
14169
  }));
14171
14170
  }
14172
- this.pendingViewSubs = this.pendingViewSubs.filter((s) => s.subscriptionId !== subscriptionId);
14173
14171
  }
14174
14172
  getState() {
14175
14173
  return this.state;
@@ -14202,10 +14200,17 @@ class EventWire {
14202
14200
  this.lastHostEventId = message.lastHostEventId;
14203
14201
  }
14204
14202
  break;
14205
- case "query-data": {
14206
- const cb = this.viewSubscriptions.get(message.subscriptionId);
14207
- if (cb) {
14208
- cb(message.data);
14203
+ case "view-snapshot": {
14204
+ const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
14205
+ if (sub) {
14206
+ sub.onSnapshot(message.items ?? []);
14207
+ }
14208
+ break;
14209
+ }
14210
+ case "view-changes": {
14211
+ const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
14212
+ if (sub && Array.isArray(message.changes)) {
14213
+ sub.onChanges(message.changes);
14209
14214
  }
14210
14215
  break;
14211
14216
  }
@@ -14233,18 +14238,19 @@ class EventWire {
14233
14238
  this.pendingEvents = [];
14234
14239
  }
14235
14240
  }
14236
- flushPendingViewSubs() {
14241
+ sendAllViewSubscriptions() {
14237
14242
  if (!this.ws || this.state !== "connected")
14238
14243
  return;
14239
- for (const sub of this.pendingViewSubs) {
14244
+ for (const key of this.viewSubscriptions.keys()) {
14245
+ const sepIdx = key.indexOf(":");
14246
+ const scope = key.slice(0, sepIdx);
14247
+ const element = key.slice(sepIdx + 1);
14240
14248
  this.ws.send(JSON.stringify({
14241
- type: "subscribe-query",
14242
- subscriptionId: sub.subscriptionId,
14243
- descriptor: sub.descriptor,
14244
- scope: sub.scope
14249
+ type: "subscribe-view",
14250
+ element,
14251
+ scope
14245
14252
  }));
14246
14253
  }
14247
- this.pendingViewSubs = [];
14248
14254
  }
14249
14255
  scheduleReconnect() {
14250
14256
  if (this.reconnectTimeout)
@@ -14261,12 +14267,19 @@ class LocalEventPublisher2 {
14261
14267
  views = [];
14262
14268
  syncCallback;
14263
14269
  subscribers = new Map;
14270
+ viewChangesCallbacks = new Set;
14264
14271
  constructor(dataStorage) {
14265
14272
  this.dataStorage = dataStorage;
14266
14273
  }
14267
14274
  onPublish(callback) {
14268
14275
  this.syncCallback = callback;
14269
14276
  }
14277
+ onViewChanges(callback) {
14278
+ this.viewChangesCallbacks.add(callback);
14279
+ return () => {
14280
+ this.viewChangesCallbacks.delete(callback);
14281
+ };
14282
+ }
14270
14283
  registerViews(views) {
14271
14284
  this.views = views;
14272
14285
  }
@@ -14334,7 +14347,19 @@ class LocalEventPublisher2 {
14334
14347
  });
14335
14348
  const viewChanges = await this.collectViewChanges(event);
14336
14349
  allChanges.push(...viewChanges);
14337
- await this.dataStorage.commitChanges(allChanges);
14350
+ const viewStoreNames = new Set(viewChanges.map((c2) => c2.store));
14351
+ const committed = await this.dataStorage.commitChanges(allChanges, {
14352
+ captureRowsFor: viewStoreNames
14353
+ });
14354
+ if (committed.length > 0 && this.viewChangesCallbacks.size > 0) {
14355
+ for (const callback of this.viewChangesCallbacks) {
14356
+ try {
14357
+ callback(committed);
14358
+ } catch (error) {
14359
+ console.error(`[EventPublisher] onViewChanges callback error:`, error);
14360
+ }
14361
+ }
14362
+ }
14338
14363
  await this.notifySubscribers(event);
14339
14364
  if (this.syncCallback) {
14340
14365
  this.syncCallback(event);
@@ -14715,12 +14740,13 @@ function typeValidatorBuilder(typeName, comparatorStrategy) {
14715
14740
  }
14716
14741
 
14717
14742
  class DataStorage {
14718
- async commitChanges(changes) {
14743
+ async commitChanges(changes, _options) {
14719
14744
  await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
14745
+ return [];
14720
14746
  }
14721
14747
  }
14722
14748
 
14723
- class ScopedStore2 {
14749
+ class ScopedStore {
14724
14750
  #inner;
14725
14751
  #restrictions;
14726
14752
  #canWrite;
@@ -15082,7 +15108,7 @@ function resolveQueryChange(currentResult, event3, options) {
15082
15108
  }
15083
15109
  return false;
15084
15110
  }
15085
- const shouldBeInResult = checkItemMatchesWhere2(event3.item, options.where);
15111
+ const shouldBeInResult = checkItemMatchesWhere(event3.item, options.where);
15086
15112
  if (isInCurrentResult && shouldBeInResult) {
15087
15113
  const newResult = currentResult.toSpliced(index, 1, event3.item);
15088
15114
  return applyOrderByAndLimit(newResult, options);
@@ -15094,7 +15120,7 @@ function resolveQueryChange(currentResult, event3, options) {
15094
15120
  }
15095
15121
  return false;
15096
15122
  }
15097
- function checkItemMatchesWhere2(item, where) {
15123
+ function checkItemMatchesWhere(item, where) {
15098
15124
  if (!where) {
15099
15125
  return true;
15100
15126
  }
@@ -15174,8 +15200,8 @@ class ObservableDataStorage {
15174
15200
  getReadWriteTransaction() {
15175
15201
  return this.source.getReadWriteTransaction();
15176
15202
  }
15177
- commitChanges(changes) {
15178
- return this.source.commitChanges(changes);
15203
+ commitChanges(changes, options) {
15204
+ return this.source.commitChanges(changes, options);
15179
15205
  }
15180
15206
  trackQuery(storeName, options, result, listener4) {
15181
15207
  const key = this.getQueryKey(storeName, options);
@@ -15346,7 +15372,7 @@ function executeDescriptor(descriptor, context2, adapters, contextMethod, option
15346
15372
  return method(...descriptor.args);
15347
15373
  }
15348
15374
 
15349
- class ScopedModel4 {
15375
+ class ScopedModel3 {
15350
15376
  context;
15351
15377
  scopeName;
15352
15378
  parent;
@@ -15418,13 +15444,6 @@ class ScopedModel4 {
15418
15444
  }
15419
15445
  return wire.query(viewName, options, this.getAuth());
15420
15446
  }
15421
- subscribeQuery(descriptor, callback) {
15422
- const wire = this.parent.getAdapters().eventWire;
15423
- if (!wire) {
15424
- throw new Error(`Cannot subscribe to query: no eventWire available.`);
15425
- }
15426
- return wire.subscribeQuery(descriptor, callback, this.scopeName);
15427
- }
15428
15447
  get query() {
15429
15448
  return buildContextAccessor(this.context, this.scopedAdapters, "queryContext", (descriptor) => descriptor);
15430
15449
  }
@@ -15474,7 +15493,7 @@ class Model2 {
15474
15493
  scope(name) {
15475
15494
  let s = this.scopes.get(name);
15476
15495
  if (!s) {
15477
- s = new ScopedModel4(this, name);
15496
+ s = new ScopedModel3(this, name);
15478
15497
  this.scopes.set(name, s);
15479
15498
  }
15480
15499
  return s;
@@ -15486,115 +15505,100 @@ class StreamingQueryCache {
15486
15505
  views = [];
15487
15506
  activeStreams = new Map;
15488
15507
  pendingUnsubscribes = new Map;
15489
- streamScopes = new Map;
15490
15508
  static UNSUBSCRIBE_DELAY_MS = 5000;
15509
+ storeKey(viewName, scope) {
15510
+ return `${scope ?? DEFAULT_SCOPE}:${viewName}`;
15511
+ }
15491
15512
  registerViews(views) {
15492
15513
  this.views = views;
15493
- for (const view3 of views) {
15494
- if (!this.stores.has(view3.name)) {
15495
- this.stores.set(view3.name, new StreamingStore);
15496
- }
15497
- }
15498
15514
  }
15499
- getStore(viewName) {
15500
- if (!this.stores.has(viewName)) {
15501
- this.stores.set(viewName, new StreamingStore);
15515
+ getStore(viewName, scope) {
15516
+ const key = this.storeKey(viewName, scope);
15517
+ if (!this.stores.has(key)) {
15518
+ this.stores.set(key, new StreamingStore);
15502
15519
  }
15503
- return this.stores.get(viewName);
15520
+ return this.stores.get(key);
15504
15521
  }
15505
- hasData(viewName) {
15506
- const store = this.stores.get(viewName);
15522
+ hasData(viewName, scope) {
15523
+ const store = this.stores.get(this.storeKey(viewName, scope));
15507
15524
  return store ? store.hasData() : false;
15508
15525
  }
15509
- hasActiveStream(viewName) {
15510
- return this.activeStreams.has(viewName);
15511
- }
15512
- registerStream(viewName, createStream) {
15513
- const pending = this.pendingUnsubscribes.get(viewName);
15526
+ registerStream(key, createStream) {
15527
+ const pending = this.pendingUnsubscribes.get(key);
15514
15528
  if (pending) {
15515
15529
  clearTimeout(pending);
15516
- this.pendingUnsubscribes.delete(viewName);
15530
+ this.pendingUnsubscribes.delete(key);
15517
15531
  }
15518
- const existing = this.activeStreams.get(viewName);
15532
+ const existing = this.activeStreams.get(key);
15519
15533
  if (existing) {
15520
15534
  existing.refCount++;
15521
15535
  return {
15522
- unsubscribe: () => this.unregisterStream(viewName),
15536
+ unsubscribe: () => this.unregisterStream(key),
15523
15537
  wasReused: true
15524
15538
  };
15525
15539
  }
15526
15540
  const streamConn = createStream();
15527
- this.activeStreams.set(viewName, {
15541
+ this.activeStreams.set(key, {
15528
15542
  unsubscribe: streamConn.unsubscribe,
15529
15543
  refCount: 1
15530
15544
  });
15531
15545
  return {
15532
- unsubscribe: () => this.unregisterStream(viewName),
15546
+ unsubscribe: () => this.unregisterStream(key),
15533
15547
  wasReused: false
15534
15548
  };
15535
15549
  }
15536
- unregisterStream(viewName) {
15537
- const stream2 = this.activeStreams.get(viewName);
15550
+ unregisterStream(key) {
15551
+ const stream2 = this.activeStreams.get(key);
15538
15552
  if (!stream2)
15539
15553
  return;
15540
15554
  stream2.refCount--;
15541
15555
  if (stream2.refCount <= 0) {
15542
15556
  const timeout = setTimeout(() => {
15543
- this.pendingUnsubscribes.delete(viewName);
15544
- const current2 = this.activeStreams.get(viewName);
15557
+ this.pendingUnsubscribes.delete(key);
15558
+ const current2 = this.activeStreams.get(key);
15545
15559
  if (current2 && current2.refCount <= 0) {
15546
15560
  current2.unsubscribe();
15547
- this.activeStreams.delete(viewName);
15548
- this.streamScopes.delete(viewName);
15561
+ this.activeStreams.delete(key);
15549
15562
  }
15550
15563
  }, StreamingQueryCache.UNSUBSCRIBE_DELAY_MS);
15551
- this.pendingUnsubscribes.set(viewName, timeout);
15564
+ this.pendingUnsubscribes.set(key, timeout);
15552
15565
  }
15553
15566
  }
15554
- subscribeQuery(descriptor, eventWire, scope) {
15555
- const key = descriptor.element;
15556
- if (scope)
15557
- this.streamScopes.set(key, scope);
15567
+ subscribeView(viewName, eventWire, scope) {
15568
+ const key = this.storeKey(viewName, scope);
15558
15569
  const { unsubscribe } = this.registerStream(key, () => {
15559
- const subId = eventWire.subscribeQuery(descriptor, (data) => {
15560
- this.setViewData(descriptor.element, data);
15561
- }, scope);
15562
- return { unsubscribe: () => eventWire.unsubscribeQuery(subId) };
15570
+ const store = this.stores.get(key) ?? new StreamingStore;
15571
+ this.stores.set(key, store);
15572
+ eventWire.subscribeView(viewName, scope ?? DEFAULT_SCOPE, {
15573
+ onSnapshot: (items) => store.setAll(items),
15574
+ onChanges: (changes) => store.applyChanges(changes)
15575
+ });
15576
+ return {
15577
+ unsubscribe: () => eventWire.unsubscribeView(viewName, scope ?? DEFAULT_SCOPE)
15578
+ };
15563
15579
  });
15564
15580
  return unsubscribe;
15565
15581
  }
15566
15582
  invalidateScope(scope) {
15567
- for (const [viewName, viewScope] of this.streamScopes) {
15568
- if (viewScope !== scope)
15583
+ const prefix = `${scope}:`;
15584
+ for (const [key, timeout] of this.pendingUnsubscribes) {
15585
+ if (!key.startsWith(prefix))
15569
15586
  continue;
15570
- const pending = this.pendingUnsubscribes.get(viewName);
15571
- if (pending) {
15572
- clearTimeout(pending);
15573
- this.pendingUnsubscribes.delete(viewName);
15574
- }
15575
- const stream2 = this.activeStreams.get(viewName);
15576
- if (stream2) {
15577
- try {
15578
- stream2.unsubscribe();
15579
- } catch {}
15580
- this.activeStreams.delete(viewName);
15581
- }
15582
- this.streamScopes.delete(viewName);
15583
- const store = this.stores.get(viewName);
15584
- if (store)
15585
- store.clear();
15587
+ clearTimeout(timeout);
15588
+ this.pendingUnsubscribes.delete(key);
15586
15589
  }
15587
- }
15588
- setViewData(viewName, data) {
15589
- const store = this.stores.get(viewName);
15590
- if (!store)
15591
- return;
15592
- if (Array.isArray(data)) {
15593
- store.setAll(data);
15594
- } else if (data && typeof data === "object" && "_id" in data) {
15595
- store.setAll([data]);
15596
- } else {
15597
- store.setAll([]);
15590
+ for (const [key, stream2] of this.activeStreams) {
15591
+ if (!key.startsWith(prefix))
15592
+ continue;
15593
+ try {
15594
+ stream2.unsubscribe();
15595
+ } catch {}
15596
+ this.activeStreams.delete(key);
15597
+ }
15598
+ for (const [key, store] of this.stores) {
15599
+ if (!key.startsWith(prefix))
15600
+ continue;
15601
+ store.clear();
15598
15602
  }
15599
15603
  }
15600
15604
  async applyEvent(event3) {
@@ -15603,28 +15607,30 @@ class StreamingQueryCache {
15603
15607
  const handler = handlers[event3.type];
15604
15608
  if (!handler)
15605
15609
  continue;
15606
- const store = this.stores.get(view3.name);
15607
- if (!store)
15608
- continue;
15609
- const ctx = {
15610
- set: async (id3, data) => {
15611
- store.set(String(id3), { _id: String(id3), ...data });
15612
- },
15613
- modify: async (id3, data) => {
15614
- store.modify(String(id3), data);
15615
- },
15616
- remove: async (id3) => {
15617
- store.remove(String(id3));
15618
- },
15619
- find: async (options) => {
15620
- return store.find(options);
15621
- },
15622
- findOne: async (where) => {
15623
- return store.findOne(where);
15624
- },
15625
- $auth: {}
15626
- };
15627
- await handler(ctx, event3);
15610
+ const suffix = `:${view3.name}`;
15611
+ for (const [key, store] of this.stores) {
15612
+ if (!key.endsWith(suffix))
15613
+ continue;
15614
+ const ctx = {
15615
+ set: async (id3, data) => {
15616
+ store.set(String(id3), { _id: String(id3), ...data });
15617
+ },
15618
+ modify: async (id3, data) => {
15619
+ store.modify(String(id3), data);
15620
+ },
15621
+ remove: async (id3) => {
15622
+ store.remove(String(id3));
15623
+ },
15624
+ find: async (options) => {
15625
+ return store.find(options);
15626
+ },
15627
+ findOne: async (where) => {
15628
+ return store.findOne(where);
15629
+ },
15630
+ $auth: {}
15631
+ };
15632
+ await handler(ctx, event3);
15633
+ }
15628
15634
  }
15629
15635
  }
15630
15636
  clear() {
@@ -15632,7 +15638,6 @@ class StreamingQueryCache {
15632
15638
  stream2.unsubscribe();
15633
15639
  }
15634
15640
  this.activeStreams.clear();
15635
- this.streamScopes.clear();
15636
15641
  for (const timeout of this.pendingUnsubscribes.values()) {
15637
15642
  clearTimeout(timeout);
15638
15643
  }
@@ -15658,6 +15663,18 @@ class StreamingStore {
15658
15663
  }
15659
15664
  this.notifyListeners(null);
15660
15665
  }
15666
+ applyChanges(events) {
15667
+ if (events.length === 0)
15668
+ return;
15669
+ for (const event3 of events) {
15670
+ if (event3.type === "set" && event3.item) {
15671
+ this.data.set(event3.id, event3.item);
15672
+ } else if (event3.type === "delete") {
15673
+ this.data.delete(event3.id);
15674
+ }
15675
+ }
15676
+ this.notifyListeners(events);
15677
+ }
15661
15678
  set(id3, item) {
15662
15679
  this.data.set(id3, item);
15663
15680
  this.notifyListeners([{ type: "set", id: id3, item }]);
@@ -15683,7 +15700,7 @@ class StreamingStore {
15683
15700
  find(options = {}) {
15684
15701
  let results = Array.from(this.data.values());
15685
15702
  if (options.where) {
15686
- results = results.filter((item) => checkItemMatchesWhere2(item, options.where));
15703
+ results = results.filter((item) => checkItemMatchesWhere(item, options.where));
15687
15704
  }
15688
15705
  return applyOrderByAndLimit(results, options);
15689
15706
  }
@@ -18125,7 +18142,7 @@ var Operation, PROXY_DRAFT, RAW_RETURN_SYMBOL, iteratorSymbol, dataTypes, intern
18125
18142
  }
18126
18143
  return returnValue(result);
18127
18144
  };
18128
- }, create, constructorString, TOKEN_PREFIX = "arc:token:", eventWireInstanceCounter = 0, EVENT_TABLES, arrayValidator, ArcArray, objectValidator, ArcObject, ScopedDataStorage, ArcPrimitive, stringValidator, ArcString, ArcContextElement, ArcBoolean, ArcId, numberValidator, ArcNumber, ArcEvent, ArcCommand, ArcListener, ArcRoute, ForkedStoreState, ForkedDataStorage, MasterStoreState, MasterDataStorage2, dateValidator, originCache, originStackCache, originError, CLOSE, Query, PostgresError, Errors, types2, Identifier, Parameter, Builder, defaultHandlers, builders, serializers, parsers, mergeUserTypes = function(types22) {
18145
+ }, create, constructorString, TOKEN_PREFIX = "arc:token:", eventWireInstanceCounter = 0, EVENT_TABLES, arrayValidator, ArcArray, objectValidator, ArcObject, ScopedDataStorage, ArcPrimitive, stringValidator, ArcString, ArcContextElement, ArcBoolean, ArcId, numberValidator, ArcNumber, ArcEvent, ArcCommand, ArcListener, ArcRoute, ForkedStoreState, ForkedDataStorage, MasterStoreState, MasterDataStorage2, dateValidator, DEFAULT_SCOPE = "default", originCache, originStackCache, originError, CLOSE, Query, PostgresError, Errors, types2, Identifier, Parameter, Builder, defaultHandlers, builders, serializers, parsers, mergeUserTypes = function(types22) {
18129
18146
  const user = typeHandlers(types22 || {});
18130
18147
  return {
18131
18148
  serializers: Object.assign({}, serializers, user.serializers),
@@ -18862,7 +18879,7 @@ var init_dist = __esm(() => {
18862
18879
  throw new Error(`Scope violation: access denied to store "${storeName}" (not declared in query/mutate)`);
18863
18880
  }
18864
18881
  const inner = this.#inner.getStore(storeName);
18865
- return new ScopedStore2(inner, this.#restrictions, permission === "read-write");
18882
+ return new ScopedStore(inner, this.#restrictions, permission === "read-write");
18866
18883
  }
18867
18884
  fork() {
18868
18885
  return this.#inner.fork();
@@ -19724,12 +19741,23 @@ var init_dist = __esm(() => {
19724
19741
  constructor(storeName, dataStorage, deserialize) {
19725
19742
  super(storeName, dataStorage, deserialize);
19726
19743
  }
19727
- async applyChangeAndReturnEvent(transaction, change, transactionCache) {
19744
+ async readExisting(transaction, id2, transactionCache) {
19745
+ const cacheKey = `${this.storeName}:${id2}`;
19746
+ if (transactionCache && transactionCache.has(cacheKey)) {
19747
+ return transactionCache.get(cacheKey);
19748
+ }
19749
+ return transaction.find(this.storeName, { where: { _id: id2 } }).then((results) => results[0]);
19750
+ }
19751
+ async applyChangeAndReturnEvent(transaction, change, transactionCache, options) {
19728
19752
  if (change.type === "set") {
19753
+ let existing;
19754
+ if (options?.captureRows) {
19755
+ existing = await this.readExisting(transaction, change.data._id, transactionCache);
19756
+ }
19729
19757
  await transaction.set(this.storeName, change.data);
19730
19758
  const item = this.deserialize ? this.deserialize(change.data) : change.data;
19731
19759
  if (transactionCache) {
19732
- transactionCache.set(change.data._id, item);
19760
+ transactionCache.set(`${this.storeName}:${change.data._id}`, item);
19733
19761
  }
19734
19762
  return {
19735
19763
  from: null,
@@ -19738,11 +19766,20 @@ var init_dist = __esm(() => {
19738
19766
  type: "set",
19739
19767
  item: change.data,
19740
19768
  id: change.data._id
19741
- }
19769
+ },
19770
+ oldRow: existing ?? null,
19771
+ newRow: change.data
19742
19772
  };
19743
19773
  }
19744
19774
  if (change.type === "delete") {
19775
+ let existing;
19776
+ if (options?.captureRows) {
19777
+ existing = await this.readExisting(transaction, change.id, transactionCache);
19778
+ }
19745
19779
  await transaction.remove(this.storeName, change.id);
19780
+ if (transactionCache) {
19781
+ transactionCache.delete(`${this.storeName}:${change.id}`);
19782
+ }
19746
19783
  return {
19747
19784
  from: null,
19748
19785
  to: null,
@@ -19750,21 +19787,18 @@ var init_dist = __esm(() => {
19750
19787
  type: "delete",
19751
19788
  item: null,
19752
19789
  id: change.id
19753
- }
19790
+ },
19791
+ oldRow: existing ?? null,
19792
+ newRow: null
19754
19793
  };
19755
19794
  }
19756
19795
  if (change.type === "modify") {
19757
- let existing;
19758
- if (transactionCache && transactionCache.has(change.id)) {
19759
- existing = transactionCache.get(change.id);
19760
- } else {
19761
- existing = await transaction.find(this.storeName, { where: { _id: change.id } }).then((results) => results[0]);
19762
- }
19796
+ const existing = await this.readExisting(transaction, change.id, transactionCache);
19763
19797
  const updated = existing ? deepMerge(existing, change.data) : { _id: change.id, ...change.data };
19764
19798
  await transaction.set(this.storeName, updated);
19765
19799
  const item = this.deserialize ? this.deserialize(updated) : updated;
19766
19800
  if (transactionCache) {
19767
- transactionCache.set(change.id, item);
19801
+ transactionCache.set(`${this.storeName}:${change.id}`, item);
19768
19802
  }
19769
19803
  return {
19770
19804
  from: null,
@@ -19773,21 +19807,18 @@ var init_dist = __esm(() => {
19773
19807
  type: "set",
19774
19808
  item,
19775
19809
  id: change.id
19776
- }
19810
+ },
19811
+ oldRow: existing ?? null,
19812
+ newRow: updated
19777
19813
  };
19778
19814
  }
19779
19815
  if (change.type === "mutate") {
19780
- let existing;
19781
- if (transactionCache && transactionCache.has(change.id)) {
19782
- existing = transactionCache.get(change.id);
19783
- } else {
19784
- existing = await transaction.find(this.storeName, { where: { _id: change.id } }).then((results) => results[0]);
19785
- }
19816
+ const existing = await this.readExisting(transaction, change.id, transactionCache);
19786
19817
  const updated = apply(existing || {}, change.patches);
19787
19818
  await transaction.set(this.storeName, updated);
19788
19819
  const item = this.deserialize ? this.deserialize(updated) : updated;
19789
19820
  if (transactionCache) {
19790
- transactionCache.set(change.id, item);
19821
+ transactionCache.set(`${this.storeName}:${change.id}`, item);
19791
19822
  }
19792
19823
  return {
19793
19824
  from: null,
@@ -19796,7 +19827,9 @@ var init_dist = __esm(() => {
19796
19827
  type: "set",
19797
19828
  item,
19798
19829
  id: change.id
19799
- }
19830
+ },
19831
+ oldRow: existing ?? null,
19832
+ newRow: updated
19800
19833
  };
19801
19834
  }
19802
19835
  throw new Error("Unknown change type");
@@ -19873,17 +19906,22 @@ var init_dist = __esm(() => {
19873
19906
  applySerializedChanges(changes) {
19874
19907
  return Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applySerializedChanges(changes2)));
19875
19908
  }
19876
- async commitChanges(changes) {
19909
+ async commitChanges(changes, options) {
19877
19910
  const transaction = await this.getReadWriteTransaction();
19878
19911
  const transactionCache = new Map;
19879
19912
  const eventsByStore = new Map;
19913
+ const committed = [];
19880
19914
  for (const { store, changes: storeChanges } of changes) {
19881
19915
  const storeState = this.getStore(store);
19882
19916
  const storeEvents = [];
19917
+ const capture = options?.captureRowsFor?.has(store) ?? false;
19883
19918
  for (const change of storeChanges) {
19884
- const { event: event3 } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache);
19919
+ const { event: event3, oldRow, newRow } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache, { captureRows: capture });
19885
19920
  if (event3)
19886
19921
  storeEvents.push(event3);
19922
+ if (capture) {
19923
+ committed.push({ store, id: event3.id, oldRow, newRow });
19924
+ }
19887
19925
  }
19888
19926
  if (storeEvents.length > 0) {
19889
19927
  eventsByStore.set(store, storeEvents);
@@ -19894,6 +19932,7 @@ var init_dist = __esm(() => {
19894
19932
  const storeState = this.getStore(store);
19895
19933
  storeState.notifyListenersPublic(events);
19896
19934
  }
19935
+ return committed;
19897
19936
  }
19898
19937
  fork() {
19899
19938
  return new ForkedDataStorage(this);
@@ -21410,11 +21449,11 @@ class EventWire2 {
21410
21449
  reconnectTimeout;
21411
21450
  syncRequested = false;
21412
21451
  viewSubscriptions = new Map;
21413
- viewSubCounter = 0;
21414
- pendingViewSubs = [];
21415
- constructor(baseUrl) {
21452
+ enableEventSync;
21453
+ constructor(baseUrl, options) {
21416
21454
  this.baseUrl = baseUrl;
21417
21455
  this.instanceId = ++eventWireInstanceCounter2;
21456
+ this.enableEventSync = options?.enableEventSync ?? true;
21418
21457
  }
21419
21458
  setScopeToken(scope, token) {
21420
21459
  if (token === null) {
@@ -21460,9 +21499,11 @@ class EventWire2 {
21460
21499
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
21461
21500
  this.state = "connected";
21462
21501
  this.sendAllScopeTokens();
21463
- this.requestSync();
21502
+ if (this.enableEventSync) {
21503
+ this.requestSync();
21504
+ }
21464
21505
  this.flushPendingEvents();
21465
- this.flushPendingViewSubs();
21506
+ this.sendAllViewSubscriptions();
21466
21507
  } else {
21467
21508
  console.log(`[EventWire] onopen called but ws is not OPEN, readyState:`, this.ws?.readyState);
21468
21509
  }
@@ -21538,30 +21579,26 @@ class EventWire2 {
21538
21579
  onSynced(callback) {
21539
21580
  this.onSyncedCallback = callback;
21540
21581
  }
21541
- subscribeQuery(descriptor, callback, scope) {
21542
- const subscriptionId = `qs_${++this.viewSubCounter}_${Date.now()}`;
21543
- this.viewSubscriptions.set(subscriptionId, callback);
21582
+ subscribeView(element, scope, callbacks) {
21583
+ const key = `${scope}:${element}`;
21584
+ this.viewSubscriptions.set(key, callbacks);
21544
21585
  if (this.state === "connected" && this.ws) {
21545
21586
  this.ws.send(JSON.stringify({
21546
- type: "subscribe-query",
21547
- subscriptionId,
21548
- descriptor,
21587
+ type: "subscribe-view",
21588
+ element,
21549
21589
  scope
21550
21590
  }));
21551
- } else {
21552
- this.pendingViewSubs.push({ subscriptionId, descriptor, scope });
21553
21591
  }
21554
- return subscriptionId;
21555
21592
  }
21556
- unsubscribeQuery(subscriptionId) {
21557
- this.viewSubscriptions.delete(subscriptionId);
21593
+ unsubscribeView(element, scope) {
21594
+ this.viewSubscriptions.delete(`${scope}:${element}`);
21558
21595
  if (this.state === "connected" && this.ws) {
21559
21596
  this.ws.send(JSON.stringify({
21560
- type: "unsubscribe-query",
21561
- subscriptionId
21597
+ type: "unsubscribe-view",
21598
+ element,
21599
+ scope
21562
21600
  }));
21563
21601
  }
21564
- this.pendingViewSubs = this.pendingViewSubs.filter((s) => s.subscriptionId !== subscriptionId);
21565
21602
  }
21566
21603
  getState() {
21567
21604
  return this.state;
@@ -21594,10 +21631,17 @@ class EventWire2 {
21594
21631
  this.lastHostEventId = message.lastHostEventId;
21595
21632
  }
21596
21633
  break;
21597
- case "query-data": {
21598
- const cb = this.viewSubscriptions.get(message.subscriptionId);
21599
- if (cb) {
21600
- cb(message.data);
21634
+ case "view-snapshot": {
21635
+ const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
21636
+ if (sub) {
21637
+ sub.onSnapshot(message.items ?? []);
21638
+ }
21639
+ break;
21640
+ }
21641
+ case "view-changes": {
21642
+ const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
21643
+ if (sub && Array.isArray(message.changes)) {
21644
+ sub.onChanges(message.changes);
21601
21645
  }
21602
21646
  break;
21603
21647
  }
@@ -21625,18 +21669,19 @@ class EventWire2 {
21625
21669
  this.pendingEvents = [];
21626
21670
  }
21627
21671
  }
21628
- flushPendingViewSubs() {
21672
+ sendAllViewSubscriptions() {
21629
21673
  if (!this.ws || this.state !== "connected")
21630
21674
  return;
21631
- for (const sub of this.pendingViewSubs) {
21675
+ for (const key of this.viewSubscriptions.keys()) {
21676
+ const sepIdx = key.indexOf(":");
21677
+ const scope = key.slice(0, sepIdx);
21678
+ const element = key.slice(sepIdx + 1);
21632
21679
  this.ws.send(JSON.stringify({
21633
- type: "subscribe-query",
21634
- subscriptionId: sub.subscriptionId,
21635
- descriptor: sub.descriptor,
21636
- scope: sub.scope
21680
+ type: "subscribe-view",
21681
+ element,
21682
+ scope
21637
21683
  }));
21638
21684
  }
21639
- this.pendingViewSubs = [];
21640
21685
  }
21641
21686
  scheduleReconnect() {
21642
21687
  if (this.reconnectTimeout)
@@ -21653,12 +21698,19 @@ class LocalEventPublisher3 {
21653
21698
  views = [];
21654
21699
  syncCallback;
21655
21700
  subscribers = new Map;
21701
+ viewChangesCallbacks = new Set;
21656
21702
  constructor(dataStorage) {
21657
21703
  this.dataStorage = dataStorage;
21658
21704
  }
21659
21705
  onPublish(callback) {
21660
21706
  this.syncCallback = callback;
21661
21707
  }
21708
+ onViewChanges(callback) {
21709
+ this.viewChangesCallbacks.add(callback);
21710
+ return () => {
21711
+ this.viewChangesCallbacks.delete(callback);
21712
+ };
21713
+ }
21662
21714
  registerViews(views) {
21663
21715
  this.views = views;
21664
21716
  }
@@ -21726,7 +21778,19 @@ class LocalEventPublisher3 {
21726
21778
  });
21727
21779
  const viewChanges = await this.collectViewChanges(event);
21728
21780
  allChanges.push(...viewChanges);
21729
- await this.dataStorage.commitChanges(allChanges);
21781
+ const viewStoreNames = new Set(viewChanges.map((c2) => c2.store));
21782
+ const committed = await this.dataStorage.commitChanges(allChanges, {
21783
+ captureRowsFor: viewStoreNames
21784
+ });
21785
+ if (committed.length > 0 && this.viewChangesCallbacks.size > 0) {
21786
+ for (const callback of this.viewChangesCallbacks) {
21787
+ try {
21788
+ callback(committed);
21789
+ } catch (error) {
21790
+ console.error(`[EventPublisher] onViewChanges callback error:`, error);
21791
+ }
21792
+ }
21793
+ }
21730
21794
  await this.notifySubscribers(event);
21731
21795
  if (this.syncCallback) {
21732
21796
  this.syncCallback(event);
@@ -22107,12 +22171,13 @@ function typeValidatorBuilder2(typeName, comparatorStrategy) {
22107
22171
  }
22108
22172
 
22109
22173
  class DataStorage2 {
22110
- async commitChanges(changes) {
22174
+ async commitChanges(changes, _options) {
22111
22175
  await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
22176
+ return [];
22112
22177
  }
22113
22178
  }
22114
22179
 
22115
- class ScopedStore3 {
22180
+ class ScopedStore2 {
22116
22181
  #inner;
22117
22182
  #restrictions;
22118
22183
  #canWrite;
@@ -22474,7 +22539,7 @@ function resolveQueryChange2(currentResult, event3, options) {
22474
22539
  }
22475
22540
  return false;
22476
22541
  }
22477
- const shouldBeInResult = checkItemMatchesWhere3(event3.item, options.where);
22542
+ const shouldBeInResult = checkItemMatchesWhere2(event3.item, options.where);
22478
22543
  if (isInCurrentResult && shouldBeInResult) {
22479
22544
  const newResult = currentResult.toSpliced(index, 1, event3.item);
22480
22545
  return applyOrderByAndLimit2(newResult, options);
@@ -22486,7 +22551,7 @@ function resolveQueryChange2(currentResult, event3, options) {
22486
22551
  }
22487
22552
  return false;
22488
22553
  }
22489
- function checkItemMatchesWhere3(item, where) {
22554
+ function checkItemMatchesWhere2(item, where) {
22490
22555
  if (!where) {
22491
22556
  return true;
22492
22557
  }
@@ -22566,8 +22631,8 @@ class ObservableDataStorage2 {
22566
22631
  getReadWriteTransaction() {
22567
22632
  return this.source.getReadWriteTransaction();
22568
22633
  }
22569
- commitChanges(changes) {
22570
- return this.source.commitChanges(changes);
22634
+ commitChanges(changes, options) {
22635
+ return this.source.commitChanges(changes, options);
22571
22636
  }
22572
22637
  trackQuery(storeName, options, result, listener4) {
22573
22638
  const key = this.getQueryKey(storeName, options);
@@ -22738,7 +22803,7 @@ function executeDescriptor2(descriptor, context2, adapters, contextMethod, optio
22738
22803
  return method(...descriptor.args);
22739
22804
  }
22740
22805
 
22741
- class ScopedModel5 {
22806
+ class ScopedModel4 {
22742
22807
  context;
22743
22808
  scopeName;
22744
22809
  parent;
@@ -22810,13 +22875,6 @@ class ScopedModel5 {
22810
22875
  }
22811
22876
  return wire.query(viewName, options, this.getAuth());
22812
22877
  }
22813
- subscribeQuery(descriptor, callback) {
22814
- const wire = this.parent.getAdapters().eventWire;
22815
- if (!wire) {
22816
- throw new Error(`Cannot subscribe to query: no eventWire available.`);
22817
- }
22818
- return wire.subscribeQuery(descriptor, callback, this.scopeName);
22819
- }
22820
22878
  get query() {
22821
22879
  return buildContextAccessor2(this.context, this.scopedAdapters, "queryContext", (descriptor) => descriptor);
22822
22880
  }
@@ -22866,7 +22924,7 @@ class Model3 {
22866
22924
  scope(name) {
22867
22925
  let s = this.scopes.get(name);
22868
22926
  if (!s) {
22869
- s = new ScopedModel5(this, name);
22927
+ s = new ScopedModel4(this, name);
22870
22928
  this.scopes.set(name, s);
22871
22929
  }
22872
22930
  return s;
@@ -22878,115 +22936,100 @@ class StreamingQueryCache2 {
22878
22936
  views = [];
22879
22937
  activeStreams = new Map;
22880
22938
  pendingUnsubscribes = new Map;
22881
- streamScopes = new Map;
22882
22939
  static UNSUBSCRIBE_DELAY_MS = 5000;
22940
+ storeKey(viewName, scope) {
22941
+ return `${scope ?? DEFAULT_SCOPE2}:${viewName}`;
22942
+ }
22883
22943
  registerViews(views) {
22884
22944
  this.views = views;
22885
- for (const view3 of views) {
22886
- if (!this.stores.has(view3.name)) {
22887
- this.stores.set(view3.name, new StreamingStore2);
22888
- }
22889
- }
22890
22945
  }
22891
- getStore(viewName) {
22892
- if (!this.stores.has(viewName)) {
22893
- this.stores.set(viewName, new StreamingStore2);
22946
+ getStore(viewName, scope) {
22947
+ const key = this.storeKey(viewName, scope);
22948
+ if (!this.stores.has(key)) {
22949
+ this.stores.set(key, new StreamingStore2);
22894
22950
  }
22895
- return this.stores.get(viewName);
22951
+ return this.stores.get(key);
22896
22952
  }
22897
- hasData(viewName) {
22898
- const store = this.stores.get(viewName);
22953
+ hasData(viewName, scope) {
22954
+ const store = this.stores.get(this.storeKey(viewName, scope));
22899
22955
  return store ? store.hasData() : false;
22900
22956
  }
22901
- hasActiveStream(viewName) {
22902
- return this.activeStreams.has(viewName);
22903
- }
22904
- registerStream(viewName, createStream) {
22905
- const pending = this.pendingUnsubscribes.get(viewName);
22957
+ registerStream(key, createStream) {
22958
+ const pending = this.pendingUnsubscribes.get(key);
22906
22959
  if (pending) {
22907
22960
  clearTimeout(pending);
22908
- this.pendingUnsubscribes.delete(viewName);
22961
+ this.pendingUnsubscribes.delete(key);
22909
22962
  }
22910
- const existing = this.activeStreams.get(viewName);
22963
+ const existing = this.activeStreams.get(key);
22911
22964
  if (existing) {
22912
22965
  existing.refCount++;
22913
22966
  return {
22914
- unsubscribe: () => this.unregisterStream(viewName),
22967
+ unsubscribe: () => this.unregisterStream(key),
22915
22968
  wasReused: true
22916
22969
  };
22917
22970
  }
22918
22971
  const streamConn = createStream();
22919
- this.activeStreams.set(viewName, {
22972
+ this.activeStreams.set(key, {
22920
22973
  unsubscribe: streamConn.unsubscribe,
22921
22974
  refCount: 1
22922
22975
  });
22923
22976
  return {
22924
- unsubscribe: () => this.unregisterStream(viewName),
22977
+ unsubscribe: () => this.unregisterStream(key),
22925
22978
  wasReused: false
22926
22979
  };
22927
22980
  }
22928
- unregisterStream(viewName) {
22929
- const stream2 = this.activeStreams.get(viewName);
22981
+ unregisterStream(key) {
22982
+ const stream2 = this.activeStreams.get(key);
22930
22983
  if (!stream2)
22931
22984
  return;
22932
22985
  stream2.refCount--;
22933
22986
  if (stream2.refCount <= 0) {
22934
22987
  const timeout = setTimeout(() => {
22935
- this.pendingUnsubscribes.delete(viewName);
22936
- const current22 = this.activeStreams.get(viewName);
22988
+ this.pendingUnsubscribes.delete(key);
22989
+ const current22 = this.activeStreams.get(key);
22937
22990
  if (current22 && current22.refCount <= 0) {
22938
22991
  current22.unsubscribe();
22939
- this.activeStreams.delete(viewName);
22940
- this.streamScopes.delete(viewName);
22992
+ this.activeStreams.delete(key);
22941
22993
  }
22942
22994
  }, StreamingQueryCache2.UNSUBSCRIBE_DELAY_MS);
22943
- this.pendingUnsubscribes.set(viewName, timeout);
22995
+ this.pendingUnsubscribes.set(key, timeout);
22944
22996
  }
22945
22997
  }
22946
- subscribeQuery(descriptor, eventWire, scope) {
22947
- const key = descriptor.element;
22948
- if (scope)
22949
- this.streamScopes.set(key, scope);
22998
+ subscribeView(viewName, eventWire, scope) {
22999
+ const key = this.storeKey(viewName, scope);
22950
23000
  const { unsubscribe } = this.registerStream(key, () => {
22951
- const subId = eventWire.subscribeQuery(descriptor, (data) => {
22952
- this.setViewData(descriptor.element, data);
22953
- }, scope);
22954
- return { unsubscribe: () => eventWire.unsubscribeQuery(subId) };
23001
+ const store = this.stores.get(key) ?? new StreamingStore2;
23002
+ this.stores.set(key, store);
23003
+ eventWire.subscribeView(viewName, scope ?? DEFAULT_SCOPE2, {
23004
+ onSnapshot: (items) => store.setAll(items),
23005
+ onChanges: (changes) => store.applyChanges(changes)
23006
+ });
23007
+ return {
23008
+ unsubscribe: () => eventWire.unsubscribeView(viewName, scope ?? DEFAULT_SCOPE2)
23009
+ };
22955
23010
  });
22956
23011
  return unsubscribe;
22957
23012
  }
22958
23013
  invalidateScope(scope) {
22959
- for (const [viewName, viewScope] of this.streamScopes) {
22960
- if (viewScope !== scope)
23014
+ const prefix = `${scope}:`;
23015
+ for (const [key, timeout] of this.pendingUnsubscribes) {
23016
+ if (!key.startsWith(prefix))
22961
23017
  continue;
22962
- const pending = this.pendingUnsubscribes.get(viewName);
22963
- if (pending) {
22964
- clearTimeout(pending);
22965
- this.pendingUnsubscribes.delete(viewName);
22966
- }
22967
- const stream2 = this.activeStreams.get(viewName);
22968
- if (stream2) {
22969
- try {
22970
- stream2.unsubscribe();
22971
- } catch {}
22972
- this.activeStreams.delete(viewName);
22973
- }
22974
- this.streamScopes.delete(viewName);
22975
- const store = this.stores.get(viewName);
22976
- if (store)
22977
- store.clear();
23018
+ clearTimeout(timeout);
23019
+ this.pendingUnsubscribes.delete(key);
22978
23020
  }
22979
- }
22980
- setViewData(viewName, data) {
22981
- const store = this.stores.get(viewName);
22982
- if (!store)
22983
- return;
22984
- if (Array.isArray(data)) {
22985
- store.setAll(data);
22986
- } else if (data && typeof data === "object" && "_id" in data) {
22987
- store.setAll([data]);
22988
- } else {
22989
- store.setAll([]);
23021
+ for (const [key, stream2] of this.activeStreams) {
23022
+ if (!key.startsWith(prefix))
23023
+ continue;
23024
+ try {
23025
+ stream2.unsubscribe();
23026
+ } catch {}
23027
+ this.activeStreams.delete(key);
23028
+ }
23029
+ for (const [key, store] of this.stores) {
23030
+ if (!key.startsWith(prefix))
23031
+ continue;
23032
+ store.clear();
22990
23033
  }
22991
23034
  }
22992
23035
  async applyEvent(event3) {
@@ -22995,28 +23038,30 @@ class StreamingQueryCache2 {
22995
23038
  const handler = handlers[event3.type];
22996
23039
  if (!handler)
22997
23040
  continue;
22998
- const store = this.stores.get(view3.name);
22999
- if (!store)
23000
- continue;
23001
- const ctx = {
23002
- set: async (id3, data) => {
23003
- store.set(String(id3), { _id: String(id3), ...data });
23004
- },
23005
- modify: async (id3, data) => {
23006
- store.modify(String(id3), data);
23007
- },
23008
- remove: async (id3) => {
23009
- store.remove(String(id3));
23010
- },
23011
- find: async (options) => {
23012
- return store.find(options);
23013
- },
23014
- findOne: async (where) => {
23015
- return store.findOne(where);
23016
- },
23017
- $auth: {}
23018
- };
23019
- await handler(ctx, event3);
23041
+ const suffix = `:${view3.name}`;
23042
+ for (const [key, store] of this.stores) {
23043
+ if (!key.endsWith(suffix))
23044
+ continue;
23045
+ const ctx = {
23046
+ set: async (id3, data) => {
23047
+ store.set(String(id3), { _id: String(id3), ...data });
23048
+ },
23049
+ modify: async (id3, data) => {
23050
+ store.modify(String(id3), data);
23051
+ },
23052
+ remove: async (id3) => {
23053
+ store.remove(String(id3));
23054
+ },
23055
+ find: async (options) => {
23056
+ return store.find(options);
23057
+ },
23058
+ findOne: async (where) => {
23059
+ return store.findOne(where);
23060
+ },
23061
+ $auth: {}
23062
+ };
23063
+ await handler(ctx, event3);
23064
+ }
23020
23065
  }
23021
23066
  }
23022
23067
  clear() {
@@ -23024,7 +23069,6 @@ class StreamingQueryCache2 {
23024
23069
  stream2.unsubscribe();
23025
23070
  }
23026
23071
  this.activeStreams.clear();
23027
- this.streamScopes.clear();
23028
23072
  for (const timeout of this.pendingUnsubscribes.values()) {
23029
23073
  clearTimeout(timeout);
23030
23074
  }
@@ -23050,6 +23094,18 @@ class StreamingStore2 {
23050
23094
  }
23051
23095
  this.notifyListeners(null);
23052
23096
  }
23097
+ applyChanges(events) {
23098
+ if (events.length === 0)
23099
+ return;
23100
+ for (const event3 of events) {
23101
+ if (event3.type === "set" && event3.item) {
23102
+ this.data.set(event3.id, event3.item);
23103
+ } else if (event3.type === "delete") {
23104
+ this.data.delete(event3.id);
23105
+ }
23106
+ }
23107
+ this.notifyListeners(events);
23108
+ }
23053
23109
  set(id3, item) {
23054
23110
  this.data.set(id3, item);
23055
23111
  this.notifyListeners([{ type: "set", id: id3, item }]);
@@ -23075,7 +23131,7 @@ class StreamingStore2 {
23075
23131
  find(options = {}) {
23076
23132
  let results = Array.from(this.data.values());
23077
23133
  if (options.where) {
23078
- results = results.filter((item) => checkItemMatchesWhere3(item, options.where));
23134
+ results = results.filter((item) => checkItemMatchesWhere2(item, options.where));
23079
23135
  }
23080
23136
  return applyOrderByAndLimit2(results, options);
23081
23137
  }
@@ -23993,7 +24049,7 @@ var Operation2, PROXY_DRAFT2, RAW_RETURN_SYMBOL2, iteratorSymbol2, dataTypes2, i
23993
24049
  }
23994
24050
  return returnValue(result);
23995
24051
  };
23996
- }, create2, constructorString2, TOKEN_PREFIX2 = "arc:token:", eventWireInstanceCounter2 = 0, EVENT_TABLES2, arrayValidator2, ArcArray2, objectValidator2, ArcObject2, ScopedDataStorage2, ArcPrimitive2, stringValidator2, ArcString2, ArcContextElement2, ArcBoolean2, ArcId2, numberValidator2, ArcNumber2, ArcEvent2, ArcCommand2, ArcListener2, ArcRoute2, ForkedStoreState2, ForkedDataStorage2, MasterStoreState2, MasterDataStorage3, dateValidator2, SQLiteReadWriteTransaction, createSQLiteAdapterFactory = (db) => {
24052
+ }, create2, constructorString2, TOKEN_PREFIX2 = "arc:token:", eventWireInstanceCounter2 = 0, EVENT_TABLES2, arrayValidator2, ArcArray2, objectValidator2, ArcObject2, ScopedDataStorage2, ArcPrimitive2, stringValidator2, ArcString2, ArcContextElement2, ArcBoolean2, ArcId2, numberValidator2, ArcNumber2, ArcEvent2, ArcCommand2, ArcListener2, ArcRoute2, ForkedStoreState2, ForkedDataStorage2, MasterStoreState2, MasterDataStorage3, dateValidator2, DEFAULT_SCOPE2 = "default", SQLiteReadWriteTransaction, createSQLiteAdapterFactory = (db) => {
23997
24053
  return async (context) => {
23998
24054
  const adapter = new SQLiteAdapter(db, context);
23999
24055
  await adapter.initialize();
@@ -24689,7 +24745,7 @@ var init_dist2 = __esm(() => {
24689
24745
  throw new Error(`Scope violation: access denied to store "${storeName}" (not declared in query/mutate)`);
24690
24746
  }
24691
24747
  const inner = this.#inner.getStore(storeName);
24692
- return new ScopedStore3(inner, this.#restrictions, permission === "read-write");
24748
+ return new ScopedStore2(inner, this.#restrictions, permission === "read-write");
24693
24749
  }
24694
24750
  fork() {
24695
24751
  return this.#inner.fork();
@@ -25551,12 +25607,23 @@ var init_dist2 = __esm(() => {
25551
25607
  constructor(storeName, dataStorage, deserialize) {
25552
25608
  super(storeName, dataStorage, deserialize);
25553
25609
  }
25554
- async applyChangeAndReturnEvent(transaction, change, transactionCache) {
25610
+ async readExisting(transaction, id22, transactionCache) {
25611
+ const cacheKey = `${this.storeName}:${id22}`;
25612
+ if (transactionCache && transactionCache.has(cacheKey)) {
25613
+ return transactionCache.get(cacheKey);
25614
+ }
25615
+ return transaction.find(this.storeName, { where: { _id: id22 } }).then((results) => results[0]);
25616
+ }
25617
+ async applyChangeAndReturnEvent(transaction, change, transactionCache, options) {
25555
25618
  if (change.type === "set") {
25619
+ let existing;
25620
+ if (options?.captureRows) {
25621
+ existing = await this.readExisting(transaction, change.data._id, transactionCache);
25622
+ }
25556
25623
  await transaction.set(this.storeName, change.data);
25557
25624
  const item = this.deserialize ? this.deserialize(change.data) : change.data;
25558
25625
  if (transactionCache) {
25559
- transactionCache.set(change.data._id, item);
25626
+ transactionCache.set(`${this.storeName}:${change.data._id}`, item);
25560
25627
  }
25561
25628
  return {
25562
25629
  from: null,
@@ -25565,11 +25632,20 @@ var init_dist2 = __esm(() => {
25565
25632
  type: "set",
25566
25633
  item: change.data,
25567
25634
  id: change.data._id
25568
- }
25635
+ },
25636
+ oldRow: existing ?? null,
25637
+ newRow: change.data
25569
25638
  };
25570
25639
  }
25571
25640
  if (change.type === "delete") {
25641
+ let existing;
25642
+ if (options?.captureRows) {
25643
+ existing = await this.readExisting(transaction, change.id, transactionCache);
25644
+ }
25572
25645
  await transaction.remove(this.storeName, change.id);
25646
+ if (transactionCache) {
25647
+ transactionCache.delete(`${this.storeName}:${change.id}`);
25648
+ }
25573
25649
  return {
25574
25650
  from: null,
25575
25651
  to: null,
@@ -25577,21 +25653,18 @@ var init_dist2 = __esm(() => {
25577
25653
  type: "delete",
25578
25654
  item: null,
25579
25655
  id: change.id
25580
- }
25656
+ },
25657
+ oldRow: existing ?? null,
25658
+ newRow: null
25581
25659
  };
25582
25660
  }
25583
25661
  if (change.type === "modify") {
25584
- let existing;
25585
- if (transactionCache && transactionCache.has(change.id)) {
25586
- existing = transactionCache.get(change.id);
25587
- } else {
25588
- existing = await transaction.find(this.storeName, { where: { _id: change.id } }).then((results) => results[0]);
25589
- }
25662
+ const existing = await this.readExisting(transaction, change.id, transactionCache);
25590
25663
  const updated = existing ? deepMerge2(existing, change.data) : { _id: change.id, ...change.data };
25591
25664
  await transaction.set(this.storeName, updated);
25592
25665
  const item = this.deserialize ? this.deserialize(updated) : updated;
25593
25666
  if (transactionCache) {
25594
- transactionCache.set(change.id, item);
25667
+ transactionCache.set(`${this.storeName}:${change.id}`, item);
25595
25668
  }
25596
25669
  return {
25597
25670
  from: null,
@@ -25600,21 +25673,18 @@ var init_dist2 = __esm(() => {
25600
25673
  type: "set",
25601
25674
  item,
25602
25675
  id: change.id
25603
- }
25676
+ },
25677
+ oldRow: existing ?? null,
25678
+ newRow: updated
25604
25679
  };
25605
25680
  }
25606
25681
  if (change.type === "mutate") {
25607
- let existing;
25608
- if (transactionCache && transactionCache.has(change.id)) {
25609
- existing = transactionCache.get(change.id);
25610
- } else {
25611
- existing = await transaction.find(this.storeName, { where: { _id: change.id } }).then((results) => results[0]);
25612
- }
25682
+ const existing = await this.readExisting(transaction, change.id, transactionCache);
25613
25683
  const updated = apply2(existing || {}, change.patches);
25614
25684
  await transaction.set(this.storeName, updated);
25615
25685
  const item = this.deserialize ? this.deserialize(updated) : updated;
25616
25686
  if (transactionCache) {
25617
- transactionCache.set(change.id, item);
25687
+ transactionCache.set(`${this.storeName}:${change.id}`, item);
25618
25688
  }
25619
25689
  return {
25620
25690
  from: null,
@@ -25623,7 +25693,9 @@ var init_dist2 = __esm(() => {
25623
25693
  type: "set",
25624
25694
  item,
25625
25695
  id: change.id
25626
- }
25696
+ },
25697
+ oldRow: existing ?? null,
25698
+ newRow: updated
25627
25699
  };
25628
25700
  }
25629
25701
  throw new Error("Unknown change type");
@@ -25700,17 +25772,22 @@ var init_dist2 = __esm(() => {
25700
25772
  applySerializedChanges(changes) {
25701
25773
  return Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applySerializedChanges(changes2)));
25702
25774
  }
25703
- async commitChanges(changes) {
25775
+ async commitChanges(changes, options) {
25704
25776
  const transaction = await this.getReadWriteTransaction();
25705
25777
  const transactionCache = new Map;
25706
25778
  const eventsByStore = new Map;
25779
+ const committed = [];
25707
25780
  for (const { store, changes: storeChanges } of changes) {
25708
25781
  const storeState = this.getStore(store);
25709
25782
  const storeEvents = [];
25783
+ const capture = options?.captureRowsFor?.has(store) ?? false;
25710
25784
  for (const change of storeChanges) {
25711
- const { event: event3 } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache);
25785
+ const { event: event3, oldRow, newRow } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache, { captureRows: capture });
25712
25786
  if (event3)
25713
25787
  storeEvents.push(event3);
25788
+ if (capture) {
25789
+ committed.push({ store, id: event3.id, oldRow, newRow });
25790
+ }
25714
25791
  }
25715
25792
  if (storeEvents.length > 0) {
25716
25793
  eventsByStore.set(store, storeEvents);
@@ -25721,6 +25798,7 @@ var init_dist2 = __esm(() => {
25721
25798
  const storeState = this.getStore(store);
25722
25799
  storeState.notifyListenersPublic(events);
25723
25800
  }
25801
+ return committed;
25724
25802
  }
25725
25803
  fork() {
25726
25804
  return new ForkedDataStorage2(this);
@@ -40055,66 +40133,14 @@ function arcHttpHandlers(ch, cm) {
40055
40133
  ];
40056
40134
  }
40057
40135
  // ../host/src/middleware/ws.ts
40058
- import {
40059
- ScopedModel as ScopedModel3,
40060
- ScopedStore,
40061
- checkItemMatchesWhere
40062
- } from "@arcote.tech/arc";
40063
- var viewSubscribers = new Map;
40136
+ import { LiveQuery } from "@arcote.tech/arc";
40137
+ var clientQuerySubs = new Map;
40064
40138
  function cleanupClientSubs(clientId) {
40065
- const prefix = `${clientId}:`;
40066
- for (const subs of viewSubscribers.values()) {
40067
- for (const key of subs.keys()) {
40068
- if (key.startsWith(prefix))
40069
- subs.delete(key);
40070
- }
40071
- }
40072
- }
40073
- function filterChangeForSubscriber(change, restrictions) {
40074
- const newMatches = change.newRow !== null && (restrictions === null || checkItemMatchesWhere(change.newRow, restrictions));
40075
- if (newMatches) {
40076
- return { type: "set", id: change.id, item: change.newRow };
40077
- }
40078
- const oldMatches = change.oldRow !== null && (restrictions === null || checkItemMatchesWhere(change.oldRow, restrictions));
40079
- if (oldMatches) {
40080
- return { type: "delete", id: change.id, item: null };
40081
- }
40082
- return null;
40083
- }
40084
- function broadcastViewChanges(committed, connectionManager) {
40085
- const byStore = new Map;
40086
- for (const change of committed) {
40087
- let list = byStore.get(change.store);
40088
- if (!list) {
40089
- list = [];
40090
- byStore.set(change.store, list);
40091
- }
40092
- list.push(change);
40093
- }
40094
- for (const [viewName, changes] of byStore) {
40095
- const subs = viewSubscribers.get(viewName);
40096
- if (!subs || subs.size === 0)
40097
- continue;
40098
- for (const sub of subs.values()) {
40099
- const filtered = [];
40100
- for (const change of changes) {
40101
- const wireChange = filterChangeForSubscriber(change, sub.restrictions);
40102
- if (wireChange)
40103
- filtered.push(wireChange);
40104
- }
40105
- if (filtered.length === 0)
40106
- continue;
40107
- if (sub.buffer) {
40108
- sub.buffer.push(...filtered);
40109
- continue;
40110
- }
40111
- connectionManager.sendToClient(sub.clientId, {
40112
- type: "view-changes",
40113
- element: viewName,
40114
- scope: sub.scope,
40115
- changes: filtered
40116
- });
40117
- }
40139
+ const subs = clientQuerySubs.get(clientId);
40140
+ if (subs) {
40141
+ for (const live of subs.values())
40142
+ live.stop();
40143
+ clientQuerySubs.delete(clientId);
40118
40144
  }
40119
40145
  }
40120
40146
  function scopeAuthHandler() {
@@ -40213,82 +40239,62 @@ function executeCommandHandler() {
40213
40239
  return true;
40214
40240
  };
40215
40241
  }
40216
- function viewSubscriptionHandler() {
40242
+ function querySubscriptionHandler() {
40217
40243
  return async (client, message, ctx) => {
40218
- if (message.type === "subscribe-view") {
40219
- const { element: elementName } = message;
40244
+ if (message.type === "subscribe-query") {
40245
+ const { subscriptionId, descriptor } = message;
40220
40246
  const scope = message.scope ?? "default";
40221
40247
  const scopeToken = client.scopeTokens.get(scope) ?? null;
40222
40248
  let rawToken = scopeToken?.raw ?? null;
40223
40249
  if (!rawToken && client.scopeTokens.size > 0) {
40224
40250
  rawToken = client.scopeTokens.values().next().value.raw;
40225
40251
  }
40226
- const scoped = new ScopedModel3(ctx.contextHandler.getModel(), scope);
40227
- if (rawToken)
40228
- scoped.setToken(rawToken);
40229
- const element = ctx.contextHandler.getModel().context.get(elementName);
40230
- if (!element || typeof element.getRestrictionsFor !== "function") {
40231
- ctx.connectionManager.sendToClient(client.id, {
40232
- type: "error",
40233
- message: `Cannot subscribe to view "${elementName}": unknown element`
40234
- });
40235
- return true;
40236
- }
40237
- const { restrictions, denied } = element.getRestrictionsFor(scoped.getAdapters());
40238
- const subKey = `${client.id}:${scope}`;
40239
- if (denied) {
40240
- viewSubscribers.get(elementName)?.delete(subKey);
40241
- ctx.connectionManager.sendToClient(client.id, {
40242
- type: "view-snapshot",
40243
- element: elementName,
40244
- scope,
40245
- items: []
40246
- });
40247
- return true;
40248
- }
40249
- const subscriber = {
40250
- clientId: client.id,
40251
- scope,
40252
- restrictions,
40253
- buffer: []
40254
- };
40255
- if (!viewSubscribers.has(elementName)) {
40256
- viewSubscribers.set(elementName, new Map);
40252
+ clientQuerySubs.get(client.id)?.get(subscriptionId)?.stop();
40253
+ const live = new LiveQuery(ctx.contextHandler.getModel(), descriptor, scope, rawToken, (update) => {
40254
+ if (update.type === "changes") {
40255
+ ctx.connectionManager.sendToClient(client.id, {
40256
+ type: "query-changes",
40257
+ subscriptionId,
40258
+ changes: update.changes
40259
+ });
40260
+ } else {
40261
+ ctx.connectionManager.sendToClient(client.id, {
40262
+ type: "query-snapshot",
40263
+ subscriptionId,
40264
+ result: update.result ?? null
40265
+ });
40266
+ }
40267
+ });
40268
+ if (!clientQuerySubs.has(client.id)) {
40269
+ clientQuerySubs.set(client.id, new Map);
40257
40270
  }
40258
- viewSubscribers.get(elementName).set(subKey, subscriber);
40271
+ clientQuerySubs.get(client.id).set(subscriptionId, live);
40259
40272
  try {
40260
- const store = ctx.contextHandler.getDataStorage().getStore(elementName);
40261
- const items = restrictions ? await new ScopedStore(store, restrictions, false).find({}) : await store.find({});
40273
+ const result = await live.start();
40262
40274
  ctx.connectionManager.sendToClient(client.id, {
40263
- type: "view-snapshot",
40264
- element: elementName,
40265
- scope,
40266
- items
40275
+ type: "query-snapshot",
40276
+ subscriptionId,
40277
+ result: result ?? null
40267
40278
  });
40279
+ live.flush();
40268
40280
  } catch (err3) {
40269
- viewSubscribers.get(elementName)?.delete(subKey);
40270
- console.error(`[Arc] View subscription error (${elementName}):`, err3);
40281
+ live.stop();
40282
+ clientQuerySubs.get(client.id)?.delete(subscriptionId);
40283
+ console.error(`[Arc] Query subscription error (${descriptor?.element}.${descriptor?.method}):`, err3);
40271
40284
  ctx.connectionManager.sendToClient(client.id, {
40272
40285
  type: "error",
40273
- message: `Failed to subscribe to view "${elementName}"`
40274
- });
40275
- return true;
40276
- }
40277
- const buffered = subscriber.buffer;
40278
- subscriber.buffer = null;
40279
- if (buffered.length > 0) {
40280
- ctx.connectionManager.sendToClient(client.id, {
40281
- type: "view-changes",
40282
- element: elementName,
40283
- scope,
40284
- changes: buffered
40286
+ message: `Failed to subscribe to query "${descriptor?.element}.${descriptor?.method}"`
40285
40287
  });
40286
40288
  }
40287
40289
  return true;
40288
40290
  }
40289
- if (message.type === "unsubscribe-view") {
40290
- const scope = message.scope ?? "default";
40291
- viewSubscribers.get(message.element)?.delete(`${client.id}:${scope}`);
40291
+ if (message.type === "unsubscribe-query") {
40292
+ const subs = clientQuerySubs.get(client.id);
40293
+ const live = subs?.get(message.subscriptionId);
40294
+ if (live) {
40295
+ live.stop();
40296
+ subs.delete(message.subscriptionId);
40297
+ }
40292
40298
  return true;
40293
40299
  }
40294
40300
  return false;
@@ -40300,7 +40306,7 @@ function arcWsHandlers() {
40300
40306
  syncEventsHandler(),
40301
40307
  requestSyncHandler(),
40302
40308
  executeCommandHandler(),
40303
- viewSubscriptionHandler()
40309
+ querySubscriptionHandler()
40304
40310
  ];
40305
40311
  }
40306
40312
  // ../host/src/create-server.ts
@@ -40313,7 +40319,6 @@ async function createArcServer(config) {
40313
40319
  const cronScheduler = new CronScheduler(contextHandler);
40314
40320
  cronScheduler.start();
40315
40321
  const connectionManager = new ConnectionManager;
40316
- contextHandler.getEventPublisher().onViewChanges((changes) => broadcastViewChanges(changes, connectionManager));
40317
40322
  function buildCorsHeaders(req) {
40318
40323
  const origin = req?.headers.get("Origin") || "*";
40319
40324
  return {