@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,390 @@
1
+ ---
2
+ name: vantage-client
3
+ description: Use when writing a web frontend (React or plain TypeScript) against a Vantage API served by vantage-server — connecting with @vantage-ui/client, generating typed models, reading collections, windows and records, writing, running actions as runs with shared state and messages, handling refusals and resync, and testing against MockServer.
4
+ metadata:
5
+ version: "2.2.3"
6
+ ---
7
+
8
+ # @vantage-ui/client
9
+
10
+ `@vantage-ui/client` turns a Vantage API (`api/<name>.yaml`, served by
11
+ vantage-server) into a typed, live object graph. Reads are lazy and stay
12
+ current: the server pushes changes, and a component re-renders on exactly
13
+ the fields it read. Writes are optimistic and roll back when the server
14
+ refuses them.
15
+
16
+ References in this skill:
17
+ - `references/react.md`: the provider, `observer`, and the hooks;
18
+ - `references/testing.md`: `createTestVantage` and `MockServer`;
19
+ - `references/protocol.md`: the wire frames, for scripts and debugging.
20
+
21
+ ## Generate the types
22
+
23
+ The models come from the API's schema at `/v1/schema`. In a Vantage
24
+ project, the web dev container installs, generates and builds the frontend
25
+ for you; no Node is needed on the machine (the `vantage-web` skill covers
26
+ it). Outside one, with Node 22 or later, generate straight from the
27
+ running server:
28
+
29
+ ```sh
30
+ npx vantage-client generate --from http://127.0.0.1:<port>/v1/schema --out src/vantage.gen.ts
31
+ ```
32
+
33
+ As an alternative, `--from` takes a saved copy of the schema, for when the
34
+ generator can't reach the server:
35
+
36
+ ```sh
37
+ curl -fsS http://127.0.0.1:<port>/v1/schema -o schema.json
38
+ npx vantage-client generate --from schema.json --out src/vantage.gen.ts
39
+ ```
40
+
41
+ The output follows the schema's order, so regenerating an unchanged schema
42
+ gives an identical file. Never edit `vantage.gen.ts`; regenerate it after
43
+ the API's YAML changes.
44
+
45
+ Per entity, the file has `XFields` (the record as data), `XInput` (what
46
+ `create()` takes) and the model `X`. A model has typed columns, a
47
+ `LazyReference` per reference, a `LazyCollection` per has-many, a method
48
+ per row action, and `$run` with a run factory per row action. Per action
49
+ there is `<Action>Run` (`IssuesCloseIssueRun` for a row action): its `args`,
50
+ `state`, `inputs` and `messages`. Model names come from entity names: entity
51
+ `issues` gives the model `Issues`.
52
+
53
+ Every column in `XFields` is `T | null`, `mandatory` ones included. Only
54
+ `XInput` makes `mandatory` columns required. `mandatory` makes a form ask
55
+ for the value, and the generated `create()` type require it, but Vantage
56
+ doesn't enforce it on the stored data (only a database constraint such as
57
+ `NOT NULL` does). A row written some other way, or before the column
58
+ became mandatory, can hold null. A field also reads `null` until its record has loaded, and after the
59
+ record is removed. Handle `null` when you read a field, even a mandatory
60
+ one.
61
+
62
+ A model's `id` is a `string` whatever type the id column has: ids travel as
63
+ text. A SurrealDB id reads `"table:id"` (`"issues:12"`); an integer key
64
+ arrives as its digits (`"6"`). Pass ids back as you received them.
65
+
66
+ ## Connect
67
+
68
+ In the browser, the client connects to `/v1/ws` on the page's own origin.
69
+ Serve the frontend from vantage-server (the API's `frontend:` folder) and
70
+ no URL is needed:
71
+
72
+ ```ts
73
+ // test: connect#same-origin
74
+ import { createVantage } from "@vantage-ui/client";
75
+ import { schema, type Schema } from "./vantage.gen";
76
+
77
+ const db = createVantage<Schema>({ schema, token: () => sessionStorage.getItem("vantage.token") ?? undefined });
78
+ ```
79
+
80
+ `token` is sent in `hello` on every connect; a function is called each
81
+ time, so a new sign-in is picked up on the next connect. After the server
82
+ refuses a token, `db.reconnect()` connects again (see Connection states).
83
+ Outside a browser (scripts, tests, Node), pass the URL:
84
+
85
+ ```ts
86
+ // test: connect#script-url
87
+ // continues from the previous block: createVantage, schema, Schema
88
+ const db = createVantage<Schema>({ schema, url: "ws://127.0.0.1:8090/v1/ws", token: process.env.VANTAGE_TOKEN });
89
+ ```
90
+
91
+ `db.ready` resolves with the connection (`{ status, halted, error? }`) once
92
+ the API's `instant` entities have loaded, or once the connection is refused
93
+ for good (see below).
94
+
95
+ ## Read
96
+
97
+ Every entity is a property of `db`. Collections are lazy: reading one opens
98
+ it on the server, which then keeps its membership and records current. The
99
+ server filters, sorts and pages them; a collection has `map`, `forEach` and
100
+ iteration, but no `filter` or `find`. The same description gives the same
101
+ collection object, so a render can build one inline, with no `useMemo`.
102
+
103
+ ```ts
104
+ // test: read#collections
105
+ // continues from Connect: db
106
+ const open = db.issues.where({ status: "open" }).sort("created_at", "desc");
107
+ await open.load();
108
+ const lines = open.map((issue) => `${issue.id}: ${issue.title}`);
109
+ const refusal = open.error; // the server's message if it refused the query, else null
110
+ ```
111
+
112
+ `where` takes `{ column: value }` (equality), `{ column: [a, b] }` (in), or
113
+ `{ column: { gt: 3 } }` with `eq ne gt gte lt lte in not_in like`. A
114
+ reference may be named by its relation (`{ project: "JIR" }`). The API
115
+ decides which columns may be filtered and sorted (`params.filter`,
116
+ `sortable`); anything else is refused, and the refusal lands on `error`.
117
+
118
+ The generated `schema` carries those whitelists, and each enum column's
119
+ values in the API's order, so menus and chips needn't hard-code them:
120
+
121
+ ```ts
122
+ // test: read#whitelists
123
+ import { schema } from "./vantage.gen";
124
+
125
+ const issues = schema.tables.issues;
126
+ const sortChoices = issues.sortable ?? Object.keys(issues.columns); // null: any column sorts
127
+ const filterable = issues.filter; // columns `where` may name
128
+ const statusChips = issues.columns.status.enum; // ["open", "in_progress", "done"]
129
+ const initialSort = issues.defaultSort; // { column, direction }, or null
130
+ ```
131
+
132
+ For long lists, read one window at a time. Rows outside the window are not
133
+ sent. `window()` returns what has arrived so far (a row is `undefined` until
134
+ it loads); `whenLoaded()` waits for the window's rows. In React, `useWindow`
135
+ re-renders as they arrive instead.
136
+
137
+ ```ts
138
+ // test: read#window
139
+ // continues from Connect: db
140
+ const page = await db.issues.sort("created_at", "desc").window(0, 50).whenLoaded();
141
+ const rows = page.items; // (Issues | undefined)[]
142
+ const fourth = page.at(3)?.title;
143
+ const scrollHeight = page.length * 32; // the server's total, else the rows so far
144
+ const footer = page.lengthExact ? `${page.length} issues` : `${page.length}+ issues`;
145
+ ```
146
+
147
+ A window's `length` is the server's `total` when it sends one, and
148
+ otherwise the rows that have arrived without a gap. `lengthExact` says when
149
+ it is the real count: `totalExact` when there is a total (the server counts
150
+ exactly once it has sent the whole collection), else once a settled window
151
+ came back short. Size a scrollbar from `length` either way, but only show
152
+ it as a count when `lengthExact` is true. `settled` is true once the
153
+ window's rows have arrived or it failed, which is what `whenLoaded()` waits
154
+ for. A collection has `total` and `totalExact` the same way.
155
+
156
+ A window refused by the server, or waiting when the connection halts,
157
+ settles with `page.error` set.
158
+
159
+ One record, by id. `ref()` gives a `LazyReference`: `.value` is the model
160
+ once loaded (reading it starts the load, and is `undefined` until then);
161
+ `await` or `.load()` waits. A model's references are lazy the same way:
162
+
163
+ ```ts
164
+ // test: read#records
165
+ // continues from Connect: db
166
+ const issue = await db.issues.ref("JIR-12").load(); // undefined when missing or refused
167
+ if (issue) {
168
+ const project = await issue.project; // awaiting a reference loads its record
169
+ const projectName = project?.name;
170
+ }
171
+ const ghost = db.issues.ref("JIR-404");
172
+ await ghost.load();
173
+ const status = ghost.status; // "missing"
174
+ ```
175
+
176
+ Has-many references are collections through the parent (`via`), and narrow
177
+ like any other:
178
+
179
+ ```ts
180
+ // test: read#via
181
+ // continues from Connect: db
182
+ const project = await db.projects.ref("JIR").load();
183
+ if (project) {
184
+ const openInProject = await project.issues.where({ status: "open" }).load();
185
+ }
186
+ ```
187
+
188
+ ## Write
189
+
190
+ Writes are optimistic: the model shows the new value at once, and rolls
191
+ back (with `$errors` per field) if the server refuses.
192
+
193
+ ```ts
194
+ // test: write#patch
195
+ // continues from Read: issue, loaded
196
+ const result = await issue.patch({ status: "done" });
197
+ if (!result.ok) console.warn(result.error.message, issue.$errors);
198
+ ```
199
+
200
+ A draft collects several changes and sends them together. Everything a
201
+ draft adds is `$`-prefixed (`$flash()`, `$isDirty`, `$status`, `$errors`,
202
+ `$dirty(field)`, `$revert(field?)`), so any column stays editable on it:
203
+
204
+ ```ts
205
+ // test: write#draft
206
+ // continues from Read: issue, loaded
207
+ const draft = issue.edit();
208
+ draft.title = "Login fails on Safari";
209
+ const saved = await draft.$flash();
210
+ ```
211
+
212
+ ```ts
213
+ // test: write#create
214
+ // continues from Connect: db
215
+ const created = await db.issues.create({ title: "Export to CSV", project: "JIR", status: "open" });
216
+ ```
217
+
218
+ Writes made in one tick travel as one `batch` (not a transaction). The API's
219
+ `params.columns` limits which fields a write may set.
220
+
221
+ ## Actions
222
+
223
+ Row actions are methods on the model; `$actions` lists the ones the server
224
+ allows on that row right now (their `when:` holds). A method runs the action
225
+ and waits for its end; the row's new values arrive like any other change.
226
+
227
+ ```ts
228
+ // test: actions#row
229
+ // continues from Read: issue, loaded
230
+ if (issue.$actions.includes("close_issue")) {
231
+ const closed = await issue.close_issue({ resolution: "fixed" });
232
+ if (!closed.ok) console.error(closed.error.message);
233
+ }
234
+ ```
235
+
236
+ In React, `useAction` wraps one for a button; see `references/react.md`.
237
+ Forms and confirms are your UI: ask the user, then call the action with the
238
+ values. Actions that start OS processes or need the desktop (`command`,
239
+ `terminal`, `stream`, `wizard`, `builtin:`) run only in the Vantage app and
240
+ are left out of the schema.
241
+
242
+ ## Runs
243
+
244
+ Every action runs as a *run*: it shares variables (`state`) and sends
245
+ messages while it goes, then ends with a `result`. Project actions are on
246
+ `db.run.<action>`, row actions on `model.$run.<action>`. `start` sends the
247
+ action with a new run id (a UUIDv7); invoking an id again never runs the
248
+ action twice, so a retry or a reload attaches instead.
249
+
250
+ ```ts
251
+ // test: runs#start
252
+ // continues from Connect: db
253
+ const run = db.run.triage.start({ project: "JIR" });
254
+ const stop = run.on("flagged", (m) => console.warn(`${m.issue} has no assignee`));
255
+ ```
256
+
257
+ A run's getters are reactive (`status`, `state.<name>`, `result`, `error`,
258
+ `messages`), so `autorun` and `observer` follow them. `status` goes
259
+ `starting` → `running` → `done`, `failed` or `cancelled`. An `input`
260
+ variable is the one thing you write; the write shows at once and rolls back
261
+ if the server refuses it:
262
+
263
+ ```ts
264
+ // test: runs#input
265
+ // continues from runs#start: run
266
+ await run.set("confirmed", true);
267
+ const outcome = await run.done;
268
+ if (outcome.ok) console.log(outcome.value, `${run.state.progress} of ${run.state.total}`);
269
+ ```
270
+
271
+ `run.cancel()` stops it at its next wait. `attach(id)` follows a run started
272
+ earlier, by this page before a reload or by another tab; a run that isn't
273
+ yours, or whose result has expired, ends `failed` with `error.code`
274
+ `forbidden` or `expired`:
275
+
276
+ ```ts
277
+ // test: runs#attach
278
+ // continues from Connect: db; runId is a run started earlier
279
+ const again = db.run.triage.attach(runId);
280
+ ```
281
+
282
+ `on` also takes the framework's messages: `"vantage.notify"` is the body's
283
+ `notify(text)` and `"vantage.log"` its `log(text)`, both `{ text }`. A run
284
+ you started delivers to `on` what it sent before you followed it; after an
285
+ `attach`, earlier messages are history in `messages`, not events. Messages
286
+ sent while the connection was down, or across a `resync`, still reach `on`
287
+ once the run catches up, if they are among its last 100.
288
+ `run.messages` is a list of `{ name, payload }`, oldest first: the run's
289
+ last 100 messages, framework ones included. After a reload, build anything
290
+ the page lists from messages out of `run.messages`, not from `on`
291
+ (`references/react.md`, Runs, has a tested example).
292
+
293
+ A run's `result` is what the action's body returns, sent as it is: the
294
+ API's column whitelists don't filter it. The client sends an unanswered
295
+ `start` again with the same id after a reconnect or `resync`, and attaches
296
+ to its running runs for a fresh snapshot, so a dropped connection never
297
+ runs an action twice. `run.forget()` goes back to `idle`. Toolbar actions
298
+ have no `$run` yet: `db.<table>.actions.<name>(input)` runs one and waits
299
+ for its end. In React, `useRun`, `useShared` and `useRunMessage`
300
+ (`references/react.md`) keep a run across page reloads and re-render only
301
+ what reads a variable.
302
+
303
+ ## Connection states
304
+
305
+ `db.connection` is a signal of `{ status, halted, error? }`; `halted` is
306
+ true in the two final states:
307
+
308
+ | `status` | Meaning |
309
+ |---|---|
310
+ | `connecting` | Before the first `welcome` |
311
+ | `open` | Connected; reads and writes go out |
312
+ | `closed` | Lost; the client reconnects with backoff and resumes |
313
+ | `refused` | The server refused the token (at `hello`, or after an API reload). Final |
314
+ | `protocol_mismatch` | The server speaks another protocol version. Final |
315
+
316
+ After `refused` or `protocol_mismatch` the client stops reconnecting, and
317
+ everything waiting settles: `db.ready` resolves, and pending loads return
318
+ with `error` set to the server's message. Show it:
319
+
320
+ ```ts
321
+ // test: connection#status
322
+ // continues from Connect: db; showBanner is your UI's
323
+ import { autorun } from "@vantage-ui/client";
324
+
325
+ const stop = autorun(() => {
326
+ const { status, halted, error } = db.connection.get();
327
+ if (halted) showBanner(`${status}: ${error}`);
328
+ });
329
+ ```
330
+
331
+ In React, `useConnection()` gives the same; `references/react.md` shows a
332
+ provider fallback built on it.
333
+
334
+ `refused` stays final until you call `db.reconnect()`. It connects again,
335
+ calls `token` afresh, and gives the collections and records the refusal
336
+ failed another try. It resolves like `db.ready`: with the connection once
337
+ it is open, or refused again. A sign-in form stores the new token where the
338
+ `token` function reads it and calls `reconnect()`, with no page reload:
339
+
340
+ ```ts
341
+ // test: connection#reconnect
342
+ // continues from Connect: db, whose token function reads sessionStorage; newToken is what the user typed, showBanner is your UI's
343
+ sessionStorage.setItem("vantage.token", newToken);
344
+ const connection = await db.reconnect();
345
+ if (connection.halted) showBanner(`Sign-in refused: ${connection.error}`);
346
+ ```
347
+
348
+ ## Refusals
349
+
350
+ The server refuses what the API doesn't allow (an unknown column, a filter
351
+ outside `params.filter`, a record outside your scope). A refusal never
352
+ throws and is never retried on its own:
353
+ - a collection or window has `error` (the message) and `loadState: "error"`;
354
+ - a record has `status: "error"` and `error` on its reference;
355
+ - a write or action returns `{ ok: false, error }`.
356
+
357
+ Change the query (a new `where` is a new collection) to try again.
358
+
359
+ ## Resync
360
+
361
+ When the API's YAML changes, vantage-server reloads and sends `resync`. The
362
+ client reopens every collection and window on screen and reloads the
363
+ records it holds. Rows stay visible until the fresh ones arrive, so nothing
364
+ flashes empty. Refused collections and records get one more try under the
365
+ reloaded API. There is nothing to handle in app code.
366
+
367
+ ## Reload
368
+
369
+ When vantage-server serves a built frontend (`--static <dist>`) and a new
370
+ build rewrites `<dist>/index.html` or `<dist>/assets/`, it sends `reload` to every open page. By
371
+ default the client calls `location.reload()` (outside a browser, nothing
372
+ happens). Pass `onReload` to do something first, or instead:
373
+
374
+ ```ts
375
+ // test: connection#reload
376
+ // continues from Connect: createVantage, schema, Schema; saveDraft is your UI's
377
+ const db = createVantage<Schema>({
378
+ schema,
379
+ onReload: () => {
380
+ saveDraft();
381
+ location.reload();
382
+ },
383
+ });
384
+ ```
385
+
386
+ ## Scripts and the wire `where`
387
+
388
+ On the wire, `where` is a list of `[column, op, value]` tuples:
389
+ `[["status", "eq", "open"]]`. The client builds that from `.where({...})`;
390
+ only raw WebSocket scripts write it by hand (`references/protocol.md`).
@@ -0,0 +1,67 @@
1
+ # The wire protocol
2
+
3
+ JSON text frames over `ws(s)://<host>/v1/ws`, tagged by `t`. Requests carry
4
+ an `id`; replies echo it. The client handles all of this; read on for raw
5
+ scripts and debugging. HTTP: `/v1/schema` (the schema `generate` reads),
6
+ `/v1/health`, `/v1/ready` (503 naming entities whose datasource is down).
7
+
8
+ ## Session
9
+
10
+ | Client → server | Server → client |
11
+ |---|---|
12
+ | `hello {protocol: 1, token?, cursor?, schema_hash?, features?}` | `welcome {protocol, principal, epoch, seq, schema_hash, resync, features}`: the features both sides have (`runs:1`) |
13
+ | a frame with a `t` the server doesn't know | `error {code: "unknown_frame"}`; a client ignores frames and fields it doesn't know |
14
+ | | `error {kind, message, code: "protocol"}`, then close 1002: another protocol version |
15
+ | | `error {kind: "forbidden", message}`: the token is refused; the socket stays open for another `hello` |
16
+ | | `resync {epoch, seq, schema_hash}`: the API was reloaded; reopen what you show, and re-attach the runs you follow (`invoke {run}`): an `invoke` answered around the reload may not stream its run |
17
+ | | `reload`: the served frontend was rebuilt; reload the page (sent before `welcome` too) |
18
+ | | close 1008: after a reload, the API refuses the token |
19
+ | | close 1012 `reconnect`: the server is shutting down |
20
+
21
+ Nothing but `hello` is accepted before a `welcome`.
22
+
23
+ ## Reading
24
+
25
+ | Client → server | Server → client |
26
+ |---|---|
27
+ | `open_table {table, where?, search?, sort?, via?, limit?}` | `rows {sub, start, ids, records, total, complete, seq}` |
28
+ | `viewport {sub, start, count}` | `members {sub, start, ids, records, total, complete}` |
29
+ | `close {sub}` | `members` pushed when membership or order changes |
30
+ | `open_records {table, ids}` | `records {records, missing}` |
31
+ | `interest {remove: [[table, id]]}` | `changed {seq, events: upsert \| removed \| invalidated}` |
32
+
33
+ `where` is a list of `[column, op, value]` tuples, `sort` is
34
+ `[column, "asc" | "desc"]`, and `via` is `{table, id, ref}` (a parent's
35
+ has-many). A refused request gets `error {id, kind, message}`.
36
+
37
+ ## Writing and actions
38
+
39
+ | Client → server | Server → client |
40
+ |---|---|
41
+ | `flash {table, rid?, patch}` · `batch {items}` · `delete {table, rid}` | `flashed {rid, seq, record}` · `rejected {message, field_errors}` · `batch_result {results}` |
42
+ | `invoke {run, action?, rid?, args}` (no `action`: attach) | `result {run, status: started \| attached \| done}`, then `run {run, seq, status?, state?, messages?, result?, error?}`, the first a snapshot |
43
+ | `run_set {run, name, value}` · `run_cancel {run}` | `ack` · `error {kind, message, code}` |
44
+ | `options {action, field, form?}` | `options_result {items}` |
45
+
46
+ A run's `seq` counts its frames; a gap means one was missed, and an
47
+ `invoke {run}` brings a fresh snapshot. Each message is
48
+ `{name, payload, seq}`, `seq` being the frame it first went out in, so the
49
+ snapshot's messages past the last `seq` you applied are the ones you
50
+ missed. A refused `invoke` carries `code`:
51
+ `conflict` (the id was used with other args), `expired`, `forbidden` (no
52
+ such run for you, or an action the API can't run), `invalid` or `limit`.
53
+
54
+ ## A raw script
55
+
56
+ ```ts
57
+ // test: protocol#raw-script
58
+ const ws = new WebSocket("ws://127.0.0.1:8090/v1/ws");
59
+ ws.onopen = () => {
60
+ ws.send(JSON.stringify({ t: "hello", id: 1, protocol: 1, token: process.env.VANTAGE_TOKEN }));
61
+ ws.send(JSON.stringify({ t: "open_table", id: 2, table: "issues", where: [["status", "eq", "open"]], sort: ["created_at", "desc"], limit: 50 }));
62
+ };
63
+ ws.onmessage = (message) => console.log(JSON.parse(String(message.data)));
64
+ ```
65
+
66
+ A WebSocket from a page on another origin is refused (403): `Origin` must
67
+ name the host the request was sent to. Scripts send no `Origin`.