@vantage-ui/client 0.0.0-stage → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +131 -2
  3. package/bin/vantage-client.js +5 -0
  4. package/dist/client-connection.d.ts +41 -0
  5. package/dist/client-connection.js +260 -0
  6. package/dist/client-core.d.ts +70 -0
  7. package/dist/client-core.js +140 -0
  8. package/dist/client-loading.d.ts +13 -0
  9. package/dist/client-loading.js +229 -0
  10. package/dist/client-runs.d.ts +18 -0
  11. package/dist/client-runs.js +88 -0
  12. package/dist/client-writes.d.ts +14 -0
  13. package/dist/client-writes.js +114 -0
  14. package/dist/client.d.ts +32 -0
  15. package/dist/client.js +61 -0
  16. package/dist/codegen/cli.d.ts +8 -0
  17. package/dist/codegen/cli.js +54 -0
  18. package/dist/codegen/emit.d.ts +3 -0
  19. package/dist/codegen/emit.js +190 -0
  20. package/dist/codegen/fetch.d.ts +4 -0
  21. package/dist/codegen/fetch.js +41 -0
  22. package/dist/codegen/meta.d.ts +13 -0
  23. package/dist/codegen/meta.js +59 -0
  24. package/dist/codegen/runs.d.ts +20 -0
  25. package/dist/codegen/runs.js +58 -0
  26. package/dist/codegen/schema.d.ts +97 -0
  27. package/dist/codegen/schema.js +23 -0
  28. package/dist/collection-cell.d.ts +69 -0
  29. package/dist/collection-cell.js +153 -0
  30. package/dist/collection.d.ts +32 -0
  31. package/dist/collection.js +122 -0
  32. package/dist/index.d.ts +8 -0
  33. package/dist/index.js +14 -0
  34. package/dist/mock-query.d.ts +9 -0
  35. package/dist/mock-query.js +75 -0
  36. package/dist/mock-runs.d.ts +61 -0
  37. package/dist/mock-runs.js +165 -0
  38. package/dist/mock-store.d.ts +78 -0
  39. package/dist/mock-store.js +158 -0
  40. package/dist/mock-transport.d.ts +16 -0
  41. package/dist/mock-transport.js +38 -0
  42. package/dist/model.d.ts +5 -0
  43. package/dist/model.js +169 -0
  44. package/dist/persist.d.ts +47 -0
  45. package/dist/persist.js +108 -0
  46. package/dist/protocol.d.ts +236 -0
  47. package/dist/protocol.js +5 -0
  48. package/dist/react.d.ts +59 -0
  49. package/dist/react.js +169 -0
  50. package/dist/reserved.d.ts +6 -0
  51. package/dist/reserved.js +13 -0
  52. package/dist/run/frames.d.ts +6 -0
  53. package/dist/run/frames.js +24 -0
  54. package/dist/run/hooks.d.ts +18 -0
  55. package/dist/run/hooks.js +87 -0
  56. package/dist/run/messages.d.ts +20 -0
  57. package/dist/run/messages.js +42 -0
  58. package/dist/run/outcome.d.ts +12 -0
  59. package/dist/run/outcome.js +27 -0
  60. package/dist/run/run.d.ts +82 -0
  61. package/dist/run/run.js +246 -0
  62. package/dist/run/storage.d.ts +3 -0
  63. package/dist/run/storage.js +20 -0
  64. package/dist/run/types.d.ts +92 -0
  65. package/dist/run/types.js +3 -0
  66. package/dist/run/variables.d.ts +26 -0
  67. package/dist/run/variables.js +70 -0
  68. package/dist/schema-meta.d.ts +91 -0
  69. package/dist/schema-meta.js +3 -0
  70. package/dist/signal.d.ts +45 -0
  71. package/dist/signal.js +163 -0
  72. package/dist/store.d.ts +45 -0
  73. package/dist/store.js +144 -0
  74. package/dist/table.d.ts +62 -0
  75. package/dist/table.js +82 -0
  76. package/dist/testing.d.ts +61 -0
  77. package/dist/testing.js +195 -0
  78. package/dist/transport.d.ts +28 -0
  79. package/dist/transport.js +32 -0
  80. package/dist/types.d.ts +159 -0
  81. package/dist/types.js +2 -0
  82. package/dist/util.d.ts +18 -0
  83. package/dist/util.js +46 -0
  84. package/package.json +56 -4
  85. package/skills/vantage-client/SKILL.md +390 -0
  86. package/skills/vantage-client/references/protocol.md +67 -0
  87. package/skills/vantage-client/references/react.md +242 -0
  88. package/skills/vantage-client/references/testing.md +173 -0
@@ -0,0 +1,108 @@
1
+ // Offline mirror and outbox.
2
+ //
3
+ // Tables marked `persist: true` on the server are mirrored here; on the
4
+ // next start the app paints from the mirror (records marked `stale`)
5
+ // before the socket connects. Edits made while offline wait in memory and
6
+ // go out in order on reconnect, within the same page. They are also saved
7
+ // to the outbox, but nothing sends a saved outbox after a reload: edits
8
+ // still waiting when the page reloads are lost.
9
+ /** Keeps everything in memory (tests; SSR). */
10
+ export class MemoryStorage {
11
+ records = new Map();
12
+ cursor = null;
13
+ outbox = [];
14
+ async load() {
15
+ return { records: [...this.records.values()], cursor: this.cursor, outbox: [...this.outbox] };
16
+ }
17
+ async save(change) {
18
+ for (const r of change.records ?? []) {
19
+ const key = `${r.table}\u0000${r.id}`;
20
+ if (r.record)
21
+ this.records.set(key, { table: r.table, record: r.record });
22
+ else
23
+ this.records.delete(key);
24
+ }
25
+ if (change.cursor)
26
+ this.cursor = change.cursor;
27
+ if (change.outbox)
28
+ this.outbox = [...change.outbox];
29
+ }
30
+ async clear() {
31
+ this.records.clear();
32
+ this.cursor = null;
33
+ this.outbox = [];
34
+ }
35
+ }
36
+ /**
37
+ * IndexedDB, one database per server + schema. A schema change starts a
38
+ * fresh database (`name` includes the hash), so stale shapes never load.
39
+ */
40
+ export class IndexedDBStorage {
41
+ name;
42
+ schemaHash;
43
+ db;
44
+ constructor(name, schemaHash) {
45
+ this.name = name;
46
+ this.schemaHash = schemaHash;
47
+ }
48
+ open() {
49
+ this.db ??= new Promise((resolve, reject) => {
50
+ const request = indexedDB.open(`vantage:${this.name}:${this.schemaHash}`, 1);
51
+ request.onupgradeneeded = () => {
52
+ const db = request.result;
53
+ db.createObjectStore("records");
54
+ db.createObjectStore("meta");
55
+ };
56
+ request.onsuccess = () => resolve(request.result);
57
+ request.onerror = () => reject(request.error);
58
+ });
59
+ return this.db;
60
+ }
61
+ async load() {
62
+ const db = await this.open();
63
+ const tx = db.transaction(["records", "meta"], "readonly");
64
+ const [records, cursor, outbox] = await Promise.all([
65
+ req(tx.objectStore("records").getAll()),
66
+ req(tx.objectStore("meta").get("cursor")),
67
+ req(tx.objectStore("meta").get("outbox")),
68
+ ]);
69
+ return { records, cursor: cursor ?? null, outbox: outbox ?? [] };
70
+ }
71
+ async save(change) {
72
+ const db = await this.open();
73
+ const tx = db.transaction(["records", "meta"], "readwrite");
74
+ const records = tx.objectStore("records");
75
+ for (const r of change.records ?? []) {
76
+ const key = `${r.table}\u0000${r.id}`;
77
+ if (r.record)
78
+ records.put({ table: r.table, record: r.record }, key);
79
+ else
80
+ records.delete(key);
81
+ }
82
+ if (change.cursor)
83
+ tx.objectStore("meta").put(change.cursor, "cursor");
84
+ if (change.outbox)
85
+ tx.objectStore("meta").put(change.outbox, "outbox");
86
+ await done(tx);
87
+ }
88
+ async clear() {
89
+ const db = await this.open();
90
+ const tx = db.transaction(["records", "meta"], "readwrite");
91
+ tx.objectStore("records").clear();
92
+ tx.objectStore("meta").clear();
93
+ await done(tx);
94
+ }
95
+ }
96
+ function req(request) {
97
+ return new Promise((resolve, reject) => {
98
+ request.onsuccess = () => resolve(request.result);
99
+ request.onerror = () => reject(request.error);
100
+ });
101
+ }
102
+ function done(tx) {
103
+ return new Promise((resolve, reject) => {
104
+ tx.oncomplete = () => resolve();
105
+ tx.onerror = () => reject(tx.error);
106
+ tx.onabort = () => reject(tx.error);
107
+ });
108
+ }
@@ -0,0 +1,236 @@
1
+ /** The wire protocol this client speaks; sent in `hello`. */
2
+ export declare const PROTOCOL = 1;
3
+ /** What this client understands beyond the protocol; sent in `hello`. */
4
+ export declare const FEATURES: string[];
5
+ export type Json = null | boolean | number | string | Json[] | {
6
+ [key: string]: Json;
7
+ };
8
+ export type WireRecord = {
9
+ id: string;
10
+ $actions?: string[];
11
+ } & Record<string, unknown>;
12
+ export type Op = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "not_in" | "like";
13
+ export type Where = [column: string, op: Op, value: unknown];
14
+ export type Sort = [column: string, direction: "asc" | "desc"];
15
+ export interface CollectionSpec {
16
+ table: string;
17
+ where?: Where[];
18
+ search?: string;
19
+ sort?: Sort;
20
+ via?: {
21
+ table: string;
22
+ id: string;
23
+ ref: string;
24
+ };
25
+ limit?: number;
26
+ }
27
+ export interface Cursor {
28
+ epoch: string;
29
+ seq: number;
30
+ }
31
+ export type ChangeEvent = {
32
+ k: "upsert";
33
+ table: string;
34
+ id: string;
35
+ record: WireRecord;
36
+ } | {
37
+ k: "removed";
38
+ table: string;
39
+ id: string;
40
+ } | {
41
+ k: "invalidated";
42
+ table: string;
43
+ };
44
+ export type ErrorKind = "invalid" | "not_found" | "forbidden" | "unavailable" | "conflict" | "failed" | "offline";
45
+ export type ServerFrame = {
46
+ t: "welcome";
47
+ id: number;
48
+ principal: string;
49
+ epoch: string;
50
+ seq: number;
51
+ schema_hash: string;
52
+ resync: boolean;
53
+ protocol?: number;
54
+ features?: string[];
55
+ }
56
+ /** The server swapped to a reloaded API: every collection and record interest is gone. */
57
+ | {
58
+ t: "resync";
59
+ epoch: string;
60
+ seq: number;
61
+ schema_hash: string;
62
+ } | {
63
+ t: "rows";
64
+ id: number;
65
+ sub: number;
66
+ table: string;
67
+ start: number;
68
+ ids: string[];
69
+ records: WireRecord[];
70
+ total: number | null;
71
+ complete: boolean;
72
+ seq: number;
73
+ } | {
74
+ t: "members";
75
+ id?: number;
76
+ sub: number;
77
+ start: number;
78
+ ids: string[];
79
+ records: WireRecord[];
80
+ total: number | null;
81
+ complete: boolean;
82
+ } | {
83
+ t: "records";
84
+ id: number;
85
+ table: string;
86
+ records: WireRecord[];
87
+ missing: string[];
88
+ } | {
89
+ t: "changed";
90
+ seq: number;
91
+ events: ChangeEvent[];
92
+ } | {
93
+ t: "flashed";
94
+ id: number;
95
+ rid: string;
96
+ seq: number;
97
+ record: WireRecord | null;
98
+ } | {
99
+ t: "rejected";
100
+ id: number;
101
+ message: string;
102
+ field_errors?: Record<string, string>;
103
+ } | {
104
+ t: "batch_result";
105
+ id: number;
106
+ results: ({
107
+ t: "flashed";
108
+ id: number;
109
+ rid: string;
110
+ seq: number;
111
+ record: WireRecord | null;
112
+ } | {
113
+ t: "rejected";
114
+ id: number;
115
+ message: string;
116
+ field_errors?: Record<string, string>;
117
+ })[];
118
+ }
119
+ /** The answer to `invoke`; the run's frames follow. */
120
+ | {
121
+ t: "result";
122
+ id: number;
123
+ run: string;
124
+ status: "started" | "attached" | "done";
125
+ } | {
126
+ t: "options_result";
127
+ id: number;
128
+ items: {
129
+ value: Json;
130
+ label: string;
131
+ }[];
132
+ }
133
+ /** `code` names errors a client acts on: `protocol`, `unknown_frame`, and a run's {@link RunCode}. */
134
+ | {
135
+ t: "error";
136
+ id: number;
137
+ kind: ErrorKind;
138
+ message: string;
139
+ code?: string;
140
+ } | {
141
+ t: "pong";
142
+ id: number;
143
+ }
144
+ /** The frontend the server serves changed: reload the page. */
145
+ | {
146
+ t: "reload";
147
+ }
148
+ /** `run_set` or `run_cancel` was applied. */
149
+ | {
150
+ t: "ack";
151
+ id: number;
152
+ }
153
+ /** What a run did; the first after `invoke` is a snapshot. */
154
+ | RunFrame;
155
+ export type RunStatus = "running" | "done" | "failed" | "cancelled";
156
+ export type RunCode = "conflict" | "expired" | "forbidden" | "invalid" | "limit" | "failed";
157
+ export interface RunFrame {
158
+ t: "run";
159
+ run: string;
160
+ seq: number;
161
+ status?: RunStatus;
162
+ state?: Record<string, Json>;
163
+ /** Each with the seq it was first sent with; older servers leave it out. */
164
+ messages?: {
165
+ name: string;
166
+ payload: Json;
167
+ seq?: number;
168
+ }[];
169
+ result?: Json;
170
+ error?: {
171
+ code: RunCode;
172
+ message: string;
173
+ };
174
+ }
175
+ export type FlashItem = {
176
+ table: string;
177
+ rid?: string;
178
+ patch: Record<string, unknown>;
179
+ };
180
+ export type ClientFrame = {
181
+ t: "hello";
182
+ protocol: number;
183
+ token?: string;
184
+ cursor?: Cursor;
185
+ schema_hash?: string;
186
+ features?: string[];
187
+ } | ({
188
+ t: "open_table";
189
+ } & CollectionSpec) | {
190
+ t: "viewport";
191
+ sub: number;
192
+ start: number;
193
+ count: number;
194
+ } | {
195
+ t: "close";
196
+ sub: number;
197
+ } | {
198
+ t: "open_records";
199
+ table: string;
200
+ ids: string[];
201
+ } | {
202
+ t: "interest";
203
+ remove: [string, string][];
204
+ } | ({
205
+ t: "flash";
206
+ } & FlashItem) | {
207
+ t: "batch";
208
+ items: FlashItem[];
209
+ } | {
210
+ t: "delete";
211
+ table: string;
212
+ rid: string;
213
+ }
214
+ /** Start run `run` of `action`, or attach to it; without `action`, attach only. */
215
+ | {
216
+ t: "invoke";
217
+ run: string;
218
+ action?: string;
219
+ rid?: string;
220
+ args?: Record<string, unknown>;
221
+ } | {
222
+ t: "options";
223
+ action: string;
224
+ field: string;
225
+ form?: Record<string, unknown>;
226
+ } | {
227
+ t: "run_set";
228
+ run: string;
229
+ name: string;
230
+ value: Json;
231
+ } | {
232
+ t: "run_cancel";
233
+ run: string;
234
+ } | {
235
+ t: "ping";
236
+ };
@@ -0,0 +1,5 @@
1
+ // Wire frames (mirror of vantage-server `src/protocol.rs`).
2
+ /** The wire protocol this client speaks; sent in `hello`. */
3
+ export const PROTOCOL = 1;
4
+ /** What this client understands beyond the protocol; sent in `hello`. */
5
+ export const FEATURES = ["runs:1"];
@@ -0,0 +1,59 @@
1
+ import { type FunctionComponent, type ReactNode } from "react";
2
+ import type { VantageDb } from "./client.js";
3
+ import type { ActionResult, CollectionWindow, Connection, LazyCollection, LazyReference, Model, Presence, SchemaTypes, Servo } from "./types.js";
4
+ export { useRun, useRunMessage, useShared } from "./run/hooks.js";
5
+ /** Provide the client. With `fallback`, children wait for `instant` tables. */
6
+ export declare function VantageProvider({ db, fallback, children }: {
7
+ db: VantageDb<any>;
8
+ fallback?: ReactNode;
9
+ children: ReactNode;
10
+ }): import("react").JSX.Element;
11
+ export declare function useVantage<S extends SchemaTypes>(): VantageDb<S>;
12
+ /** Re-render `Component` when anything it read during render changes. Generic components keep their type. */
13
+ export declare function observer<C extends FunctionComponent<any>>(Component: C): C;
14
+ /** The value of `fn`, re-computed when what it reads changes. */
15
+ export declare function useLive<T>(fn: () => T, deps?: unknown[]): T;
16
+ /** A collection's rows (models), live; `error` is the server's message when it refused the collection. */
17
+ export declare function useCollection<T>(collection: LazyCollection<T>): {
18
+ items: readonly T[];
19
+ } & Pick<LazyCollection<T>, "loadState" | "total" | "totalExact" | "length" | "error">;
20
+ /** One window of a collection, for virtualised lists; `error` as in `useCollection`. */
21
+ export declare function useWindow<T>(collection: LazyCollection<T>, start: number, count: number): CollectionWindow<T>;
22
+ /**
23
+ * A reference's record (`undefined` until loaded), its load `status`, and
24
+ * `error`, the server's message when it refused to load it.
25
+ */
26
+ export declare function useRecord<T>(reference: LazyReference<T>): {
27
+ value: T | undefined;
28
+ status: Presence;
29
+ error: string | null;
30
+ };
31
+ /** A draft of `model` for a form; a new draft when the model changes. */
32
+ export declare function useServo<M extends Model>(model: M): Servo<M>;
33
+ /** Wrap an action for a button: `run`, `pending`, the last `error`. */
34
+ export declare function useAction<A extends unknown[], R>(fn: (...args: A) => Promise<ActionResult<R>>): {
35
+ run: (...args: A) => Promise<ActionResult<R>>;
36
+ pending: boolean;
37
+ error: import("./types.js").ActionError | null;
38
+ };
39
+ /**
40
+ * Suspend until `target` (a model, reference or collection) has loaded
41
+ * `keys`; then return it. Wrap the component in <Suspense>.
42
+ */
43
+ export declare function useHydrated<T>(target: T, keys?: readonly string[]): T;
44
+ /** The connection's `status`, whether it `halted` (`refused` or `protocol_mismatch`), and then the server's `error`. */
45
+ export declare function useConnection(): Connection & {
46
+ seq: number;
47
+ principal: string | null;
48
+ };
49
+ /**
50
+ * Start `load` when the pointer rests on (or focus reaches) an element for
51
+ * `delay` ms — by the click, the data is usually there.
52
+ * <a {...usePreloadOnIntent(() => db.bakery.ref(id).load())} href=…>
53
+ */
54
+ export declare function usePreloadOnIntent(load: () => unknown, delay?: number): {
55
+ onPointerEnter: () => void;
56
+ onPointerLeave: () => void;
57
+ onFocus: () => void;
58
+ onBlur: () => void;
59
+ };
package/dist/react.js ADDED
@@ -0,0 +1,169 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // React bindings.
3
+ //
4
+ // `observer(Component)` tracks what a render reads (fields, references,
5
+ // collections) and re-renders on exactly those changes, through
6
+ // `useSyncExternalStore` — tear-free under concurrent rendering, correct in
7
+ // StrictMode. Hooks cover code that prefers explicit subscriptions.
8
+ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
9
+ import { Reaction } from "./signal.js";
10
+ export { useRun, useRunMessage, useShared } from "./run/hooks.js";
11
+ const Context = createContext(null);
12
+ /** Provide the client. With `fallback`, children wait for `instant` tables. */
13
+ export function VantageProvider({ db, fallback, children }) {
14
+ const [ready, setReady] = useState(fallback === undefined);
15
+ useEffect(() => {
16
+ let live = true;
17
+ void db.ready.then(() => live && setReady(true));
18
+ return () => {
19
+ live = false;
20
+ };
21
+ }, [db]);
22
+ return _jsx(Context.Provider, { value: db, children: ready ? children : fallback });
23
+ }
24
+ export function useVantage() {
25
+ const db = useContext(Context);
26
+ if (!db)
27
+ throw new Error("vantage: useVantage() outside <VantageProvider>");
28
+ return db;
29
+ }
30
+ /** Re-render `Component` when anything it read during render changes. Generic components keep their type. */
31
+ export function observer(Component) {
32
+ const Observed = (props) => {
33
+ const reaction = useRef(null);
34
+ reaction.current ??= new Reaction();
35
+ useSyncExternalStore(reaction.current.subscribe, reaction.current.getVersion, reaction.current.getVersion);
36
+ return reaction.current.track(() => Component(props));
37
+ };
38
+ Observed.displayName = `observer(${Component.displayName ?? Component.name ?? "Component"})`;
39
+ return memo(Observed);
40
+ }
41
+ /** The value of `fn`, re-computed when what it reads changes. */
42
+ export function useLive(fn, deps = []) {
43
+ const reaction = useMemo(() => new Reaction(), []);
44
+ const cache = useRef(null);
45
+ const read = useCallback(() => {
46
+ const c = cache.current;
47
+ if (!c || c.version !== reaction.version || !sameDeps(c.deps, deps)) {
48
+ cache.current = { version: reaction.version, value: reaction.track(fn), deps };
49
+ }
50
+ return cache.current.value;
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, [reaction, ...deps]);
53
+ return useSyncExternalStore(reaction.subscribe, read, read);
54
+ }
55
+ /** A collection's rows (models), live; `error` is the server's message when it refused the collection. */
56
+ export function useCollection(collection) {
57
+ return useLive(() => ({
58
+ items: collection.elements,
59
+ loadState: collection.loadState,
60
+ total: collection.total,
61
+ totalExact: collection.totalExact,
62
+ length: collection.length,
63
+ error: collection.error,
64
+ }), [key(collection)]);
65
+ }
66
+ /** One window of a collection, for virtualised lists; `error` as in `useCollection`. */
67
+ export function useWindow(collection, start, count) {
68
+ return useLive(() => collection.window(start, count), [key(collection), start, count]);
69
+ }
70
+ /**
71
+ * A reference's record (`undefined` until loaded), its load `status`, and
72
+ * `error`, the server's message when it refused to load it.
73
+ */
74
+ export function useRecord(reference) {
75
+ return useLive(() => ({ value: reference.value, status: reference.status, error: reference.error }), [reference]);
76
+ }
77
+ /** A draft of `model` for a form; a new draft when the model changes. */
78
+ export function useServo(model) {
79
+ const servo = useMemo(() => model.edit(), [model]);
80
+ useLive(() => {
81
+ // Read the draft's tracked parts so the form re-renders as it changes.
82
+ void servo.$isDirty;
83
+ void servo.$status;
84
+ void servo.$errors;
85
+ return null;
86
+ }, [servo]);
87
+ return servo;
88
+ }
89
+ /** Wrap an action for a button: `run`, `pending`, the last `error`. */
90
+ export function useAction(fn) {
91
+ const [pending, setPending] = useState(false);
92
+ const [error, setError] = useState(null);
93
+ const latest = useRef(fn);
94
+ latest.current = fn;
95
+ const run = useCallback(async (...args) => {
96
+ setPending(true);
97
+ setError(null);
98
+ const result = await latest.current(...args);
99
+ setPending(false);
100
+ if (!result.ok)
101
+ setError(result.error);
102
+ return result;
103
+ }, []);
104
+ return { run, pending, error };
105
+ }
106
+ /**
107
+ * Suspend until `target` (a model, reference or collection) has loaded
108
+ * `keys`; then return it. Wrap the component in <Suspense>.
109
+ */
110
+ export function useHydrated(target, keys) {
111
+ const promise = hydrationOf(target, keys);
112
+ if (!promise.done)
113
+ throw promise.wait;
114
+ if (promise.error)
115
+ throw promise.error;
116
+ return (promise.value ?? target);
117
+ }
118
+ const hydrations = new WeakMap();
119
+ function hydrationOf(target, keys) {
120
+ const obj = target;
121
+ let perKeys = hydrations.get(obj);
122
+ if (!perKeys)
123
+ hydrations.set(obj, (perKeys = new Map()));
124
+ const k = (keys ?? ["*"]).join(",");
125
+ let h = perKeys.get(k);
126
+ if (!h) {
127
+ const entry = { wait: Promise.resolve(), done: false };
128
+ const start = obj.hydrate ? obj.hydrate(keys) : obj.load ? obj.load() : Promise.resolve(obj);
129
+ entry.wait = start.then((value) => {
130
+ entry.value = value;
131
+ entry.done = true;
132
+ }, (error) => {
133
+ entry.error = error;
134
+ entry.done = true;
135
+ });
136
+ perKeys.set(k, (h = entry));
137
+ }
138
+ return h;
139
+ }
140
+ /** The connection's `status`, whether it `halted` (`refused` or `protocol_mismatch`), and then the server's `error`. */
141
+ export function useConnection() {
142
+ const db = useVantage();
143
+ return useLive(() => ({ ...db.connection.get(), seq: db.seq.get(), principal: db.principal.get() }));
144
+ }
145
+ /**
146
+ * Start `load` when the pointer rests on (or focus reaches) an element for
147
+ * `delay` ms — by the click, the data is usually there.
148
+ * <a {...usePreloadOnIntent(() => db.bakery.ref(id).load())} href=…>
149
+ */
150
+ export function usePreloadOnIntent(load, delay = 60) {
151
+ const timer = useRef(undefined);
152
+ const latest = useRef(load);
153
+ latest.current = load;
154
+ const db = useContext(Context);
155
+ return useMemo(() => {
156
+ const start = () => {
157
+ clearTimeout(timer.current);
158
+ timer.current = setTimeout(() => void (db ? db.preload(latest.current) : latest.current()), delay);
159
+ };
160
+ const cancel = () => clearTimeout(timer.current);
161
+ return { onPointerEnter: start, onPointerLeave: cancel, onFocus: start, onBlur: cancel };
162
+ }, [db, delay]);
163
+ }
164
+ function key(collection) {
165
+ return JSON.stringify(collection.spec ?? null);
166
+ }
167
+ function sameDeps(a, b) {
168
+ return a.length === b.length && a.every((x, i) => Object.is(x, b[i]));
169
+ }
@@ -0,0 +1,6 @@
1
+ export type Member = {
2
+ kind: "column" | "reference" | "action";
3
+ name: string;
4
+ };
5
+ /** The first member of `entity` that takes a reserved name, as a one-line message. */
6
+ export declare function reservedClash(entity: string, members: Member[]): string | undefined;
@@ -0,0 +1,13 @@
1
+ // Names a model keeps for itself. A column, reference or row action can't
2
+ // take one, or it would hide the model's own member: `id`, `edit`, `patch`,
3
+ // `delete`, `hydrate`, and anything starting with `$` (`$status`,
4
+ // `$actions`, …). The id column itself is named `id`, and hidden columns
5
+ // aren't on the model, so neither clashes.
6
+ const RESERVED = new Set(["id", "edit", "patch", "delete", "hydrate"]);
7
+ /** The first member of `entity` that takes a reserved name, as a one-line message. */
8
+ export function reservedClash(entity, members) {
9
+ const clash = members.find((m) => RESERVED.has(m.name) || m.name.startsWith("$"));
10
+ if (!clash)
11
+ return undefined;
12
+ return `entity \`${entity}\`: ${clash.kind} \`${clash.name}\` clashes with a name the model reserves (id, edit, patch, delete, hydrate, $…); rename it in the API`;
13
+ }
@@ -0,0 +1,6 @@
1
+ import type { RunFrame } from "../protocol.js";
2
+ export type FrameKind = "snapshot" | "live" | "stale" | "gap";
3
+ /** How to treat `frame` for a run that has applied frames up to `lastSeq` (-1: none yet). */
4
+ export declare function classify(frame: RunFrame, lastSeq: number): FrameKind;
5
+ /** A run that ended, by the frame's status. A status this client doesn't know is not an end. */
6
+ export declare function isFinal(frame: RunFrame): boolean;
@@ -0,0 +1,24 @@
1
+ // Reading `run` frames: which are snapshots, which are live, and when one
2
+ // was missed. Unknown fields are ignored, so a newer server's frames still
3
+ // apply.
4
+ //
5
+ // The server numbers a run's frames with `seq`. The first frame after an
6
+ // invoke or attach is a snapshot: the whole state, the buffered messages
7
+ // and the status. Each later frame carries one change. A frame whose seq
8
+ // skips ahead means one was lost: the client attaches again for a fresh
9
+ // snapshot.
10
+ /** How to treat `frame` for a run that has applied frames up to `lastSeq` (-1: none yet). */
11
+ export function classify(frame, lastSeq) {
12
+ const snapshot = frame.status !== undefined && frame.state !== undefined && frame.messages !== undefined;
13
+ if (snapshot)
14
+ return frame.seq < lastSeq ? "stale" : "snapshot";
15
+ if (lastSeq < 0)
16
+ return "gap"; // a live frame before any snapshot
17
+ if (frame.seq <= lastSeq)
18
+ return "stale";
19
+ return frame.seq === lastSeq + 1 ? "live" : "gap";
20
+ }
21
+ /** A run that ended, by the frame's status. A status this client doesn't know is not an end. */
22
+ export function isFinal(frame) {
23
+ return frame.status === "done" || frame.status === "failed" || frame.status === "cancelled";
24
+ }
@@ -0,0 +1,18 @@
1
+ import type { ActionResult } from "../types.js";
2
+ import type { MessagesOf, Run, RunFactory, RunTypes } from "./types.js";
3
+ /**
4
+ * One run of `factory`'s action for this component: idle until `start`.
5
+ * With `key`, its id is kept in `sessionStorage` (per action), so after a
6
+ * page reload the same component attaches to the run again; a stored run
7
+ * the server no longer has (expired, or never yours) is forgotten and the
8
+ * run is back to `idle`. `run.forget()` forgets it too.
9
+ */
10
+ export declare function useRun<T extends RunTypes>(factory: RunFactory<T>, options?: {
11
+ key?: string;
12
+ }): Run<T>;
13
+ type Shared<T extends RunTypes, K extends keyof T["state"]> = K extends keyof T["inputs"] ? [value: T["state"][K], set: (value: T["state"][K]) => Promise<ActionResult<undefined>>] : T["state"][K];
14
+ /** A run's variable, live; for an input, `[value, set]`. */
15
+ export declare function useShared<T extends RunTypes, K extends keyof T["state"] & string>(run: Run<T>, name: K): Shared<T, K>;
16
+ /** Call `fn` for each message `name` the run sends while mounted, in order; `vantage.notify` is a body's `notify(text)`. */
17
+ export declare function useRunMessage<T extends RunTypes, K extends keyof MessagesOf<T> & string>(run: Run<T>, name: K, fn: (payload: MessagesOf<T>[K]) => void): void;
18
+ export {};