@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ ## 0.4.4 — 2026-10-13
4
+
5
+ - A README for the npm page, plus `keywords` and `homepage`.
6
+
7
+ ## 0.4.3 — 2026-10-13
8
+
9
+ - A run delivers to `on`/`useRunMessage` the messages sent while the connection was down or across a `resync`, from the server's `seq` on each message.
10
+
11
+ ## 0.4.2 — 2026-10-13
12
+
13
+ - MockServer fixture `actions` also accept a project action's YAML key with dashes (`check-overdue` for `check_overdue`); naming one action both ways throws.
14
+ - Docs: fixture action keys are wire names, handlers are typed with `ActionHandler` and `MockRunControl`, and `run.start()` on a finished run begins a new run.
15
+ - Docs: rebuilding a list from `run.messages` after a reload, `start()` with no argument, and why `XFields` columns are nullable.
16
+
17
+ ## 0.4.1 — 2026-10-13
18
+
19
+ - `db.reconnect()` connects again after `refused`, calling `token` afresh, for a sign-in form.
20
+ - The generated file's header says how it is regenerated: by the web dev container from `schema.json`, or with `npx vantage-client generate`.
21
+
22
+ ## 0.4.0 — 2026-10-13
23
+
24
+ - Actions run as runs: `db.run.<action>` and `model.$run.<action>` give `start(args, { run? })`, `attach(id)` and `idle()`; a `Run` has reactive `status`, `state`, `result`, `error` and `messages`, plus `on`, `set` (inputs, optimistic), `cancel` and `done`. Breaking: `db.run.x(input)` is now `db.run.x.start(input)`.
25
+ - Row and toolbar action methods run the action and wait for its end; they no longer return the server's record.
26
+ - React: `useRun(factory, { key })` (re-attaches after a page reload), `useShared` and `useRunMessage`.
27
+ - `generate` emits `<Action>Run` (`args`, `state`, `inputs`, `messages`) per action and `$run` on models; the runtime schema carries each action's `state` and `messages`.
28
+ - `hello` sends `features: ["runs:1"]`; a server without it fails runs with a clear message. An unanswered `start` is sent again with the same id after a reconnect or `resync`, and running runs attach again; unknown frames, fields and statuses are ignored.
29
+ - `run.on`/`useRunMessage` take `vantage.notify` and `vantage.log`; `run.forget()` returns a run to `idle`; `useRun` keys are scoped to the action.
30
+ - Removed: `db.jobs`, `stopJob`, `useJobs`, the `Job` type and `ActionResult.job`.
31
+ - MockServer runs action handlers as runs (`(row, args, run)`), with `server.runs`.
32
+
33
+ ## 0.3.0 — 2026-10-13
34
+
35
+ - Connects to `/v1/ws` on the page's origin by default; outside a browser a `url` is required.
36
+ - `hello` sends `protocol: 1` and the `token` option.
37
+ - `db.connection` is `{ status, halted, error? }`: `online`/`offline` became `open`/`closed`, plus final `refused` and `protocol_mismatch` (`halted`); `db.ready` settles on them and resolves with the connection. `useConnection()` returns `halted` too.
38
+ - `resync` reopens what is shown without flashing rows; refusals land on `error` and are not retried.
39
+ - `useRecord` returns `{ value, status, error }`; collections and windows carry `error`.
40
+ - Windows add `total`, `totalExact`, `settled`, `length`, `lengthExact` and `whenLoaded()`; collections and `useCollection` add `totalExact`.
41
+ - The same collection description gives the same object while it is in use.
42
+ - Removed: `filter`/`find`/`some`/`every` on collections, and the root exports `Reaction`, `track`, `untracked`, `WebSocketTransport`, `defaultUrl`, `PROTOCOL`, `uuidv7` and `DraftFields`. `Vantage` is exported as a type only, and internal members are left out of the published types.
43
+ - `observer` keeps a generic component's type.
44
+ - MockServer adds `opened` and `refuseWrites`/`allowWrites`.
45
+ - Edits queued while offline are sent on reconnect in the same page only; a reload loses them.
46
+ - Draft members are `$`-prefixed (`$flash`, `$isDirty`, `$status`, `$errors`, `$dirty`, `$revert`); columns, references and row actions with reserved names are refused.
47
+ - `npx vantage-client generate --from <url | file>` replaces the Rust `generate-ts`; it reads the running server's `/v1/schema` directly.
48
+ - Development runs on local Node 22+; no Docker.
49
+ - The generated `schema` carries `sortable`, `filter`, `defaultSort` (`{ column, direction }`) and each enum column's values; labels in generated comments are escaped.
50
+ - Ships an ESM build with types, the `generate` CLI, and the `vantage-client` skill.
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # Temporary Holding Version
1
+ # @vantage-ui/client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The TypeScript client for a [Vantage](https://vantage-ui.com) API. It turns
4
+ the API into typed models. Reads are lazy and stay current: the server pushes
5
+ changes, and a React component re-renders on exactly the fields it read.
6
+ Writes are optimistic and roll back when the server refuses them.
7
+
8
+ It runs in the browser and in Node 22 or later, and has no runtime
9
+ dependencies. The React bindings need React 18 or later.
10
+
11
+ ```sh
12
+ npm install @vantage-ui/client
13
+ ```
14
+
15
+ ## Generate the models
16
+
17
+ The models come from the API's schema at `/v1/schema`:
18
+
19
+ ```sh
20
+ npx vantage-client generate --from http://127.0.0.1:8090/v1/schema --out src/vantage.gen.ts
21
+ ```
22
+
23
+ `--from` also takes a saved copy of the schema (`schema.json`). Regenerate
24
+ after the API changes; never edit the generated file. Per entity it has
25
+ `XFields` (the record), `XInput` (what `create()` takes) and the model `X`,
26
+ with typed columns, lazy references, has-many collections and a method per
27
+ row action.
28
+
29
+ ## Connect
30
+
31
+ A page served by the API connects to `/v1/ws` on its own origin:
32
+
33
+ ```ts
34
+ import { createVantage } from "@vantage-ui/client";
35
+ import { schema, type Schema } from "./vantage.gen";
36
+
37
+ const db = createVantage<Schema>({ schema, token: () => sessionStorage.getItem("vantage.token") ?? undefined });
38
+ ```
39
+
40
+ Outside a browser, pass `url: "ws://127.0.0.1:8090/v1/ws"`. `db.ready`
41
+ resolves once the API's `instant` entities have loaded, and `db.connection`
42
+ reports `connecting`, `open`, `closed` (reconnecting), `refused` or
43
+ `protocol_mismatch`.
44
+
45
+ ## Read
46
+
47
+ ```ts
48
+ const open = db.issues.where({ status: "open" }).sort("created_at", "desc");
49
+ await open.load();
50
+ const titles = open.map((issue) => issue.title);
51
+
52
+ const page = await db.issues.window(0, 50).whenLoaded(); // one window of a long list
53
+
54
+ const issue = await db.issues.ref("JIR-12").load();
55
+ const project = await issue?.project; // references load on first read
56
+ ```
57
+
58
+ The server filters, sorts and pages; the API decides which columns may be
59
+ filtered and sorted. A refused query never throws: the collection's `error`
60
+ holds the server's message.
61
+
62
+ ## Write
63
+
64
+ ```ts
65
+ const result = await issue.patch({ status: "done" });
66
+ if (!result.ok) console.warn(result.error.message, issue.$errors);
67
+
68
+ const draft = issue.edit();
69
+ draft.title = "Login fails on Safari";
70
+ await draft.$flash();
71
+
72
+ await db.issues.create({ title: "Export to CSV", project: "JIR", status: "open" });
73
+ ```
74
+
75
+ ## Actions and runs
76
+
77
+ Row actions are methods on the model, and `$actions` lists the ones the row
78
+ allows right now. Every action runs as a run, with shared state and messages,
79
+ that survives a reconnect or a page reload without running twice:
80
+
81
+ ```ts
82
+ if (issue.$actions.includes("close_issue")) await issue.close_issue({ resolution: "fixed" });
83
+
84
+ const run = db.run.triage.start({ project: "JIR" });
85
+ run.on("flagged", (m) => console.warn(`${m.issue} has no assignee`));
86
+ const outcome = await run.done;
87
+ ```
88
+
89
+ ## React
90
+
91
+ ```tsx
92
+ import { VantageProvider, observer, useCollection, useVantage } from "@vantage-ui/client/react";
93
+
94
+ const OpenIssues = observer(() => {
95
+ const db = useVantage<Schema>();
96
+ const { items, error } = useCollection(db.issues.where({ status: "open" }));
97
+ if (error) return <p role="alert">{error}</p>;
98
+ return <ul>{items.map((issue) => <li key={issue.id}>{issue.title}</li>)}</ul>;
99
+ });
100
+
101
+ <VantageProvider db={db} fallback={<p>Connecting…</p>}>
102
+ <OpenIssues />
103
+ </VantageProvider>;
104
+ ```
105
+
106
+ There are also hooks for windows (`useWindow`), records, actions, runs
107
+ (`useRun`, `useShared`, `useRunMessage`) and the connection (`useConnection`).
108
+
109
+ ## Testing
110
+
111
+ `@vantage-ui/client/testing` has `MockServer`, an in-memory server with no
112
+ network, for unit tests and Storybook:
113
+
114
+ ```ts
115
+ import { createTestVantage } from "@vantage-ui/client/testing";
116
+
117
+ const { db, server } = createTestVantage<Schema>(schema, {
118
+ tables: { issues: [{ id: "JIR-1", title: "Login fails", status: "open", project: "JIR" }] },
119
+ actions: { "issues.close_issue": () => ({ status: "done" }) },
120
+ });
121
+ ```
122
+
123
+ ## Full documentation
124
+
125
+ The package ships an agent skill that is also its reference documentation:
126
+ `skills/vantage-client/SKILL.md`, with `references/react.md`,
127
+ `references/testing.md` and `references/protocol.md`. Every example in it is
128
+ type-checked and run in this package's tests.
129
+
130
+ ## License
131
+
132
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // `npx @vantage-ui/client generate --from <url-or-file> --out <file>`
3
+ import { main } from "../dist/codegen/cli.js";
4
+
5
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,41 @@
1
+ import { ClientRuns } from "./client-runs.js";
2
+ import type { Connection } from "./types.js";
3
+ export declare class ClientConnection extends ClientRuns {
4
+ /** Bumps on every connect: events from an earlier socket are ignored. */
5
+ private attempt;
6
+ /** `reconnect()` calls waiting for the attempt to open or be refused. */
7
+ private reconnecting;
8
+ /**
9
+ * Connect again after the server refused this client, calling `token`
10
+ * afresh: what a sign-in form does once the user has entered a new token.
11
+ * Collections and records the refusal failed get another try. Resolves
12
+ * like `ready` does, once the new connection has loaded the `instant`
13
+ * tables or has been refused again. Only `refused` reconnects; in any
14
+ * other state this resolves with the connection as it is.
15
+ */
16
+ reconnect(): Promise<Connection>;
17
+ /** Answer the `reconnect()` calls waiting on this attempt. */
18
+ private settleReconnect;
19
+ protected start(): Promise<void>;
20
+ private connect;
21
+ private hello;
22
+ /**
23
+ * Reopen every collection that is pinned or shown, and send the record
24
+ * loads queued meanwhile. With `reload`, loaded records outside the shown
25
+ * collections are fetched again too. Rows on screen stay until their fresh
26
+ * copies arrive; records already loading keep their request.
27
+ */
28
+ private reopen;
29
+ /** The server swapped to a reloaded API: follow its new journal and reopen what is shown. */
30
+ private resync;
31
+ private checkSchema;
32
+ /**
33
+ * A final answer from the server: stop, don't reconnect, and settle
34
+ * everything waiting (`ready`, collection and record loads, queued writes)
35
+ * with `error`, writes queued offline included.
36
+ */
37
+ private halt;
38
+ private lost;
39
+ private receive;
40
+ private applyChange;
41
+ }
@@ -0,0 +1,260 @@
1
+ // The connection lifecycle: connect and `hello`, reconnect with backoff,
2
+ // the final `refused`/`protocol_mismatch` states, `resync` after an API
3
+ // reload, and the server-pushed frames (`changed`, `members`, `run`,
4
+ // `reload`).
5
+ import { ClientRuns } from "./client-runs.js";
6
+ import { FEATURES, PROTOCOL } from "./protocol.js";
7
+ import { batch } from "./signal.js";
8
+ export class ClientConnection extends ClientRuns {
9
+ /** Bumps on every connect: events from an earlier socket are ignored. */
10
+ attempt = 0;
11
+ /** `reconnect()` calls waiting for the attempt to open or be refused. */
12
+ reconnecting = [];
13
+ /**
14
+ * Connect again after the server refused this client, calling `token`
15
+ * afresh: what a sign-in form does once the user has entered a new token.
16
+ * Collections and records the refusal failed get another try. Resolves
17
+ * like `ready` does, once the new connection has loaded the `instant`
18
+ * tables or has been refused again. Only `refused` reconnects; in any
19
+ * other state this resolves with the connection as it is.
20
+ */
21
+ reconnect() {
22
+ if (this.connection.peek().status !== "refused")
23
+ return Promise.resolve(this.connection.peek());
24
+ const settled = new Promise((resolve) => this.reconnecting.push(resolve));
25
+ this.closed = false;
26
+ this.retry = 0;
27
+ for (const cell of this.collections.values())
28
+ cell.reset();
29
+ for (const cell of this.store.allCells())
30
+ if (cell.presence.peek() === "error")
31
+ cell.presence.set("none");
32
+ this.sweeper = setInterval(() => this.sweep(), 5_000);
33
+ this.connect();
34
+ return settled;
35
+ }
36
+ /** Answer the `reconnect()` calls waiting on this attempt. */
37
+ settleReconnect() {
38
+ const connection = this.connection.peek();
39
+ for (const resolve of this.reconnecting.splice(0))
40
+ resolve(connection);
41
+ }
42
+ async start() {
43
+ if (this.storage) {
44
+ const saved = await this.storage.load().catch(() => undefined);
45
+ if (saved) {
46
+ batch(() => {
47
+ for (const { table, record } of saved.records) {
48
+ if (this.schema.tables[table]?.persist)
49
+ this.store.cell(table, record.id).applyServer(record, "stale");
50
+ }
51
+ });
52
+ this.cursor = saved.cursor ?? undefined;
53
+ this.outbox = saved.outbox ?? [];
54
+ }
55
+ }
56
+ this.connect();
57
+ }
58
+ connect() {
59
+ if (this.closed)
60
+ return;
61
+ this.setStatus("connecting");
62
+ const attempt = ++this.attempt;
63
+ const current = () => attempt === this.attempt;
64
+ this.transport.connect({
65
+ open: () => current() && void this.hello(),
66
+ message: (frame) => current() && this.receive(frame),
67
+ close: (code, reason) => current() && this.lost(code, reason),
68
+ });
69
+ }
70
+ async hello() {
71
+ let token;
72
+ try {
73
+ token = typeof this.opts.token === "function" ? await this.opts.token() : this.opts.token;
74
+ }
75
+ catch (e) {
76
+ return this.halt("refused", e instanceof Error ? e.message : String(e));
77
+ }
78
+ const welcome = await this.request({ t: "hello", protocol: PROTOCOL, token, cursor: this.cursor, schema_hash: this.schema.hash, features: FEATURES }, true);
79
+ if (welcome.t === "error" && welcome.kind === "offline")
80
+ return; // lost mid-hello: the reconnect says hello again
81
+ if (welcome.t === "error" && welcome.code === "protocol")
82
+ return this.halt("protocol_mismatch", welcome.message);
83
+ if (welcome.t !== "welcome")
84
+ return this.halt("refused", welcome.t === "error" ? welcome.message : `unexpected reply \`${welcome.t}\``);
85
+ if (welcome.protocol !== undefined && welcome.protocol !== PROTOCOL) {
86
+ return this.halt("protocol_mismatch", `the server speaks protocol ${welcome.protocol}, this client ${PROTOCOL}`);
87
+ }
88
+ this.checkSchema(welcome.schema_hash);
89
+ this.retry = 0;
90
+ this.cursor = { epoch: welcome.epoch, seq: welcome.seq };
91
+ this.principal.set(welcome.principal);
92
+ this.features = new Set(welcome.features ?? []);
93
+ this.setStatus("open");
94
+ this.resumeRuns();
95
+ // Bootstrap: instant tables load whole and stay open.
96
+ const instant = Object.entries(this.schema.tables)
97
+ .filter(([, meta]) => meta.load === "instant")
98
+ .map(([name]) => this.collectionCell({ table: name }));
99
+ for (const cell of instant)
100
+ cell.pinned = true;
101
+ await this.reopen(welcome.resync);
102
+ this.markReady();
103
+ this.settleReconnect();
104
+ // Edits made offline go out now, in order.
105
+ const queued = this.flashQueue.splice(0);
106
+ if (queued.length) {
107
+ this.flashQueue = queued;
108
+ void this.flush();
109
+ }
110
+ }
111
+ /**
112
+ * Reopen every collection that is pinned or shown, and send the record
113
+ * loads queued meanwhile. With `reload`, loaded records outside the shown
114
+ * collections are fetched again too. Rows on screen stay until their fresh
115
+ * copies arrive; records already loading keep their request.
116
+ */
117
+ async reopen(reload) {
118
+ const shown = [...this.collections.values()].filter((c) => !c.refused && (c.pinned || c.wanted()));
119
+ await Promise.all(shown.map((c) => c.open()));
120
+ if (reload) {
121
+ const inCollections = new Set(shown.flatMap((c) => c.ids.peek().map((id) => `${c.spec.table}\u0000${id}`)));
122
+ const held = new Map();
123
+ for (const cell of this.store.allCells()) {
124
+ if (cell.presence.peek() === "loaded" && !inCollections.has(`${cell.table}\u0000${cell.id}`)) {
125
+ held.set(cell.table, [...(held.get(cell.table) ?? []), cell.id]);
126
+ }
127
+ }
128
+ for (const [table, ids] of held)
129
+ void this.reloadRecords(table, ids);
130
+ }
131
+ if (this.loadQueue.size)
132
+ void this.flushLoads();
133
+ }
134
+ /** The server swapped to a reloaded API: follow its new journal and reopen what is shown. */
135
+ resync(frame) {
136
+ this.checkSchema(frame.schema_hash);
137
+ this.generation++;
138
+ this.cursor = { epoch: frame.epoch, seq: frame.seq };
139
+ this.seq.set(frame.seq);
140
+ this.persistCursor();
141
+ this.bySub.clear();
142
+ // The reloaded API may allow what the old one refused: collections and records get one more try.
143
+ for (const cell of this.collections.values())
144
+ cell.reset();
145
+ for (const cell of this.store.allCells())
146
+ if (cell.presence.peek() === "error")
147
+ cell.presence.set("none");
148
+ void this.reopen(true);
149
+ // An invoke answered around the reload may not stream; asking again does.
150
+ this.resumeRuns();
151
+ }
152
+ checkSchema(hash) {
153
+ if (hash !== this.schema.hash) {
154
+ console.warn(`vantage: client types were generated from schema ${this.schema.hash}, server runs ${hash} — regenerate vantage.gen.ts`);
155
+ }
156
+ }
157
+ /**
158
+ * A final answer from the server: stop, don't reconnect, and settle
159
+ * everything waiting (`ready`, collection and record loads, queued writes)
160
+ * with `error`, writes queued offline included.
161
+ */
162
+ halt(status, error) {
163
+ this.setStatus(status, error);
164
+ this.close();
165
+ const kind = status === "refused" ? "forbidden" : "unavailable";
166
+ for (const { resolve } of this.flashQueue.splice(0))
167
+ resolve({ t: "error", id: 0, kind, message: error });
168
+ for (const cell of this.collections.values()) {
169
+ const state = cell.loadState.peek();
170
+ // Unloaded cells, and windows still waiting for rows (a viewport in flight).
171
+ if (state === "idle" || state === "loading" || cell.waitingForWindow)
172
+ cell.failed(error);
173
+ }
174
+ for (const [table, ids] of this.loadQueue) {
175
+ for (const id of ids) {
176
+ this.store.cell(table, id).refused(error);
177
+ this.wake(table, id);
178
+ }
179
+ }
180
+ this.loadQueue.clear();
181
+ this.markReady();
182
+ this.settleReconnect();
183
+ }
184
+ lost(code, reason) {
185
+ for (const resolve of this.requests.values())
186
+ resolve({ t: "error", id: 0, kind: "offline", message: "connection lost" });
187
+ this.requests.clear();
188
+ for (const cell of this.collections.values())
189
+ cell.sub = null;
190
+ this.bySub.clear();
191
+ const halted = this.haltedWith !== undefined;
192
+ // 1008 (policy violation): the reloaded API refuses this client's token.
193
+ if (code === 1008 && !halted)
194
+ return this.halt("refused", reason || "the server refused this client");
195
+ if (halted)
196
+ return;
197
+ this.setStatus("closed");
198
+ if (this.closed)
199
+ return;
200
+ const delay = Math.min(10_000, 200 * 2 ** this.retry++);
201
+ setTimeout(() => this.connect(), delay);
202
+ }
203
+ receive(frame) {
204
+ if ("id" in frame && frame.id && this.requests.has(frame.id)) {
205
+ const resolve = this.requests.get(frame.id);
206
+ this.requests.delete(frame.id);
207
+ resolve(frame);
208
+ return;
209
+ }
210
+ // Frames of a type this client doesn't know (a newer server's) fall through.
211
+ switch (frame.t) {
212
+ case "changed":
213
+ batch(() => frame.events.forEach((e) => this.applyChange(e)));
214
+ if (frame.seq && this.cursor) {
215
+ this.cursor = { ...this.cursor, seq: Math.max(this.cursor.seq, frame.seq) };
216
+ this.seq.set(this.cursor.seq);
217
+ this.persistCursor();
218
+ }
219
+ break;
220
+ case "members":
221
+ this.applyMembers(frame);
222
+ break;
223
+ case "resync":
224
+ this.resync(frame);
225
+ break;
226
+ case "run":
227
+ this.onRun(frame);
228
+ break;
229
+ case "reload":
230
+ if (this.opts.onReload)
231
+ this.opts.onReload();
232
+ else if (typeof globalThis.location?.reload === "function")
233
+ globalThis.location.reload();
234
+ break;
235
+ }
236
+ }
237
+ applyChange(event) {
238
+ if (event.k === "upsert") {
239
+ const cell = this.store.cell(event.table, event.id);
240
+ cell.applyServer(event.record);
241
+ this.persistLater(cell);
242
+ }
243
+ else if (event.k === "removed") {
244
+ const cell = this.store.peek(event.table, event.id);
245
+ if (cell) {
246
+ cell.markRemoved();
247
+ this.persistLater(cell);
248
+ }
249
+ for (const c of this.collections.values())
250
+ if (c.spec.table === event.table)
251
+ c.drop(event.id);
252
+ }
253
+ else {
254
+ for (const c of this.collections.values()) {
255
+ if (c.spec.table === event.table && c.sub !== null)
256
+ void this.openCollection(c);
257
+ }
258
+ }
259
+ }
260
+ }
@@ -0,0 +1,70 @@
1
+ import type { Vantage, VantageOptions } from "./client.js";
2
+ import type { CollectionCell } from "./collection-cell.js";
3
+ import type { Collection } from "./collection.js";
4
+ import { type AnyModel } from "./model.js";
5
+ import type { ClientFrame, Cursor, FlashItem, ServerFrame } from "./protocol.js";
6
+ import type { Storage } from "./persist.js";
7
+ import { State } from "./signal.js";
8
+ import { type RecordCell } from "./store.js";
9
+ import type { TableHandle } from "./table.js";
10
+ import { type Transport } from "./transport.js";
11
+ import type { Connection, ConnectionStatus, SchemaMeta } from "./types.js";
12
+ type Pending = (frame: ServerFrame) => void;
13
+ export declare class ClientCore {
14
+ readonly connection: State<Connection>;
15
+ readonly principal: State<string | null>;
16
+ readonly seq: State<number>;
17
+ /**
18
+ * Resolves with the connection once `instant` tables have loaded for the
19
+ * first time, or once the server refused the client for good (`halted`).
20
+ */
21
+ readonly ready: Promise<Connection>;
22
+ readonly schema: SchemaMeta;
23
+ protected transport: Transport;
24
+ protected opts: VantageOptions;
25
+ protected storage?: Storage;
26
+ protected models: Map<RecordCell, AnyModel>;
27
+ protected classes: Map<string, new (cell: RecordCell) => AnyModel>;
28
+ protected collections: Map<string, CollectionCell>;
29
+ /** One Collection per description, kept while one of its cells is. */
30
+ protected interned: Map<string, Collection>;
31
+ protected bySub: Map<number, CollectionCell>;
32
+ protected handles: Map<string, TableHandle<any, any, any>>;
33
+ protected requests: Map<number, Pending>;
34
+ protected nextId: number;
35
+ protected cursor?: Cursor;
36
+ protected loadQueue: Map<string, Set<string>>;
37
+ protected loadWaiters: Map<string, (() => void)[]>;
38
+ protected flashQueue: {
39
+ item: FlashItem;
40
+ resolve: (frame: ServerFrame) => void;
41
+ }[];
42
+ protected outbox: FlashItem[];
43
+ protected undoStack: {
44
+ table: string;
45
+ id: string;
46
+ before: Record<string, unknown>;
47
+ after: Record<string, unknown>;
48
+ }[];
49
+ protected retry: number;
50
+ /** Bumps on every `resync`: replies to frames sent before it are stale. */
51
+ protected generation: number;
52
+ protected closed: boolean;
53
+ protected sweeper?: ReturnType<typeof setInterval>;
54
+ private resolveReady;
55
+ protected persistTimer?: ReturnType<typeof setTimeout>;
56
+ protected dirtyCells: Set<RecordCell>;
57
+ constructor(options: VantageOptions);
58
+ /** Settle `ready` with the connection as it is now; later calls change nothing. */
59
+ protected markReady(): void;
60
+ /** This client as the typed `Vantage` the models and collections hold. */
61
+ protected get db(): Vantage<any>;
62
+ close(): void;
63
+ protected get online(): boolean;
64
+ protected setStatus(status: ConnectionStatus, error?: string): void;
65
+ protected send(frame: ClientFrame): void;
66
+ protected persistLater(cell: RecordCell): void;
67
+ protected persistCursor(): void;
68
+ private modelClass;
69
+ }
70
+ export {};