@arcote.tech/arc 0.7.13 → 0.7.15

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.
@@ -14,7 +14,7 @@
14
14
  import type { ArcEventAny } from "../context-element/event";
15
15
  import type { ArcEventInstance } from "../context-element/event/instance";
16
16
  import type { ArcViewAny } from "../context-element/view/view";
17
- import type { DataStorage } from "../data-storage";
17
+ import type { CommittedChange, DataStorage } from "../data-storage";
18
18
  /** Shared table names for all events */
19
19
  export declare const EVENT_TABLES: {
20
20
  readonly events: "events";
@@ -99,12 +99,19 @@ export declare class LocalEventPublisher implements EventPublisher {
99
99
  private views;
100
100
  private syncCallback?;
101
101
  private subscribers;
102
+ private viewChangesCallbacks;
102
103
  constructor(dataStorage: DataStorage);
103
104
  /**
104
105
  * Set a callback to be called after each event is published
105
106
  * Used for syncing events to the host
106
107
  */
107
108
  onPublish(callback: (event: ArcEventInstance<ArcEventAny>) => void): void;
109
+ /**
110
+ * Register a callback fired after each publish commit with the committed
111
+ * VIEW store changes (old/new rows). Used by the host to broadcast
112
+ * incremental view deltas to subscribed streaming clients.
113
+ */
114
+ onViewChanges(callback: (changes: CommittedChange[]) => void): () => void;
108
115
  /**
109
116
  * Register views that should be updated when events are published
110
117
  */
@@ -24,8 +24,17 @@ export interface ReceivedEvent {
24
24
  clientId: string;
25
25
  authContext: EventAuthContext | null;
26
26
  }
27
- import type { ContextDescriptor } from "../model/context-accessor";
27
+ import type { ListenerEvent } from "../data-storage/data-storage.abstract";
28
28
  type EventWireState = "disconnected" | "connecting" | "connected";
29
+ /**
30
+ * Callbacks for a view replica subscription. The server answers
31
+ * `subscribe-view` with a full `view-snapshot`, then pushes incremental
32
+ * `view-changes` deltas (already filtered by the scope's token).
33
+ */
34
+ export interface ViewSubscriptionCallbacks {
35
+ onSnapshot: (items: any[]) => void;
36
+ onChanges: (changes: ListenerEvent<any>[]) => void;
37
+ }
29
38
  export declare class EventWire {
30
39
  private readonly baseUrl;
31
40
  private instanceId;
@@ -38,10 +47,17 @@ export declare class EventWire {
38
47
  private onSyncedCallback?;
39
48
  private reconnectTimeout?;
40
49
  private syncRequested;
50
+ /** Active view subscriptions keyed by `${scope}:${element}`. Re-sent in
51
+ * full on every (re)connect — the server drops its registry on
52
+ * disconnect, and a fresh snapshot follows each re-subscribe. */
41
53
  private viewSubscriptions;
42
- private viewSubCounter;
43
- private pendingViewSubs;
44
- constructor(baseUrl: string);
54
+ /** When false (streaming mode), the client neither requests the event
55
+ * log (`request-sync`) nor consumes domain events — view replicas are
56
+ * the only data channel. Local mode keeps full event sync. */
57
+ private readonly enableEventSync;
58
+ constructor(baseUrl: string, options?: {
59
+ enableEventSync?: boolean;
60
+ });
45
61
  /**
46
62
  * Set a scope token. If connected, sends scope:auth message to server.
47
63
  * If token is null, removes the scope.
@@ -81,14 +97,16 @@ export declare class EventWire {
81
97
  */
82
98
  onSynced(callback: (localIds: string[]) => void): void;
83
99
  /**
84
- * Subscribe to a server-side query via WebSocket.
85
- * Server executes the descriptor and pushes results on change.
100
+ * Subscribe to a view replica. The server responds with a full
101
+ * `view-snapshot` of the token's slice, then pushes `view-changes`
102
+ * deltas. One subscription per `${scope}:${element}` — callers dedupe
103
+ * (StreamingQueryCache.registerStream).
86
104
  */
87
- subscribeQuery(descriptor: ContextDescriptor, callback: (data: any[]) => void, scope?: string): string;
105
+ subscribeView(element: string, scope: string, callbacks: ViewSubscriptionCallbacks): void;
88
106
  /**
89
- * Unsubscribe from a server-side query.
107
+ * Unsubscribe from a view replica.
90
108
  */
91
- unsubscribeQuery(subscriptionId: string): void;
109
+ unsubscribeView(element: string, scope: string): void;
92
110
  /**
93
111
  * Get current connection state
94
112
  */
@@ -101,7 +119,12 @@ export declare class EventWire {
101
119
  private handleMessage;
102
120
  private requestSync;
103
121
  private flushPendingEvents;
104
- private flushPendingViewSubs;
122
+ /**
123
+ * (Re)send every active view subscription. Called on each (re)connect —
124
+ * covers both subscriptions made while offline and re-establishing the
125
+ * server-side registry after a reconnect (server cleans it on disconnect).
126
+ */
127
+ private sendAllViewSubscriptions;
105
128
  private scheduleReconnect;
106
129
  }
107
130
  export {};
@@ -5,16 +5,15 @@
5
5
  * - Wire: Client-server communication
6
6
  * - CommandWire: Command execution over network
7
7
  * - EventPublisher: Event persistence and synchronization
8
- * - QueryWire: Remote view queries via HTTP/SSE
8
+ * - QueryWire: Remote view queries via HTTP
9
9
  * - DataStorage: Data persistence (defined elsewhere)
10
10
  */
11
11
  export { AuthAdapter } from "./auth-adapter";
12
12
  export type { DecodedToken } from "./auth-adapter";
13
13
  export { CommandWire } from "./command-wire";
14
14
  export { EventWire } from "./event-wire";
15
- export type { ReceivedEvent, SyncableEvent } from "./event-wire";
15
+ export type { ReceivedEvent, SyncableEvent, ViewSubscriptionCallbacks, } from "./event-wire";
16
16
  export { QueryWire } from "./query-wire";
17
- export type { StreamConnection } from "./query-wire";
18
17
  export { Wire } from "./wire";
19
18
  export type { WireAuth } from "./wire";
20
19
  export { EVENT_TABLES, LocalEventPublisher } from "./event-publisher";
@@ -1,16 +1,14 @@
1
1
  /**
2
- * QueryWire - Wire adapter for remote view queries via HTTP/SSE
2
+ * QueryWire - Wire adapter for remote view queries via HTTP
3
3
  *
4
4
  * Provides:
5
5
  * - One-shot queries via HTTP POST
6
- * - Live queries via Server-Sent Events (SSE)
7
6
  * - Per-request auth via scope + token
7
+ *
8
+ * Live updates go through EventWire view subscriptions
9
+ * (view-snapshot + view-changes), not through this adapter.
8
10
  */
9
11
  import { Wire, type WireAuth } from "./wire";
10
- export interface StreamConnection {
11
- eventSource: EventSource;
12
- unsubscribe: () => void;
13
- }
14
12
  export declare class QueryWire extends Wire {
15
13
  constructor(baseUrl: string);
16
14
  /**
@@ -22,15 +20,5 @@ export declare class QueryWire extends Wire {
22
20
  * @returns Query results
23
21
  */
24
22
  query(viewName: string, options?: any, auth?: WireAuth): Promise<any[]>;
25
- /**
26
- * Create a live query stream using SSE
27
- *
28
- * @param viewName - Name of the view to stream
29
- * @param options - Query options (where, orderBy, limit)
30
- * @param callback - Called when data changes
31
- * @param auth - Scope and token for this request (SSE uses URL params since headers aren't supported)
32
- * @returns StreamConnection with unsubscribe method
33
- */
34
- stream(viewName: string, options: any, callback: (data: any[]) => void, auth?: WireAuth): StreamConnection;
35
23
  }
36
24
  //# sourceMappingURL=query-wire.d.ts.map
@@ -196,6 +196,16 @@ export declare class ArcAggregateElement<Name extends string = string, Id extend
196
196
  get cronMethods(): AggregateCronMethodEntry[];
197
197
  queryContext(adapters: ModelAdapters): AggregateQueryContext<QueryMethods>;
198
198
  private buildPrivateQuery;
199
+ /**
200
+ * Resolve this aggregate's protection for the current auth context.
201
+ * Public entry point for the host's view-subscription handler — used to
202
+ * compute the subscriber's restrictions once at subscribe time (snapshot
203
+ * filtering + per-delta filtering).
204
+ */
205
+ getRestrictionsFor(adapters: ModelAdapters): {
206
+ restrictions: Record<string, unknown> | null;
207
+ denied: boolean;
208
+ };
199
209
  private isScopeDenied;
200
210
  private getAuth;
201
211
  private getScopeRestrictions;
@@ -104,6 +104,16 @@ export declare class ArcView<const Data extends ArcViewData> extends ArcContextE
104
104
  * @returns Context object with find/findOne methods
105
105
  */
106
106
  queryContext(adapters: any): ArcViewQueryContext<Data["id"], Data["schema"]>;
107
+ /**
108
+ * Resolve this view's protection for the current auth context.
109
+ * Public entry point for the host's view-subscription handler — used to
110
+ * compute the subscriber's restrictions once at subscribe time (snapshot
111
+ * filtering + per-delta filtering).
112
+ */
113
+ getRestrictionsFor(adapters: any): {
114
+ restrictions: Record<string, unknown> | null;
115
+ denied: boolean;
116
+ };
107
117
  private resolveProtection;
108
118
  /**
109
119
  * Get event handlers for this view
@@ -1,5 +1,5 @@
1
1
  import { ForkedDataStorage } from "./data-storage-forked";
2
- import { DataStorage, type DataStorageChanges } from "./data-storage.abstract";
2
+ import { DataStorage, type CommittedChange, type DataStorageChanges } from "./data-storage.abstract";
3
3
  import type { DatabaseAdapter, ReadTransaction, ReadWriteTransaction } from "./database-adapter";
4
4
  import type { StoreState } from "./store-state.abstract";
5
5
  export declare class MasterDataStorage extends DataStorage {
@@ -22,7 +22,9 @@ export declare class MasterDataStorage extends DataStorage {
22
22
  _id: string;
23
23
  } | null;
24
24
  }[][]>;
25
- commitChanges(changes: DataStorageChanges[]): Promise<void>;
25
+ commitChanges(changes: DataStorageChanges[], options?: {
26
+ captureRowsFor?: Set<string>;
27
+ }): Promise<CommittedChange[]>;
26
28
  fork(): ForkedDataStorage;
27
29
  /**
28
30
  * Destroy the data storage - clears all stores and destroys the database
@@ -1,5 +1,5 @@
1
1
  import type { ForkedDataStorage } from "./data-storage-forked";
2
- import type { DataStorage, DataStorageChanges, ListenerEvent, QueryListenerCallback } from "./data-storage.abstract";
2
+ import type { CommittedChange, DataStorage, DataStorageChanges, ListenerEvent, QueryListenerCallback } from "./data-storage.abstract";
3
3
  import type { ReadTransaction, ReadWriteTransaction } from "./database-adapter";
4
4
  import type { FindOptions } from "./find-options";
5
5
  import type { StoreState } from "./store-state.abstract";
@@ -25,7 +25,9 @@ export declare class ObservableDataStorage {
25
25
  fork(): ForkedDataStorage;
26
26
  getReadTransaction(): Promise<ReadTransaction>;
27
27
  getReadWriteTransaction(): Promise<ReadWriteTransaction>;
28
- commitChanges(changes: DataStorageChanges[]): Promise<void>;
28
+ commitChanges(changes: DataStorageChanges[], options?: {
29
+ captureRowsFor?: Set<string>;
30
+ }): Promise<CommittedChange[]>;
29
31
  /**
30
32
  * Register a tracked query
31
33
  */
@@ -9,8 +9,21 @@ export declare abstract class DataStorage {
9
9
  abstract fork(): ForkedDataStorage;
10
10
  abstract getReadTransaction(): Promise<ReadTransaction>;
11
11
  abstract getReadWriteTransaction(): Promise<ReadWriteTransaction>;
12
- commitChanges(changes: DataStorageChanges[]): Promise<void>;
12
+ commitChanges(changes: DataStorageChanges[], _options?: {
13
+ captureRowsFor?: Set<string>;
14
+ }): Promise<CommittedChange[]>;
13
15
  }
16
+ /**
17
+ * A change applied and committed to a store, with the row state before and
18
+ * after the write (store/DB form). Returned by `commitChanges` for stores
19
+ * requested via `captureRowsFor` — consumed by view-change broadcasting.
20
+ */
21
+ export type CommittedChange = {
22
+ store: string;
23
+ id: string;
24
+ oldRow: any | null;
25
+ newRow: any | null;
26
+ };
14
27
  export type StoreStateChange<Item> = {
15
28
  type: "set";
16
29
  data: Item;
@@ -6,10 +6,27 @@ export declare class MasterStoreState<Item extends {
6
6
  _id: string;
7
7
  }> extends StoreState<Item> {
8
8
  constructor(storeName: string, dataStorage: DataStorage, deserialize?: (data: any) => Item);
9
- applyChangeAndReturnEvent(transaction: ReadWriteTransaction, change: StoreStateChange<Item>, transactionCache?: Map<string, Item>): Promise<{
9
+ /**
10
+ * Read the current row for `id` — transaction cache first, then the store.
11
+ * Cache keys are prefixed with the store name: two stores in one
12
+ * transaction can hold rows under the same `_id` (e.g. two views keyed by
13
+ * the same aggregate id) and must not shadow each other.
14
+ */
15
+ private readExisting;
16
+ applyChangeAndReturnEvent(transaction: ReadWriteTransaction, change: StoreStateChange<Item>, transactionCache?: Map<string, Item>, options?: {
17
+ captureRows?: boolean;
18
+ }): Promise<{
10
19
  from: Item | null;
11
20
  to: Item | null;
12
21
  event: ListenerEvent<Item>;
22
+ /**
23
+ * Rows in store form (as written to the DB) for change broadcasting:
24
+ * state before the write and after. Captured for modify/mutate always
25
+ * (existing is read for the merge anyway); for set/delete only when
26
+ * `options.captureRows` — avoids extra PK lookups on event tables.
27
+ */
28
+ oldRow: Item | null;
29
+ newRow: Item | null;
13
30
  }>;
14
31
  applyChange(change: StoreStateChange<Item>): Promise<{
15
32
  from: Item | null;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=store-state-master.test.d.ts.map
package/dist/index.js CHANGED
@@ -212,11 +212,11 @@ class EventWire {
212
212
  reconnectTimeout;
213
213
  syncRequested = false;
214
214
  viewSubscriptions = new Map;
215
- viewSubCounter = 0;
216
- pendingViewSubs = [];
217
- constructor(baseUrl) {
215
+ enableEventSync;
216
+ constructor(baseUrl, options) {
218
217
  this.baseUrl = baseUrl;
219
218
  this.instanceId = ++eventWireInstanceCounter;
219
+ this.enableEventSync = options?.enableEventSync ?? true;
220
220
  }
221
221
  setScopeToken(scope, token) {
222
222
  if (token === null) {
@@ -262,9 +262,11 @@ class EventWire {
262
262
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
263
263
  this.state = "connected";
264
264
  this.sendAllScopeTokens();
265
- this.requestSync();
265
+ if (this.enableEventSync) {
266
+ this.requestSync();
267
+ }
266
268
  this.flushPendingEvents();
267
- this.flushPendingViewSubs();
269
+ this.sendAllViewSubscriptions();
268
270
  } else {
269
271
  console.log(`[EventWire] onopen called but ws is not OPEN, readyState:`, this.ws?.readyState);
270
272
  }
@@ -340,30 +342,26 @@ class EventWire {
340
342
  onSynced(callback) {
341
343
  this.onSyncedCallback = callback;
342
344
  }
343
- subscribeQuery(descriptor, callback, scope) {
344
- const subscriptionId = `qs_${++this.viewSubCounter}_${Date.now()}`;
345
- this.viewSubscriptions.set(subscriptionId, callback);
345
+ subscribeView(element, scope, callbacks) {
346
+ const key = `${scope}:${element}`;
347
+ this.viewSubscriptions.set(key, callbacks);
346
348
  if (this.state === "connected" && this.ws) {
347
349
  this.ws.send(JSON.stringify({
348
- type: "subscribe-query",
349
- subscriptionId,
350
- descriptor,
350
+ type: "subscribe-view",
351
+ element,
351
352
  scope
352
353
  }));
353
- } else {
354
- this.pendingViewSubs.push({ subscriptionId, descriptor, scope });
355
354
  }
356
- return subscriptionId;
357
355
  }
358
- unsubscribeQuery(subscriptionId) {
359
- this.viewSubscriptions.delete(subscriptionId);
356
+ unsubscribeView(element, scope) {
357
+ this.viewSubscriptions.delete(`${scope}:${element}`);
360
358
  if (this.state === "connected" && this.ws) {
361
359
  this.ws.send(JSON.stringify({
362
- type: "unsubscribe-query",
363
- subscriptionId
360
+ type: "unsubscribe-view",
361
+ element,
362
+ scope
364
363
  }));
365
364
  }
366
- this.pendingViewSubs = this.pendingViewSubs.filter((s) => s.subscriptionId !== subscriptionId);
367
365
  }
368
366
  getState() {
369
367
  return this.state;
@@ -396,10 +394,17 @@ class EventWire {
396
394
  this.lastHostEventId = message.lastHostEventId;
397
395
  }
398
396
  break;
399
- case "query-data": {
400
- const cb = this.viewSubscriptions.get(message.subscriptionId);
401
- if (cb) {
402
- cb(message.data);
397
+ case "view-snapshot": {
398
+ const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
399
+ if (sub) {
400
+ sub.onSnapshot(message.items ?? []);
401
+ }
402
+ break;
403
+ }
404
+ case "view-changes": {
405
+ const sub = this.viewSubscriptions.get(`${message.scope}:${message.element}`);
406
+ if (sub && Array.isArray(message.changes)) {
407
+ sub.onChanges(message.changes);
403
408
  }
404
409
  break;
405
410
  }
@@ -427,18 +432,19 @@ class EventWire {
427
432
  this.pendingEvents = [];
428
433
  }
429
434
  }
430
- flushPendingViewSubs() {
435
+ sendAllViewSubscriptions() {
431
436
  if (!this.ws || this.state !== "connected")
432
437
  return;
433
- for (const sub of this.pendingViewSubs) {
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);
434
442
  this.ws.send(JSON.stringify({
435
- type: "subscribe-query",
436
- subscriptionId: sub.subscriptionId,
437
- descriptor: sub.descriptor,
438
- scope: sub.scope
443
+ type: "subscribe-view",
444
+ element,
445
+ scope
439
446
  }));
440
447
  }
441
- this.pendingViewSubs = [];
442
448
  }
443
449
  scheduleReconnect() {
444
450
  if (this.reconnectTimeout)
@@ -465,47 +471,6 @@ class QueryWire extends Wire {
465
471
  }
466
472
  return await response.json();
467
473
  }
468
- stream(viewName, options, callback, auth) {
469
- const params = new URLSearchParams;
470
- if (options?.where) {
471
- params.set("where", JSON.stringify(options.where));
472
- }
473
- if (options?.orderBy) {
474
- params.set("orderBy", JSON.stringify(options.orderBy));
475
- }
476
- if (options?.limit) {
477
- params.set("limit", String(options.limit));
478
- }
479
- if (auth?.token) {
480
- params.set("token", auth.token);
481
- }
482
- if (auth?.scope) {
483
- params.set("scope", auth.scope);
484
- }
485
- const queryString = params.toString();
486
- const url = `${this.getBaseUrl()}/stream/${viewName}${queryString ? `?${queryString}` : ""}`;
487
- const eventSource = new EventSource(url);
488
- eventSource.onmessage = (event) => {
489
- try {
490
- const message = JSON.parse(event.data);
491
- if (message.type === "data") {
492
- callback(message.data);
493
- }
494
- } catch (err) {
495
- console.error("QueryWire: Failed to parse SSE message", err);
496
- }
497
- };
498
- eventSource.onerror = (err) => {
499
- console.error("QueryWire: SSE error", err);
500
- };
501
- const unsubscribe = () => {
502
- eventSource.close();
503
- };
504
- return {
505
- eventSource,
506
- unsubscribe
507
- };
508
- }
509
474
  }
510
475
  // src/adapters/event-publisher.ts
511
476
  var EVENT_TABLES = {
@@ -519,12 +484,19 @@ class LocalEventPublisher {
519
484
  views = [];
520
485
  syncCallback;
521
486
  subscribers = new Map;
487
+ viewChangesCallbacks = new Set;
522
488
  constructor(dataStorage) {
523
489
  this.dataStorage = dataStorage;
524
490
  }
525
491
  onPublish(callback) {
526
492
  this.syncCallback = callback;
527
493
  }
494
+ onViewChanges(callback) {
495
+ this.viewChangesCallbacks.add(callback);
496
+ return () => {
497
+ this.viewChangesCallbacks.delete(callback);
498
+ };
499
+ }
528
500
  registerViews(views) {
529
501
  this.views = views;
530
502
  }
@@ -592,7 +564,19 @@ class LocalEventPublisher {
592
564
  });
593
565
  const viewChanges = await this.collectViewChanges(event);
594
566
  allChanges.push(...viewChanges);
595
- await this.dataStorage.commitChanges(allChanges);
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
+ }
596
580
  await this.notifySubscribers(event);
597
581
  if (this.syncCallback) {
598
582
  this.syncCallback(event);
@@ -1278,8 +1262,9 @@ function object(element) {
1278
1262
 
1279
1263
  // src/data-storage/data-storage.abstract.ts
1280
1264
  class DataStorage {
1281
- async commitChanges(changes) {
1265
+ async commitChanges(changes, _options) {
1282
1266
  await Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applyChanges(changes2)));
1267
+ return [];
1283
1268
  }
1284
1269
  }
1285
1270
 
@@ -2244,8 +2229,11 @@ class ArcAggregateElement extends ArcContextElement {
2244
2229
  }
2245
2230
  return adapters.dataStorage.getStore(viewName).find(options);
2246
2231
  }
2247
- if (adapters.streamingCache)
2248
- return adapters.streamingCache.getStore(viewName).find(options);
2232
+ if (adapters.streamingCache) {
2233
+ const store = adapters.streamingCache.getStore(viewName, adapters.scope?.scopeName);
2234
+ if (store.hasData())
2235
+ return store.find(options);
2236
+ }
2249
2237
  if (adapters.queryWire)
2250
2238
  return adapters.queryWire.query(viewName, options);
2251
2239
  return [];
@@ -2273,6 +2261,12 @@ class ArcAggregateElement extends ArcContextElement {
2273
2261
  }
2274
2262
  };
2275
2263
  }
2264
+ getRestrictionsFor(adapters) {
2265
+ return {
2266
+ restrictions: this.getScopeRestrictions(adapters),
2267
+ denied: this.isScopeDenied(adapters)
2268
+ };
2269
+ }
2276
2270
  isScopeDenied(adapters, protections = this._protections) {
2277
2271
  if (protections.length === 0)
2278
2272
  return false;
@@ -2918,6 +2912,13 @@ class ArcView extends ArcContextElement {
2918
2912
  }
2919
2913
  return adapters.dataStorage.getStore(viewName).find(options);
2920
2914
  }
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
+ }
2921
2922
  if (adapters.queryWire) {
2922
2923
  const where = restrictions ? { ...options?.where || {}, ...restrictions } : options?.where;
2923
2924
  return adapters.queryWire.query(viewName, { ...options, where });
@@ -2936,6 +2937,13 @@ class ArcView extends ArcContextElement {
2936
2937
  const results = await adapters.dataStorage.getStore(viewName).find({ where });
2937
2938
  return results[0];
2938
2939
  }
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
+ }
2939
2947
  if (adapters.queryWire) {
2940
2948
  const mergedWhere = restrictions ? { ...where, ...restrictions } : where;
2941
2949
  const results = await adapters.queryWire.query(viewName, { where: mergedWhere });
@@ -2945,6 +2953,9 @@ class ArcView extends ArcContextElement {
2945
2953
  }
2946
2954
  };
2947
2955
  }
2956
+ getRestrictionsFor(adapters) {
2957
+ return this.resolveProtection(this.data.protections || [], adapters);
2958
+ }
2948
2959
  resolveProtection(protections, adapters) {
2949
2960
  if (protections.length === 0)
2950
2961
  return { restrictions: null, denied: false };
@@ -3385,12 +3396,23 @@ class MasterStoreState extends StoreState {
3385
3396
  constructor(storeName, dataStorage, deserialize) {
3386
3397
  super(storeName, dataStorage, deserialize);
3387
3398
  }
3388
- async applyChangeAndReturnEvent(transaction, change, transactionCache) {
3399
+ async readExisting(transaction, id2, transactionCache) {
3400
+ const cacheKey = `${this.storeName}:${id2}`;
3401
+ if (transactionCache && transactionCache.has(cacheKey)) {
3402
+ return transactionCache.get(cacheKey);
3403
+ }
3404
+ return transaction.find(this.storeName, { where: { _id: id2 } }).then((results) => results[0]);
3405
+ }
3406
+ async applyChangeAndReturnEvent(transaction, change, transactionCache, options) {
3389
3407
  if (change.type === "set") {
3408
+ let existing;
3409
+ if (options?.captureRows) {
3410
+ existing = await this.readExisting(transaction, change.data._id, transactionCache);
3411
+ }
3390
3412
  await transaction.set(this.storeName, change.data);
3391
3413
  const item = this.deserialize ? this.deserialize(change.data) : change.data;
3392
3414
  if (transactionCache) {
3393
- transactionCache.set(change.data._id, item);
3415
+ transactionCache.set(`${this.storeName}:${change.data._id}`, item);
3394
3416
  }
3395
3417
  return {
3396
3418
  from: null,
@@ -3399,11 +3421,20 @@ class MasterStoreState extends StoreState {
3399
3421
  type: "set",
3400
3422
  item: change.data,
3401
3423
  id: change.data._id
3402
- }
3424
+ },
3425
+ oldRow: existing ?? null,
3426
+ newRow: change.data
3403
3427
  };
3404
3428
  }
3405
3429
  if (change.type === "delete") {
3430
+ let existing;
3431
+ if (options?.captureRows) {
3432
+ existing = await this.readExisting(transaction, change.id, transactionCache);
3433
+ }
3406
3434
  await transaction.remove(this.storeName, change.id);
3435
+ if (transactionCache) {
3436
+ transactionCache.delete(`${this.storeName}:${change.id}`);
3437
+ }
3407
3438
  return {
3408
3439
  from: null,
3409
3440
  to: null,
@@ -3411,21 +3442,18 @@ class MasterStoreState extends StoreState {
3411
3442
  type: "delete",
3412
3443
  item: null,
3413
3444
  id: change.id
3414
- }
3445
+ },
3446
+ oldRow: existing ?? null,
3447
+ newRow: null
3415
3448
  };
3416
3449
  }
3417
3450
  if (change.type === "modify") {
3418
- let existing;
3419
- if (transactionCache && transactionCache.has(change.id)) {
3420
- existing = transactionCache.get(change.id);
3421
- } else {
3422
- existing = await transaction.find(this.storeName, { where: { _id: change.id } }).then((results) => results[0]);
3423
- }
3451
+ const existing = await this.readExisting(transaction, change.id, transactionCache);
3424
3452
  const updated = existing ? deepMerge(existing, change.data) : { _id: change.id, ...change.data };
3425
3453
  await transaction.set(this.storeName, updated);
3426
3454
  const item = this.deserialize ? this.deserialize(updated) : updated;
3427
3455
  if (transactionCache) {
3428
- transactionCache.set(change.id, item);
3456
+ transactionCache.set(`${this.storeName}:${change.id}`, item);
3429
3457
  }
3430
3458
  return {
3431
3459
  from: null,
@@ -3434,21 +3462,18 @@ class MasterStoreState extends StoreState {
3434
3462
  type: "set",
3435
3463
  item,
3436
3464
  id: change.id
3437
- }
3465
+ },
3466
+ oldRow: existing ?? null,
3467
+ newRow: updated
3438
3468
  };
3439
3469
  }
3440
3470
  if (change.type === "mutate") {
3441
- let existing;
3442
- if (transactionCache && transactionCache.has(change.id)) {
3443
- existing = transactionCache.get(change.id);
3444
- } else {
3445
- existing = await transaction.find(this.storeName, { where: { _id: change.id } }).then((results) => results[0]);
3446
- }
3471
+ const existing = await this.readExisting(transaction, change.id, transactionCache);
3447
3472
  const updated = apply2(existing || {}, change.patches);
3448
3473
  await transaction.set(this.storeName, updated);
3449
3474
  const item = this.deserialize ? this.deserialize(updated) : updated;
3450
3475
  if (transactionCache) {
3451
- transactionCache.set(change.id, item);
3476
+ transactionCache.set(`${this.storeName}:${change.id}`, item);
3452
3477
  }
3453
3478
  return {
3454
3479
  from: null,
@@ -3457,7 +3482,9 @@ class MasterStoreState extends StoreState {
3457
3482
  type: "set",
3458
3483
  item,
3459
3484
  id: change.id
3460
- }
3485
+ },
3486
+ oldRow: existing ?? null,
3487
+ newRow: updated
3461
3488
  };
3462
3489
  }
3463
3490
  throw new Error("Unknown change type");
@@ -3536,17 +3563,22 @@ class MasterDataStorage extends DataStorage {
3536
3563
  applySerializedChanges(changes) {
3537
3564
  return Promise.all(changes.map(({ store, changes: changes2 }) => this.getStore(store).applySerializedChanges(changes2)));
3538
3565
  }
3539
- async commitChanges(changes) {
3566
+ async commitChanges(changes, options) {
3540
3567
  const transaction = await this.getReadWriteTransaction();
3541
3568
  const transactionCache = new Map;
3542
3569
  const eventsByStore = new Map;
3570
+ const committed = [];
3543
3571
  for (const { store, changes: storeChanges } of changes) {
3544
3572
  const storeState = this.getStore(store);
3545
3573
  const storeEvents = [];
3574
+ const capture = options?.captureRowsFor?.has(store) ?? false;
3546
3575
  for (const change of storeChanges) {
3547
- const { event: event3 } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache);
3576
+ const { event: event3, oldRow, newRow } = await storeState.applyChangeAndReturnEvent(transaction, change, transactionCache, { captureRows: capture });
3548
3577
  if (event3)
3549
3578
  storeEvents.push(event3);
3579
+ if (capture) {
3580
+ committed.push({ store, id: event3.id, oldRow, newRow });
3581
+ }
3550
3582
  }
3551
3583
  if (storeEvents.length > 0) {
3552
3584
  eventsByStore.set(store, storeEvents);
@@ -3557,6 +3589,7 @@ class MasterDataStorage extends DataStorage {
3557
3589
  const storeState = this.getStore(store);
3558
3590
  storeState.notifyListenersPublic(events);
3559
3591
  }
3592
+ return committed;
3560
3593
  }
3561
3594
  fork() {
3562
3595
  return new ForkedDataStorage(this);
@@ -3673,8 +3706,8 @@ class ObservableDataStorage {
3673
3706
  getReadWriteTransaction() {
3674
3707
  return this.source.getReadWriteTransaction();
3675
3708
  }
3676
- commitChanges(changes) {
3677
- return this.source.commitChanges(changes);
3709
+ commitChanges(changes, options) {
3710
+ return this.source.commitChanges(changes, options);
3678
3711
  }
3679
3712
  trackQuery(storeName, options, result, listener4) {
3680
3713
  const key = this.getQueryKey(storeName, options);
@@ -4516,13 +4549,6 @@ class ScopedModel {
4516
4549
  }
4517
4550
  return wire.query(viewName, options, this.getAuth());
4518
4551
  }
4519
- subscribeQuery(descriptor, callback) {
4520
- const wire = this.parent.getAdapters().eventWire;
4521
- if (!wire) {
4522
- throw new Error(`Cannot subscribe to query: no eventWire available.`);
4523
- }
4524
- return wire.subscribeQuery(descriptor, callback, this.scopeName);
4525
- }
4526
4552
  get query() {
4527
4553
  return buildContextAccessor(this.context, this.scopedAdapters, "queryContext", (descriptor) => descriptor);
4528
4554
  }
@@ -4596,120 +4622,107 @@ function mutationExecutor(model) {
4596
4622
  });
4597
4623
  }
4598
4624
  // src/streaming/streaming-query-cache.ts
4625
+ var DEFAULT_SCOPE = "default";
4626
+
4599
4627
  class StreamingQueryCache {
4600
4628
  stores = new Map;
4601
4629
  views = [];
4602
4630
  activeStreams = new Map;
4603
4631
  pendingUnsubscribes = new Map;
4604
- streamScopes = new Map;
4605
4632
  static UNSUBSCRIBE_DELAY_MS = 5000;
4633
+ storeKey(viewName, scope) {
4634
+ return `${scope ?? DEFAULT_SCOPE}:${viewName}`;
4635
+ }
4606
4636
  registerViews(views) {
4607
4637
  this.views = views;
4608
- for (const view3 of views) {
4609
- if (!this.stores.has(view3.name)) {
4610
- this.stores.set(view3.name, new StreamingStore);
4611
- }
4612
- }
4613
4638
  }
4614
- getStore(viewName) {
4615
- if (!this.stores.has(viewName)) {
4616
- this.stores.set(viewName, new StreamingStore);
4639
+ getStore(viewName, scope) {
4640
+ const key = this.storeKey(viewName, scope);
4641
+ if (!this.stores.has(key)) {
4642
+ this.stores.set(key, new StreamingStore);
4617
4643
  }
4618
- return this.stores.get(viewName);
4644
+ return this.stores.get(key);
4619
4645
  }
4620
- hasData(viewName) {
4621
- const store = this.stores.get(viewName);
4646
+ hasData(viewName, scope) {
4647
+ const store = this.stores.get(this.storeKey(viewName, scope));
4622
4648
  return store ? store.hasData() : false;
4623
4649
  }
4624
- hasActiveStream(viewName) {
4625
- return this.activeStreams.has(viewName);
4626
- }
4627
- registerStream(viewName, createStream) {
4628
- const pending = this.pendingUnsubscribes.get(viewName);
4650
+ registerStream(key, createStream) {
4651
+ const pending = this.pendingUnsubscribes.get(key);
4629
4652
  if (pending) {
4630
4653
  clearTimeout(pending);
4631
- this.pendingUnsubscribes.delete(viewName);
4654
+ this.pendingUnsubscribes.delete(key);
4632
4655
  }
4633
- const existing = this.activeStreams.get(viewName);
4656
+ const existing = this.activeStreams.get(key);
4634
4657
  if (existing) {
4635
4658
  existing.refCount++;
4636
4659
  return {
4637
- unsubscribe: () => this.unregisterStream(viewName),
4660
+ unsubscribe: () => this.unregisterStream(key),
4638
4661
  wasReused: true
4639
4662
  };
4640
4663
  }
4641
4664
  const streamConn = createStream();
4642
- this.activeStreams.set(viewName, {
4665
+ this.activeStreams.set(key, {
4643
4666
  unsubscribe: streamConn.unsubscribe,
4644
4667
  refCount: 1
4645
4668
  });
4646
4669
  return {
4647
- unsubscribe: () => this.unregisterStream(viewName),
4670
+ unsubscribe: () => this.unregisterStream(key),
4648
4671
  wasReused: false
4649
4672
  };
4650
4673
  }
4651
- unregisterStream(viewName) {
4652
- const stream = this.activeStreams.get(viewName);
4674
+ unregisterStream(key) {
4675
+ const stream = this.activeStreams.get(key);
4653
4676
  if (!stream)
4654
4677
  return;
4655
4678
  stream.refCount--;
4656
4679
  if (stream.refCount <= 0) {
4657
4680
  const timeout = setTimeout(() => {
4658
- this.pendingUnsubscribes.delete(viewName);
4659
- const current = this.activeStreams.get(viewName);
4681
+ this.pendingUnsubscribes.delete(key);
4682
+ const current = this.activeStreams.get(key);
4660
4683
  if (current && current.refCount <= 0) {
4661
4684
  current.unsubscribe();
4662
- this.activeStreams.delete(viewName);
4663
- this.streamScopes.delete(viewName);
4685
+ this.activeStreams.delete(key);
4664
4686
  }
4665
4687
  }, StreamingQueryCache.UNSUBSCRIBE_DELAY_MS);
4666
- this.pendingUnsubscribes.set(viewName, timeout);
4688
+ this.pendingUnsubscribes.set(key, timeout);
4667
4689
  }
4668
4690
  }
4669
- subscribeQuery(descriptor, eventWire, scope) {
4670
- const key = descriptor.element;
4671
- if (scope)
4672
- this.streamScopes.set(key, scope);
4691
+ subscribeView(viewName, eventWire, scope) {
4692
+ const key = this.storeKey(viewName, scope);
4673
4693
  const { unsubscribe } = this.registerStream(key, () => {
4674
- const subId = eventWire.subscribeQuery(descriptor, (data) => {
4675
- this.setViewData(descriptor.element, data);
4676
- }, scope);
4677
- return { unsubscribe: () => eventWire.unsubscribeQuery(subId) };
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
+ };
4678
4703
  });
4679
4704
  return unsubscribe;
4680
4705
  }
4681
4706
  invalidateScope(scope) {
4682
- for (const [viewName, viewScope] of this.streamScopes) {
4683
- if (viewScope !== scope)
4707
+ const prefix = `${scope}:`;
4708
+ for (const [key, timeout] of this.pendingUnsubscribes) {
4709
+ if (!key.startsWith(prefix))
4684
4710
  continue;
4685
- const pending = this.pendingUnsubscribes.get(viewName);
4686
- if (pending) {
4687
- clearTimeout(pending);
4688
- this.pendingUnsubscribes.delete(viewName);
4689
- }
4690
- const stream = this.activeStreams.get(viewName);
4691
- if (stream) {
4692
- try {
4693
- stream.unsubscribe();
4694
- } catch {}
4695
- this.activeStreams.delete(viewName);
4696
- }
4697
- this.streamScopes.delete(viewName);
4698
- const store = this.stores.get(viewName);
4699
- if (store)
4700
- store.clear();
4711
+ clearTimeout(timeout);
4712
+ this.pendingUnsubscribes.delete(key);
4701
4713
  }
4702
- }
4703
- setViewData(viewName, data) {
4704
- const store = this.stores.get(viewName);
4705
- if (!store)
4706
- return;
4707
- if (Array.isArray(data)) {
4708
- store.setAll(data);
4709
- } else if (data && typeof data === "object" && "_id" in data) {
4710
- store.setAll([data]);
4711
- } else {
4712
- store.setAll([]);
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();
4713
4726
  }
4714
4727
  }
4715
4728
  async applyEvent(event3) {
@@ -4718,28 +4731,30 @@ class StreamingQueryCache {
4718
4731
  const handler = handlers[event3.type];
4719
4732
  if (!handler)
4720
4733
  continue;
4721
- const store = this.stores.get(view3.name);
4722
- if (!store)
4723
- continue;
4724
- const ctx = {
4725
- set: async (id3, data) => {
4726
- store.set(String(id3), { _id: String(id3), ...data });
4727
- },
4728
- modify: async (id3, data) => {
4729
- store.modify(String(id3), data);
4730
- },
4731
- remove: async (id3) => {
4732
- store.remove(String(id3));
4733
- },
4734
- find: async (options) => {
4735
- return store.find(options);
4736
- },
4737
- findOne: async (where) => {
4738
- return store.findOne(where);
4739
- },
4740
- $auth: {}
4741
- };
4742
- await handler(ctx, event3);
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
+ }
4743
4758
  }
4744
4759
  }
4745
4760
  clear() {
@@ -4747,7 +4762,6 @@ class StreamingQueryCache {
4747
4762
  stream.unsubscribe();
4748
4763
  }
4749
4764
  this.activeStreams.clear();
4750
- this.streamScopes.clear();
4751
4765
  for (const timeout of this.pendingUnsubscribes.values()) {
4752
4766
  clearTimeout(timeout);
4753
4767
  }
@@ -4773,6 +4787,18 @@ class StreamingStore {
4773
4787
  }
4774
4788
  this.notifyListeners(null);
4775
4789
  }
4790
+ applyChanges(events) {
4791
+ if (events.length === 0)
4792
+ 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);
4798
+ }
4799
+ }
4800
+ this.notifyListeners(events);
4801
+ }
4776
4802
  set(id3, item) {
4777
4803
  this.data.set(id3, item);
4778
4804
  this.notifyListeners([{ type: "set", id: id3, item }]);
@@ -38,10 +38,6 @@ export declare class ScopedModel<Context extends ArcContextAny> implements Model
38
38
  * Execute a remote query via QueryWire with auto-injected scope + token.
39
39
  */
40
40
  remoteQuery(viewName: string, options?: any): Promise<any[]>;
41
- /**
42
- * Subscribe to a server-side query via EventWire with auto-injected scope.
43
- */
44
- subscribeQuery(descriptor: ContextDescriptor, callback: (data: any[]) => void): string;
45
41
  /**
46
42
  * Query descriptor builder. Returns serializable descriptors.
47
43
  *
@@ -2,14 +2,15 @@
2
2
  * StreamingQueryCache - Lightweight in-memory cache for streaming mode
3
3
  *
4
4
  * Used when client connects without local database (no SQLite/IndexedDB).
5
- * Holds view state in memory, receives data from SSE, applies local events.
5
+ * Holds per-(scope, view) replicas of the token's slice of each view:
6
+ * the server sends a full `view-snapshot` on subscribe, then incremental
7
+ * `view-changes` deltas. All queries resolve locally against the replica.
6
8
  *
7
9
  * Features:
8
- * - Stores view data in memory (Map-based)
9
- * - Supports reactive queries with listeners
10
- * - Applies view handlers for local event emission
11
- * - Receives updates from SSE stream
12
- * - Deduplicates SSE streams (one stream per view)
10
+ * - Stores view data in memory (Map-based), keyed by `${scope}:${viewName}`
11
+ * - Supports reactive queries with listeners (ListenerEvent[] | null)
12
+ * - Applies view handlers for optimistic local event emission
13
+ * - Deduplicates WS subscriptions (one stream per scope:view)
13
14
  */
14
15
  import type { EventWire } from "../adapters/event-wire";
15
16
  import type { ArcEventAny } from "../context-element/event/event";
@@ -38,39 +39,29 @@ export declare class StreamingQueryCache {
38
39
  private views;
39
40
  private activeStreams;
40
41
  private pendingUnsubscribes;
41
- /**
42
- * Tag each active stream with the scope name that subscribed it. Used by
43
- * `invalidateScope()` to force-close streams when a scope's token changes
44
- * (workspace switch / re-auth) — without this tag, `registerStream()` would
45
- * reuse the stale WS subscription (refCount > 0, or within the
46
- * UNSUBSCRIBE_DELAY_MS grace window) and the client would keep receiving
47
- * data filtered by the previous token.
48
- */
49
- private streamScopes;
50
42
  private static UNSUBSCRIBE_DELAY_MS;
43
+ /** Replica key — restrictions depend on the scope's token, so two scopes
44
+ * subscribing the same view must hold separate replicas. */
45
+ private storeKey;
51
46
  /**
52
47
  * Register views that this cache will handle
53
48
  */
54
49
  registerViews(views: ArcViewAny[]): void;
55
50
  /**
56
- * Get a store for a specific view
51
+ * Get the replica store for a view in a given scope
57
52
  */
58
53
  getStore<Item extends {
59
54
  _id: string;
60
- }>(viewName: string): StreamingQueryCacheStore<Item>;
61
- /**
62
- * Check if a store has any data
63
- */
64
- hasData(viewName: string): boolean;
55
+ }>(viewName: string, scope?: string): StreamingQueryCacheStore<Item>;
65
56
  /**
66
- * Check if a stream is already active for a view
57
+ * Check if a replica has received its snapshot
67
58
  */
68
- hasActiveStream(viewName: string): boolean;
59
+ hasData(viewName: string, scope?: string): boolean;
69
60
  /**
70
- * Register an active stream for a view (increment ref count if exists)
61
+ * Register an active stream for a key (increment ref count if exists)
71
62
  * Returns object with unsubscribe function and whether stream was reused
72
63
  */
73
- registerStream(viewName: string, createStream: () => {
64
+ registerStream(key: string, createStream: () => {
74
65
  unsubscribe: () => void;
75
66
  }): {
76
67
  unsubscribe: () => void;
@@ -83,21 +74,19 @@ export declare class StreamingQueryCache {
83
74
  */
84
75
  private unregisterStream;
85
76
  /**
86
- * Subscribe to a query via WebSocket with deduplication.
87
- * Multiple callers share a single WS subscription per descriptor key.
77
+ * Subscribe to a view replica via WebSocket with deduplication.
78
+ * Multiple callers share a single WS subscription per (scope, view).
79
+ * Snapshot → setAll (listeners get null → full re-query);
80
+ * deltas → applyChanges (listeners get ListenerEvent[] → local resolve).
88
81
  * Returns unsubscribe function that decrements refcount.
89
82
  */
90
- subscribeQuery(descriptor: {
91
- element: string;
92
- method: string;
93
- args: any[];
94
- }, eventWire: EventWire, scope?: string): () => void;
83
+ subscribeView(viewName: string, eventWire: EventWire, scope?: string): () => void;
95
84
  /**
96
- * Force-close every active stream tagged with `scope`. Called when a
97
- * scope's token changes (workspace switch / re-auth) so the next
98
- * `subscribeQuery()` creates a fresh WS subscription with the new token
99
- * instead of reusing the stale one (which would keep pumping data filtered
100
- * by the previous token until the page reload).
85
+ * Force-close every active stream of `scope`. Called when a scope's token
86
+ * changes (workspace switch / re-auth) so the next `subscribeView()`
87
+ * creates a fresh WS subscription with the new token instead of reusing
88
+ * the stale one (which would keep pumping data filtered by the previous
89
+ * token until the page reload).
101
90
  *
102
91
  * Bypasses both `refCount` (other subscribers still mounted) and the
103
92
  * UNSUBSCRIBE_DELAY_MS grace window — both became invalid the moment the
@@ -105,19 +94,15 @@ export declare class StreamingQueryCache {
105
94
  * via the `subKey` change (token is in the key), getting a fresh stream.
106
95
  *
107
96
  * Bonus: each affected store is also cleared so any in-progress render
108
- * that reads `store.find()` between `setToken` and the new WS data
97
+ * that reads `store.find()` between `setToken` and the new WS snapshot
109
98
  * arriving gets `[]` rather than stale rows from the previous workspace.
110
99
  */
111
100
  invalidateScope(scope: string): void;
112
101
  /**
113
- * Set data for a view. Accepts array or single item (from queryMethod findOne).
114
- */
115
- setViewData<Item extends {
116
- _id: string;
117
- }>(viewName: string, data: Item[] | Item | undefined | null): void;
118
- /**
119
- * Apply an event to update view state
120
- * Runs view handlers to update the cache
102
+ * Apply an event to update view state optimistic local update for
103
+ * mutations executed client-side. Runs view handlers against every
104
+ * existing scope-replica of the view (the authoritative per-scope delta
105
+ * arrives from the server afterwards; sets/deletes are idempotent).
121
106
  */
122
107
  applyEvent(event: ArcEventInstance<ArcEventAny>): Promise<void>;
123
108
  /**
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=streaming-query-cache.test.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc",
3
3
  "type": "module",
4
- "version": "0.7.13",
4
+ "version": "0.7.15",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Arc framework core rewrite with improved event emission and type safety",