@vantage-ui/client 0.5.1 → 0.6.0
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 +11 -0
- package/dist/client-connection.d.ts +12 -0
- package/dist/client-connection.js +66 -2
- package/dist/client-core.d.ts +6 -0
- package/dist/client-core.js +6 -0
- package/dist/client.d.ts +7 -0
- package/dist/codegen/cli.js +16 -4
- package/dist/mock-store.d.ts +7 -0
- package/dist/protocol.d.ts +9 -3
- package/dist/testing.d.ts +11 -0
- package/dist/testing.js +39 -0
- package/package.json +1 -1
- package/skills/vantage-client/SKILL.md +11 -0
- package/skills/vantage-client/references/protocol.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
- Anonymous sessions (vantage-server 1.9.0, `session: { anonymous: true }`): `db.sessionToken` holds `welcome.session_token`, and `hello` resends it whenever `token` gives none, so a reconnect resumes the session and its grants. `createVantage({ session: "localStorage" })` keeps it across reloads and tabs (default `"memory"`). A 1008 `signed out` for an anonymous session drops its token and connects again for a new one, instead of halting.
|
|
6
|
+
- An invoke past its action's `rate_limit` (vantage-server 1.9.0) is an `error` frame with `kind: "rate_limited"`, `code: "rate_limited"` and `retry_after` (whole seconds); the types carry both.
|
|
7
|
+
- A `hello` refused as `rate_limited` (too many new sessions or failed tokens from one address) is not final: the client closes, stays `closed` with the message, and connects again after `retry_after` seconds. `MockServer.limitHellos(times, retryAfter)` answers hellos so.
|
|
8
|
+
- MockServer fixture `anonymousSessions`: tokenless or unknown `hello`s get a new session (`anon-1`, …); issued tokens resume.
|
|
9
|
+
|
|
10
|
+
## 0.5.2 — 2026-10-25
|
|
11
|
+
|
|
12
|
+
- `vantage-client generate` leaves `--out` untouched when the content would be the same, and logs `unchanged <out> (schema <hash>)`.
|
|
13
|
+
|
|
3
14
|
## 0.5.1 — 2026-10-25
|
|
4
15
|
|
|
5
16
|
- MockServer traverses `via` over a numeric foreign key (it compared the number with the wire's string id and found nothing).
|
|
@@ -35,6 +35,12 @@ export declare class ClientConnection extends ClientRuns {
|
|
|
35
35
|
private settleReconnect;
|
|
36
36
|
protected start(): Promise<void>;
|
|
37
37
|
private connect;
|
|
38
|
+
/** Where `session: "localStorage"` keeps the anonymous session's token. */
|
|
39
|
+
private get sessionKey();
|
|
40
|
+
/** The anonymous session to resume: this client's, else the one localStorage kept. */
|
|
41
|
+
private savedSession;
|
|
42
|
+
/** Hold `token` as the anonymous session's (`null`: there is none), in localStorage too when asked. */
|
|
43
|
+
private keepSession;
|
|
38
44
|
private hello;
|
|
39
45
|
/**
|
|
40
46
|
* Reopen every collection that is pinned or shown, and send the record
|
|
@@ -52,6 +58,12 @@ export declare class ClientConnection extends ClientRuns {
|
|
|
52
58
|
* with `error`, writes queued offline included.
|
|
53
59
|
*/
|
|
54
60
|
private halt;
|
|
61
|
+
/**
|
|
62
|
+
* The server refused a `hello` for now (`rate_limited`: too many new
|
|
63
|
+
* sessions or failed tokens from this address): not final. Close, stay
|
|
64
|
+
* `closed` with its message, and connect again once `seconds` have passed.
|
|
65
|
+
*/
|
|
66
|
+
private retryAfter;
|
|
55
67
|
private lost;
|
|
56
68
|
private receive;
|
|
57
69
|
private applyChange;
|
|
@@ -114,6 +114,39 @@ export class ClientConnection extends ClientRuns {
|
|
|
114
114
|
close: (code, reason) => current() && this.lost(code, reason),
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
+
/** Where `session: "localStorage"` keeps the anonymous session's token. */
|
|
118
|
+
get sessionKey() {
|
|
119
|
+
return `vantage.session:${this.opts.url ?? "page"}`;
|
|
120
|
+
}
|
|
121
|
+
/** The anonymous session to resume: this client's, else the one localStorage kept. */
|
|
122
|
+
savedSession() {
|
|
123
|
+
const held = this.sessionToken.peek();
|
|
124
|
+
if (held !== null)
|
|
125
|
+
return held;
|
|
126
|
+
if (this.opts.session !== "localStorage")
|
|
127
|
+
return undefined;
|
|
128
|
+
try {
|
|
129
|
+
return globalThis.localStorage?.getItem(this.sessionKey) ?? undefined;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Hold `token` as the anonymous session's (`null`: there is none), in localStorage too when asked. */
|
|
136
|
+
keepSession(token) {
|
|
137
|
+
this.sessionToken.set(token);
|
|
138
|
+
if (this.opts.session !== "localStorage")
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
if (token === null)
|
|
142
|
+
globalThis.localStorage?.removeItem(this.sessionKey);
|
|
143
|
+
else
|
|
144
|
+
globalThis.localStorage?.setItem(this.sessionKey, token);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Storage refused (private mode, quota): the session lasts as long as the page.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
117
150
|
async hello() {
|
|
118
151
|
let token;
|
|
119
152
|
try {
|
|
@@ -122,11 +155,16 @@ export class ClientConnection extends ClientRuns {
|
|
|
122
155
|
catch (e) {
|
|
123
156
|
return this.halt("refused", e instanceof Error ? e.message : String(e));
|
|
124
157
|
}
|
|
158
|
+
// No token of the app's own: resume the anonymous session, if there is one.
|
|
159
|
+
const saved = token === undefined ? this.savedSession() : undefined;
|
|
160
|
+
token ??= saved;
|
|
125
161
|
const welcome = await this.request({ t: "hello", protocol: PROTOCOL, token, cursor: this.cursor, schema_hash: this.schema.hash, features: FEATURES }, true);
|
|
126
162
|
if (welcome.t === "error" && welcome.kind === "offline")
|
|
127
163
|
return; // lost mid-hello: the reconnect says hello again
|
|
128
164
|
if (welcome.t === "error" && welcome.code === "protocol")
|
|
129
165
|
return this.halt("protocol_mismatch", welcome.message);
|
|
166
|
+
if (welcome.t === "error" && welcome.code === "rate_limited")
|
|
167
|
+
return this.retryAfter(welcome.retry_after ?? 1, welcome.message);
|
|
130
168
|
if (welcome.t !== "welcome")
|
|
131
169
|
return this.halt("refused", welcome.t === "error" ? welcome.message : `unexpected reply \`${welcome.t}\``);
|
|
132
170
|
if (welcome.protocol !== undefined && welcome.protocol !== PROTOCOL) {
|
|
@@ -145,6 +183,13 @@ export class ClientConnection extends ClientRuns {
|
|
|
145
183
|
this.dropRestoredEditsOfOthers(id);
|
|
146
184
|
this.principalId = id;
|
|
147
185
|
this.principal.set(welcome.principal);
|
|
186
|
+
// The session sent may have ended: the server then made a new one.
|
|
187
|
+
if (welcome.session_token !== undefined)
|
|
188
|
+
this.keepSession(welcome.session_token);
|
|
189
|
+
else if (saved !== undefined)
|
|
190
|
+
this.keepSession(null);
|
|
191
|
+
else
|
|
192
|
+
this.sessionToken.set(null);
|
|
148
193
|
// Anonymous (a sign-in page before sign-in): the server reads no table
|
|
149
194
|
// for it, so nothing opens, `instant` tables included; they load at the
|
|
150
195
|
// signed-in `welcome` that `reconnect()` brings.
|
|
@@ -241,13 +286,32 @@ export class ClientConnection extends ClientRuns {
|
|
|
241
286
|
this.markReady();
|
|
242
287
|
this.settleReconnect();
|
|
243
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* The server refused a `hello` for now (`rate_limited`: too many new
|
|
291
|
+
* sessions or failed tokens from this address): not final. Close, stay
|
|
292
|
+
* `closed` with its message, and connect again once `seconds` have passed.
|
|
293
|
+
*/
|
|
294
|
+
retryAfter(seconds, error) {
|
|
295
|
+
// The close of this socket is not a loss to back off from.
|
|
296
|
+
this.attempt++;
|
|
297
|
+
this.transport.close();
|
|
298
|
+
this.forgetSocket();
|
|
299
|
+
this.setStatus("closed", error);
|
|
300
|
+
if (this.closed)
|
|
301
|
+
return;
|
|
302
|
+
clearTimeout(this.backoff);
|
|
303
|
+
this.backoff = setTimeout(() => this.connect(), Math.max(1, seconds) * 1000);
|
|
304
|
+
}
|
|
244
305
|
lost(code, reason) {
|
|
245
306
|
this.forgetSocket();
|
|
246
307
|
const halted = this.haltedWith !== undefined;
|
|
247
308
|
// 1008 (policy violation): the reloaded API refuses this client's token,
|
|
248
309
|
// or its session ended (`signed out`). Either way the token is spent;
|
|
249
|
-
// `reconnect()` with another one.
|
|
250
|
-
|
|
310
|
+
// `reconnect()` with another one. An anonymous session that ended needs
|
|
311
|
+
// no sign-in: the next `hello`, without it, gets a new one.
|
|
312
|
+
if (code === 1008 && !halted && this.sessionToken.peek() !== null)
|
|
313
|
+
this.keepSession(null);
|
|
314
|
+
else if (code === 1008 && !halted)
|
|
251
315
|
return this.halt("refused", reason || "the server refused this client");
|
|
252
316
|
if (halted)
|
|
253
317
|
return;
|
package/dist/client-core.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ type Pending = (frame: ServerFrame) => void;
|
|
|
13
13
|
export declare class ClientCore {
|
|
14
14
|
readonly connection: State<Connection>;
|
|
15
15
|
readonly principal: State<string | null>;
|
|
16
|
+
/**
|
|
17
|
+
* The token of this client's anonymous session (`welcome.session_token`,
|
|
18
|
+
* under `session: { anonymous: true }`), `null` without one. The client
|
|
19
|
+
* resends it in `hello` when `token` gives none (see `session`).
|
|
20
|
+
*/
|
|
21
|
+
readonly sessionToken: State<string | null>;
|
|
16
22
|
readonly seq: State<number>;
|
|
17
23
|
/**
|
|
18
24
|
* Resolves with the connection once `instant` tables have loaded for the
|
package/dist/client-core.js
CHANGED
|
@@ -9,6 +9,12 @@ export class ClientCore {
|
|
|
9
9
|
/** @internal */ store = new Store();
|
|
10
10
|
connection = new State({ status: "connecting", halted: false });
|
|
11
11
|
principal = new State(null);
|
|
12
|
+
/**
|
|
13
|
+
* The token of this client's anonymous session (`welcome.session_token`,
|
|
14
|
+
* under `session: { anonymous: true }`), `null` without one. The client
|
|
15
|
+
* resends it in `hello` when `token` gives none (see `session`).
|
|
16
|
+
*/
|
|
17
|
+
sessionToken = new State(null);
|
|
12
18
|
/**
|
|
13
19
|
* @internal Who the last `welcome` said this client is, stably
|
|
14
20
|
* (`welcome.principal_id`, else its name from an older server): what the
|
package/dist/client.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface VantageOptions {
|
|
|
10
10
|
url?: string;
|
|
11
11
|
/** Sent in `hello`: a token, or a function returning one, called on every connect. */
|
|
12
12
|
token?: string | (() => string | undefined | Promise<string | undefined>);
|
|
13
|
+
/**
|
|
14
|
+
* Where the token of an anonymous session (`welcome.session_token`) is
|
|
15
|
+
* kept, to resend in `hello` whenever `token` gives none. Default:
|
|
16
|
+
* `"memory"`, so a reconnect resumes the session. `"localStorage"` also
|
|
17
|
+
* keeps it across reloads and tabs of the page.
|
|
18
|
+
*/
|
|
19
|
+
session?: "memory" | "localStorage";
|
|
13
20
|
/** Offline mirror of `persist: true` tables (see ./persist). */
|
|
14
21
|
storage?: Storage;
|
|
15
22
|
/** Swap the WebSocket (tests use the MockServer). */
|
package/dist/codegen/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// `vantage-client generate --from <url-or-file> --out <file>`.
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { emit } from "./emit.js";
|
|
5
5
|
import { loadSchema } from "./fetch.js";
|
|
@@ -20,8 +20,8 @@ export async function main(args, log = console.log, fail = console.error) {
|
|
|
20
20
|
return 1;
|
|
21
21
|
}
|
|
22
22
|
const schema = await loadSchema(from);
|
|
23
|
-
await write(out, emit(schema, from));
|
|
24
|
-
log(
|
|
23
|
+
const wrote = await write(out, emit(schema, from));
|
|
24
|
+
log(`${wrote ? "wrote" : "unchanged"} ${out} (schema ${schema.hash})`);
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
27
|
catch (e) {
|
|
@@ -41,11 +41,23 @@ function option(args, name) {
|
|
|
41
41
|
throw new GenerateError(`${name} needs a value`);
|
|
42
42
|
return value;
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Write `text` to `path`, creating its directory when missing; false when the
|
|
46
|
+
* file already holds `text`. Leaving it alone keeps its mtime, so a watcher
|
|
47
|
+
* downstream (Vite) doesn't rebuild for the same output.
|
|
48
|
+
*/
|
|
45
49
|
async function write(path, text) {
|
|
50
|
+
try {
|
|
51
|
+
if ((await readFile(path, "utf8")) === text)
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Missing or unreadable: write it.
|
|
56
|
+
}
|
|
46
57
|
try {
|
|
47
58
|
await mkdir(dirname(path), { recursive: true });
|
|
48
59
|
await writeFile(path, text);
|
|
60
|
+
return true;
|
|
49
61
|
}
|
|
50
62
|
catch (e) {
|
|
51
63
|
const code = e.code ?? (e instanceof Error ? e.message : String(e));
|
package/dist/mock-store.d.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface Fixtures {
|
|
|
46
46
|
* with "sign in first".
|
|
47
47
|
*/
|
|
48
48
|
signIn?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Like vantage-server under `session: { anonymous: true }`: a `hello`
|
|
51
|
+
* without a token, or with one this server never issued, gets a new
|
|
52
|
+
* session (`welcome.session_token`, `anon-1`, `anon-2`, …); a `hello`
|
|
53
|
+
* with an issued one resumes it.
|
|
54
|
+
*/
|
|
55
|
+
anonymousSessions?: boolean;
|
|
49
56
|
}
|
|
50
57
|
export declare class MockStore {
|
|
51
58
|
readonly schema: SchemaMeta;
|
package/dist/protocol.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type ChangeEvent = {
|
|
|
45
45
|
k: "invalidated";
|
|
46
46
|
table: string;
|
|
47
47
|
};
|
|
48
|
-
export type ErrorKind = "invalid" | "not_found" | "forbidden" | "unavailable" | "conflict" | "failed" | "offline";
|
|
48
|
+
export type ErrorKind = "invalid" | "not_found" | "forbidden" | "unavailable" | "conflict" | "failed" | "rate_limited" | "offline";
|
|
49
49
|
/**
|
|
50
50
|
* Which access rule refused a request, sent with `code: "forbidden"` on a
|
|
51
51
|
* `rejected`, an `error` and a failed run's `error`: the table's catalog
|
|
@@ -61,7 +61,9 @@ export type ServerFrame =
|
|
|
61
61
|
/**
|
|
62
62
|
* `principal` is the display name; `principal_id` tells people apart (an
|
|
63
63
|
* older server sends none); `anonymous` is a socket not signed in yet,
|
|
64
|
-
* which reads no table.
|
|
64
|
+
* which reads no table. `session_token`, under `session: { anonymous:
|
|
65
|
+
* true }`: the token of the client's anonymous session, which a `hello`
|
|
66
|
+
* resumes it with.
|
|
65
67
|
*/
|
|
66
68
|
{
|
|
67
69
|
t: "welcome";
|
|
@@ -69,6 +71,7 @@ export type ServerFrame =
|
|
|
69
71
|
principal: string;
|
|
70
72
|
principal_id?: string;
|
|
71
73
|
anonymous?: boolean;
|
|
74
|
+
session_token?: string;
|
|
72
75
|
epoch: string;
|
|
73
76
|
seq: number;
|
|
74
77
|
schema_hash: string;
|
|
@@ -145,7 +148,9 @@ export type ServerFrame =
|
|
|
145
148
|
}
|
|
146
149
|
/**
|
|
147
150
|
* `code` names errors a client acts on: `protocol`, `unknown_frame`,
|
|
148
|
-
* `forbidden` (with the {@link AccessRefusal})
|
|
151
|
+
* `forbidden` (with the {@link AccessRefusal}), `rate_limited` (an invoke
|
|
152
|
+
* past its action's `rate_limit`, with `retry_after`, whole seconds) and a
|
|
153
|
+
* run's {@link RunCode}.
|
|
149
154
|
*/
|
|
150
155
|
| ({
|
|
151
156
|
t: "error";
|
|
@@ -153,6 +158,7 @@ export type ServerFrame =
|
|
|
153
158
|
kind: ErrorKind;
|
|
154
159
|
message: string;
|
|
155
160
|
code?: string;
|
|
161
|
+
retry_after?: number;
|
|
156
162
|
} & Partial<AccessRefusal>) | {
|
|
157
163
|
t: "pong";
|
|
158
164
|
id: number;
|
package/dist/testing.d.ts
CHANGED
|
@@ -22,8 +22,13 @@ export declare class MockServer extends MockStore {
|
|
|
22
22
|
private held;
|
|
23
23
|
private refusals;
|
|
24
24
|
private writeRefusal?;
|
|
25
|
+
/** `hello`s still to answer `rate_limited` (`limitHellos`). */
|
|
26
|
+
private limitedHellos;
|
|
27
|
+
private helloRetryAfter;
|
|
25
28
|
/** Sockets whose `hello` had no token, under fixture `signIn`. */
|
|
26
29
|
private anonymous;
|
|
30
|
+
/** Anonymous sessions issued, by token (fixture `anonymousSessions`). */
|
|
31
|
+
private sessions;
|
|
27
32
|
/** A server with one table, its schema made from the rows' keys. */
|
|
28
33
|
static withTable(table: string, rows: Row[], fixtures?: Omit<Fixtures, "tables">): MockServer;
|
|
29
34
|
transport(): MockTransport;
|
|
@@ -33,6 +38,12 @@ export declare class MockServer extends MockStore {
|
|
|
33
38
|
reload(): void;
|
|
34
39
|
/** Answer every `op` request with an error carrying `message`. */
|
|
35
40
|
refuse(op: ClientFrame["t"], message: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Answer the next `times` `hello`s with `rate_limited` and `retry_after`
|
|
43
|
+
* (whole seconds), as vantage-server does once an address has made too
|
|
44
|
+
* many sessions.
|
|
45
|
+
*/
|
|
46
|
+
limitHellos(times: number, retryAfter?: number): void;
|
|
36
47
|
/** Stop refusing `op`. */
|
|
37
48
|
allow(op: ClientFrame["t"]): void;
|
|
38
49
|
/** Refuse every write with `message`, however the client sends it (`flash`, `batch` or `delete`). */
|
package/dist/testing.js
CHANGED
|
@@ -44,8 +44,13 @@ export class MockServer extends MockStore {
|
|
|
44
44
|
held = new Map();
|
|
45
45
|
refusals = new Map();
|
|
46
46
|
writeRefusal;
|
|
47
|
+
/** `hello`s still to answer `rate_limited` (`limitHellos`). */
|
|
48
|
+
limitedHellos = 0;
|
|
49
|
+
helloRetryAfter = 1;
|
|
47
50
|
/** Sockets whose `hello` had no token, under fixture `signIn`. */
|
|
48
51
|
anonymous = new Set();
|
|
52
|
+
/** Anonymous sessions issued, by token (fixture `anonymousSessions`). */
|
|
53
|
+
sessions = new Set();
|
|
49
54
|
/** A server with one table, its schema made from the rows' keys. */
|
|
50
55
|
static withTable(table, rows, fixtures = {}) {
|
|
51
56
|
return new MockServer(tableSchema(table, rows), { ...fixtures, tables: { [table]: rows } });
|
|
@@ -70,6 +75,15 @@ export class MockServer extends MockStore {
|
|
|
70
75
|
refuse(op, message) {
|
|
71
76
|
this.refusals.set(op, message);
|
|
72
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Answer the next `times` `hello`s with `rate_limited` and `retry_after`
|
|
80
|
+
* (whole seconds), as vantage-server does once an address has made too
|
|
81
|
+
* many sessions.
|
|
82
|
+
*/
|
|
83
|
+
limitHellos(times, retryAfter = 1) {
|
|
84
|
+
this.limitedHellos = times;
|
|
85
|
+
this.helloRetryAfter = retryAfter;
|
|
86
|
+
}
|
|
73
87
|
/** Stop refusing `op`. */
|
|
74
88
|
allow(op) {
|
|
75
89
|
this.refusals.delete(op);
|
|
@@ -128,8 +142,33 @@ export class MockServer extends MockStore {
|
|
|
128
142
|
// The close follows the error, as vantage-server's writer sends it.
|
|
129
143
|
return void setTimeout(() => client.drop(1002, "unsupported protocol"), 0);
|
|
130
144
|
}
|
|
145
|
+
if (this.limitedHellos > 0) {
|
|
146
|
+
this.limitedHellos--;
|
|
147
|
+
const wait = this.helloRetryAfter;
|
|
148
|
+
const message = `too many new sessions from this address; try again in ${wait} s`;
|
|
149
|
+
return reply({ t: "error", id, kind: "rate_limited", code: "rate_limited", retry_after: wait, message });
|
|
150
|
+
}
|
|
131
151
|
if (this.fixtures.refuseHello)
|
|
132
152
|
return reply({ t: "error", id, kind: "forbidden", message: this.fixtures.refuseHello });
|
|
153
|
+
if (this.fixtures.anonymousSessions) {
|
|
154
|
+
const resumed = frame.token !== undefined && this.sessions.has(frame.token);
|
|
155
|
+
const token = resumed ? frame.token : `anon-${this.sessions.size + 1}`;
|
|
156
|
+
this.sessions.add(token);
|
|
157
|
+
return reply({
|
|
158
|
+
t: "welcome",
|
|
159
|
+
id,
|
|
160
|
+
principal: `anonymous-${token}`,
|
|
161
|
+
principal_id: `session:${token}`,
|
|
162
|
+
anonymous: false,
|
|
163
|
+
session_token: token,
|
|
164
|
+
epoch: `mock-${this.epoch}`,
|
|
165
|
+
seq: this.seq,
|
|
166
|
+
schema_hash: this.schema.hash,
|
|
167
|
+
resync: true,
|
|
168
|
+
protocol: this.fixtures.protocol ?? PROTOCOL,
|
|
169
|
+
features: (this.fixtures.features ?? FEATURES).filter((f) => !frame.features || frame.features.includes(f)),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
133
172
|
{
|
|
134
173
|
const anonymous = this.fixtures.signIn === true && !frame.token;
|
|
135
174
|
if (anonymous)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantage-ui/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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
5
|
"keywords": ["vantage", "vantage-ui", "websocket", "realtime", "react", "codegen"],
|
|
6
6
|
"homepage": "https://vantage-ui.com",
|
|
@@ -360,6 +360,17 @@ sign-out action, an expired session, or one deleted on the server closes
|
|
|
360
360
|
the socket with 1008 `signed out`, which reads as `refused` with that
|
|
361
361
|
error: clear the stored token and `reconnect()` to continue anonymously.
|
|
362
362
|
|
|
363
|
+
When the project gives every visitor a session (`application.yaml`
|
|
364
|
+
`session: { anonymous: true }`), there is no sign-in: the `welcome` carries
|
|
365
|
+
`session_token`, which `db.sessionToken` holds, and the client resends it
|
|
366
|
+
in `hello` whenever `token` gives none, so a lost socket comes back to the
|
|
367
|
+
same session and whatever its actions granted (an unlocked plan, say).
|
|
368
|
+
`createVantage({ schema, session: "localStorage" })` keeps it across
|
|
369
|
+
reloads and tabs too; the default, `"memory"`, only for this client. A
|
|
370
|
+
session that ended (idle on the server) closes with 1008 `signed out`
|
|
371
|
+
without halting: the client drops the token and the next `hello` gets a
|
|
372
|
+
new session, without the old one's grants.
|
|
373
|
+
|
|
363
374
|
This sign-in form is the only one such a project needs. Its desktop app never
|
|
364
375
|
signs in (vantage-ui's
|
|
365
376
|
`vantage-application-settings/references/sessions-and-audit.md`).
|
|
@@ -9,10 +9,11 @@ scripts and debugging. HTTP: `/v1/schema` (the schema `generate` reads),
|
|
|
9
9
|
|
|
10
10
|
| Client → server | Server → client |
|
|
11
11
|
|---|---|
|
|
12
|
-
| `hello {protocol: 1, token?, cursor?, schema_hash?, features?}` | `welcome {protocol, principal, principal_id, anonymous, epoch, seq, schema_hash, resync, features}`: `principal` is the name to show, `principal_id` the stable id that tells people apart (`identity:<id>`, `anonymous:<socket>`, `token:<name>`), `anonymous` true for a socket not signed in (the client then opens no table, `instant` ones included, until a signed-in `welcome`); the features both sides have (`runs:1`, `session:1`). Under auth `session` a `hello` without a token is welcomed as `anonymous`, which may only `invoke` the actions the project lists in `session.signed_out` (sign in), `run_set`, `run_cancel` and `ping`; anything else gets `error {kind: "forbidden", message: "sign in first"}` |
|
|
12
|
+
| `hello {protocol: 1, token?, cursor?, schema_hash?, features?}` | `welcome {protocol, principal, principal_id, anonymous, session_token?, epoch, seq, schema_hash, resync, features}`: `session_token`, under `session: { anonymous: true }`, is the token of the client's anonymous session (a `hello` with it resumes the session; one that ended gets a new session and token); `principal` is the name to show, `principal_id` the stable id that tells people apart (`identity:<id>`, `anonymous:<socket>`, `token:<name>`), `anonymous` true for a socket not signed in (the client then opens no table, `instant` ones included, until a signed-in `welcome`); the features both sides have (`runs:1`, `session:1`). Under auth `session` a `hello` without a token is welcomed as `anonymous`, which may only `invoke` the actions the project lists in `session.signed_out` (sign in), `run_set`, `run_cancel` and `ping`; anything else gets `error {kind: "forbidden", message: "sign in first"}` |
|
|
13
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
14
|
| | `error {kind, message, code: "protocol"}`, then close 1002: another protocol version |
|
|
15
15
|
| | `error {kind: "forbidden", message}`: the token is refused; the socket stays open for another `hello` |
|
|
16
|
+
| | `error {kind: "rate_limited", code: "rate_limited", retry_after, message}`: too many new sessions or failed tokens from this address; say `hello` again after `retry_after` seconds (`@vantage-ui/client` closes, stays `closed` with the message, and reconnects then) |
|
|
16
17
|
| | `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
18
|
| | `reload`: the served frontend was rebuilt; reload the page (sent before `welcome` too) |
|
|
18
19
|
| | close 1008: after a reload, the API refuses the token; or `signed out`: the session ended (signed out, expired, deleted, its person deactivated) |
|