@volter/world-runtime 3.0.55 → 3.0.57
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/src/consumers.d.ts +9 -0
- package/dist/src/consumers.js +20 -9
- package/dist/src/runtime.js +19 -11
- package/package.json +4 -4
- package/src/consumers.ts +21 -8
- package/src/runtime.ts +18 -5
package/dist/src/consumers.d.ts
CHANGED
|
@@ -21,6 +21,15 @@ export declare function createConsumer(root: string, name: string, instanceCreat
|
|
|
21
21
|
}): WorldConsumer;
|
|
22
22
|
export declare function refreshConsumer(root: string, name: string, record: WorldConsumer): void;
|
|
23
23
|
export declare function releaseConsumer(root: string, name: string, record: WorldConsumer): void;
|
|
24
|
+
export type ConsumerProcessState = {
|
|
25
|
+
state: 'present' | 'absent';
|
|
26
|
+
} | {
|
|
27
|
+
state: 'unknown';
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
/** Signal delivery cannot prove existence. Only a successful signal-0 probe or ESRCH
|
|
31
|
+
* answers it; permission and registry errors retain uncertainty, never cleanup authority. */
|
|
32
|
+
export declare function consumerProcessState(pid: number): ConsumerProcessState;
|
|
24
33
|
/** Inspection never deletes records or interprets incomplete coverage as no consumers. */
|
|
25
34
|
export declare function worldConsumers(root: string, name: string, instanceCreatedAt?: string): {
|
|
26
35
|
consumers: (WorldConsumer & {
|
package/dist/src/consumers.js
CHANGED
|
@@ -80,15 +80,18 @@ export function releaseConsumer(root, name, record) {
|
|
|
80
80
|
rmSync(join(dir, `${record.id}.json`), { force: true });
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
/** Signal delivery cannot prove existence. Only a successful signal-0 probe or ESRCH
|
|
84
|
+
* answers it; permission and registry errors retain uncertainty, never cleanup authority. */
|
|
85
|
+
export function consumerProcessState(pid) {
|
|
86
86
|
try {
|
|
87
87
|
process.kill(pid, 0);
|
|
88
|
-
return
|
|
88
|
+
return { state: 'present' };
|
|
89
89
|
}
|
|
90
90
|
catch (error) {
|
|
91
|
-
|
|
91
|
+
const cause = error;
|
|
92
|
+
if (cause?.code === 'ESRCH')
|
|
93
|
+
return { state: 'absent' };
|
|
94
|
+
return { state: 'unknown', reason: cause?.code ?? cause?.message ?? String(error) };
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
/** Inspection never deletes records or interprets incomplete coverage as no consumers. */
|
|
@@ -131,10 +134,18 @@ export function worldConsumers(root, name, instanceCreatedAt) {
|
|
|
131
134
|
checkedOwner(r.owner);
|
|
132
135
|
const age = Date.now() - Date.parse(r.heartbeatAt);
|
|
133
136
|
const sameInstance = instanceCreatedAt === r.instanceCreatedAt;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
// Never interpret another host's numeric PIDs as processes on this host.
|
|
138
|
+
const local = r.hostname === hostname() && sameInstance;
|
|
139
|
+
const runner = local ? consumerProcessState(r.runnerPid) : undefined;
|
|
140
|
+
const command = local && r.consumerPid !== undefined ? consumerProcessState(r.consumerPid) : undefined;
|
|
141
|
+
if (runner?.state === 'unknown')
|
|
142
|
+
warnings.push(`Consumer ${r.id}: runner ${r.runnerPid} existence unknown (${runner.reason}).`);
|
|
143
|
+
if (command?.state === 'unknown')
|
|
144
|
+
warnings.push(`Consumer ${r.id}: command ${r.consumerPid} existence unknown (${command.reason}).`);
|
|
145
|
+
const state = r.retirementUncertain || !local || runner?.state === 'unknown' || command?.state === 'unknown' ? 'uncertain'
|
|
146
|
+
: runner?.state === 'absent' && r.consumerPid === undefined ? 'uncertain'
|
|
147
|
+
: runner?.state === 'absent' && command?.state === 'absent' ? (r.completionRequired ? 'uncertain' : 'exited')
|
|
148
|
+
: runner?.state === 'present' && age >= -STALE_MS && age <= STALE_MS ? 'active' : 'uncertain';
|
|
138
149
|
// Explicit fields only: malformed records must never leak arbitrary metadata into reports.
|
|
139
150
|
consumers.push({ id: r.id, instanceCreatedAt: r.instanceCreatedAt, kind: r.kind, owner: r.owner,
|
|
140
151
|
uid: r.uid, hostname: r.hostname, runnerPid: r.runnerPid, consumerPid: r.consumerPid,
|
package/dist/src/runtime.js
CHANGED
|
@@ -38,7 +38,7 @@ import { readAppUrl } from "./app-url.js";
|
|
|
38
38
|
import { inertInjectEnvWarnings } from "./inject-map.js";
|
|
39
39
|
import { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, proxyEnvFor, startRedirectProxy, tearDownCa } from "./redirect-proxy.js";
|
|
40
40
|
import { CaTrustError, sessionTrustEnv } from "./ca-trust.js";
|
|
41
|
-
import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers } from "./consumers.js";
|
|
41
|
+
import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers, consumerProcessState } from "./consumers.js";
|
|
42
42
|
import { assertLifecycleStartable, beginWorldLifecycle, handoffWorldLifecycle, recordWorldLifecycleEvent, releaseWorldLifecycle, retainWorldLifecycleForCleanup, hasWorldLifecycle, isWorldLifecycleRunning, lifecycleLogPath, releaseAbandonedWorldLifecycle, exitedWorldLifecyclePids } from "./lifecycle-record.js";
|
|
43
43
|
// Births captured at actual spawn; retries never remeasure a retained PID.
|
|
44
44
|
const spawnedIdentities = new Map();
|
|
@@ -510,6 +510,15 @@ function injectPreloadSpecifier() {
|
|
|
510
510
|
// resolved on use: importing the runtime has no side effects in a host without module resolution
|
|
511
511
|
return createRequire(import.meta.url).resolve('@volter/world-core/inject');
|
|
512
512
|
}
|
|
513
|
+
/** A World's NODE_OPTIONS: its own injector, then the launcher's options without an outer World's injector. One
|
|
514
|
+
* injector per process (the innermost World's, which already routes on to the outer World's twins); a second one
|
|
515
|
+
* patches the same doors twice, and tools that re-serialize NODE_OPTIONS (Next.js's build workers) merge the two
|
|
516
|
+
* `--require` paths into one filename. */
|
|
517
|
+
function worldNodeOptions(inherited) {
|
|
518
|
+
const own = `--require ${injectPreloadSpecifier()}`;
|
|
519
|
+
const rest = (inherited ?? '').replace(/(^|\s)(?:--require|-r)[ =]\S*[\\/]world-core[\\/]inject\.cjs(?=\s|$)/g, ' ').trim();
|
|
520
|
+
return rest ? `${own} ${rest}` : own;
|
|
521
|
+
}
|
|
513
522
|
function writeProxyEnv(root, name, env) {
|
|
514
523
|
writeTextAtomic(proxyEnvFile(root, name), `${JSON.stringify(env, null, 2)}\n`);
|
|
515
524
|
}
|
|
@@ -2162,7 +2171,7 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2162
2171
|
// non-loopback host once this is set. Without it "sealed" only records intent and every
|
|
2163
2172
|
// untwinned host is blind-tunneled to the real internet (TWIN-60).
|
|
2164
2173
|
...(mode === 'sealed' ? { VOLTER_WORLD_SEALED: '1', VOLTER_TWIN_STRICT_EGRESS: '1' } : {}),
|
|
2165
|
-
NODE_OPTIONS:
|
|
2174
|
+
NODE_OPTIONS: worldNodeOptions(inheritedEnv.NODE_OPTIONS),
|
|
2166
2175
|
};
|
|
2167
2176
|
env.NO_PROXY = `127.0.0.1,localhost${env.NO_PROXY || inheritedEnv.NO_PROXY ? `,${env.NO_PROXY ?? inheritedEnv.NO_PROXY}` : ''}`;
|
|
2168
2177
|
env.no_proxy = `127.0.0.1,localhost${env.no_proxy || inheritedEnv.no_proxy ? `,${env.no_proxy ?? inheritedEnv.no_proxy}` : ''}`;
|
|
@@ -3822,24 +3831,23 @@ export function retireWorldConsumers(name, root = process.cwd(), options = {}) {
|
|
|
3822
3831
|
const chosen = consumers.filter(c => c.state === 'uncertain' && (options.consumer === undefined || c.id === options.consumer));
|
|
3823
3832
|
if (options.consumer !== undefined && chosen.length === 0)
|
|
3824
3833
|
throw new Error(`World "${name}" has no uncertain consumer ${options.consumer}`);
|
|
3825
|
-
const gone = (pid) => { try {
|
|
3826
|
-
process.kill(pid, 0);
|
|
3827
|
-
return false;
|
|
3828
|
-
}
|
|
3829
|
-
catch (error) {
|
|
3830
|
-
return error.code === 'ESRCH';
|
|
3831
|
-
} };
|
|
3832
3834
|
return chosen.map((c) => {
|
|
3833
3835
|
const keep = (reason) => ({ id: c.id, owner: c.owner, retired: false, reason });
|
|
3834
3836
|
if (c.hostname !== osHostname())
|
|
3835
3837
|
return keep(`recorded on host ${c.hostname}; retire it there`);
|
|
3836
3838
|
if (c.instanceCreatedAt !== current.createdAt)
|
|
3837
3839
|
return keep('belongs to another instance of this World');
|
|
3838
|
-
|
|
3840
|
+
const runner = consumerProcessState(c.runnerPid);
|
|
3841
|
+
if (runner.state === 'unknown')
|
|
3842
|
+
return keep(`runner ${c.runnerPid} existence is unknown (${runner.reason})`);
|
|
3843
|
+
if (runner.state === 'present')
|
|
3839
3844
|
return keep(`runner ${c.runnerPid} is still alive`);
|
|
3840
3845
|
if (c.consumerPid === undefined)
|
|
3841
3846
|
return keep('its command was never recorded as started, so its processes cannot be proven gone');
|
|
3842
|
-
|
|
3847
|
+
const command = consumerProcessState(c.consumerPid);
|
|
3848
|
+
if (command.state === 'unknown')
|
|
3849
|
+
return keep(`command ${c.consumerPid} existence is unknown (${command.reason})`);
|
|
3850
|
+
if (command.state === 'present')
|
|
3843
3851
|
return keep(`command ${c.consumerPid} is still alive`);
|
|
3844
3852
|
const surviving = survivingOwnedGroups([c.consumerPid]);
|
|
3845
3853
|
if (surviving.length)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.57",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -69,14 +69,14 @@
|
|
|
69
69
|
"node": ">=22.3"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
|
-
"@volter/world-core": "3.0.
|
|
73
|
-
"@volter/world-console": "3.0.
|
|
72
|
+
"@volter/world-core": "3.0.57",
|
|
73
|
+
"@volter/world-console": "3.0.52"
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@electric-sql/pglite": "0.5.8",
|
|
77
77
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
78
78
|
"@volter/world-access": "3.0.1",
|
|
79
|
-
"@volter/world-core": "3.0.
|
|
79
|
+
"@volter/world-core": "3.0.57",
|
|
80
80
|
"mongodb": "~6.20.0",
|
|
81
81
|
"pg-gateway": "0.3.0-beta.4",
|
|
82
82
|
"smol-toml": "^1.8.0",
|
package/src/consumers.ts
CHANGED
|
@@ -91,10 +91,17 @@ export function releaseConsumer(root: string, name: string, record: WorldConsume
|
|
|
91
91
|
} finally { rmSync(join(dir, `${record.id}.json`), { force: true }); }
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
export type ConsumerProcessState = { state: 'present' | 'absent' } | { state: 'unknown'; reason: string };
|
|
95
|
+
|
|
96
|
+
/** Signal delivery cannot prove existence. Only a successful signal-0 probe or ESRCH
|
|
97
|
+
* answers it; permission and registry errors retain uncertainty, never cleanup authority. */
|
|
98
|
+
export function consumerProcessState(pid: number): ConsumerProcessState {
|
|
99
|
+
try { process.kill(pid, 0); return { state: 'present' }; }
|
|
100
|
+
catch (error) {
|
|
101
|
+
const cause = error as NodeJS.ErrnoException;
|
|
102
|
+
if (cause?.code === 'ESRCH') return { state: 'absent' };
|
|
103
|
+
return { state: 'unknown', reason: cause?.code ?? cause?.message ?? String(error) };
|
|
104
|
+
}
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
/** Inspection never deletes records or interprets incomplete coverage as no consumers. */
|
|
@@ -126,10 +133,16 @@ export function worldConsumers(root: string, name: string, instanceCreatedAt?: s
|
|
|
126
133
|
checkedOwner(r.owner);
|
|
127
134
|
const age = Date.now() - Date.parse(r.heartbeatAt);
|
|
128
135
|
const sameInstance = instanceCreatedAt === r.instanceCreatedAt;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
// Never interpret another host's numeric PIDs as processes on this host.
|
|
137
|
+
const local = r.hostname === hostname() && sameInstance;
|
|
138
|
+
const runner = local ? consumerProcessState(r.runnerPid) : undefined;
|
|
139
|
+
const command = local && r.consumerPid !== undefined ? consumerProcessState(r.consumerPid) : undefined;
|
|
140
|
+
if (runner?.state === 'unknown') warnings.push(`Consumer ${r.id}: runner ${r.runnerPid} existence unknown (${runner.reason}).`);
|
|
141
|
+
if (command?.state === 'unknown') warnings.push(`Consumer ${r.id}: command ${r.consumerPid} existence unknown (${command.reason}).`);
|
|
142
|
+
const state = r.retirementUncertain || !local || runner?.state === 'unknown' || command?.state === 'unknown' ? 'uncertain'
|
|
143
|
+
: runner?.state === 'absent' && r.consumerPid === undefined ? 'uncertain'
|
|
144
|
+
: runner?.state === 'absent' && command?.state === 'absent' ? (r.completionRequired ? 'uncertain' : 'exited')
|
|
145
|
+
: runner?.state === 'present' && age >= -STALE_MS && age <= STALE_MS ? 'active' : 'uncertain';
|
|
133
146
|
// Explicit fields only: malformed records must never leak arbitrary metadata into reports.
|
|
134
147
|
consumers.push({ id: r.id, instanceCreatedAt: r.instanceCreatedAt, kind: r.kind, owner: r.owner,
|
|
135
148
|
uid: r.uid, hostname: r.hostname, runnerPid: r.runnerPid, consumerPid: r.consumerPid,
|
package/src/runtime.ts
CHANGED
|
@@ -41,7 +41,7 @@ import { inertInjectEnvWarnings } from './inject-map.ts';
|
|
|
41
41
|
import { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, proxyEnvFor, startRedirectProxy, tearDownCa } from './redirect-proxy.ts';
|
|
42
42
|
import { CaTrustError, sessionTrustEnv, type CaTrustInputs } from './ca-trust.ts';
|
|
43
43
|
import type { WorldConfig, WorldExternalReadyWhen, WorldInstance, WorldIsolation, WorldMode, WorldRunOutcome, WorldRunRecord, WorldServiceConfig, WorldServiceInstance } from './schema.ts';
|
|
44
|
-
import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers } from './consumers.ts';
|
|
44
|
+
import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers, consumerProcessState } from './consumers.ts';
|
|
45
45
|
import { assertLifecycleStartable, beginWorldLifecycle, handoffWorldLifecycle, recordWorldLifecycleEvent, releaseWorldLifecycle, retainWorldLifecycleForCleanup, hasWorldLifecycle, isWorldLifecycleRunning, lifecycleLogPath, releaseAbandonedWorldLifecycle, exitedWorldLifecyclePids } from './lifecycle-record.ts';
|
|
46
46
|
|
|
47
47
|
// Births captured at actual spawn; retries never remeasure a retained PID.
|
|
@@ -553,6 +553,16 @@ function injectPreloadSpecifier(): string {
|
|
|
553
553
|
return createRequire(import.meta.url).resolve('@volter/world-core/inject');
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
+
/** A World's NODE_OPTIONS: its own injector, then the launcher's options without an outer World's injector. One
|
|
557
|
+
* injector per process (the innermost World's, which already routes on to the outer World's twins); a second one
|
|
558
|
+
* patches the same doors twice, and tools that re-serialize NODE_OPTIONS (Next.js's build workers) merge the two
|
|
559
|
+
* `--require` paths into one filename. */
|
|
560
|
+
function worldNodeOptions(inherited: string | undefined): string {
|
|
561
|
+
const own = `--require ${injectPreloadSpecifier()}`;
|
|
562
|
+
const rest = (inherited ?? '').replace(/(^|\s)(?:--require|-r)[ =]\S*[\\/]world-core[\\/]inject\.cjs(?=\s|$)/g, ' ').trim();
|
|
563
|
+
return rest ? `${own} ${rest}` : own;
|
|
564
|
+
}
|
|
565
|
+
|
|
556
566
|
function writeProxyEnv(root: string, name: string, env: Record<string, string>): void {
|
|
557
567
|
writeTextAtomic(proxyEnvFile(root, name), `${JSON.stringify(env, null, 2)}\n`);
|
|
558
568
|
}
|
|
@@ -2049,7 +2059,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
2049
2059
|
// non-loopback host once this is set. Without it "sealed" only records intent and every
|
|
2050
2060
|
// untwinned host is blind-tunneled to the real internet (TWIN-60).
|
|
2051
2061
|
...(mode === 'sealed' ? { VOLTER_WORLD_SEALED: '1', VOLTER_TWIN_STRICT_EGRESS: '1' } : {}),
|
|
2052
|
-
NODE_OPTIONS:
|
|
2062
|
+
NODE_OPTIONS: worldNodeOptions(inheritedEnv.NODE_OPTIONS),
|
|
2053
2063
|
};
|
|
2054
2064
|
env.NO_PROXY = `127.0.0.1,localhost${env.NO_PROXY || inheritedEnv.NO_PROXY ? `,${env.NO_PROXY ?? inheritedEnv.NO_PROXY}` : ''}`;
|
|
2055
2065
|
env.no_proxy = `127.0.0.1,localhost${env.no_proxy || inheritedEnv.no_proxy ? `,${env.no_proxy ?? inheritedEnv.no_proxy}` : ''}`;
|
|
@@ -3618,14 +3628,17 @@ export function retireWorldConsumers(name: string, root = process.cwd(), options
|
|
|
3618
3628
|
const { consumers } = worldConsumers(resolvedRoot, name, current.createdAt);
|
|
3619
3629
|
const chosen = consumers.filter(c => c.state === 'uncertain' && (options.consumer === undefined || c.id === options.consumer));
|
|
3620
3630
|
if (options.consumer !== undefined && chosen.length === 0) throw new Error(`World "${name}" has no uncertain consumer ${options.consumer}`);
|
|
3621
|
-
const gone = (pid: number) => { try { process.kill(pid, 0); return false; } catch (error) { return (error as NodeJS.ErrnoException).code === 'ESRCH'; } };
|
|
3622
3631
|
return chosen.map((c): ConsumerRetirement => {
|
|
3623
3632
|
const keep = (reason: string): ConsumerRetirement => ({ id: c.id, owner: c.owner, retired: false, reason });
|
|
3624
3633
|
if (c.hostname !== osHostname()) return keep(`recorded on host ${c.hostname}; retire it there`);
|
|
3625
3634
|
if (c.instanceCreatedAt !== current.createdAt) return keep('belongs to another instance of this World');
|
|
3626
|
-
|
|
3635
|
+
const runner = consumerProcessState(c.runnerPid);
|
|
3636
|
+
if (runner.state === 'unknown') return keep(`runner ${c.runnerPid} existence is unknown (${runner.reason})`);
|
|
3637
|
+
if (runner.state === 'present') return keep(`runner ${c.runnerPid} is still alive`);
|
|
3627
3638
|
if (c.consumerPid === undefined) return keep('its command was never recorded as started, so its processes cannot be proven gone');
|
|
3628
|
-
|
|
3639
|
+
const command = consumerProcessState(c.consumerPid);
|
|
3640
|
+
if (command.state === 'unknown') return keep(`command ${c.consumerPid} existence is unknown (${command.reason})`);
|
|
3641
|
+
if (command.state === 'present') return keep(`command ${c.consumerPid} is still alive`);
|
|
3629
3642
|
const surviving = survivingOwnedGroups([c.consumerPid]);
|
|
3630
3643
|
if (surviving.length) return keep(`process group ${c.consumerPid} still has live members`);
|
|
3631
3644
|
removeConsumerRecord(resolvedRoot, name, c.id);
|