@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.
- package/CHANGELOG.md +50 -0
- package/README.md +131 -2
- package/bin/vantage-client.js +5 -0
- package/dist/client-connection.d.ts +41 -0
- package/dist/client-connection.js +260 -0
- package/dist/client-core.d.ts +70 -0
- package/dist/client-core.js +140 -0
- package/dist/client-loading.d.ts +13 -0
- package/dist/client-loading.js +229 -0
- package/dist/client-runs.d.ts +18 -0
- package/dist/client-runs.js +88 -0
- package/dist/client-writes.d.ts +14 -0
- package/dist/client-writes.js +114 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +61 -0
- package/dist/codegen/cli.d.ts +8 -0
- package/dist/codegen/cli.js +54 -0
- package/dist/codegen/emit.d.ts +3 -0
- package/dist/codegen/emit.js +190 -0
- package/dist/codegen/fetch.d.ts +4 -0
- package/dist/codegen/fetch.js +41 -0
- package/dist/codegen/meta.d.ts +13 -0
- package/dist/codegen/meta.js +59 -0
- package/dist/codegen/runs.d.ts +20 -0
- package/dist/codegen/runs.js +58 -0
- package/dist/codegen/schema.d.ts +97 -0
- package/dist/codegen/schema.js +23 -0
- package/dist/collection-cell.d.ts +69 -0
- package/dist/collection-cell.js +153 -0
- package/dist/collection.d.ts +32 -0
- package/dist/collection.js +122 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +14 -0
- package/dist/mock-query.d.ts +9 -0
- package/dist/mock-query.js +75 -0
- package/dist/mock-runs.d.ts +61 -0
- package/dist/mock-runs.js +165 -0
- package/dist/mock-store.d.ts +78 -0
- package/dist/mock-store.js +158 -0
- package/dist/mock-transport.d.ts +16 -0
- package/dist/mock-transport.js +38 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +169 -0
- package/dist/persist.d.ts +47 -0
- package/dist/persist.js +108 -0
- package/dist/protocol.d.ts +236 -0
- package/dist/protocol.js +5 -0
- package/dist/react.d.ts +59 -0
- package/dist/react.js +169 -0
- package/dist/reserved.d.ts +6 -0
- package/dist/reserved.js +13 -0
- package/dist/run/frames.d.ts +6 -0
- package/dist/run/frames.js +24 -0
- package/dist/run/hooks.d.ts +18 -0
- package/dist/run/hooks.js +87 -0
- package/dist/run/messages.d.ts +20 -0
- package/dist/run/messages.js +42 -0
- package/dist/run/outcome.d.ts +12 -0
- package/dist/run/outcome.js +27 -0
- package/dist/run/run.d.ts +82 -0
- package/dist/run/run.js +246 -0
- package/dist/run/storage.d.ts +3 -0
- package/dist/run/storage.js +20 -0
- package/dist/run/types.d.ts +92 -0
- package/dist/run/types.js +3 -0
- package/dist/run/variables.d.ts +26 -0
- package/dist/run/variables.js +70 -0
- package/dist/schema-meta.d.ts +91 -0
- package/dist/schema-meta.js +3 -0
- package/dist/signal.d.ts +45 -0
- package/dist/signal.js +163 -0
- package/dist/store.d.ts +45 -0
- package/dist/store.js +144 -0
- package/dist/table.d.ts +62 -0
- package/dist/table.js +82 -0
- package/dist/testing.d.ts +61 -0
- package/dist/testing.js +195 -0
- package/dist/transport.d.ts +28 -0
- package/dist/transport.js +32 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.js +2 -0
- package/dist/util.d.ts +18 -0
- package/dist/util.js +46 -0
- package/package.json +56 -4
- package/skills/vantage-client/SKILL.md +390 -0
- package/skills/vantage-client/references/protocol.md +67 -0
- package/skills/vantage-client/references/react.md +242 -0
- package/skills/vantage-client/references/testing.md +173 -0
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantage-ui/client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"description": "A vantage-ui project as a typed, live object graph: lazy references and collections, optimistic edits, server-side actions, React bindings.",
|
|
5
|
+
"keywords": ["vantage", "vantage-ui", "websocket", "realtime", "react", "codegen"],
|
|
6
|
+
"homepage": "https://vantage-ui.com",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./react": {
|
|
16
|
+
"types": "./dist/react.d.ts",
|
|
17
|
+
"import": "./dist/react.js"
|
|
18
|
+
},
|
|
19
|
+
"./testing": {
|
|
20
|
+
"types": "./dist/testing.d.ts",
|
|
21
|
+
"import": "./dist/testing.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"vantage-client": "bin/vantage-client.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"bin",
|
|
30
|
+
"skills",
|
|
31
|
+
"CHANGELOG.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.build.json",
|
|
35
|
+
"prepack": "npm run build",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"react": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@testing-library/react": "^16.0.0",
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"@types/react": "^19.0.0",
|
|
51
|
+
"fake-indexeddb": "^6.0.0",
|
|
52
|
+
"happy-dom": "^15.0.0",
|
|
53
|
+
"react": "^19.0.0",
|
|
54
|
+
"react-dom": "^19.0.0",
|
|
55
|
+
"typescript": "^5.6.0",
|
|
56
|
+
"vitest": "^3.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -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`.
|