@arcote.tech/arc 0.7.15 → 0.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Diff between two query results → positional deltas for the wire.
3
+ *
4
+ * The server re-executes a subscribed query and diffs the previous result
5
+ * against the new one. For lists of items with `_id` it produces minimal
6
+ * `set`/`delete` changes WITH target positions — the client applies them
7
+ * blindly (remove-by-id + splice at index), so result order is always the
8
+ * server's order (only the query handler knows its orderBy).
9
+ *
10
+ * Correctness over cleverness: the deltas are verified by simulating their
11
+ * application; any mismatch falls back to a full snapshot.
12
+ */
13
+ export type QueryResultChange = {
14
+ type: "set";
15
+ id: string;
16
+ item: any;
17
+ index: number;
18
+ } | {
19
+ type: "delete";
20
+ id: string;
21
+ };
22
+ export type QueryDiff = {
23
+ kind: "none";
24
+ } | {
25
+ kind: "changes";
26
+ changes: QueryResultChange[];
27
+ } | {
28
+ kind: "snapshot";
29
+ result: any;
30
+ };
31
+ export declare function diffResults(prev: any, next: any): QueryDiff;
32
+ /**
33
+ * Apply positional deltas to a result list — the exact client-side
34
+ * algorithm (exported so client cache and tests share one implementation):
35
+ * all deletes first, then sets in ascending index order.
36
+ */
37
+ export declare function applyQueryChanges(result: Array<{
38
+ _id: string;
39
+ }>, changes: QueryResultChange[]): Array<{
40
+ _id: string;
41
+ }>;
42
+ //# sourceMappingURL=diff.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=diff.test.d.ts.map
@@ -1,2 +1,4 @@
1
1
  export * from "./live-query";
2
+ export * from "./live-query-subscription";
3
+ export * from "./diff";
2
4
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * LiveQuery — a server-side query subscription that owns its deltas.
3
+ *
4
+ * The SAME query (any descriptor: bare view find or custom clientQuery
5
+ * handler) is executed with an ObservableDataStorage wrapped around the
6
+ * real storage. Every `find` the handler makes — already merged with token
7
+ * restrictions by ScopedStore — gets tracked. After each store commit,
8
+ * `resolveQueryChange` updates the tracked results in memory; when any of
9
+ * them changed, the descriptor is re-executed AGAINST THE CACHE (0 SQL)
10
+ * and the new result is diffed against the previous one.
11
+ *
12
+ * The transport layer (WS) only forwards `onUpdate` payloads — all query
13
+ * logic (filtering, scoping, ordering) lives here, in the query layer.
14
+ */
15
+ import { type ContextDescriptor } from "../context-accessor";
16
+ import type { ModelLike } from "../model-like";
17
+ import { type QueryResultChange } from "./diff";
18
+ export type LiveQueryUpdate = {
19
+ type: "changes";
20
+ changes: QueryResultChange[];
21
+ } | {
22
+ type: "snapshot";
23
+ result: any;
24
+ };
25
+ export declare class LiveQuery {
26
+ private readonly model;
27
+ private readonly descriptor;
28
+ private readonly scope;
29
+ private readonly rawToken;
30
+ private readonly onUpdate;
31
+ private observable;
32
+ private adapters;
33
+ private lastResult;
34
+ private scheduled;
35
+ private running;
36
+ private rerunRequested;
37
+ private stopped;
38
+ constructor(model: ModelLike<any>, descriptor: ContextDescriptor, scope: string, rawToken: string | null, onUpdate: (update: LiveQueryUpdate) => void);
39
+ /**
40
+ * Execute the descriptor with tracking and return the initial result.
41
+ */
42
+ start(): Promise<any>;
43
+ /**
44
+ * Close the initial-execute window: the store listener is registered
45
+ * before the read transaction, but trackQuery happens after the await —
46
+ * a commit in between could slip past the tracked entry. One forced
47
+ * re-execute (cache-backed, no SQL when nothing changed) diffs out any
48
+ * missed delta.
49
+ *
50
+ * Call AFTER the initial result has been delivered (e.g. the snapshot
51
+ * message was sent) — otherwise the catch-up delta could overtake it.
52
+ */
53
+ flush(): void;
54
+ /**
55
+ * Stop tracking — unsubscribes all store listeners.
56
+ */
57
+ stop(): void;
58
+ /**
59
+ * Coalesce re-executes: a single commit can touch multiple stores and
60
+ * fire onChange several times — one microtask handles them all. A commit
61
+ * landing DURING a re-execute requests another pass afterwards.
62
+ */
63
+ private schedule;
64
+ private run;
65
+ }
66
+ //# sourceMappingURL=live-query-subscription.d.ts.map
@@ -21,7 +21,15 @@ export declare class ScopedModel<Context extends ArcContextAny> implements Model
21
21
  private readonly scopedAdapters;
22
22
  private tokenListeners;
23
23
  constructor(parent: ModelLike<Context>, scopeName: string);
24
- setToken(token: string | null): void;
24
+ /**
25
+ * Set this scope's token. Returns a promise that resolves when the platform
26
+ * module loader has synced the chunks for the new token state (so routes in
27
+ * newly-gated modules exist before the caller navigates). With no module-sync
28
+ * provider registered (server, SSR, tests, app-without-platform) it resolves
29
+ * immediately — preserving the historical synchronous semantics for the many
30
+ * server-side `setToken(rawToken)` callers that ignore the return value.
31
+ */
32
+ setToken(token: string | null): Promise<void>;
25
33
  getToken(): string | null;
26
34
  getDecoded(): DecodedToken | null;
27
35
  getParams(): Record<string, any> | null;
@@ -1,4 +1,4 @@
1
1
  export { StreamingQueryCache } from "./streaming-query-cache";
2
- export type { CacheChangeListener, StreamingQueryCacheStore, } from "./streaming-query-cache";
2
+ export type { QuerySubscriptionHandle } from "./streaming-query-cache";
3
3
  export { StreamingEventPublisher } from "./streaming-event-publisher";
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,35 +1,33 @@
1
1
  /**
2
2
  * StreamingEventPublisher - Event publisher for streaming mode (no local database)
3
3
  *
4
- * When events are emitted:
5
- * 1. Apply to local cache (optimistic update)
6
- * 2. Send to server via EventWire
7
- *
8
- * This enables instant UI updates while syncing with server.
4
+ * When events are emitted locally (client-side mutate handlers), they are
5
+ * sent to the server via EventWire; the server commits them and pushes the
6
+ * resulting query deltas back through live query subscriptions. No local
7
+ * optimistic apply — the client holds query RESULTS, not view data, so it
8
+ * cannot compute how an event affects a custom query handler's output.
9
9
  */
10
10
  import type { EventPublisher, EventWithSyncStatus } from "../adapters/event-publisher";
11
11
  import type { EventWire } from "../adapters/event-wire";
12
12
  import type { ArcEventAny } from "../context-element/event/event";
13
13
  import type { ArcEventInstance } from "../context-element/event/instance";
14
14
  import type { ArcViewAny } from "../context-element/view/view";
15
- import type { StreamingQueryCache } from "./streaming-query-cache";
16
15
  /**
17
16
  * StreamingEventPublisher
18
17
  */
19
18
  export declare class StreamingEventPublisher implements EventPublisher {
20
- private readonly cache;
21
19
  private readonly eventWire;
22
20
  private views;
23
21
  private subscribers;
24
- constructor(cache: StreamingQueryCache, eventWire: EventWire);
22
+ constructor(eventWire: EventWire);
25
23
  /**
26
24
  * Register views for event handling
27
25
  */
28
26
  registerViews(views: ArcViewAny[]): void;
29
27
  /**
30
28
  * Publish an event
31
- * 1. Apply to local cache (optimistic)
32
- * 2. Send to server
29
+ * 1. Notify local subscribers
30
+ * 2. Send to server (server commits → live query deltas come back)
33
31
  */
34
32
  publish(event: ArcEventInstance<ArcEventAny>): Promise<void>;
35
33
  /**
@@ -1,113 +1,52 @@
1
1
  /**
2
- * StreamingQueryCache - Lightweight in-memory cache for streaming mode
2
+ * StreamingQueryCache - per-query result cache for streaming mode
3
3
  *
4
- * Used when client connects without local database (no SQLite/IndexedDB).
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.
4
+ * Used when the client connects without a local database. Each unique
5
+ * (scope, descriptor) gets ONE live subscription over the EventWire:
6
+ * the server executes the query with tracking (LiveQuery), sends a full
7
+ * `query-snapshot`, then positional `query-changes` deltas. The client
8
+ * applies deltas blindly — ALL query logic (filtering, scoping, ordering)
9
+ * lives on the server, in the query layer.
8
10
  *
9
11
  * Features:
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)
12
+ * - Dedup: many components with the same query share one subscription
13
+ * (refCount + UNSUBSCRIBE_DELAY grace window for quick remounts)
14
+ * - Snapshot/delta application with listener notifications
15
+ * - Scope invalidation on token change (workspace switch / re-auth)
14
16
  */
15
17
  import type { EventWire } from "../adapters/event-wire";
16
- import type { ArcEventAny } from "../context-element/event/event";
17
- import type { ArcEventInstance } from "../context-element/event/instance";
18
- import type { ArcViewAny } from "../context-element/view/view";
19
- import type { FindOptions } from "../data-storage/find-options";
20
- import type { ListenerEvent } from "../data-storage/data-storage.abstract";
21
- /**
22
- * Cache change listener receives ListenerEvent[] for incremental changes,
23
- * or null for bulk replacement (setAll) which requires full re-query.
24
- */
25
- export type CacheChangeListener = (events: ListenerEvent<any>[] | null) => void;
26
- export interface StreamingQueryCacheStore<Item extends {
27
- _id: string;
28
- }> {
29
- find(options?: FindOptions<Item>): Item[];
30
- findOne(where?: Record<string, any>): Item | undefined;
31
- subscribe(listener: CacheChangeListener): () => void;
32
- hasData(): boolean;
18
+ import type { ContextDescriptor } from "../model/context-accessor";
19
+ export interface QuerySubscriptionHandle {
20
+ /** Current state stable shape for React reads. */
21
+ read(): {
22
+ result: any;
23
+ loading: boolean;
24
+ };
25
+ unsubscribe(): void;
33
26
  }
34
- /**
35
- * StreamingQueryCache - Main cache class
36
- */
37
27
  export declare class StreamingQueryCache {
38
- private stores;
39
- private views;
40
- private activeStreams;
41
- private pendingUnsubscribes;
28
+ private entries;
42
29
  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;
46
- /**
47
- * Register views that this cache will handle
48
- */
49
- registerViews(views: ArcViewAny[]): void;
50
- /**
51
- * Get the replica store for a view in a given scope
52
- */
53
- getStore<Item extends {
54
- _id: string;
55
- }>(viewName: string, scope?: string): StreamingQueryCacheStore<Item>;
56
- /**
57
- * Check if a replica has received its snapshot
58
- */
59
- hasData(viewName: string, scope?: string): boolean;
60
- /**
61
- * Register an active stream for a key (increment ref count if exists)
62
- * Returns object with unsubscribe function and whether stream was reused
63
- */
64
- registerStream(key: string, createStream: () => {
65
- unsubscribe: () => void;
66
- }): {
67
- unsubscribe: () => void;
68
- wasReused: boolean;
69
- };
70
- /**
71
- * Unregister from a stream. When refCount hits 0, delays actual WS
72
- * unsubscribe by UNSUBSCRIBE_DELAY_MS. If re-registered within the
73
- * window, the existing subscription is reused (cache serves immediately).
74
- */
75
- private unregisterStream;
76
- /**
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).
81
- * Returns unsubscribe function that decrements refcount.
82
- */
83
- subscribeView(viewName: string, eventWire: EventWire, scope?: string): () => void;
30
+ private entryKey;
84
31
  /**
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).
90
- *
91
- * Bypasses both `refCount` (other subscribers still mounted) and the
92
- * UNSUBSCRIBE_DELAY_MS grace window — both became invalid the moment the
93
- * token changed. React's `useQuery` re-subscribes immediately afterwards
94
- * via the `subKey` change (token is in the key), getting a fresh stream.
95
- *
96
- * Bonus: each affected store is also cleared so any in-progress render
97
- * that reads `store.find()` between `setToken` and the new WS snapshot
98
- * arriving gets `[]` rather than stale rows from the previous workspace.
32
+ * Subscribe to a live query. Identical (scope, descriptor) pairs share
33
+ * one WS subscription; the last unsubscriber tears it down after a grace
34
+ * window (instant remounts reuse the cached result without a snapshot
35
+ * round-trip).
99
36
  */
100
- invalidateScope(scope: string): void;
37
+ subscribe(descriptor: ContextDescriptor, scope: string, eventWire: EventWire, onChange: () => void): QuerySubscriptionHandle;
101
38
  /**
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).
39
+ * Force-drop every cached query of `scope`. Called when the scope's token
40
+ * changes (workspace switch / re-auth) cached results and the WS
41
+ * subscriptions behind them were computed with the previous token.
42
+ * React's `useQuery` re-subscribes immediately afterwards (token is part
43
+ * of its subscription key), getting fresh snapshots.
106
44
  */
107
- applyEvent(event: ArcEventInstance<ArcEventAny>): Promise<void>;
45
+ invalidateScope(scope: string, eventWire?: EventWire): void;
108
46
  /**
109
- * Clear all cached data and close all streams
47
+ * Clear all cached queries and tear down their subscriptions.
110
48
  */
111
- clear(): void;
49
+ clear(eventWire?: EventWire): void;
50
+ private notify;
112
51
  }
113
52
  //# sourceMappingURL=streaming-query-cache.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.15",
4
+ "version": "0.7.17",
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",