@bitmagic/cli 0.1.1 → 0.1.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.
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +23 -11
- package/dist/cli.js.map +1 -1
- package/dist/commands/forge.d.ts +107 -0
- package/dist/commands/forge.js +393 -0
- package/dist/commands/forge.js.map +1 -0
- package/dist/commands/generate.js +58 -45
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/verify.js +16 -16
- package/dist/commands/verify.js.map +1 -1
- package/dist/forge/apply-modifications.d.ts +19 -0
- package/dist/forge/apply-modifications.js +184 -0
- package/dist/forge/apply-modifications.js.map +1 -0
- package/dist/forge/bake-progress.d.ts +44 -0
- package/dist/forge/bake-progress.js +77 -0
- package/dist/forge/bake-progress.js.map +1 -0
- package/dist/forge/browser-host.d.ts +100 -0
- package/dist/forge/browser-host.js +351 -0
- package/dist/forge/browser-host.js.map +1 -0
- package/dist/forge/local-store.d.ts +30 -0
- package/dist/forge/local-store.js +130 -0
- package/dist/forge/local-store.js.map +1 -0
- package/dist/forge/run-pipeline.d.ts +105 -0
- package/dist/forge/run-pipeline.js +269 -0
- package/dist/forge/run-pipeline.js.map +1 -0
- package/dist/forge/stream.d.ts +106 -0
- package/dist/forge/stream.js +286 -0
- package/dist/forge/stream.js.map +1 -0
- package/dist/forge/transport.d.ts +58 -0
- package/dist/forge/transport.js +145 -0
- package/dist/forge/transport.js.map +1 -0
- package/dist/forge/upload-proxy.d.ts +21 -0
- package/dist/forge/upload-proxy.js +134 -0
- package/dist/forge/upload-proxy.js.map +1 -0
- package/dist/generate/stream.d.ts +1 -10
- package/dist/generate/stream.js +5 -62
- package/dist/generate/stream.js.map +1 -1
- package/dist/http/sse.d.ts +40 -0
- package/dist/http/sse.js +98 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/local-port.d.ts +30 -0
- package/dist/local-port.js +53 -0
- package/dist/local-port.js.map +1 -0
- package/dist/scaffold/project-files.js +89 -9
- package/dist/scaffold/project-files.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { CliError } from '../errors.js';
|
|
2
|
+
import { consumeSseStream } from '../http/sse.js';
|
|
3
|
+
/**
|
|
4
|
+
* Did the SERVER'S REQUEST DEADLINE fire, rather than a step failing?
|
|
5
|
+
*
|
|
6
|
+
* This has to be answered BEFORE {@link classifyForgeFailure}, because the classifier is
|
|
7
|
+
* structurally unable to get it right: a deadline that fires during the geometry step has
|
|
8
|
+
* `Scene design complete` in its progress, so the classifier correctly reports stage `forge` — and
|
|
9
|
+
* the advice for a genuine forge failure is "that step is deterministic, change the prompt rather
|
|
10
|
+
* than resuming", which is the exact opposite of what a timed-out run needs. The run was cut off
|
|
11
|
+
* mid-flight, is unharmed, and IS resumable; api-server's own deadline message says so, so without
|
|
12
|
+
* this the creator gets two contradictory sentences with the wrong one last.
|
|
13
|
+
*
|
|
14
|
+
* The deadline is not an edge case — `cli-forge.ts` notes the designer alone takes minutes with up
|
|
15
|
+
* to four attempts against a 280s budget, so it is expected to fire routinely.
|
|
16
|
+
*
|
|
17
|
+
* Primary signal is the structural `timedOut` flag on the frame. The message fallback exists
|
|
18
|
+
* because the CLI and api-server ship independently: a CLI carrying this fix can talk to a server
|
|
19
|
+
* deployed before the flag existed, and that is precisely the window in which getting this wrong
|
|
20
|
+
* costs a creator a second design. It matches on the server's own distinctive phrase and is
|
|
21
|
+
* deliberately narrow — a false positive would suppress correct "change the prompt" advice, which
|
|
22
|
+
* is the cheaper of the two errors.
|
|
23
|
+
*/
|
|
24
|
+
const DEADLINE_MESSAGE_MARKER = 's server limit';
|
|
25
|
+
export function isDeadlineFailure(failure) {
|
|
26
|
+
return failure.timedOut || failure.message.includes(DEADLINE_MESSAGE_MARKER);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What to tell a creator whose run hit the server deadline: resume, unconditionally. No stage
|
|
30
|
+
* advice, because no stage failed.
|
|
31
|
+
*/
|
|
32
|
+
export function forgeDeadlineAdvice(jobId) {
|
|
33
|
+
const unchanged = 'world.json is unchanged.';
|
|
34
|
+
if (jobId === null) {
|
|
35
|
+
return `${unchanged} No job id was reported, so there is nothing to resume — run the command again.`;
|
|
36
|
+
}
|
|
37
|
+
return `${unchanged} Nothing failed — the server simply ran out of request time, and the work so `
|
|
38
|
+
+ `far is saved. Resume it with \`bitmagic forge --resume ${jobId}\`; the steps already `
|
|
39
|
+
+ 'completed are skipped and are not billed again. Do NOT start a fresh forge.';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* What to tell a creator whose stream ended with NO terminal frame at all.
|
|
43
|
+
*
|
|
44
|
+
* This is the deadline's twin and needs the same answer — the run was cut off mid-flight, api-server
|
|
45
|
+
* deliberately does not cancel it (`cli-forge.ts`'s run promise is left to finish), and whatever it
|
|
46
|
+
* completes is written to the job's store — except that here nothing said so, because no frame
|
|
47
|
+
* arrived to say it. Without a job id the creator's only move is a fresh `--prompt` run, which mints
|
|
48
|
+
* a NEW job id and re-runs (and re-bills) a design the abandoned job may already have finished and
|
|
49
|
+
* saved. That is why {@link requestForge} tracks the job id off EVERY frame rather than only the
|
|
50
|
+
* terminal one: api-server puts it on every progress frame precisely so this case has an answer.
|
|
51
|
+
*/
|
|
52
|
+
export function forgeTruncationMessage(jobId) {
|
|
53
|
+
const opening = 'The connection to api-server ended before the forge finished. This is NOT a '
|
|
54
|
+
+ 'confirmed failure: the server may already have designed the level (and billed for it) before '
|
|
55
|
+
+ 'the stream broke. world.json is unchanged.';
|
|
56
|
+
if (jobId === null) {
|
|
57
|
+
return `${opening} No job id arrived before the stream broke, so there is nothing to resume — `
|
|
58
|
+
+ 'run `bitmagic forge` again with the same prompt.';
|
|
59
|
+
}
|
|
60
|
+
return `${opening} The run was reported as job "${jobId}", and the server keeps whatever it `
|
|
61
|
+
+ `completed — resume it with \`bitmagic forge --resume ${jobId}\` so the design is not re-run `
|
|
62
|
+
+ 'and not billed a second time. Do NOT start a fresh forge.';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The progress lines `cli-forge.ts` emits around step 1. They bracket the step precisely — one
|
|
66
|
+
* before `steps.design(...)` is awaited, one after it returns — so "did a design-completed line
|
|
67
|
+
* arrive before the error frame?" is an exact answer to "did step 1 succeed in this request?".
|
|
68
|
+
*
|
|
69
|
+
* This is a coupling to another service's log strings, and it is deliberate but bounded: there is
|
|
70
|
+
* no step key on the wire, and `classifyForgeFailure` degrades to `unknown` (the both-cases
|
|
71
|
+
* message) the moment any of these stops matching. A drift therefore costs precision, never
|
|
72
|
+
* correctness — which is why the fallback exists rather than a default guess of one stage.
|
|
73
|
+
*/
|
|
74
|
+
const DESIGN_STARTED_MARKER = 'Designing the level scene';
|
|
75
|
+
const DESIGN_FINISHED_MARKERS = ['Scene design complete', 'Design already complete'];
|
|
76
|
+
export function classifyForgeFailure(progress) {
|
|
77
|
+
const designFinished = progress.some((line) => DESIGN_FINISHED_MARKERS.some((marker) => line.includes(marker)));
|
|
78
|
+
if (designFinished)
|
|
79
|
+
return 'forge';
|
|
80
|
+
if (progress.some((line) => line.includes(DESIGN_STARTED_MARKER)))
|
|
81
|
+
return 'design';
|
|
82
|
+
return 'unknown';
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The resume advice for a failure, as a paragraph appended to the server's own message. Every
|
|
86
|
+
* branch says explicitly that world.json is unchanged, because a creator who does not know has to
|
|
87
|
+
* go and look — and a forge costs enough that "did I just pay for nothing, and is my project
|
|
88
|
+
* still intact?" is the first question.
|
|
89
|
+
*/
|
|
90
|
+
export function forgeFailureAdvice(stage, jobId) {
|
|
91
|
+
const unchanged = 'world.json is unchanged.';
|
|
92
|
+
if (jobId === null) {
|
|
93
|
+
return `${unchanged} No job id was reported, so there is nothing to resume — run the command again.`;
|
|
94
|
+
}
|
|
95
|
+
const resume = `\`bitmagic forge --resume ${jobId}\``;
|
|
96
|
+
switch (stage) {
|
|
97
|
+
case 'design':
|
|
98
|
+
return `${unchanged} The scene design did NOT complete, and it is saved only on success — `
|
|
99
|
+
+ `so ${resume} re-runs the design and is billed for it again. It may well succeed on a `
|
|
100
|
+
+ 'second attempt; if it keeps failing, change the prompt.';
|
|
101
|
+
case 'forge':
|
|
102
|
+
return `${unchanged} The scene design IS saved, but building its geometry failed. That step `
|
|
103
|
+
+ `is deterministic: ${resume} feeds the same design to the same code and fails the same `
|
|
104
|
+
+ 'way. Change the prompt and run a fresh forge rather than resuming this one.';
|
|
105
|
+
case 'unknown':
|
|
106
|
+
return `${unchanged} If the DESIGN step failed, it was not saved and ${resume} re-runs (and `
|
|
107
|
+
+ 're-charges) it. If the GEOMETRY step failed, resuming fails identically — that step is '
|
|
108
|
+
+ 'deterministic — so change the prompt instead of retrying.';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The complete message for a failed forge: the server's own text, then the ONE piece of advice
|
|
113
|
+
* that fits. The deadline check comes first and short-circuits the stage classification — see
|
|
114
|
+
* {@link isDeadlineFailure} for why the two must never both speak.
|
|
115
|
+
*
|
|
116
|
+
* The single entry point the command uses, so "which advice wins" is decided in one place and is
|
|
117
|
+
* testable without a network.
|
|
118
|
+
*/
|
|
119
|
+
export function describeForgeFailure(failure) {
|
|
120
|
+
const advice = isDeadlineFailure(failure)
|
|
121
|
+
? forgeDeadlineAdvice(failure.jobId)
|
|
122
|
+
: forgeFailureAdvice(classifyForgeFailure(failure.progress), failure.jobId);
|
|
123
|
+
const glb = failure.glbUrl
|
|
124
|
+
? ` The forged GLB is still available at ${failure.glbUrl} and can be voxelized by hand.`
|
|
125
|
+
: '';
|
|
126
|
+
return `${failure.message} ${advice}${glb}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Excludes arrays, unlike the `typeof x === 'object'` shorthand — matching `browser-host.ts`'s
|
|
130
|
+
* definition. It matters at the one call site that narrows a payload rather than a wrapper:
|
|
131
|
+
* `!isRecord(data.artifact)` is what rejects a malformed terminal frame, and a JSON array is an
|
|
132
|
+
* `object` that is emphatically not a `ForgeArtifact`.
|
|
133
|
+
*/
|
|
134
|
+
function isRecord(value) {
|
|
135
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
function stringField(value) {
|
|
138
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
139
|
+
}
|
|
140
|
+
async function readJsonBody(response) {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = await response.json();
|
|
143
|
+
return isRecord(parsed) ? parsed : {};
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Maps the pre-stream failure shapes `cli-forge.ts` documents into a message an agent can act on.
|
|
151
|
+
* Never includes the access token — only the game id and the server's own text.
|
|
152
|
+
*/
|
|
153
|
+
async function toCliError(response, gameId) {
|
|
154
|
+
const body = await readJsonBody(response);
|
|
155
|
+
const serverMessage = stringField(body.error);
|
|
156
|
+
if (response.status === 403) {
|
|
157
|
+
// Covers both "you do not own this game" and "this job was created for a different game" —
|
|
158
|
+
// the server deliberately does not distinguish them, so neither does this.
|
|
159
|
+
return new CliError(`${serverMessage ?? `You cannot forge a level for game "${gameId}".`} world.json is unchanged.`);
|
|
160
|
+
}
|
|
161
|
+
if (response.status === 402) {
|
|
162
|
+
const { balance, required } = body;
|
|
163
|
+
const detail = typeof balance === 'number' && typeof required === 'number'
|
|
164
|
+
? ` You have ${balance} sparks; starting a forge requires at least ${required}.`
|
|
165
|
+
: '';
|
|
166
|
+
return new CliError(`Not enough sparks to start a forge.${detail} world.json is unchanged.`);
|
|
167
|
+
}
|
|
168
|
+
if (response.status === 400) {
|
|
169
|
+
return new CliError(`${serverMessage ?? 'The server rejected this forge request.'} world.json is unchanged.`);
|
|
170
|
+
}
|
|
171
|
+
if (response.status === 503) {
|
|
172
|
+
return new CliError(`${serverMessage ?? 'Forge storage is not configured on the server.'} `
|
|
173
|
+
+ 'This is a server-side configuration problem, not something this project can fix. '
|
|
174
|
+
+ 'world.json is unchanged.');
|
|
175
|
+
}
|
|
176
|
+
return new CliError(`api-server returned HTTP ${response.status} starting the forge`
|
|
177
|
+
+ (serverMessage ? `: ${serverMessage}.` : '.')
|
|
178
|
+
+ ' world.json is unchanged.');
|
|
179
|
+
}
|
|
180
|
+
/** The terminal `result` frame: `{ success: true, jobId, artifact, platformerMovement? }`. */
|
|
181
|
+
function asForgeSuccess(data) {
|
|
182
|
+
if (!isRecord(data) || data.success !== true)
|
|
183
|
+
return null;
|
|
184
|
+
const jobId = stringField(data.jobId);
|
|
185
|
+
if (jobId === null || !isRecord(data.artifact))
|
|
186
|
+
return null;
|
|
187
|
+
// `artifact` is trusted structurally, not field-by-field: it is a large schema produced by the
|
|
188
|
+
// very package this CLI then hands it back to, and `requireArtifact` plus the step code itself
|
|
189
|
+
// are what validate it in the only way that matters. A shallow re-validation here would give
|
|
190
|
+
// false confidence without catching anything the steps would not.
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
jobId,
|
|
194
|
+
artifact: data.artifact,
|
|
195
|
+
// Absent on every non-platformer forge, and absent from any server deployed before the field
|
|
196
|
+
// existed — so it is optional here rather than a reason to reject the frame.
|
|
197
|
+
...(isRecord(data.platformerMovement) ? { platformerMovement: data.platformerMovement } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* POSTs to `/api/cli/v1/forge/stream` and consumes the SSE response.
|
|
202
|
+
*
|
|
203
|
+
* A non-2xx response fails before any stream opens (see `toCliError`). On 200, each `progress`
|
|
204
|
+
* event is forwarded AND recorded, and the terminal `result`/`error` event resolves this call. A
|
|
205
|
+
* stream that closes with no terminal event is a truncated connection, not a success — and it fails
|
|
206
|
+
* with {@link forgeTruncationMessage}, naming the job id the frames already reported so the
|
|
207
|
+
* expensive half can be resumed rather than paid for twice.
|
|
208
|
+
*/
|
|
209
|
+
export async function requestForge(environment, accessToken, body, deps) {
|
|
210
|
+
const gameId = typeof body.gameId === 'string' ? body.gameId : '(unknown game)';
|
|
211
|
+
const url = `${environment.apiUrl}/api/cli/v1/forge/stream`;
|
|
212
|
+
let response;
|
|
213
|
+
try {
|
|
214
|
+
response = await deps.fetch(url, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: {
|
|
217
|
+
Authorization: `Bearer ${accessToken}`,
|
|
218
|
+
Accept: 'text/event-stream',
|
|
219
|
+
'Content-Type': 'application/json',
|
|
220
|
+
},
|
|
221
|
+
body: JSON.stringify(body),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// A transport failure and an error status send people to very different places.
|
|
226
|
+
throw new CliError(`Cannot reach api-server at ${environment.apiUrl}. world.json is unchanged.`);
|
|
227
|
+
}
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
throw await toCliError(response, gameId);
|
|
230
|
+
}
|
|
231
|
+
if (!response.body) {
|
|
232
|
+
throw new CliError('The server accepted the forge but returned no stream body. world.json is unchanged.');
|
|
233
|
+
}
|
|
234
|
+
const progress = [];
|
|
235
|
+
let outcome = null;
|
|
236
|
+
/**
|
|
237
|
+
* The job id from the LAST frame that carried one, whatever kind of frame that was.
|
|
238
|
+
*
|
|
239
|
+
* Read off every frame in ONE place rather than per-branch, because the paths that most need it
|
|
240
|
+
* are exactly the ones no per-branch read covers: a stream that breaks before any terminal frame,
|
|
241
|
+
* and a terminal frame malformed enough that `asForgeSuccess` rejects it. api-server puts the job
|
|
242
|
+
* id on every progress frame (`{ jobId, message }`) for precisely this reason, so as soon as the
|
|
243
|
+
* first one arrives the work is resumable — and losing it costs the creator a second paid design.
|
|
244
|
+
*/
|
|
245
|
+
let lastJobId = null;
|
|
246
|
+
const onEvent = (event) => {
|
|
247
|
+
if (isRecord(event.data)) {
|
|
248
|
+
lastJobId = stringField(event.data.jobId) ?? lastJobId;
|
|
249
|
+
}
|
|
250
|
+
if (event.event === 'progress') {
|
|
251
|
+
const message = isRecord(event.data) ? stringField(event.data.message) : null;
|
|
252
|
+
if (message !== null) {
|
|
253
|
+
progress.push(message);
|
|
254
|
+
deps.onProgress(message);
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
if (event.event === 'result') {
|
|
259
|
+
const success = asForgeSuccess(event.data);
|
|
260
|
+
if (success) {
|
|
261
|
+
outcome = success;
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
if (event.event === 'error') {
|
|
267
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
268
|
+
outcome = {
|
|
269
|
+
success: false,
|
|
270
|
+
jobId: lastJobId,
|
|
271
|
+
message: stringField(data.message) ?? 'The forge failed without reporting a reason.',
|
|
272
|
+
...(stringField(data.glbUrl) !== null ? { glbUrl: stringField(data.glbUrl) } : {}),
|
|
273
|
+
timedOut: data.timedOut === true,
|
|
274
|
+
progress,
|
|
275
|
+
};
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
};
|
|
280
|
+
const sawTerminal = await consumeSseStream(response.body, onEvent);
|
|
281
|
+
if (!sawTerminal || outcome === null) {
|
|
282
|
+
throw new CliError(forgeTruncationMessage(lastJobId));
|
|
283
|
+
}
|
|
284
|
+
return outcome;
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/forge/stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI lane's `ForgeTransport` — the same seam game-play-agent fills with a WebSocket
|
|
3
|
+
* (`utils/voxel-ws.ts`'s `sendAndWaitForResult`), filled here by posting straight into a page the
|
|
4
|
+
* CLI owns. Semantics are deliberately re-implemented, not redesigned: null on timeout or
|
|
5
|
+
* transport failure, 3 attempts by default, an idle timeout when `progressType` is given, and
|
|
6
|
+
* matching on type AND requestId. Read `voxel-ws.ts` alongside this file — every rule below has a
|
|
7
|
+
* counterpart there, and the two must not drift.
|
|
8
|
+
*
|
|
9
|
+
* Two things differ from the web lane, both because there is no relay in between:
|
|
10
|
+
*
|
|
11
|
+
* - The command envelope. The engine's message handler reads `{ type, data }`, so the flat
|
|
12
|
+
* command the pipeline builds (`{ type, requestId, ...payload }`) is repacked here.
|
|
13
|
+
* - The reply rename. The engine posts `*_SAVED` / `*_PLACED`; the pipeline awaits `*_RESULT`.
|
|
14
|
+
* In the web lane the creator does that rename in the browser. Doing it HERE, in Node, is what
|
|
15
|
+
* keeps the extracted pipeline byte-identical for both lanes.
|
|
16
|
+
*
|
|
17
|
+
* NOTE on the import below: `@bitmagic/world-forger` is a real **dependency** of this package,
|
|
18
|
+
* like `@bitmagic/asset-core`. It is the PUBLISHABLE half of the forger — the artifact schema, the
|
|
19
|
+
* host seams and the three browser-driven pipeline steps — and `publish-npm.yml` publishes it
|
|
20
|
+
* ahead of the CLI so npm can resolve it at install time. The private design/geometry half lives
|
|
21
|
+
* in `@bitmagic/world-forger-internal`, which is never published and must never be imported from
|
|
22
|
+
* here, nor bundled into `dist/`: bundled JS is readable, and minification is not protection.
|
|
23
|
+
*/
|
|
24
|
+
import type { ForgeTransport } from '@bitmagic/world-forger/pipeline/transport-types.js';
|
|
25
|
+
/** The `{ type, data }` envelope `CreatorMessageHandler` dispatches on. */
|
|
26
|
+
export interface PageCommand {
|
|
27
|
+
type: string;
|
|
28
|
+
data: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The narrow slice of "a loaded game page" the transport needs. `browser-host.ts` implements it
|
|
32
|
+
* over Playwright; tests implement it with an object, which is why every rule in this file is
|
|
33
|
+
* unit-testable without a browser.
|
|
34
|
+
*/
|
|
35
|
+
export interface ForgePageBridge {
|
|
36
|
+
/** Post a command into the page. Rejecting means the page is gone — a transport failure. */
|
|
37
|
+
postCommand(command: PageCommand): Promise<void>;
|
|
38
|
+
/** Subscribe to every message the page posted. Returns an unsubscribe. */
|
|
39
|
+
onMessage(listener: (message: Record<string, unknown>) => void): () => void;
|
|
40
|
+
}
|
|
41
|
+
export interface ForgePageTransportOptions {
|
|
42
|
+
log?: (message: string) => void;
|
|
43
|
+
/** Gap between attempts. Overridden in tests; 1s in production, matching the web lane. */
|
|
44
|
+
retryDelayMs?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reply-name → awaited-name. Authoritative list carried over from the creator's own relay
|
|
48
|
+
* (`game-play-agent/src/lab/browser.ts:112-123`). The last two entries are identity: the level
|
|
49
|
+
* bake already posts its result and its progress under the names the pipeline awaits, and they are
|
|
50
|
+
* listed rather than omitted so this map reads as the complete forge vocabulary.
|
|
51
|
+
*/
|
|
52
|
+
export declare const RESULT_TYPE_MAP: Record<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* Build the transport over a page bridge. The returned object is the shared package's
|
|
55
|
+
* `ForgeTransport` and nothing more — `commands/forge.ts` hands it straight to
|
|
56
|
+
* `ForgeDeps.transport` via `buildCliForgeDeps`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createForgePageTransport(bridge: ForgePageBridge, options?: ForgePageTransportOptions): ForgeTransport;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reply-name → awaited-name. Authoritative list carried over from the creator's own relay
|
|
3
|
+
* (`game-play-agent/src/lab/browser.ts:112-123`). The last two entries are identity: the level
|
|
4
|
+
* bake already posts its result and its progress under the names the pipeline awaits, and they are
|
|
5
|
+
* listed rather than omitted so this map reads as the complete forge vocabulary.
|
|
6
|
+
*/
|
|
7
|
+
export const RESULT_TYPE_MAP = {
|
|
8
|
+
VOXEL_OBJECT_PLACED: 'VOXEL_PLACE_RESULT',
|
|
9
|
+
BATCH_VOXEL_OBJECTS_PLACED: 'BATCH_VOXEL_PLACE_RESULT',
|
|
10
|
+
VOXEL_OBJECT_MODIFIED: 'VOXEL_MODIFY_RESULT',
|
|
11
|
+
VOXEL_OBJECT_DELETED: 'VOXEL_DELETE_RESULT',
|
|
12
|
+
VOXEL_ASSET_SAVED: 'VOXEL_ASSET_RESULT',
|
|
13
|
+
ASSET_FROM_GLB_URL_SAVED: 'ASSET_FROM_GLB_URL_RESULT',
|
|
14
|
+
VOXELIZE_GLB_AS_LEVEL_RESULT: 'VOXELIZE_GLB_AS_LEVEL_RESULT',
|
|
15
|
+
VOXELIZE_GLB_AS_LEVEL_PROGRESS: 'VOXELIZE_GLB_AS_LEVEL_PROGRESS',
|
|
16
|
+
};
|
|
17
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
18
|
+
const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether a page message answers a caller awaiting `expected`.
|
|
24
|
+
*
|
|
25
|
+
* BOTH the message's own type and its renamed form count, because the pipeline is not consistent
|
|
26
|
+
* about which vocabulary it awaits — and it cannot be made consistent from this side, since the
|
|
27
|
+
* same code runs behind the web lane's relay:
|
|
28
|
+
*
|
|
29
|
+
* - `create-level-archetypes.ts` awaits `ASSET_FROM_GLB_URL_RESULT` — the RENAMED name.
|
|
30
|
+
* - `voxelize-level.ts` awaits `VOXELIZE_GLB_AS_LEVEL_RESULT` — renamed, but identity.
|
|
31
|
+
* - `place-and-persist-level.ts` awaits `BATCH_VOXEL_OBJECTS_PLACED` — the ENGINE's OWN name,
|
|
32
|
+
* which the map renames to `BATCH_VOXEL_PLACE_RESULT`.
|
|
33
|
+
*
|
|
34
|
+
* Renaming alone therefore answers two of the three and silently starves the third: the engine
|
|
35
|
+
* posts `BATCH_VOXEL_OBJECTS_PLACED`, this returns `BATCH_VOXEL_PLACE_RESULT`, nothing matches,
|
|
36
|
+
* and placement waits out its full idle timeout three times over. That is not hypothetical — it
|
|
37
|
+
* is what the first end-to-end forge did, after successfully baking every chunk.
|
|
38
|
+
*
|
|
39
|
+
* Accepting either name is safe here because no engine reply name is another's renamed form; the
|
|
40
|
+
* map's values are all distinct from its keys except the two deliberate identities.
|
|
41
|
+
*/
|
|
42
|
+
function matchesAwaited(message, expected) {
|
|
43
|
+
const raw = message.type;
|
|
44
|
+
if (typeof raw !== 'string')
|
|
45
|
+
return false;
|
|
46
|
+
// Unmapped types compare on their own name rather than being dropped, so a reply the engine
|
|
47
|
+
// gains later reaches a caller awaiting it instead of vanishing into a timeout.
|
|
48
|
+
return raw === expected || RESULT_TYPE_MAP[raw] === expected;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One send + wait. Resolves the matching message, or null on timeout / a failed post — never
|
|
52
|
+
* rejects, because callers distinguish "no answer" from "answered with an error".
|
|
53
|
+
*/
|
|
54
|
+
function singleAttempt(bridge, command, requestId, resultType, timeoutMs, progressType, log) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
let settled = false;
|
|
57
|
+
let timer;
|
|
58
|
+
let unsubscribe;
|
|
59
|
+
const finish = (result) => {
|
|
60
|
+
if (settled)
|
|
61
|
+
return;
|
|
62
|
+
settled = true;
|
|
63
|
+
if (timer)
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
unsubscribe?.();
|
|
66
|
+
resolve(result);
|
|
67
|
+
};
|
|
68
|
+
// (Re)arm the idle timer. Called once up front and again on every progress event, so the
|
|
69
|
+
// deadline is "no progress (or result) for timeoutMs", not a flat cap on the whole job.
|
|
70
|
+
const armTimer = () => {
|
|
71
|
+
if (settled)
|
|
72
|
+
return;
|
|
73
|
+
if (timer)
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
timer = setTimeout(() => {
|
|
76
|
+
log(`forge transport: no ${resultType} within ${timeoutMs}ms of silence`);
|
|
77
|
+
finish(null);
|
|
78
|
+
}, timeoutMs);
|
|
79
|
+
};
|
|
80
|
+
unsubscribe = bridge.onMessage((message) => {
|
|
81
|
+
// Progress for THIS request keeps the attempt alive and resets the idle timer.
|
|
82
|
+
if (progressType && matchesAwaited(message, progressType) && (!requestId || message.requestId === requestId)) {
|
|
83
|
+
armTimer();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!matchesAwaited(message, resultType))
|
|
87
|
+
return;
|
|
88
|
+
if (requestId && message.requestId !== requestId)
|
|
89
|
+
return;
|
|
90
|
+
// Surface the real failure reason: some engine paths put it in `message`, but every caller
|
|
91
|
+
// reads `result.error` — mirror it rather than let a clear reason degrade to "unknown".
|
|
92
|
+
// Copied, never mutated in place: `ForgeBrowserHost.dispatch` hands the SAME object to every
|
|
93
|
+
// subscribed listener, and step 5 places instances in batches with concurrent waits, so a
|
|
94
|
+
// mutation here would be visible to another waiter. (`voxel-ws.ts` mutates safely only
|
|
95
|
+
// because each of its connections parses its own object.)
|
|
96
|
+
if (message.success === false && !message.error && typeof message.message === 'string') {
|
|
97
|
+
finish({ ...message, error: message.message });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
finish(message);
|
|
101
|
+
});
|
|
102
|
+
// Armed before the post, not after it, so a `page.evaluate` that never settles (a hung or
|
|
103
|
+
// crashed renderer) is bounded by the same timeout as a silent game.
|
|
104
|
+
armTimer();
|
|
105
|
+
bridge.postCommand(command).catch((error) => {
|
|
106
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
107
|
+
log(`forge transport: could not post ${command.type} into the page: ${detail}`);
|
|
108
|
+
finish(null);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build the transport over a page bridge. The returned object is the shared package's
|
|
114
|
+
* `ForgeTransport` and nothing more — `commands/forge.ts` hands it straight to
|
|
115
|
+
* `ForgeDeps.transport` via `buildCliForgeDeps`.
|
|
116
|
+
*/
|
|
117
|
+
export function createForgePageTransport(bridge, options) {
|
|
118
|
+
const log = options?.log ?? (() => { });
|
|
119
|
+
const retryDelayMs = options?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
120
|
+
return {
|
|
121
|
+
async sendAndWait(command, resultType, timeoutMs, waitOptions) {
|
|
122
|
+
const { type, ...payload } = command;
|
|
123
|
+
if (typeof type !== 'string') {
|
|
124
|
+
// Not a timeout and not a transport failure — a malformed command is a caller bug, and
|
|
125
|
+
// returning null here would hide it as "the browser never answered".
|
|
126
|
+
throw new Error(`A forge command must carry a string \`type\`, got ${JSON.stringify(type)}`);
|
|
127
|
+
}
|
|
128
|
+
const requestId = typeof command.requestId === 'string' ? command.requestId : undefined;
|
|
129
|
+
const envelope = { type, data: payload };
|
|
130
|
+
const maxRetries = waitOptions?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
131
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
132
|
+
const result = await singleAttempt(bridge, envelope, requestId, resultType, timeoutMs, waitOptions?.progressType, log);
|
|
133
|
+
if (result !== null)
|
|
134
|
+
return result;
|
|
135
|
+
if (attempt < maxRetries) {
|
|
136
|
+
log(`forge transport: ${type} attempt ${attempt} failed, retrying in ${retryDelayMs}ms`);
|
|
137
|
+
await sleep(retryDelayMs);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
log(`forge transport: ${type} exhausted ${maxRetries} attempt(s) waiting for ${resultType}`);
|
|
141
|
+
return null;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/forge/transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The path the ENGINE calls. Not api-server's — that is what this proxy translates. */
|
|
2
|
+
export declare const ENGINE_UPLOAD_PATH = "/api/generate-upload-url";
|
|
3
|
+
/** The api-server route it forwards to. */
|
|
4
|
+
export declare const API_SERVER_UPLOAD_PATH = "/api/cli/v1/uploads/signed-url";
|
|
5
|
+
export interface UploadProxyOptions {
|
|
6
|
+
/** api-server base URL, e.g. `https://beta.portal.bitmagic.cloud`. */
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
/** The creator's access token. Never leaves this process. */
|
|
9
|
+
token: string;
|
|
10
|
+
/** The game the CLI project owns. Overrides whatever the page asks for. */
|
|
11
|
+
gameId: string;
|
|
12
|
+
log?: (message: string) => void;
|
|
13
|
+
/** Injected in tests. Defaults to global fetch. */
|
|
14
|
+
fetchImpl?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
export interface UploadProxy {
|
|
17
|
+
/** What `agentUrl` should be set to, e.g. `http://127.0.0.1:51234`. */
|
|
18
|
+
readonly url: string;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function startUploadProxy(options: UploadProxyOptions): Promise<UploadProxy>;
|