@vgai/editor-sdk 0.5.5 → 0.5.6
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/package.json +2 -2
- package/src/client.ts +67 -7
- package/src/types.ts +85 -5
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/editor-sdk",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.6",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@types/three": "^0.180.0",
|
|
25
|
-
"@vgai/sdk": "0.5.
|
|
25
|
+
"@vgai/sdk": "0.5.6"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": "^19.0.0",
|
package/src/client.ts
CHANGED
|
@@ -51,13 +51,51 @@ const DEFAULT_URL = 'http://127.0.0.1:20173';
|
|
|
51
51
|
*/
|
|
52
52
|
export class EditorCommandError extends Error {
|
|
53
53
|
readonly code: string | undefined;
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* True when the RELAY ended the command itself rather than the editor
|
|
56
|
+
* answering it — the HTTP 504 that `server/server-utils.ts`'s
|
|
57
|
+
* `commandResponseFor` gives any `timedOut` result, or this client's own
|
|
58
|
+
* deadline below.
|
|
59
|
+
*
|
|
60
|
+
* Read it as "no answer", not as "the budget expired". `editor-server.ts`
|
|
61
|
+
* raises `timedOut` for five conditions and only one of them takes the full
|
|
62
|
+
* budget: the command's timer expiring, the controlling tab's socket dying,
|
|
63
|
+
* the receipt window closing unanswered, a beating-but-dead tab, and no tab
|
|
64
|
+
* present at all. The last four can fail in milliseconds.
|
|
65
|
+
*
|
|
66
|
+
* A caller that converges by retrying (`vgai restart`) needs the distinction
|
|
67
|
+
* because a refusal the editor ANSWERED may go differently next time, while
|
|
68
|
+
* a command the relay abandoned tells you nothing new on a second identical
|
|
69
|
+
* attempt — and when the abandonment was a 120s budget, re-running it three
|
|
70
|
+
* times is `restart-readiness.ts`'s 361-seconds-of-silence defect.
|
|
71
|
+
*/
|
|
72
|
+
readonly timedOut: boolean;
|
|
73
|
+
constructor(message: string, code?: string | undefined, timedOut = false) {
|
|
55
74
|
super(message);
|
|
56
75
|
this.name = 'EditorCommandError';
|
|
57
76
|
this.code = code;
|
|
77
|
+
this.timedOut = timedOut;
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The client's own ceiling on ONE relayed command.
|
|
83
|
+
*
|
|
84
|
+
* A backstop for a LOST server, not a per-command budget: the server already
|
|
85
|
+
* owns per-type budgets (`server/server-utils.ts`'s `relayCommandTimeoutMs`)
|
|
86
|
+
* and its timer must always be the one that fires, because its message names
|
|
87
|
+
* the tab and the remedy while this one can only say "no answer". So this is
|
|
88
|
+
* deliberately ONE number, comfortably above the longest server budget
|
|
89
|
+
* (120s, `play`/`capture-story-variants`) rather than a mirror of that table —
|
|
90
|
+
* a second copy of it would drift silently, and the drift would show up as
|
|
91
|
+
* this timer winning a race it must always lose.
|
|
92
|
+
*
|
|
93
|
+
* Without it a `fetch` with no `AbortSignal` waits on the OS: a dev server
|
|
94
|
+
* that stops answering mid-command holds the CLI open indefinitely, with no
|
|
95
|
+
* output and nothing to read.
|
|
96
|
+
*/
|
|
97
|
+
const COMMAND_DEADLINE_MS = 150_000;
|
|
98
|
+
|
|
61
99
|
/**
|
|
62
100
|
* The GAME DEBUG PLANE, as a contribution's client sees it.
|
|
63
101
|
*
|
|
@@ -131,14 +169,36 @@ export class EditorClient {
|
|
|
131
169
|
private async command<T extends object = Record<string, never>>(
|
|
132
170
|
body: Record<string, unknown>,
|
|
133
171
|
): Promise<T> {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
let res: Response;
|
|
173
|
+
try {
|
|
174
|
+
res = await fetch(`${this.baseUrl}/__editor/command`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify(body),
|
|
178
|
+
signal: AbortSignal.timeout(COMMAND_DEADLINE_MS),
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Only the deadline is reshaped; a connection refused / DNS failure
|
|
182
|
+
// still surfaces as itself, because those name their own cause.
|
|
183
|
+
if ((error as { name?: string } | null)?.name !== 'TimeoutError') throw error;
|
|
184
|
+
throw new EditorCommandError(
|
|
185
|
+
`The editor at ${this.baseUrl} never answered "${String(body['type'] ?? 'command')}" ` +
|
|
186
|
+
`within ${Math.round(COMMAND_DEADLINE_MS / 1000)}s — past every server-side budget, so ` +
|
|
187
|
+
'the server itself is not answering. Check the terminal running `vgai edit`.',
|
|
188
|
+
undefined,
|
|
189
|
+
true,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
139
192
|
const data = (await res.json()) as { ok: boolean; error?: string; code?: string } & T;
|
|
140
193
|
if (!data.ok) {
|
|
141
|
-
throw new EditorCommandError(
|
|
194
|
+
throw new EditorCommandError(
|
|
195
|
+
data.error ?? `Editor command failed: ${res.status}`,
|
|
196
|
+
data.code,
|
|
197
|
+
// 504 is every `timedOut` result (`commandResponseFor`) — the relay
|
|
198
|
+
// gave up, on any of its five grounds. A 200 body with `ok: false` is
|
|
199
|
+
// an ANSWER from the editor, however unwelcome. See `timedOut` above.
|
|
200
|
+
res.status === 504,
|
|
201
|
+
);
|
|
142
202
|
}
|
|
143
203
|
return data;
|
|
144
204
|
}
|
package/src/types.ts
CHANGED
|
@@ -356,15 +356,77 @@ export interface EditorState {
|
|
|
356
356
|
/** Flattened live authoring hierarchy, useful for agent entity discovery. */
|
|
357
357
|
entities?: EditorEntitySummary[];
|
|
358
358
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
359
|
+
* How many browser tabs are PRESENT for this session right now, read from
|
|
360
|
+
* the server's tab table (`server/tab-presence.ts`) rather than from a
|
|
361
|
+
* socket count — so a tab mid-reload still counts (it is beating), and a
|
|
362
|
+
* socket with no tab behind it does not.
|
|
363
|
+
*
|
|
364
|
+
* The rest of this object is the last snapshot a browser POSTed and
|
|
365
|
+
* persists even after every tab goes — so a `0` here means the other fields
|
|
366
|
+
* are stale cache and commands (`play`, `scene`, …) will refuse with the
|
|
367
|
+
* table's own reason. Added by the server on every `/__editor/state` read.
|
|
364
368
|
*/
|
|
365
369
|
editorsConnected?: number;
|
|
366
370
|
/** Convenience: `editorsConnected > 0`. */
|
|
367
371
|
connected?: boolean;
|
|
372
|
+
/**
|
|
373
|
+
* ONE ROW PER PRESENT TAB — the whole table, because the owner's rule for
|
|
374
|
+
* this seam is "if they DO get disconnected, make it clear that it
|
|
375
|
+
* happened" and a single boolean can never say that.
|
|
376
|
+
*
|
|
377
|
+
* `lastBeatAgo` is the heartbeat age (null for a tab that cannot beat, e.g.
|
|
378
|
+
* one bridged through the share tunnel); a number climbing past a second or
|
|
379
|
+
* two is a gap in progress. `epochCount` counts page-loads, so a number
|
|
380
|
+
* that keeps rising is a reload loop. `channel: 'down'` with a fresh beat
|
|
381
|
+
* is a tab mid-reload — present, and briefly unable to take a command.
|
|
382
|
+
* `unresponsive` is the zombie: beating, but its page has never opened a
|
|
383
|
+
* command channel this page-load. Absent against an older server.
|
|
384
|
+
*
|
|
385
|
+
* `commandListener` is the standing verdict on the DOCUMENT, and it is a
|
|
386
|
+
* different question from all of the above: the control channel is opened by
|
|
387
|
+
* the tiny pre-React entry before any module loads, so a page that dies
|
|
388
|
+
* during boot beats, holds a channel, gets blessed, and executes nothing. It
|
|
389
|
+
* reads `'not attached'` for that page, `'silent since <t>'` for one whose
|
|
390
|
+
* listener stopped acknowledging relayed commands, and `'ready'` otherwise —
|
|
391
|
+
* each from a timestamp the server already stamps (the listener's own
|
|
392
|
+
* attach/detach report, and the command receipts). Absent against an older
|
|
393
|
+
* server, and absent for a tab the server cannot measure.
|
|
394
|
+
*
|
|
395
|
+
* `census` is the tab's RESOURCE PROFILE, sampled by the page every five
|
|
396
|
+
* seconds and carried on the heartbeat: what a browser-level renderer death
|
|
397
|
+
* would otherwise leave unexplained. `heapUsedMB`/`heapLimitMB` are null off
|
|
398
|
+
* Chromium (`performance.memory` is non-standard); the renderer counts are
|
|
399
|
+
* absent, never zero, when no game has registered a render-debug adapter.
|
|
400
|
+
* `censusAgeMs` says how stale the profile is — a hidden tab is not sampled.
|
|
401
|
+
*/
|
|
402
|
+
tabs?: Array<{
|
|
403
|
+
tabId8: string;
|
|
404
|
+
presentFor: number;
|
|
405
|
+
lastBeatAgo: number | null;
|
|
406
|
+
epochCount: number;
|
|
407
|
+
visibility: 'visible' | 'hidden';
|
|
408
|
+
route: 'project' | 'no-project' | 'unknown';
|
|
409
|
+
blessed: boolean;
|
|
410
|
+
channel: 'open' | 'down';
|
|
411
|
+
unresponsive: boolean;
|
|
412
|
+
commandListener?: 'ready' | 'not attached' | (string & {});
|
|
413
|
+
census?: {
|
|
414
|
+
heapUsedMB: number | null;
|
|
415
|
+
heapLimitMB: number | null;
|
|
416
|
+
canvases: number;
|
|
417
|
+
canvasMB: number;
|
|
418
|
+
textures?: number;
|
|
419
|
+
geometries?: number;
|
|
420
|
+
programs?: number;
|
|
421
|
+
} | null;
|
|
422
|
+
censusAgeMs?: number | null;
|
|
423
|
+
}>;
|
|
424
|
+
/**
|
|
425
|
+
* The auto-open runaway guard. `stopped` means this session opened
|
|
426
|
+
* `attempts` tabs, none of them ever appeared in the table, and it has
|
|
427
|
+
* stopped trying — the browser, not the editor, is what to check.
|
|
428
|
+
*/
|
|
429
|
+
tabAutoOpen?: { attempts: number; stopped: boolean };
|
|
368
430
|
/**
|
|
369
431
|
* Epoch ms when the server last received a state POST from a browser tab —
|
|
370
432
|
* i.e. the age of the cached snapshot above. Omitted if no tab has ever
|
|
@@ -412,6 +474,24 @@ export interface EditorState {
|
|
|
412
474
|
* neighbors above.
|
|
413
475
|
*/
|
|
414
476
|
sourceValidation?: 'active' | 'awaiting-src' | 'no-project';
|
|
477
|
+
/**
|
|
478
|
+
* The compatibility verdict between this editor and the project it serves —
|
|
479
|
+
* `null` when they agree, otherwise the refusal the browser renders when it
|
|
480
|
+
* declines to activate the project, with its recovery guidance.
|
|
481
|
+
*
|
|
482
|
+
* Server-computed per read like its neighbors above. It is here because the
|
|
483
|
+
* gate was previously reported ONLY in the browser: an editor started on an
|
|
484
|
+
* incompatible project serves happily (it activates nothing), so the tab
|
|
485
|
+
* showed "This project is pinned to @vgai/engine X, but this editor is
|
|
486
|
+
* running Y" while `vgai status` reported a connected session with empty
|
|
487
|
+
* validation and `vgai play` timed out into a retry message about the tab
|
|
488
|
+
* reloading. An agent drives this editor through the CLI, so a gate visible
|
|
489
|
+
* only in pixels is invisible by construction.
|
|
490
|
+
*/
|
|
491
|
+
projectCompatibility?: {
|
|
492
|
+
error: string;
|
|
493
|
+
recovery?: { kind: string; title: string; guidance: string; command?: string };
|
|
494
|
+
} | null;
|
|
415
495
|
/**
|
|
416
496
|
* #124: the absolute path of the project this editor server currently has
|
|
417
497
|
* open — server-computed (never part of the browser-POSTed snapshot,
|