@volter/world-runtime 3.0.38 → 3.0.39
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/known-external-services.json +3 -10
- package/dist/src/actions-runner-cli.js +3 -3
- package/dist/src/actions-runner.js +2 -1
- package/dist/src/app-url.js +2 -1
- package/dist/src/backings.d.ts +10 -1
- package/dist/src/backings.js +66 -7
- package/dist/src/branch.js +85 -52
- package/dist/src/browser-routes.cjs +59 -0
- package/dist/src/ca-trust.js +4 -3
- package/dist/src/changeset.js +4 -3
- package/dist/src/cli-commands.js +25 -9
- package/dist/src/cli.js +2 -1
- package/dist/src/command-lifetime.d.mts +12 -0
- package/dist/src/command-lifetime.d.ts +1 -15
- package/dist/src/command-lifetime.js +1 -98
- package/dist/src/command-lifetime.mjs +90 -0
- package/dist/src/configs.js +1 -1
- package/dist/src/console-apart.js +3 -2
- package/dist/src/covers.d.ts +3 -2
- package/dist/src/covers.js +95 -18
- package/dist/src/error-message.d.mts +1 -0
- package/dist/src/error-message.d.ts +1 -0
- package/dist/src/error-message.js +2 -0
- package/dist/src/error-message.mjs +10 -0
- package/dist/src/guest-command.d.mts +4 -0
- package/dist/src/guest-command.mjs +26 -0
- package/dist/src/guest-control.d.mts +1 -0
- package/dist/src/guest-control.mjs +17 -0
- package/dist/src/guest-entry.mjs +146 -0
- package/dist/src/guest-processes.d.mts +9 -0
- package/dist/src/guest-processes.mjs +47 -0
- package/dist/src/guest-runner.d.ts +6 -0
- package/dist/src/guest-runner.js +567 -0
- package/dist/src/host.js +5 -4
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/infra-cli.js +4 -0
- package/dist/src/infra.js +133 -23
- package/dist/src/init.js +60 -23
- package/dist/src/lifecycle-record.d.ts +3 -1
- package/dist/src/lifecycle-record.js +29 -6
- package/dist/src/livekit-backing.mjs +37 -0
- package/dist/src/local-branches.js +11 -3
- package/dist/src/mongodb-backing.d.ts +7 -0
- package/dist/src/mongodb-backing.js +58 -0
- package/dist/src/native-postgres-backing.d.ts +4 -0
- package/dist/src/native-postgres-backing.js +404 -0
- package/dist/src/origin.js +2 -1
- package/dist/src/owned-path.d.ts +4 -0
- package/dist/src/owned-path.js +37 -0
- package/dist/src/pack-facts.d.ts +1 -0
- package/dist/src/pglite-backing.d.ts +4 -13
- package/dist/src/pglite-backing.js +198 -106
- package/dist/src/pglite-host.mjs +47 -17
- package/dist/src/placeholder.d.ts +3 -2
- package/dist/src/placeholder.js +9 -4
- package/dist/src/process-groups.d.mts +2 -0
- package/dist/src/process-groups.d.ts +1 -4
- package/dist/src/process-groups.js +1 -49
- package/dist/src/process-groups.mjs +59 -0
- package/dist/src/process-receipt.d.mts +6 -0
- package/dist/src/process-receipt.d.ts +11 -0
- package/dist/src/process-receipt.js +3 -0
- package/dist/src/process-receipt.mjs +45 -0
- package/dist/src/process-signal.d.mts +9 -0
- package/dist/src/process-signal.mjs +92 -0
- package/dist/src/project-inspect.d.ts +1 -1
- package/dist/src/project-inspect.js +4 -2
- package/dist/src/proxy-daemon.js +2 -1
- package/dist/src/redirect-proxy.js +186 -19
- package/dist/src/redis-backing.js +99 -35
- package/dist/src/reflect.js +2 -1
- package/dist/src/root.js +7 -6
- package/dist/src/run-task-worker.js +2 -1
- package/dist/src/runtime.d.ts +24 -7
- package/dist/src/runtime.js +703 -286
- package/dist/src/schema.d.ts +12 -1
- package/dist/src/schema.js +20 -5
- package/dist/src/served-world.d.ts +1 -0
- package/dist/src/served-world.js +26 -14
- package/dist/src/service-recorder.js +8 -3
- package/dist/src/signals.d.mts +1 -0
- package/dist/src/signals.d.ts +1 -1
- package/dist/src/signals.js +1 -11
- package/dist/src/signals.mjs +10 -0
- package/dist/src/tail.d.ts +2 -0
- package/dist/src/tail.js +9 -2
- package/dist/src/tcp-port.js +27 -13
- package/dist/src/temporal-backing.mjs +28 -0
- package/dist/src/up-cli.js +8 -3
- package/dist/src/up-task-worker.js +2 -1
- package/dist/src/websocket-relay.d.ts +1 -1
- package/dist/src/websocket-relay.js +23 -10
- package/dist/src/world-view.js +2 -2
- package/known-external-services.json +3 -10
- package/package.json +12 -7
- package/src/actions-runner-cli.ts +3 -3
- package/src/actions-runner.ts +2 -1
- package/src/app-url.ts +2 -1
- package/src/backings.ts +65 -7
- package/src/branch.ts +73 -34
- package/src/browser-routes.cjs +59 -0
- package/src/ca-trust.ts +4 -3
- package/src/changeset.ts +4 -3
- package/src/cli-commands.ts +21 -9
- package/src/cli.ts +2 -1
- package/src/command-lifetime.d.mts +12 -0
- package/src/command-lifetime.mjs +90 -0
- package/src/command-lifetime.ts +1 -79
- package/src/configs.ts +1 -1
- package/src/console-apart.ts +2 -1
- package/src/covers.ts +97 -19
- package/src/error-message.d.mts +1 -0
- package/src/error-message.mjs +10 -0
- package/src/error-message.ts +2 -0
- package/src/guest-command.d.mts +4 -0
- package/src/guest-command.mjs +26 -0
- package/src/guest-control.d.mts +1 -0
- package/src/guest-control.mjs +17 -0
- package/src/guest-entry.mjs +146 -0
- package/src/guest-processes.d.mts +9 -0
- package/src/guest-processes.mjs +47 -0
- package/src/guest-runner.ts +414 -0
- package/src/host.ts +5 -4
- package/src/index.ts +1 -0
- package/src/infra-cli.ts +4 -0
- package/src/infra.ts +103 -22
- package/src/init.ts +55 -22
- package/src/lifecycle-record.ts +21 -6
- package/src/livekit-backing.mjs +37 -0
- package/src/local-branches.ts +6 -3
- package/src/mongodb-backing.ts +55 -0
- package/src/native-postgres-backing.ts +329 -0
- package/src/origin.ts +2 -1
- package/src/owned-path.ts +32 -0
- package/src/pack-facts.ts +1 -0
- package/src/pglite-backing.ts +155 -79
- package/src/pglite-host.mjs +47 -17
- package/src/placeholder.ts +10 -5
- package/src/process-groups.d.mts +2 -0
- package/src/process-groups.mjs +59 -0
- package/src/process-groups.ts +1 -33
- package/src/process-receipt.d.mts +6 -0
- package/src/process-receipt.mjs +45 -0
- package/src/process-receipt.ts +5 -0
- package/src/process-signal.d.mts +9 -0
- package/src/process-signal.mjs +92 -0
- package/src/project-inspect.ts +4 -2
- package/src/proxy-daemon.ts +2 -1
- package/src/redirect-proxy.ts +153 -18
- package/src/redis-backing.ts +74 -25
- package/src/reflect.ts +2 -1
- package/src/root.ts +7 -6
- package/src/run-task-worker.ts +2 -1
- package/src/runtime.ts +596 -265
- package/src/schema.ts +30 -6
- package/src/served-world.ts +21 -15
- package/src/service-recorder.ts +4 -2
- package/src/signals.d.mts +1 -0
- package/src/signals.mjs +10 -0
- package/src/signals.ts +1 -10
- package/src/tail.ts +10 -3
- package/src/tcp-port.ts +24 -13
- package/src/temporal-backing.mjs +28 -0
- package/src/up-cli.ts +6 -3
- package/src/up-task-worker.ts +2 -1
- package/src/websocket-relay.ts +18 -11
- package/src/world-view.ts +2 -2
package/dist/src/schema.d.ts
CHANGED
|
@@ -209,6 +209,8 @@ export type WorldServiceConfig = {
|
|
|
209
209
|
* to loopback services directly while presenting a disguised world to the app/user.
|
|
210
210
|
*/
|
|
211
211
|
controlPlane?: boolean;
|
|
212
|
+
/** Refuse branching before child compute when this service cannot carry its state to a branch. */
|
|
213
|
+
branchState?: 'unsupported';
|
|
212
214
|
injectEnv?: string;
|
|
213
215
|
/**
|
|
214
216
|
* Additional env vars exported into the generated world env after this service starts.
|
|
@@ -293,7 +295,7 @@ export type WorldConfig = {
|
|
|
293
295
|
id: string;
|
|
294
296
|
/** How changesets are made and readied here; omitted: one approval, a summary required. */
|
|
295
297
|
changesets?: WorldChangesetRules;
|
|
296
|
-
/**
|
|
298
|
+
/** HTTPS origin/exact-path/subtree grants with declared phase and explicit build passthrough; [] closes real egress. */
|
|
297
299
|
network?: WorldNetwork;
|
|
298
300
|
/** Internal normalized source format and saved discovery selection. */
|
|
299
301
|
manifestVersion?: 1 | 2;
|
|
@@ -335,6 +337,8 @@ export type WorldConfig = {
|
|
|
335
337
|
};
|
|
336
338
|
};
|
|
337
339
|
export type WorldServiceInstance = {
|
|
340
|
+
/** Explicit boot permission survives config edits and missing-field refusals. */
|
|
341
|
+
branchState?: 'supported' | 'unsupported';
|
|
338
342
|
id: string;
|
|
339
343
|
type: WorldServiceType;
|
|
340
344
|
command: string[];
|
|
@@ -434,6 +438,8 @@ export type WorldRunRecord = WorldRunOutcome | {
|
|
|
434
438
|
};
|
|
435
439
|
export type WorldInstance = {
|
|
436
440
|
name: string;
|
|
441
|
+
/** Unique boot identity, independent of the World clock; ordinary reading completes older records. */
|
|
442
|
+
instance: string;
|
|
437
443
|
/** Snapshot of the selection in force when this instance booted. */
|
|
438
444
|
selection?: WorldSelection;
|
|
439
445
|
config: string;
|
|
@@ -448,6 +454,10 @@ export type WorldInstance = {
|
|
|
448
454
|
};
|
|
449
455
|
services: Record<string, WorldServiceInstance>;
|
|
450
456
|
env: Record<string, string>;
|
|
457
|
+
/** Always written at boot; optional only so cleanup can read incomplete legacy records. */
|
|
458
|
+
networkCeiling?: WorldNetwork | 'unrestricted';
|
|
459
|
+
bootServices?: string[];
|
|
460
|
+
bootOwner?: string | null;
|
|
451
461
|
envFile: string;
|
|
452
462
|
pidsFile: string;
|
|
453
463
|
actors?: Record<string, unknown>;
|
|
@@ -477,6 +487,7 @@ export type WorldInstance = {
|
|
|
477
487
|
* (docs/contributing/architecture.md, "The catalog: where twins come from"). */
|
|
478
488
|
packFacts?: string[];
|
|
479
489
|
};
|
|
490
|
+
export declare function isEnvironmentName(value: unknown): value is string;
|
|
480
491
|
export declare const DEFAULT_WORLD_SELECTION: WorldSelection;
|
|
481
492
|
export declare const LEGACY_WORLD_SELECTION: WorldSelection;
|
|
482
493
|
export declare function assertWorldSelection(value: unknown, path: string): asserts value is WorldSelection;
|
package/dist/src/schema.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorMessage } from "./error-message.js";
|
|
1
2
|
import { normalizeWorldNetwork } from '@volter/world-core/network-policy';
|
|
2
3
|
export const WORLD_SCHEMA_VERSION = 2;
|
|
3
4
|
export const WORLD_USAGES = ['application', 'dependencies', 'build', 'deployment'];
|
|
@@ -10,6 +11,12 @@ export const ISSUE_ENV_PREFIX = '$issue:';
|
|
|
10
11
|
* attaches, from the application URL the booter recorded (`volter-world app-url <world> --set`), so the app names
|
|
11
12
|
* the address it is reached at, which only its booter knows (a browser tab's preview origin, a local port). */
|
|
12
13
|
export const APP_URL_ENV = '$app-url';
|
|
14
|
+
export function isEnvironmentName(value) {
|
|
15
|
+
return typeof value === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
16
|
+
}
|
|
17
|
+
function environmentMap(value) {
|
|
18
|
+
return record(value) && Object.entries(value).every(([name, entry]) => isEnvironmentName(name) && typeof entry === 'string');
|
|
19
|
+
}
|
|
13
20
|
function assertStringArray(value, what, path) {
|
|
14
21
|
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string')) {
|
|
15
22
|
throw new Error(`${what} must be a non-empty string array in ${path}`);
|
|
@@ -20,7 +27,7 @@ function assertValidRegex(pattern, what, path) {
|
|
|
20
27
|
new RegExp(pattern);
|
|
21
28
|
}
|
|
22
29
|
catch (error) {
|
|
23
|
-
throw new Error(`${what} has an invalid regex (${
|
|
30
|
+
throw new Error(`${what} has an invalid regex (${errorMessage(error)}) in ${path}`);
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
/** Shared validator for a readiness probe — used by both `external.readyWhen` and a process/twin
|
|
@@ -125,7 +132,7 @@ function assertLegacyWorldConfig(value, path) {
|
|
|
125
132
|
config.network = normalizeWorldNetwork(config.network);
|
|
126
133
|
if (config.description !== undefined && typeof config.description !== 'string')
|
|
127
134
|
throw new Error(`World config description must be a string in ${path}`);
|
|
128
|
-
if (config.env !== undefined &&
|
|
135
|
+
if (config.env !== undefined && !environmentMap(config.env))
|
|
129
136
|
throw new Error(`World config env must be a string→string object in ${path}`);
|
|
130
137
|
if (config.env?.VOLTER_WORLD_NETWORK_POLICY !== undefined)
|
|
131
138
|
throw new Error(`World network permission belongs in runtime.network, not environment values in ${path}`);
|
|
@@ -173,7 +180,13 @@ function assertLegacyWorldConfig(value, path) {
|
|
|
173
180
|
throw new Error(`Invalid service in ${path}`);
|
|
174
181
|
if (!service.id || typeof service.id !== 'string')
|
|
175
182
|
throw new Error(`Service must define string id in ${path}`);
|
|
183
|
+
if (service.branchState !== undefined && service.branchState !== 'unsupported')
|
|
184
|
+
throw new Error(`Service ${service.id} branchState must be unsupported in ${path}`);
|
|
176
185
|
const exports = [service.injectEnv, ...Object.keys(service.injectEnvTemplates ?? {}), ...Object.keys(service.cliRedirect ?? {}), ...(Array.isArray(service.external?.discover) ? service.external.discover.map(entry => entry?.as) : [])];
|
|
186
|
+
if (exports.some(name => name !== undefined && !isEnvironmentName(name)))
|
|
187
|
+
throw new Error(`Service ${service.id} bindings must name environment variables in ${path}`);
|
|
188
|
+
if (service.issues !== undefined && (!record(service.issues) || typeof service.issues.path !== 'string' || !record(service.issues.body) || !environmentMap(service.issues.fill)))
|
|
189
|
+
throw new Error(`Service ${service.id} issues must define path, body and string→string fill in ${path}`);
|
|
177
190
|
if (service.env?.VOLTER_WORLD_NETWORK_POLICY !== undefined || exports.includes('VOLTER_WORLD_NETWORK_POLICY'))
|
|
178
191
|
throw new Error(`Service ${service.id} cannot replace World network permission in ${path}`);
|
|
179
192
|
if (!/^[a-z0-9][a-z0-9._-]*$/.test(service.id))
|
|
@@ -265,7 +278,7 @@ function assertLegacyWorldConfig(value, path) {
|
|
|
265
278
|
}
|
|
266
279
|
if (service.version !== undefined && !(typeof service.version === 'string' && /^\^?\d+\.\d+(\.\d+)?$/.test(service.version)))
|
|
267
280
|
throw new Error(`Service "${service.id}" version must pin the twin package as "major.minor.patch" or "^major.minor" in ${path}`);
|
|
268
|
-
if (service.env !== undefined &&
|
|
281
|
+
if (service.env !== undefined && !environmentMap(service.env))
|
|
269
282
|
throw new Error(`Service "${service.id}" env must be a string→string object in ${path}`);
|
|
270
283
|
if (service.injectEnv !== undefined && typeof service.injectEnv !== 'string') {
|
|
271
284
|
throw new Error(`Service "${service.id}" injectEnv must be a string in ${path}`);
|
|
@@ -378,7 +391,7 @@ function serviceV2ToLegacy(value, path) {
|
|
|
378
391
|
if (!record(source) || !record(execution) || !record(endpoint) || !record(bindings))
|
|
379
392
|
throw new Error(`Service "${String(value.id ?? '')}" sections must be objects in ${path}`);
|
|
380
393
|
assertKeys(source, ['package', 'version'], 'service.source', path);
|
|
381
|
-
assertKeys(execution, ['process', 'colocate', 'lifecycle', 'cwd', 'environment', 'preload', 'controlPlane'], 'service.execution', path);
|
|
394
|
+
assertKeys(execution, ['process', 'colocate', 'lifecycle', 'cwd', 'environment', 'preload', 'controlPlane', 'branchState'], 'service.execution', path);
|
|
382
395
|
assertKeys(endpoint, ['port', 'portReason', 'ready'], 'service.endpoint', path);
|
|
383
396
|
assertKeys(bindings, ['injectEnv', 'injectEnvTemplates', 'cliRedirect', 'discover'], 'service.bindings', path);
|
|
384
397
|
const process = execution.process === undefined ? {} : execution.process;
|
|
@@ -442,6 +455,7 @@ function serviceV2ToLegacy(value, path) {
|
|
|
442
455
|
...(environment.values === undefined ? {} : { env: environment.values }),
|
|
443
456
|
...(execution.preload === undefined ? {} : { preload: execution.preload }),
|
|
444
457
|
...(execution.controlPlane === undefined ? {} : { controlPlane: execution.controlPlane }),
|
|
458
|
+
...(execution.branchState === undefined ? {} : { branchState: execution.branchState }),
|
|
445
459
|
...(endpoint.port === undefined ? {} : { port: endpoint.port }),
|
|
446
460
|
...(endpoint.portReason === undefined ? {} : { portReason: endpoint.portReason }),
|
|
447
461
|
...(endpoint.ready === undefined ? {} : { ready: endpoint.ready }),
|
|
@@ -488,7 +502,7 @@ function normalizeV2(value, path) {
|
|
|
488
502
|
if (!record(environment))
|
|
489
503
|
throw new Error(`runtime.environment must be an object in ${path}`);
|
|
490
504
|
assertKeys(environment, ['values', 'strip'], 'runtime.environment', path);
|
|
491
|
-
if (environment.values !== undefined &&
|
|
505
|
+
if (environment.values !== undefined && !environmentMap(environment.values))
|
|
492
506
|
throw new Error(`runtime.environment.values must be a string→string object in ${path}`);
|
|
493
507
|
if (scenario.actors !== undefined && !record(scenario.actors))
|
|
494
508
|
throw new Error(`scenario.actors must be an object in ${path}`);
|
|
@@ -561,6 +575,7 @@ function serviceToV2(service) {
|
|
|
561
575
|
...(service.env === undefined ? {} : { environment: { values: service.env } }),
|
|
562
576
|
...(service.preload === undefined ? {} : { preload: service.preload }),
|
|
563
577
|
...(service.controlPlane === undefined ? {} : { controlPlane: service.controlPlane }),
|
|
578
|
+
...(service.branchState === undefined ? {} : { branchState: service.branchState }),
|
|
564
579
|
};
|
|
565
580
|
const endpoint = {
|
|
566
581
|
...(service.port === undefined ? {} : { port: service.port }),
|
|
@@ -46,6 +46,7 @@ export declare function servedName(worldRoot: string, configRef: string): string
|
|
|
46
46
|
* `twins()` names the world's twins once booted; `rotate()` mints both tokens anew — the old die
|
|
47
47
|
* with the call — and keeps them beside the world like `boot` does. */
|
|
48
48
|
export type MountedWorld = {
|
|
49
|
+
maxRequestBodySize: number;
|
|
49
50
|
name: string;
|
|
50
51
|
served: string;
|
|
51
52
|
root: string;
|
package/dist/src/served-world.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorMessage } from "./error-message.js";
|
|
1
2
|
import { boardFrameKey, getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant, historyDigest, historyEntries, readHistoryView, worldPaths } from '@volter/world-core/runtime';
|
|
2
3
|
// A SERVED WORLD (docs/reference/http-api.md#a-served-world): `volter world serve` puts a world at
|
|
3
4
|
// `http://<host>:<port>/<org>/<world>/`. Each twin's vendor API is under its vendor id — the
|
|
@@ -227,6 +228,7 @@ export async function mountWorld(name, opts = {}) {
|
|
|
227
228
|
let stopping = null;
|
|
228
229
|
const mounted = {
|
|
229
230
|
name, served, root: worldRoot,
|
|
231
|
+
maxRequestBodySize: Math.max(1024 * 1024 * 1024, ...loadWorldConfig(configRef, worldRoot).config.services.map((service) => packFacts()[service.package?.replace(/^@volter\/twin-/, '') ?? service.id]?.maxRequestBodySize ?? 0)),
|
|
230
232
|
get token() { return world.token; }, get readToken() { return world.readToken; },
|
|
231
233
|
twins: () => world.twins(),
|
|
232
234
|
rotate: () => { world.retoken(mintToken('tok_'), mintToken('tok_r_')); keep(); return { token: world.token, readToken: world.readToken }; },
|
|
@@ -317,9 +319,9 @@ export async function serveWorld(name, opts = {}) {
|
|
|
317
319
|
// stale server on the port must never be mistaken for this one. Until the world is up the
|
|
318
320
|
// doors answer 503.
|
|
319
321
|
let consoleOrigin = null;
|
|
320
|
-
//
|
|
322
|
+
// Keep the 1 GiB front baseline unless a served twin declares a larger transport cap.
|
|
321
323
|
// a front for the whole World: its twins and links keep their own journals (architecture: a multiplexing front journals nothing)
|
|
322
|
-
const server = await serveHttp({ hostname: host, port: opts.port ?? 0, maxRequestBodySize:
|
|
324
|
+
const server = await serveHttp({ hostname: host, port: opts.port ?? 0, maxRequestBodySize: mounted.maxRequestBodySize, twinRequestJournal: false, fetch: (request) => consoleRedirect(request, consoleOrigin) ?? mounted.handle(request) });
|
|
323
325
|
const url = `http://${host}:${server.port}`;
|
|
324
326
|
try {
|
|
325
327
|
await mounted.boot(url);
|
|
@@ -369,12 +371,21 @@ export async function serveWorld(name, opts = {}) {
|
|
|
369
371
|
}
|
|
370
372
|
catch (error) {
|
|
371
373
|
// Boot succeeded: this call owns the World, but cannot return its stop handle.
|
|
374
|
+
const errors = [error];
|
|
372
375
|
try {
|
|
373
376
|
await stopAll();
|
|
374
377
|
}
|
|
375
|
-
|
|
378
|
+
catch (cleanup) {
|
|
379
|
+
errors.push(cleanup);
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
376
382
|
await mounted.stop();
|
|
377
383
|
}
|
|
384
|
+
catch (cleanup) {
|
|
385
|
+
errors.push(cleanup);
|
|
386
|
+
}
|
|
387
|
+
if (errors.length > 1)
|
|
388
|
+
throw new AggregateError(errors, 'World announcement and host cleanup failed');
|
|
378
389
|
throw error;
|
|
379
390
|
}
|
|
380
391
|
return { url, base: `${url}/${mounted.served}`, name: mounted.served, port: server.port ?? 0, token: mounted.token, readToken: mounted.readToken, console: consoleUrl, stop: async () => { await stopAll(); await mounted.stop(); } };
|
|
@@ -591,6 +602,7 @@ export class WorldDoors {
|
|
|
591
602
|
}
|
|
592
603
|
if (door === 'open.js' && request.method === 'GET') {
|
|
593
604
|
const script = `(() => {
|
|
605
|
+
const errorMessage = (${errorMessage.toString()});
|
|
594
606
|
const world = document.body.dataset.world;
|
|
595
607
|
const carried = new URLSearchParams(location.hash.slice(1));
|
|
596
608
|
history.replaceState(null, '', location.pathname);
|
|
@@ -608,7 +620,7 @@ export class WorldDoors {
|
|
|
608
620
|
if (!r.ok) throw new Error(opened.error || 'This World did not open.');
|
|
609
621
|
location.replace(to);
|
|
610
622
|
})
|
|
611
|
-
.catch((e) => { document.body.textContent = e
|
|
623
|
+
.catch((e) => { document.body.textContent = errorMessage(e) || 'This World did not answer.'; tell({ error: document.body.textContent }); });
|
|
612
624
|
})();
|
|
613
625
|
`;
|
|
614
626
|
return new Response(script, { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-store' } });
|
|
@@ -770,7 +782,7 @@ export class WorldDoors {
|
|
|
770
782
|
// the keys are gone whatever a removal answers; a branch that stays is said, not hidden (its parent key no longer opens this World)
|
|
771
783
|
for (const k of [...gone, ...also])
|
|
772
784
|
if (k.branch)
|
|
773
|
-
await this.host.branches?.remove(k.branch).catch((error) => { console.error(`${this.served}: branch ${k.branch} stays after its key was revoked: ${
|
|
785
|
+
await this.host.branches?.remove(k.branch).catch((error) => { console.error(`${this.served}: branch ${k.branch} stays after its key was revoked: ${errorMessage(error)}`); return false; });
|
|
774
786
|
return [...ids];
|
|
775
787
|
}
|
|
776
788
|
async keysDoor(request, id, scope, via, session) {
|
|
@@ -1347,7 +1359,7 @@ export class WorldDoors {
|
|
|
1347
1359
|
return Response.json({ error: `not this world: serving /${this.served}/` }, { status: 404 });
|
|
1348
1360
|
}
|
|
1349
1361
|
catch (error) {
|
|
1350
|
-
return Response.json({ error:
|
|
1362
|
+
return Response.json({ error: errorMessage(error) }, { status: 500 });
|
|
1351
1363
|
}
|
|
1352
1364
|
}
|
|
1353
1365
|
/** A twin's workspaces as its discovery door lists them (`GET /twin`'s `screens`: each built workspace screen's id and
|
|
@@ -1561,7 +1573,7 @@ export class WorldDoors {
|
|
|
1561
1573
|
credential = await openSealedAt(credentialPath(this.worldRoot, name), name, destinationOf(base.origin));
|
|
1562
1574
|
}
|
|
1563
1575
|
catch (error) {
|
|
1564
|
-
return Response.json({ error: error
|
|
1576
|
+
return Response.json({ error: errorMessage(error) }, { status: 409 });
|
|
1565
1577
|
}
|
|
1566
1578
|
if (credential === null)
|
|
1567
1579
|
return Response.json({ error: `the ${name} link has no credential sealed` }, { status: 409 });
|
|
@@ -2039,7 +2051,7 @@ export class WorldDoors {
|
|
|
2039
2051
|
validateRemoteOrigin(body.origin ?? '');
|
|
2040
2052
|
}
|
|
2041
2053
|
catch (error) {
|
|
2042
|
-
return Response.json({ error:
|
|
2054
|
+
return Response.json({ error: errorMessage(error) }, { status: 400 });
|
|
2043
2055
|
}
|
|
2044
2056
|
getActiveWorldStore().write(this.linksPath(), `${JSON.stringify({ ...this.links(), [name]: { origin: body.origin } }, null, 2)}\n`);
|
|
2045
2057
|
return Response.json({ link: name, origin: body.origin });
|
|
@@ -2070,7 +2082,7 @@ export class WorldDoors {
|
|
|
2070
2082
|
}
|
|
2071
2083
|
catch (error) {
|
|
2072
2084
|
store.remove(path);
|
|
2073
|
-
return Response.json({ error: `not a check: ${
|
|
2085
|
+
return Response.json({ error: `not a check: ${errorMessage(error)}` }, { status: 400 });
|
|
2074
2086
|
}
|
|
2075
2087
|
return Response.json({ check: name });
|
|
2076
2088
|
}
|
|
@@ -2160,7 +2172,7 @@ export class WorldDoors {
|
|
|
2160
2172
|
assertChangesetRules(body?.changesets, path);
|
|
2161
2173
|
}
|
|
2162
2174
|
catch (error) {
|
|
2163
|
-
return Response.json({ error: error
|
|
2175
|
+
return Response.json({ error: errorMessage(error).replace(` in ${path}`, '') }, { status: 400 });
|
|
2164
2176
|
}
|
|
2165
2177
|
const { config } = loadWorldConfig(path, this.worldRoot);
|
|
2166
2178
|
const rules = body.changesets;
|
|
@@ -2506,7 +2518,7 @@ export class WorldDoors {
|
|
|
2506
2518
|
await this.host.serveBranch(target);
|
|
2507
2519
|
}
|
|
2508
2520
|
catch (error) {
|
|
2509
|
-
return Response.json({ error:
|
|
2521
|
+
return Response.json({ error: errorMessage(error) }, { status: 409 });
|
|
2510
2522
|
}
|
|
2511
2523
|
return Response.json({ world: this.served, checkedOut: target });
|
|
2512
2524
|
}
|
|
@@ -2562,7 +2574,7 @@ export class WorldDoors {
|
|
|
2562
2574
|
}
|
|
2563
2575
|
catch (error) {
|
|
2564
2576
|
this.keepKeys(this.keys().filter((k) => k.id !== keyId)); // no branch, no key
|
|
2565
|
-
return Response.json({ error:
|
|
2577
|
+
return Response.json({ error: errorMessage(error) }, { status: 409 });
|
|
2566
2578
|
}
|
|
2567
2579
|
}
|
|
2568
2580
|
if (rest.length === 2 && request.method === 'DELETE') {
|
|
@@ -2914,7 +2926,7 @@ export class WorldDoors {
|
|
|
2914
2926
|
return Response.json({ world: this.served, changeset: changeset.name, receipts, position, snapshots, ...(kept?.owner ? { owner: kept.owner } : {}), ...(kept?.pushedBy ? { pushedBy: kept.pushedBy } : {}) });
|
|
2915
2927
|
}
|
|
2916
2928
|
catch (error) {
|
|
2917
|
-
return Response.json({ error:
|
|
2929
|
+
return Response.json({ error: errorMessage(error) }, { status: 409 });
|
|
2918
2930
|
}
|
|
2919
2931
|
}
|
|
2920
2932
|
}
|
|
@@ -3044,7 +3056,7 @@ export async function scheduleRefreshes(name, worldRoot, instance, log = (l) =>
|
|
|
3044
3056
|
const timers = [];
|
|
3045
3057
|
for (const { vendor, controlRoot, root, everyMs } of await refreshSchedule(worldRoot, instance, log)) {
|
|
3046
3058
|
const timer = setInterval(() => { refreshTwin({ worldRoot, vendor, controlRoot, root, force: true }).then((r) => { if (!r.refreshed)
|
|
3047
|
-
log(`${vendor}: not refreshed: ${r.reason}`); }, (e) => log(`${vendor}: refresh failed: ${
|
|
3059
|
+
log(`${vendor}: not refreshed: ${r.reason}`); }, (e) => log(`${vendor}: refresh failed: ${errorMessage(e)}`)); }, everyMs);
|
|
3048
3060
|
timer.unref?.();
|
|
3049
3061
|
timers.push(timer);
|
|
3050
3062
|
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// service's own lines carry none: lines between two marks were written between those times;
|
|
16
16
|
// - forwards TERM/INT/HUP to the service and exits with its status (128+n for a signal).
|
|
17
17
|
// The World's NODE_OPTIONS (the injector) is for the service, not this parent: it arrives renamed and is restored.
|
|
18
|
+
import { signalChild } from './process-signal.mjs';
|
|
19
|
+
import { errorMessage } from "./error-message.js";
|
|
18
20
|
import { spawn } from 'node:child_process';
|
|
19
21
|
import { constants } from 'node:os';
|
|
20
22
|
import { appendFileSync, closeSync, fstatSync, openSync, readSync, statSync, truncateSync, writeFileSync } from 'node:fs';
|
|
@@ -102,9 +104,12 @@ catch {
|
|
|
102
104
|
let forwarded = null;
|
|
103
105
|
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP'])
|
|
104
106
|
process.on(signal, () => { forwarded = forwarded || signal; try {
|
|
105
|
-
child.
|
|
107
|
+
if (child.pid)
|
|
108
|
+
signalChild(child, 'SIGTERM');
|
|
106
109
|
}
|
|
107
|
-
catch {
|
|
110
|
+
catch (error) {
|
|
111
|
+
process.stderr.write(`${errorMessage(error)}\n`);
|
|
112
|
+
} });
|
|
108
113
|
let ended = false;
|
|
109
114
|
const done = (status, end) => {
|
|
110
115
|
if (ended)
|
|
@@ -114,7 +119,7 @@ const done = (status, end) => {
|
|
|
114
119
|
record({ event: 'world.service.exited', status, ...end, pid: child.pid, uptimeMs: Date.now() - started, ...(forwarded ? { stopRequested: forwarded } : {}) }, () => process.exit(status));
|
|
115
120
|
};
|
|
116
121
|
child.once('error', (error) => {
|
|
117
|
-
const message =
|
|
122
|
+
const message = errorMessage(error);
|
|
118
123
|
process.stderr.write(`${message}\n`);
|
|
119
124
|
done(127, { code: null, signal: null, error: message });
|
|
120
125
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function offSignal(signal: NodeJS.Signals, listener: (...args: never[]) => void): void;
|
package/dist/src/signals.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { offSignal } from './signals.mjs';
|
package/dist/src/signals.js
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// `process.off('SIGINT', fn)` uninstalls the process's OS handler even while other listeners remain
|
|
3
|
-
// (`listenerCount` still answers 1): the next SIGINT then takes the default action and kills the
|
|
4
|
-
// process, so a caller's own handler (a harness's teardown) never runs. Re-registering the remaining
|
|
5
|
-
// listeners reinstalls the handler. Node keeps it either way.
|
|
6
|
-
export function offSignal(signal, listener) {
|
|
7
|
-
const rest = process.listeners(signal).filter((l) => l !== listener);
|
|
8
|
-
process.removeAllListeners(signal);
|
|
9
|
-
for (const l of rest)
|
|
10
|
-
process.on(signal, l);
|
|
11
|
-
}
|
|
1
|
+
export { offSignal } from './signals.mjs';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Removing a signal listener without removing the others. Under Bun (measured on 1.3.10),
|
|
2
|
+
// `process.off('SIGINT', fn)` uninstalls the process's OS handler even while other listeners remain
|
|
3
|
+
// (`listenerCount` still answers 1): the next SIGINT then takes the default action and kills the
|
|
4
|
+
// process, so a caller's own handler (a harness's teardown) never runs. Re-registering the remaining
|
|
5
|
+
// listeners reinstalls the handler. Node keeps it either way.
|
|
6
|
+
export function offSignal(signal, listener) {
|
|
7
|
+
const rest = process.listeners(signal).filter((l) => l !== listener);
|
|
8
|
+
process.removeAllListeners(signal);
|
|
9
|
+
for (const l of rest) process.on(signal, l);
|
|
10
|
+
}
|
package/dist/src/tail.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type TailWorldOptions = {
|
|
|
11
11
|
* status}` rows; world-core/src/serve.ts). Reads are otherwise invisible here: an
|
|
12
12
|
* empty-catalog GET that 404s leaves no trace in an actions-only tail. */
|
|
13
13
|
requests?: boolean;
|
|
14
|
+
/** Also read existing service process logs verbatim. These are vendor output, not shape-only journals. */
|
|
15
|
+
logs?: boolean;
|
|
14
16
|
/** Poll interval while following (ledger appends are cross-process, so this polls rather than
|
|
15
17
|
* fs.watch — watch descriptors can't see a file that doesn't exist yet). */
|
|
16
18
|
pollMs?: number;
|
package/dist/src/tail.js
CHANGED
|
@@ -108,8 +108,10 @@ function entryInstant(entry) {
|
|
|
108
108
|
}
|
|
109
109
|
function formatEntry(entry, json) {
|
|
110
110
|
if (json)
|
|
111
|
-
return JSON.stringify({ ...entry.record, service: entry.service });
|
|
111
|
+
return JSON.stringify({ ...entry.record, service: entry.service, ...(entry.source === 'logs' ? { source: 'logs' } : {}) });
|
|
112
112
|
const { record } = entry;
|
|
113
|
+
if (entry.source === 'logs')
|
|
114
|
+
return `${entry.service} stdout ${record.message}`;
|
|
113
115
|
if (entry.source === 'requests') {
|
|
114
116
|
// a served request, not an action: `HH:MM:SS.mmm <service> <METHOD> <path> <status> in <ms>ms`, and —
|
|
115
117
|
// when the caller presented one — which credential arrived, named and fingerprinted. The
|
|
@@ -181,6 +183,11 @@ export async function tailWorldActions(name, options = {}) {
|
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
}
|
|
186
|
+
const log = instance.services[service]?.log;
|
|
187
|
+
if (options.logs && log) {
|
|
188
|
+
for (const line of readNewLines(log, offsets))
|
|
189
|
+
entries.push({ service, record: { message: line }, source: 'logs' });
|
|
190
|
+
}
|
|
184
191
|
}
|
|
185
192
|
return entries.sort(byOccurredAt);
|
|
186
193
|
};
|
|
@@ -192,7 +199,7 @@ export async function tailWorldActions(name, options = {}) {
|
|
|
192
199
|
for (const service of services) {
|
|
193
200
|
if (links.includes(service) && !instance.services[service])
|
|
194
201
|
continue; // a link records requests, never actions
|
|
195
|
-
if (serviceLedgers(instance.dirs.data, service).length === 0) {
|
|
202
|
+
if (serviceLedgers(instance.dirs.data, service).length === 0 && !(options.logs && instance.services[service]?.log)) {
|
|
196
203
|
notice(`World ${name}: service "${service}" has no action ledger yet${follow
|
|
197
204
|
? ' — watching; drive the app (or a vendor SDK) through the world to record actions'
|
|
198
205
|
: '; drive the app (or a vendor SDK) through the world to record actions, then tail again'}`);
|
package/dist/src/tcp-port.js
CHANGED
|
@@ -18,19 +18,33 @@ export async function allocatePort(excluded = new Set()) {
|
|
|
18
18
|
}
|
|
19
19
|
throw new Error('Could not allocate a TCP port after 32 attempts');
|
|
20
20
|
}
|
|
21
|
-
function probePort() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
async function probePort() {
|
|
22
|
+
const ipv4 = createServer();
|
|
23
|
+
const ipv6 = createServer();
|
|
24
|
+
try {
|
|
25
25
|
// Match serveHttp's default scope; a loopback-only probe can miss a wildcard listener.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
server.close(() => reject(new Error('Could not allocate a TCP port')));
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const port = address.port;
|
|
33
|
-
server.close((error) => error ? reject(error) : resolvePort(port));
|
|
26
|
+
await new Promise((resolve, reject) => {
|
|
27
|
+
ipv4.once('error', reject);
|
|
28
|
+
ipv4.listen(0, '0.0.0.0', resolve);
|
|
34
29
|
});
|
|
35
|
-
|
|
30
|
+
const address = ipv4.address();
|
|
31
|
+
if (!address || typeof address === 'string')
|
|
32
|
+
throw new Error('Could not allocate a TCP port');
|
|
33
|
+
// Hold the IPv4 probe while checking the same port on IPv6, without a dual-stack bind.
|
|
34
|
+
await new Promise((resolve, reject) => {
|
|
35
|
+
ipv6.once('error', (error) => {
|
|
36
|
+
if (['EAFNOSUPPORT', 'EADDRNOTAVAIL', 'EPROTONOSUPPORT'].includes(error.code ?? ''))
|
|
37
|
+
resolve();
|
|
38
|
+
else
|
|
39
|
+
reject(error);
|
|
40
|
+
});
|
|
41
|
+
ipv6.listen({ port: address.port, host: '::', ipv6Only: true }, resolve);
|
|
42
|
+
});
|
|
43
|
+
return address.port;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await Promise.all([ipv6, ipv4].filter((server) => server.listening).map((server) => new Promise((resolve, reject) => {
|
|
47
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
48
|
+
})));
|
|
49
|
+
}
|
|
36
50
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { signalChild } from './process-signal.mjs';
|
|
2
|
+
import { errorMessage } from './error-message.mjs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
if (!process.env.VOLTER_WORLD) throw new Error('Temporal backing must run as a World service');
|
|
7
|
+
const port = Number(process.env.PORT);
|
|
8
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Missing allocated service port');
|
|
9
|
+
const health = process.argv.includes('--health');
|
|
10
|
+
const root = process.argv[process.argv.indexOf('--root') + 1];
|
|
11
|
+
if (!health && (!process.argv.includes('--root') || !root)) throw new Error('Missing World service data directory');
|
|
12
|
+
// Source: https://github.com/temporalio/cli/releases/tag/v1.9.1 (server start-dev --help).
|
|
13
|
+
// SQLite pragma DSNs: https://github.com/temporalio/temporal/blob/v1.32.0/common/persistence/sql/sqlplugin/sqlite/plugin.go
|
|
14
|
+
// The runtime creates --root; SQLite's file and sidecars stay in that owned directory.
|
|
15
|
+
const args = health
|
|
16
|
+
? ['--address', `127.0.0.1:${port}`, 'operator', 'cluster', 'health']
|
|
17
|
+
: ['server', 'start-dev', '--ip', '127.0.0.1', '--port', String(port), '--headless',
|
|
18
|
+
'--db-filename', join(root, 'temporal.sqlite'), '--sqlite-pragma', 'synchronous=FULL'];
|
|
19
|
+
const child = spawn('temporal', args, { stdio: 'inherit', env: process.env });
|
|
20
|
+
let stopping = false;
|
|
21
|
+
const stop = () => {
|
|
22
|
+
stopping = true;
|
|
23
|
+
try { if (child.pid) signalChild(child, 'SIGTERM'); }
|
|
24
|
+
catch (error) { console.error(errorMessage(error)); process.exitCode = 1; }
|
|
25
|
+
};
|
|
26
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
27
|
+
child.once('error', error => { console.error(errorMessage(error)); process.exitCode = 1; });
|
|
28
|
+
child.once('exit', (code, signal) => { process.exitCode = stopping ? 0 : code ?? (signal ? 1 : 0); });
|
package/dist/src/up-cli.js
CHANGED
|
@@ -24,10 +24,15 @@ export async function upCommand(subject, rest, root) {
|
|
|
24
24
|
// browser tab's image, comes up over its data instead of starting empty, as a fresh `up` does)
|
|
25
25
|
const keepState = rest.includes('--keep-state');
|
|
26
26
|
const instance = await superviseWorldUp(subject, { name, root, mode, envFile, forceEnvOutput: rest.includes('--force'), owner: ownerOption(rest), ...(isolation ? { isolation } : {}), ...(keepState ? { keepState: true } : {}) });
|
|
27
|
-
|
|
27
|
+
if (rest.includes('--json'))
|
|
28
|
+
process.stdout.write(JSON.stringify(instance, null, 2) + '\n');
|
|
29
|
+
else
|
|
30
|
+
printReady(instance);
|
|
28
31
|
}
|
|
29
32
|
export function printReady(instance) {
|
|
30
33
|
process.stdout.write(`World ${instance.name} is ready (${instance.config})
|
|
34
|
+
Instance: ${instance.instance}
|
|
35
|
+
Created: ${instance.createdAt}
|
|
31
36
|
|
|
32
37
|
Services:
|
|
33
38
|
${Object.values(instance.services).map((service) => ` ${service.id}: ${service.url ?? '(no listener — a World output)'}`).join('\n')}
|
|
@@ -43,11 +48,11 @@ GET <url>/twin; a scripted twin's GET <url>/twin/scenario lists handlers + misse
|
|
|
43
48
|
Env:
|
|
44
49
|
${instance.envFile}
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
Record:
|
|
47
52
|
${instance.dirs.instance}/instance.json
|
|
48
53
|
|
|
49
54
|
Stop:
|
|
50
|
-
volter-world down ${instance.name}
|
|
55
|
+
volter-world down ${instance.name} --if-instance ${instance.instance}
|
|
51
56
|
|
|
52
57
|
For a bounded job, use volter-world run ... -- <command> to tear down automatically on exit.
|
|
53
58
|
Persistent up does not expire or infer that your sessions are idle.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { recordWorldLifecycleEvent } from "./lifecycle-record.js";
|
|
2
2
|
import { upWorld } from "./runtime.js";
|
|
3
3
|
import { loadWorldConfig } from "./configs.js";
|
|
4
|
+
import { errorMessage } from "./error-message.js";
|
|
4
5
|
const worldNameOf = (configId, root) => { try {
|
|
5
6
|
return loadWorldConfig(configId, root).config.id;
|
|
6
7
|
}
|
|
@@ -36,7 +37,7 @@ process.on('message', async (message) => {
|
|
|
36
37
|
reply = { instance: await upWorld(request.configId, request.options, abort.signal) };
|
|
37
38
|
}
|
|
38
39
|
catch (error) {
|
|
39
|
-
reply = { error:
|
|
40
|
+
reply = { error: errorMessage(error) };
|
|
40
41
|
}
|
|
41
42
|
if (callerLost && 'instance' in reply) {
|
|
42
43
|
try {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ServeHttpOptions } from '@volter/world-core/runtime';
|
|
2
2
|
/** A message relay for the World's TLS fronts. Transport state is connection-local. */
|
|
3
3
|
export declare function websocketRelay(targetFor: (request: Request) => string | undefined): NonNullable<ServeHttpOptions['upgrade']>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { connectWebSocket } from '@volter/world-core/runtime';
|
|
1
2
|
/** A message relay for the World's TLS fronts. Transport state is connection-local. */
|
|
2
3
|
export function websocketRelay(targetFor) {
|
|
3
4
|
const sessions = new WeakMap();
|
|
@@ -9,21 +10,33 @@ export function websocketRelay(targetFor) {
|
|
|
9
10
|
peer.close(1011, 'no twin for request');
|
|
10
11
|
return;
|
|
11
12
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const headers = Object.fromEntries(request.headers);
|
|
14
|
+
const protocols = (headers['sec-websocket-protocol'] ?? '').split(',').map(p => p.trim()).filter(Boolean);
|
|
15
|
+
headers['x-volter-twin-original-host'] ??= new URL(request.url).host;
|
|
16
|
+
for (const name of ['host', 'connection', 'upgrade', 'sec-websocket-key', 'sec-websocket-version', 'sec-websocket-extensions', 'sec-websocket-protocol', 'proxy-connection'])
|
|
17
|
+
delete headers[name];
|
|
18
|
+
const session = { pending: [] };
|
|
15
19
|
sessions.set(peer, session);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
void connectWebSocket(target, headers, protocols).then(upstream => {
|
|
21
|
+
if (sessions.get(peer) !== session) {
|
|
22
|
+
upstream.onerror = () => { };
|
|
23
|
+
upstream.close();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
session.upstream = upstream;
|
|
27
|
+
upstream.binaryType = 'arraybuffer';
|
|
28
|
+
upstream.onopen = () => { for (const message of session.pending)
|
|
29
|
+
upstream.send(message); session.pending.length = 0; };
|
|
30
|
+
upstream.onmessage = event => { peer.send(typeof event.data === 'string' ? event.data : new Uint8Array(event.data)); };
|
|
31
|
+
upstream.onclose = event => { peer.close(event.code === 1006 ? 1011 : event.code, event.reason); };
|
|
32
|
+
upstream.onerror = () => { peer.close(1011, 'twin websocket error'); };
|
|
33
|
+
}).catch(() => peer.close(1011, 'twin websocket connection failed'));
|
|
21
34
|
},
|
|
22
35
|
message(peer, data) {
|
|
23
36
|
const session = sessions.get(peer);
|
|
24
37
|
if (!session)
|
|
25
38
|
return;
|
|
26
|
-
if (session.upstream
|
|
39
|
+
if (session.upstream?.readyState === WebSocket.OPEN)
|
|
27
40
|
session.upstream.send(data);
|
|
28
41
|
else
|
|
29
42
|
session.pending.push(data);
|
|
@@ -33,7 +46,7 @@ export function websocketRelay(targetFor) {
|
|
|
33
46
|
sessions.delete(peer);
|
|
34
47
|
if (session) {
|
|
35
48
|
session.pending.length = 0;
|
|
36
|
-
session.upstream
|
|
49
|
+
session.upstream?.close();
|
|
37
50
|
}
|
|
38
51
|
},
|
|
39
52
|
};
|
package/dist/src/world-view.js
CHANGED
|
@@ -104,8 +104,8 @@ export async function serveWorldView(name, opts = {}) {
|
|
|
104
104
|
base = world.served;
|
|
105
105
|
worlds.set(world.served, world);
|
|
106
106
|
const server = await serveHttp({
|
|
107
|
-
//
|
|
108
|
-
hostname: host, port: opts.port ?? 0, maxRequestBodySize:
|
|
107
|
+
// The front retains its 1 GiB baseline and honors the served twins' declared caps.
|
|
108
|
+
hostname: host, port: opts.port ?? 0, maxRequestBodySize: world.maxRequestBodySize,
|
|
109
109
|
// this front serves a World and its branches; each World's own twins journal their requests
|
|
110
110
|
twinRequestJournal: false,
|
|
111
111
|
async fetch(incoming) {
|