@vantage-ui/client 0.0.0-stage → 0.4.3

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 +46 -0
  2. package/bin/vantage-client.js +5 -0
  3. package/dist/client-connection.d.ts +41 -0
  4. package/dist/client-connection.js +260 -0
  5. package/dist/client-core.d.ts +70 -0
  6. package/dist/client-core.js +140 -0
  7. package/dist/client-loading.d.ts +13 -0
  8. package/dist/client-loading.js +229 -0
  9. package/dist/client-runs.d.ts +18 -0
  10. package/dist/client-runs.js +88 -0
  11. package/dist/client-writes.d.ts +14 -0
  12. package/dist/client-writes.js +114 -0
  13. package/dist/client.d.ts +32 -0
  14. package/dist/client.js +61 -0
  15. package/dist/codegen/cli.d.ts +8 -0
  16. package/dist/codegen/cli.js +54 -0
  17. package/dist/codegen/emit.d.ts +3 -0
  18. package/dist/codegen/emit.js +190 -0
  19. package/dist/codegen/fetch.d.ts +4 -0
  20. package/dist/codegen/fetch.js +41 -0
  21. package/dist/codegen/meta.d.ts +13 -0
  22. package/dist/codegen/meta.js +59 -0
  23. package/dist/codegen/runs.d.ts +20 -0
  24. package/dist/codegen/runs.js +58 -0
  25. package/dist/codegen/schema.d.ts +97 -0
  26. package/dist/codegen/schema.js +23 -0
  27. package/dist/collection-cell.d.ts +69 -0
  28. package/dist/collection-cell.js +153 -0
  29. package/dist/collection.d.ts +32 -0
  30. package/dist/collection.js +122 -0
  31. package/dist/index.d.ts +8 -0
  32. package/dist/index.js +14 -0
  33. package/dist/mock-query.d.ts +9 -0
  34. package/dist/mock-query.js +75 -0
  35. package/dist/mock-runs.d.ts +61 -0
  36. package/dist/mock-runs.js +165 -0
  37. package/dist/mock-store.d.ts +78 -0
  38. package/dist/mock-store.js +158 -0
  39. package/dist/mock-transport.d.ts +16 -0
  40. package/dist/mock-transport.js +38 -0
  41. package/dist/model.d.ts +5 -0
  42. package/dist/model.js +169 -0
  43. package/dist/persist.d.ts +47 -0
  44. package/dist/persist.js +108 -0
  45. package/dist/protocol.d.ts +236 -0
  46. package/dist/protocol.js +5 -0
  47. package/dist/react.d.ts +59 -0
  48. package/dist/react.js +169 -0
  49. package/dist/reserved.d.ts +6 -0
  50. package/dist/reserved.js +13 -0
  51. package/dist/run/frames.d.ts +6 -0
  52. package/dist/run/frames.js +24 -0
  53. package/dist/run/hooks.d.ts +18 -0
  54. package/dist/run/hooks.js +87 -0
  55. package/dist/run/messages.d.ts +20 -0
  56. package/dist/run/messages.js +42 -0
  57. package/dist/run/outcome.d.ts +12 -0
  58. package/dist/run/outcome.js +27 -0
  59. package/dist/run/run.d.ts +82 -0
  60. package/dist/run/run.js +246 -0
  61. package/dist/run/storage.d.ts +3 -0
  62. package/dist/run/storage.js +20 -0
  63. package/dist/run/types.d.ts +92 -0
  64. package/dist/run/types.js +3 -0
  65. package/dist/run/variables.d.ts +26 -0
  66. package/dist/run/variables.js +70 -0
  67. package/dist/schema-meta.d.ts +91 -0
  68. package/dist/schema-meta.js +3 -0
  69. package/dist/signal.d.ts +45 -0
  70. package/dist/signal.js +163 -0
  71. package/dist/store.d.ts +45 -0
  72. package/dist/store.js +144 -0
  73. package/dist/table.d.ts +62 -0
  74. package/dist/table.js +82 -0
  75. package/dist/testing.d.ts +61 -0
  76. package/dist/testing.js +195 -0
  77. package/dist/transport.d.ts +28 -0
  78. package/dist/transport.js +32 -0
  79. package/dist/types.d.ts +159 -0
  80. package/dist/types.js +2 -0
  81. package/dist/util.d.ts +18 -0
  82. package/dist/util.js +46 -0
  83. package/package.json +54 -4
  84. package/skills/vantage-client/SKILL.md +390 -0
  85. package/skills/vantage-client/references/protocol.md +67 -0
  86. package/skills/vantage-client/references/react.md +242 -0
  87. package/skills/vantage-client/references/testing.md +173 -0
  88. package/README.md +0 -3
@@ -0,0 +1,242 @@
1
+ # React
2
+
3
+ Import the bindings from `@vantage-ui/client/react`. React 18 or later.
4
+
5
+ ## The provider, and the connection's states
6
+
7
+ `VantageProvider` makes `db` available to `useVantage()`. With a `fallback`,
8
+ its children wait for `db.ready`: the `instant` entities loaded, or a final
9
+ refusal. One component can serve as the fallback and as a gate that
10
+ explains `refused` and `protocol_mismatch` once the children show:
11
+
12
+ ```tsx
13
+ // test: react#provider
14
+ // continues from SKILL.md's Connect: db; App is your app's root component
15
+ import { VantageProvider, useConnection } from "@vantage-ui/client/react";
16
+ import type { ReactNode } from "react";
17
+
18
+ function ConnectionGate({ children }: { children?: ReactNode }) {
19
+ const { status, error } = useConnection();
20
+ if (status === "refused") return <p role="alert">Sign-in refused: {error}</p>;
21
+ if (status === "protocol_mismatch") return <p role="alert">This page is out of date. Reload it. ({error})</p>;
22
+ return children ?? <p>Connecting…</p>;
23
+ }
24
+
25
+ function Root() {
26
+ return (
27
+ <VantageProvider db={db} fallback={<ConnectionGate />}>
28
+ <ConnectionGate>
29
+ <App />
30
+ </ConnectionGate>
31
+ </VantageProvider>
32
+ );
33
+ }
34
+ ```
35
+
36
+ `useConnection()` returns `{ status, halted, error, seq, principal }`;
37
+ `halted` is true once `refused` or `protocol_mismatch`. `closed` is
38
+ transient (the client reconnects); show it as a hint, not an error. A
39
+ sign-in form shown for `refused` stores the new token and calls
40
+ `useVantage().reconnect()`; the gate shows the children again once the
41
+ connection opens (SKILL.md, Connection states).
42
+
43
+ ## observer
44
+
45
+ `observer(Component)` re-renders a component when anything it read during
46
+ render changes: a field, a collection's members, a reference's value. It
47
+ re-renders on nothing else.
48
+
49
+ ```tsx
50
+ // test: react#collection
51
+ import { observer, useCollection, useVantage } from "@vantage-ui/client/react";
52
+ import type { Schema } from "./vantage.gen";
53
+
54
+ const OpenIssues = observer(() => {
55
+ const db = useVantage<Schema>();
56
+ const { items, loadState, error } = useCollection(db.issues.where({ status: "open" }));
57
+ if (error) return <p role="alert">{error}</p>;
58
+ if (loadState === "loading") return <p>Loading…</p>;
59
+ return <ul>{items.map((issue) => <li key={issue.id}>{issue.title}</li>)}</ul>;
60
+ });
61
+ ```
62
+
63
+ `useCollection` returns `{ items, loadState, total, totalExact, length, error }`.
64
+ Build the collection inline, as above: the same description is the same
65
+ object, so it needs no `useMemo`.
66
+
67
+ ## Windows
68
+
69
+ For virtualised lists, `useWindow(collection, start, count)` gives the rows
70
+ `start..start+count`. Moving `start` moves the server's window; rows stay
71
+ on screen until the new ones arrive. Size the virtualiser from
72
+ `page.length`, show it as a count only when `page.lengthExact`, and use
73
+ `page.settled` to tell "still loading" from "no rows here".
74
+
75
+ ```tsx
76
+ // test: react#window
77
+ import { observer, useVantage, useWindow } from "@vantage-ui/client/react";
78
+ import type { Schema } from "./vantage.gen";
79
+
80
+ const IssueRows = observer(({ start }: { start: number }) => {
81
+ const page = useWindow(useVantage<Schema>().issues.sort("created_at", "desc"), start, 50);
82
+ if (page.error) return <p role="alert">{page.error}</p>;
83
+ return <>{page.items.map((issue, i) => <div key={start + i}>{issue?.title ?? "…"}</div>)}</>;
84
+ });
85
+ ```
86
+
87
+ ## One record
88
+
89
+ `useRecord(reference)` returns `{ value, status, error }`. `status` is
90
+ `none`, `loading`, `loaded`, `missing` or `error`; `error` holds the
91
+ server's message when it refused the record. Unlike collections, each
92
+ `ref()` call is a new reference, so keep it stable with `useMemo`:
93
+
94
+ ```tsx
95
+ // test: react#record
96
+ import { useRecord, useVantage } from "@vantage-ui/client/react";
97
+ import { useMemo } from "react";
98
+ import type { Schema } from "./vantage.gen";
99
+
100
+ function IssueTitle({ id }: { id: string }) {
101
+ const db = useVantage<Schema>();
102
+ const ref = useMemo(() => db.issues.ref(id), [db, id]);
103
+ const { value, status, error } = useRecord(ref);
104
+ if (status === "error") return <p role="alert">{error}</p>;
105
+ if (status === "missing") return <p>No issue {id}</p>;
106
+ return <h1>{value?.title ?? "…"}</h1>;
107
+ }
108
+ ```
109
+
110
+ `useHydrated(target, keys?)` suspends until a model's references, a
111
+ reference or a collection have loaded; wrap the component in `<Suspense>`.
112
+ It also stops suspending when the server refuses the load or the
113
+ connection halts: the collection's `error`, or the reference's `error`
114
+ (with `status: "error"`), then holds the message.
115
+
116
+ ## Actions
117
+
118
+ `useAction(fn)` wraps an action for a button: `run`, `pending`, and the last
119
+ `error`. Call row actions on the model inside the function, so `this` is
120
+ kept:
121
+
122
+ ```tsx
123
+ // test: react#action
124
+ import { observer, useAction } from "@vantage-ui/client/react";
125
+ import type { Issues } from "./vantage.gen";
126
+
127
+ const CloseButton = observer(({ issue }: { issue: Issues }) => {
128
+ const close = useAction(() => issue.close_issue({ resolution: "fixed" }));
129
+ if (!issue.$actions.includes("close_issue")) return null;
130
+ return (
131
+ <>
132
+ <button disabled={close.pending} onClick={() => void close.run()}>Close</button>
133
+ {close.error && <span role="alert">{close.error.message}</span>}
134
+ </>
135
+ );
136
+ });
137
+ ```
138
+
139
+ ## Runs
140
+
141
+ `useRun(db.run.<action>, { key? })` gives the component one run, idle until
142
+ `start`. With `key`, its id is kept in the tab's `sessionStorage`, per
143
+ action: after a page reload the same component attaches to the run again
144
+ and continues from its current state. A stored run the server no longer has
145
+ (expired, or not yours) is forgotten and the run is `idle` again;
146
+ `run.forget()` does the same on purpose, to stop showing a finished run. A
147
+ new `key` is a new run.
148
+ `useShared(run, "name")` reads one variable, or gives `[value, set]` for an
149
+ input; only components that read a variable re-render when it changes.
150
+ `useRunMessage(run, "name", fn)` calls `fn` for each message while mounted,
151
+ in order; `"vantage.notify"` is the body's `notify(text)` (`m.text`).
152
+
153
+ ```tsx
154
+ // test: react#run
155
+ import { useRun, useRunMessage, useShared, useVantage } from "@vantage-ui/client/react";
156
+ import type { Schema } from "./vantage.gen";
157
+
158
+ // notifyUser is your UI's
159
+ function Triage({ project }: { project: string }) {
160
+ const db = useVantage<Schema>();
161
+ const triage = useRun(db.run.triage, { key: `triage:${project}` });
162
+ const progress = useShared(triage, "progress");
163
+ const total = useShared(triage, "total");
164
+ const [confirmed, setConfirmed] = useShared(triage, "confirmed");
165
+ useRunMessage(triage, "flagged", (m) => notifyUser(`${m.issue} has no assignee`));
166
+ useRunMessage(triage, "vantage.notify", (m) => notifyUser(m.text));
167
+ if (triage.status === "idle") return <button onClick={() => triage.start({ project })}>Triage</button>;
168
+ if (triage.status === "running" && !confirmed) return <button onClick={() => void setConfirmed(true)}>Confirm {total} issues</button>;
169
+ return <progress value={progress} max={total || 1} />;
170
+ }
171
+ ```
172
+
173
+ `useRunMessage` hears only the messages that arrive while the component is
174
+ mounted. After a page reload, the run attaches, and the messages sent
175
+ before then come back as history in `run.messages`, not as events. A list
176
+ that has to survive a reload (the issues a run flagged) is therefore built
177
+ from `run.messages`, in an `observer` component so that it re-renders as
178
+ messages arrive. Each entry is
179
+ `{ name, payload }`, oldest first. The list holds the run's last 100
180
+ messages, the framework's `vantage.notify` and `vantage.log` included.
181
+ Compare `name` to pick out one kind of message; the comparison narrows
182
+ `payload` to that message's type:
183
+
184
+ ```tsx
185
+ // test: react#run-history
186
+ import { observer, useRun, useVantage } from "@vantage-ui/client/react";
187
+ import type { Schema } from "./vantage.gen";
188
+
189
+ const Flagged = observer(() => {
190
+ const db = useVantage<Schema>();
191
+ const triage = useRun(db.run.triage, { key: "triage:JIR" });
192
+ const flagged = triage.messages.flatMap((m) => (m.name === "flagged" ? [m.payload.issue] : []));
193
+ if (triage.status === "idle") return <button onClick={() => triage.start({ project: "JIR" })}>Triage</button>;
194
+ return <ul>{flagged.map((issue) => <li key={issue}>{issue} has no assignee</li>)}</ul>;
195
+ });
196
+ ```
197
+
198
+ Forms, confirms and multi-step wizards are your components: collect what
199
+ the action needs, then `start` it once with the values.
200
+
201
+ `run.start(args)` always begins a new run with a new id; it never attaches.
202
+ On a finished run (`done`, `failed`, `cancelled`) that is "run it again":
203
+ no `forget()` first. With a `key`, the new id replaces the stored one. A
204
+ page reload shows the last run, finished or not, until the next `start`.
205
+ On a running run, `start` switches the handle to the new run and leaves
206
+ the old one running on the server; `cancel()` it first if it should stop.
207
+
208
+ `check.start()` with no argument type-checks when the action declares no
209
+ args (the generated `args` is `{}`), or when all its args are optional;
210
+ `start({})` means the same. When an arg is required, the argument is
211
+ required too, and leaving it out is a type error.
212
+
213
+ ```tsx
214
+ // test: react#run-again
215
+ import { useRun, useShared, useVantage } from "@vantage-ui/client/react";
216
+ import type { Schema } from "./vantage.gen";
217
+
218
+ function CheckOverdue() {
219
+ const db = useVantage<Schema>();
220
+ const check = useRun(db.run.check_overdue, { key: "desk" });
221
+ const overdue = useShared(check, "overdue");
222
+ const busy = check.status === "starting" || check.status === "running";
223
+ return (
224
+ <>
225
+ <button disabled={busy} onClick={() => check.start()}>Check overdue loans</button>
226
+ {check.status === "done" && <p>{overdue} overdue</p>}
227
+ </>
228
+ );
229
+ }
230
+ ```
231
+
232
+ ## Drafts in forms
233
+
234
+ `useServo(model)` gives an edit draft that re-renders the form as it
235
+ changes: set fields, read `$isDirty`, `$status` and `$errors`, then
236
+ `$flash()`.
237
+
238
+ ## Other hooks
239
+
240
+ - `usePreloadOnIntent(load)`: starts `load` when the pointer rests on an
241
+ element, so the data is there by the click.
242
+ - `useLive(fn, deps)`: any derived value, recomputed when what it reads changes.
@@ -0,0 +1,173 @@
1
+ # Testing
2
+
3
+ `@vantage-ui/client/testing` has `MockServer`: an in-memory vantage-server
4
+ that speaks the same frames, with no network. Collections filter, sort,
5
+ window and follow `via` like the real one; writes broadcast `changed` and
6
+ move collection membership. Use it in unit tests and Storybook.
7
+
8
+ ## With the generated schema
9
+
10
+ ```ts
11
+ // test: testing#generated
12
+ import { createTestVantage } from "@vantage-ui/client/testing";
13
+ import { schema, type Schema } from "./vantage.gen";
14
+
15
+ const { db, server } = createTestVantage<Schema>(schema, {
16
+ tables: { issues: [{ id: "JIR-1", title: "Login fails", status: "open", project: "JIR" }] },
17
+ actions: { "issues.close_issue": () => ({ status: "done" }) },
18
+ });
19
+ await db.ready;
20
+ const issue = await db.issues.ref("JIR-1").load();
21
+ server.upsert("issues", { id: "JIR-1", title: "Login fails on Safari" }); // someone else's edit
22
+ ```
23
+
24
+ Fixtures also take `available` (which row actions a record offers, standing
25
+ in for `when:`), `validate` (refuse writes with a message or field errors),
26
+ `protocol` (the version the mock speaks) and `refuseHello` (refuse every
27
+ token with a message).
28
+
29
+ A third argument passes client options, such as `{ token: "test" }`.
30
+
31
+ ## Runs
32
+
33
+ An action's handler is its body: `(row, args, run)`. Its return value (or
34
+ what its promise resolves to) is the run's `result`, and a row action's
35
+ returned object is applied to the row. `run` stands in for the body's run
36
+ vocabulary: `run.set(name, value)` sends a state patch, `run.wait(name)`
37
+ resolves once an input is set, `run.emit(name, payload)` and
38
+ `run.notify(text)` send messages. `server.runs` holds every run by id, with
39
+ its `action`, `args`, `state` and `status`:
40
+
41
+ ```ts
42
+ // test: testing#runs
43
+ // continues from "With the generated schema": createTestVantage, schema, Schema
44
+ import { expect } from "vitest";
45
+
46
+ const triage = createTestVantage<Schema>(schema, {
47
+ actions: {
48
+ triage: async (_, args, run) => {
49
+ run.set("total", 1);
50
+ await run.wait("confirmed");
51
+ run.emit("flagged", { issue: "JIR-1" });
52
+ return `triaged ${args.project}`;
53
+ },
54
+ },
55
+ });
56
+ await triage.db.ready;
57
+ const run = triage.db.run.triage.start({ project: "JIR" });
58
+ await run.set("confirmed", true);
59
+ expect(await run.done).toEqual({ ok: true, value: "triaged JIR" });
60
+ expect(triage.server.runs.get(run.id)?.args).toEqual({ project: "JIR" });
61
+ ```
62
+
63
+ Fixture `actions` are keyed by the action's wire name, the name `db.run`
64
+ uses: a project action's key with dashes turned into underscores
65
+ (`check_overdue` for `action/check-overdue.yaml`), and `<entity>.<name>`
66
+ for a row action. The YAML key with dashes (`"check-overdue"`) works too;
67
+ naming one action both ways throws. Type a handler declared on its own
68
+ with `ActionHandler`, and its `run` parameter with `MockRunControl`:
69
+
70
+ ```ts
71
+ // test: testing#action-names
72
+ // continues from "With the generated schema": createTestVantage, schema, Schema
73
+ import { type ActionHandler, type MockRunControl } from "@vantage-ui/client/testing";
74
+ import { expect } from "vitest";
75
+
76
+ const checkOverdue: ActionHandler = async (_, __, run: MockRunControl) => {
77
+ run.set("overdue", 3);
78
+ return "3 tools are overdue";
79
+ };
80
+ const desk = createTestVantage<Schema>(schema, { actions: { check_overdue: checkOverdue } });
81
+ await desk.db.ready;
82
+ const run = desk.db.run.check_overdue.start({});
83
+ expect(await run.done).toEqual({ ok: true, value: "3 tools are overdue" });
84
+ expect(run.state.overdue).toBe(3);
85
+ ```
86
+
87
+ `server.refuse("run_set", message)` refuses input writes, so a test can
88
+ check the rollback; `run_set` of a non-input or a wrong type is refused as
89
+ the real server does. `run.update({ a, b })` sends several variables in one
90
+ patch, as the server's throttle does. The fixture `eager: true` starts a
91
+ handler before the invoking socket follows the run, as vantage-server does,
92
+ so what it sends first arrives inside the first snapshot; `features: []`
93
+ is a server without runs.
94
+
95
+ ## Without generated types
96
+
97
+ `MockServer.withTable` builds a one-entity schema from the rows' keys.
98
+ `db.table(name)` reaches any entity by name:
99
+
100
+ ```ts
101
+ // test: testing#one-table
102
+ import { MockServer, createTestVantage } from "@vantage-ui/client/testing";
103
+
104
+ const server = MockServer.withTable("issues", [{ id: "1", title: "First" }]);
105
+ const { db } = createTestVantage(server, { token: "test" });
106
+ const all = await db.table("issues").all.load();
107
+ ```
108
+
109
+ ## Refusals, held replies and resync
110
+
111
+ The server-side events a test can cause:
112
+ - `server.refuse(op, message)` answers every `op` frame (`open_table`,
113
+ `open_records`, `viewport`, …) with that error; `server.allow(op)` stops it;
114
+ - `server.refuseWrites(message)` refuses every write, whether the client
115
+ sends it alone or batched with others; `server.allowWrites()` stops it;
116
+ - `server.hold(op)` keeps `op` replies pending until `server.release(op)`;
117
+ - `server.resync()` is an API reload: a new journal, every collection gone,
118
+ `resync` sent to every client;
119
+ - `server.reload()` sends `reload` to every client, which calls its
120
+ `onReload`;
121
+ - `server.invalidate(table)` is a coarse change: clients re-read the table;
122
+ - `server.opened` lists every collection a client opened (table, `where`,
123
+ `sort`, …), refused ones too; `server.count(op)` counts the frames
124
+ received, and `server.hellos` keeps every `hello`.
125
+
126
+ Assert on what the page asked for through these, not by spying on the
127
+ transport's frames:
128
+
129
+ ```ts
130
+ // test: testing#opened
131
+ // continues from "Without generated types": server, db
132
+ import { expect } from "vitest";
133
+
134
+ await db.table("issues").where({ title: "First" }).load();
135
+ expect(server.opened.at(-1)).toEqual({ table: "issues", where: [["title", "eq", "First"]] });
136
+ server.refuseWrites("issues: column `title` is not writable");
137
+ const issue = await db.table("issues").ref("1").load();
138
+ const saved = await issue!.patch({ title: "Renamed" });
139
+ expect(saved.ok).toBe(false);
140
+ ```
141
+
142
+ ```ts
143
+ // test: testing#refusal
144
+ // continues from the previous block: server, db
145
+ import { expect } from "vitest";
146
+
147
+ server.refuse("open_table", "invalid column `x` is not filterable");
148
+ const refused = await db.table("issues").where({ x: 1 }).load();
149
+ expect(refused.error).toBe("invalid column `x` is not filterable");
150
+ expect(server.count("open_table")).toBe(1); // a refusal is not retried
151
+ ```
152
+
153
+ ```ts
154
+ // test: testing#resync
155
+ // continues from "Without generated types": server, db
156
+ import { expect } from "vitest";
157
+
158
+ const rows = () => db.table("issues").all.map((r) => r.title);
159
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 5));
160
+ await db.table("issues").all.load();
161
+ server.setRows("issues", [{ id: "1", title: "First, renamed" }]); // the reloaded API's data
162
+
163
+ server.hold("open_table"); // keep the reopen pending
164
+ server.resync();
165
+ await settle();
166
+ expect(rows()).toEqual(["First"]); // still shown, no flash to empty
167
+ server.release("open_table");
168
+ await settle();
169
+ expect(rows()).toEqual(["First, renamed"]);
170
+ ```
171
+
172
+ `transport.drop()` (from `createTestVantage`'s result) simulates a lost
173
+ connection; `transport.drop(1008, reason)` a refused one.
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
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.