@arcote.tech/arc 0.7.14 → 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.
- package/dist/adapters/event-publisher.d.ts +8 -1
- package/dist/adapters/event-wire.d.ts +33 -10
- package/dist/adapters/index.d.ts +2 -3
- package/dist/adapters/query-wire.d.ts +4 -16
- package/dist/context-element/aggregate/aggregate-element.d.ts +10 -0
- package/dist/context-element/view/view.d.ts +10 -0
- package/dist/data-storage/data-storage-master.d.ts +4 -2
- package/dist/data-storage/data-storage-observable.d.ts +4 -2
- package/dist/data-storage/data-storage.abstract.d.ts +14 -1
- package/dist/data-storage/store-state-master.d.ts +18 -1
- package/dist/data-storage/store-state-master.test.d.ts +2 -0
- package/dist/index.js +221 -195
- package/dist/model/scoped-model.d.ts +0 -4
- package/dist/streaming/streaming-query-cache.d.ts +31 -46
- package/dist/streaming/streaming-query-cache.test.d.ts +2 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
85
|
-
*
|
|
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
|
-
|
|
105
|
+
subscribeView(element: string, scope: string, callbacks: ViewSubscriptionCallbacks): void;
|
|
88
106
|
/**
|
|
89
|
-
* Unsubscribe from a
|
|
107
|
+
* Unsubscribe from a view replica.
|
|
90
108
|
*/
|
|
91
|
-
|
|
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
|
-
|
|
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 {};
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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[]
|
|
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[]
|
|
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[]
|
|
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
|
-
|
|
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;
|
package/dist/index.js
CHANGED
|
@@ -212,11 +212,11 @@ class EventWire {
|
|
|
212
212
|
reconnectTimeout;
|
|
213
213
|
syncRequested = false;
|
|
214
214
|
viewSubscriptions = new Map;
|
|
215
|
-
|
|
216
|
-
|
|
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.
|
|
265
|
+
if (this.enableEventSync) {
|
|
266
|
+
this.requestSync();
|
|
267
|
+
}
|
|
266
268
|
this.flushPendingEvents();
|
|
267
|
-
this.
|
|
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
|
-
|
|
344
|
-
const
|
|
345
|
-
this.viewSubscriptions.set(
|
|
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-
|
|
349
|
-
|
|
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
|
-
|
|
359
|
-
this.viewSubscriptions.delete(
|
|
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-
|
|
363
|
-
|
|
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 "
|
|
400
|
-
const
|
|
401
|
-
if (
|
|
402
|
-
|
|
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
|
-
|
|
435
|
+
sendAllViewSubscriptions() {
|
|
431
436
|
if (!this.ws || this.state !== "connected")
|
|
432
437
|
return;
|
|
433
|
-
for (const
|
|
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-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4616
|
-
|
|
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(
|
|
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
|
-
|
|
4625
|
-
|
|
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(
|
|
4654
|
+
this.pendingUnsubscribes.delete(key);
|
|
4632
4655
|
}
|
|
4633
|
-
const existing = this.activeStreams.get(
|
|
4656
|
+
const existing = this.activeStreams.get(key);
|
|
4634
4657
|
if (existing) {
|
|
4635
4658
|
existing.refCount++;
|
|
4636
4659
|
return {
|
|
4637
|
-
unsubscribe: () => this.unregisterStream(
|
|
4660
|
+
unsubscribe: () => this.unregisterStream(key),
|
|
4638
4661
|
wasReused: true
|
|
4639
4662
|
};
|
|
4640
4663
|
}
|
|
4641
4664
|
const streamConn = createStream();
|
|
4642
|
-
this.activeStreams.set(
|
|
4665
|
+
this.activeStreams.set(key, {
|
|
4643
4666
|
unsubscribe: streamConn.unsubscribe,
|
|
4644
4667
|
refCount: 1
|
|
4645
4668
|
});
|
|
4646
4669
|
return {
|
|
4647
|
-
unsubscribe: () => this.unregisterStream(
|
|
4670
|
+
unsubscribe: () => this.unregisterStream(key),
|
|
4648
4671
|
wasReused: false
|
|
4649
4672
|
};
|
|
4650
4673
|
}
|
|
4651
|
-
unregisterStream(
|
|
4652
|
-
const stream = this.activeStreams.get(
|
|
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(
|
|
4659
|
-
const current = this.activeStreams.get(
|
|
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(
|
|
4663
|
-
this.streamScopes.delete(viewName);
|
|
4685
|
+
this.activeStreams.delete(key);
|
|
4664
4686
|
}
|
|
4665
4687
|
}, StreamingQueryCache.UNSUBSCRIBE_DELAY_MS);
|
|
4666
|
-
this.pendingUnsubscribes.set(
|
|
4688
|
+
this.pendingUnsubscribes.set(key, timeout);
|
|
4667
4689
|
}
|
|
4668
4690
|
}
|
|
4669
|
-
|
|
4670
|
-
const key =
|
|
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
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
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
|
-
|
|
4683
|
-
|
|
4707
|
+
const prefix = `${scope}:`;
|
|
4708
|
+
for (const [key, timeout] of this.pendingUnsubscribes) {
|
|
4709
|
+
if (!key.startsWith(prefix))
|
|
4684
4710
|
continue;
|
|
4685
|
-
|
|
4686
|
-
|
|
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
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
}
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
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
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
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
|
|
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
|
-
* -
|
|
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
|
|
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
|
|
57
|
+
* Check if a replica has received its snapshot
|
|
67
58
|
*/
|
|
68
|
-
|
|
59
|
+
hasData(viewName: string, scope?: string): boolean;
|
|
69
60
|
/**
|
|
70
|
-
* Register an active stream for a
|
|
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(
|
|
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
|
|
87
|
-
* Multiple callers share a single WS subscription per
|
|
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
|
-
|
|
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
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
|
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
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
/**
|
package/package.json
CHANGED