@volter/world-runtime 2.0.22 → 2.0.24

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.
@@ -114,11 +114,13 @@ async function untilReadyOrEnded(wait, ended, boot, what, log) {
114
114
  * always runs in full: one port's answer says nothing of the others'. Programs that are not twins are never asked. */
115
115
  async function confirmOwnPort(port, boot, ended, twin, what, log, early = false) {
116
116
  // A twin is asked at once, raced with the settle: a server slow to answer costs no more than the settle did.
117
- const answer = twin ? worldBootOf(port) : Promise.resolve(null);
117
+ boot.signal.throwIfAborted();
118
+ const answer = twin ? worldBootOf(port, boot.signal) : Promise.resolve(null);
118
119
  const own = early && twin ? answer.then((a) => (a && a.boot === boot.bootId ? 'own' : new Promise(() => undefined))) : new Promise(() => undefined);
119
120
  // `ended` absent: the process was already settled (a co-located host settles once for all its twins).
120
121
  if (ended) {
121
122
  const settled = await Promise.race([own, ended.then(() => 'ended'), new Promise((resolveAlive) => setTimeout(() => resolveAlive('alive'), 300))]);
123
+ boot.signal.throwIfAborted();
122
124
  if (settled === 'own')
123
125
  return;
124
126
  if (settled === 'ended')
@@ -127,28 +129,65 @@ async function confirmOwnPort(port, boot, ended, twin, what, log, early = false)
127
129
  if (!twin)
128
130
  return;
129
131
  const answered = await answer;
132
+ boot.signal.throwIfAborted();
130
133
  if (answered && typeof answered.boot === 'string' && answered.boot !== boot.bootId) {
131
134
  throw new Error(`${what}: its port is answered by a twin of another World boot (${answered.boot}), not this one; a leftover World holds the port. Stop it (\`volter-world list\`, then \`down\`) and retry.\nLog: ${log}`);
132
135
  }
133
136
  }
134
137
  /** What the server on a loopback port says its World boot is (WORLD_BOOT_PATH), or null. A direct loopback request:
135
138
  * never through a proxy an activated shell's environment names. */
136
- function worldBootOf(port) {
139
+ function worldBootOf(port, abort) {
137
140
  return new Promise((resolveBoot) => {
138
- const request = httpGet({ host: '127.0.0.1', port, path: WORLD_BOOT_PATH, timeout: 1000, agent: false }, (response) => {
139
- let body = '';
140
- response.setEncoding('utf8');
141
- response.on('data', (chunk) => { body += chunk; if (body.length > 4096)
142
- request.destroy(); });
143
- response.on('end', () => { try {
144
- resolveBoot(response.statusCode === 200 ? JSON.parse(body) : null);
145
- }
146
- catch {
147
- resolveBoot(null);
148
- } });
149
- });
150
- request.on('timeout', () => request.destroy());
151
- request.on('error', () => resolveBoot(null));
141
+ let request;
142
+ let deadline;
143
+ let settled = false;
144
+ let responseStarted = false;
145
+ const finish = (value) => {
146
+ if (settled)
147
+ return;
148
+ settled = true;
149
+ if (deadline !== undefined)
150
+ clearTimeout(deadline);
151
+ abort.removeEventListener('abort', cancel);
152
+ resolveBoot(value);
153
+ };
154
+ const cancel = () => { finish(null); request?.destroy(); };
155
+ if (abort.aborted) {
156
+ finish(null);
157
+ return;
158
+ }
159
+ abort.addEventListener('abort', cancel, { once: true });
160
+ // A socket timeout alone is insufficient: destroying a request after
161
+ // headers can close/abort its response without emitting request 'error'.
162
+ // Bound the whole probe, including a peer that dribbles bytes forever.
163
+ deadline = setTimeout(cancel, 1000);
164
+ try {
165
+ request = httpGet({ host: '127.0.0.1', port, path: WORLD_BOOT_PATH, timeout: 1000, agent: false }, (response) => {
166
+ responseStarted = true;
167
+ let body = '';
168
+ response.on('error', cancel);
169
+ response.on('aborted', cancel);
170
+ response.on('close', () => finish(null));
171
+ response.setEncoding('utf8');
172
+ response.on('data', (chunk) => { body += chunk; if (body.length > 4096)
173
+ cancel(); });
174
+ response.on('end', () => {
175
+ try {
176
+ finish(response.statusCode === 200 ? JSON.parse(body) : null);
177
+ }
178
+ catch {
179
+ finish(null);
180
+ }
181
+ });
182
+ });
183
+ request.on('timeout', cancel);
184
+ request.on('error', () => finish(null));
185
+ request.on('close', () => { if (!responseStarted)
186
+ finish(null); });
187
+ }
188
+ catch {
189
+ cancel();
190
+ }
152
191
  });
153
192
  }
154
193
  /** A stopped service's own account from its log: how it ended and the last lines it wrote. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.22",
3
+ "version": "2.0.24",
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",
@@ -35,7 +35,7 @@
35
35
  "url": "git+https://github.com/volter-ai/twin.git",
36
36
  "directory": "packages/world-runtime"
37
37
  },
38
- "homepage": "https://github.com/volter-ai/twin/tree/main/packages/world-runtime#readme",
38
+ "homepage": "https://world.volter.ai/docs/reference/cli",
39
39
  "type": "module",
40
40
  "exports": {
41
41
  ".": {
@@ -67,14 +67,14 @@
67
67
  "node": ">=22.3"
68
68
  },
69
69
  "peerDependencies": {
70
- "@volter/world-core": "2.0.14",
71
- "@volter/world-console": "2.0.9"
70
+ "@volter/world-core": "2.0.15",
71
+ "@volter/world-console": "2.0.10"
72
72
  },
73
73
  "dependencies": {
74
74
  "@electric-sql/pglite": "0.5.8",
75
75
  "@electric-sql/pglite-pgvector": "0.0.9",
76
- "@volter/world-access": "2.0.1",
77
- "@volter/world-core": "2.0.14",
76
+ "@volter/world-access": "2.0.2",
77
+ "@volter/world-core": "2.0.15",
78
78
  "pg-gateway": "0.3.0-beta.4",
79
79
  "smol-toml": "^1.8.0"
80
80
  },
@@ -84,6 +84,6 @@
84
84
  }
85
85
  },
86
86
  "optionalDependencies": {
87
- "@volter/twin-mongodb": "0.1.14"
87
+ "@volter/twin-mongodb": "0.1.15"
88
88
  }
89
89
  }
package/src/runtime.ts CHANGED
@@ -3,7 +3,7 @@ import { offSignal } from './signals.ts';
3
3
  import { assertWorldStateRemovable, removeWorldState } from './ancestry.ts';
4
4
  import { packCli, packEntry } from './catalog.ts';
5
5
  import { spawn, spawnSync } from 'node:child_process';
6
- import { get as httpGet } from 'node:http';
6
+ import { get as httpGet, type ClientRequest } from 'node:http';
7
7
  import { siblingScript } from './sibling.ts';
8
8
  import { commandLifetime, commandProcessGroup, CommandRetirementError } from './command-lifetime.ts';
9
9
  import { superviseWorldRun } from './run-task.ts';
@@ -108,16 +108,19 @@ async function untilReadyOrEnded<T>(wait: (abort: AbortSignal) => Promise<T>, en
108
108
  * always runs in full: one port's answer says nothing of the others'. Programs that are not twins are never asked. */
109
109
  async function confirmOwnPort(port: number, boot: BootContext, ended: Promise<void> | undefined, twin: boolean, what: string, log: string, early = false): Promise<void> {
110
110
  // A twin is asked at once, raced with the settle: a server slow to answer costs no more than the settle did.
111
- const answer = twin ? worldBootOf(port) : Promise.resolve(null);
111
+ boot.signal.throwIfAborted();
112
+ const answer = twin ? worldBootOf(port, boot.signal) : Promise.resolve(null);
112
113
  const own = early && twin ? answer.then((a) => (a && a.boot === boot.bootId ? 'own' as const : new Promise<never>(() => undefined))) : new Promise<never>(() => undefined);
113
114
  // `ended` absent: the process was already settled (a co-located host settles once for all its twins).
114
115
  if (ended) {
115
116
  const settled = await Promise.race([own, ended.then(() => 'ended' as const), new Promise<'alive'>((resolveAlive) => setTimeout(() => resolveAlive('alive'), 300))]);
117
+ boot.signal.throwIfAborted();
116
118
  if (settled === 'own') return;
117
119
  if (settled === 'ended') throw new Error(`${what} ended right after its port first answered, so another process probably holds the port: the service ${endedAccount(log)}\nLog: ${log}`);
118
120
  }
119
121
  if (!twin) return;
120
122
  const answered = await answer;
123
+ boot.signal.throwIfAborted();
121
124
  if (answered && typeof answered.boot === 'string' && answered.boot !== boot.bootId) {
122
125
  throw new Error(`${what}: its port is answered by a twin of another World boot (${answered.boot}), not this one; a leftover World holds the port. Stop it (\`volter-world list\`, then \`down\`) and retry.\nLog: ${log}`);
123
126
  }
@@ -125,16 +128,44 @@ async function confirmOwnPort(port: number, boot: BootContext, ended: Promise<vo
125
128
 
126
129
  /** What the server on a loopback port says its World boot is (WORLD_BOOT_PATH), or null. A direct loopback request:
127
130
  * never through a proxy an activated shell's environment names. */
128
- function worldBootOf(port: number): Promise<{ boot?: unknown } | null> {
131
+ function worldBootOf(port: number, abort: AbortSignal): Promise<{ boot?: unknown } | null> {
129
132
  return new Promise((resolveBoot) => {
130
- const request = httpGet({ host: '127.0.0.1', port, path: WORLD_BOOT_PATH, timeout: 1000, agent: false }, (response) => {
131
- let body = '';
132
- response.setEncoding('utf8');
133
- response.on('data', (chunk: string) => { body += chunk; if (body.length > 4096) request.destroy(); });
134
- response.on('end', () => { try { resolveBoot(response.statusCode === 200 ? JSON.parse(body) as { boot?: unknown } : null); } catch { resolveBoot(null); } });
135
- });
136
- request.on('timeout', () => request.destroy());
137
- request.on('error', () => resolveBoot(null));
133
+ let request: ClientRequest | undefined;
134
+ let deadline: ReturnType<typeof setTimeout> | undefined;
135
+ let settled = false;
136
+ let responseStarted = false;
137
+ const finish = (value: { boot?: unknown } | null): void => {
138
+ if (settled) return;
139
+ settled = true;
140
+ if (deadline !== undefined) clearTimeout(deadline);
141
+ abort.removeEventListener('abort', cancel);
142
+ resolveBoot(value);
143
+ };
144
+ const cancel = (): void => { finish(null); request?.destroy(); };
145
+ if (abort.aborted) { finish(null); return; }
146
+ abort.addEventListener('abort', cancel, { once: true });
147
+ // A socket timeout alone is insufficient: destroying a request after
148
+ // headers can close/abort its response without emitting request 'error'.
149
+ // Bound the whole probe, including a peer that dribbles bytes forever.
150
+ deadline = setTimeout(cancel, 1000);
151
+ try {
152
+ request = httpGet({ host: '127.0.0.1', port, path: WORLD_BOOT_PATH, timeout: 1000, agent: false }, (response) => {
153
+ responseStarted = true;
154
+ let body = '';
155
+ response.on('error', cancel);
156
+ response.on('aborted', cancel);
157
+ response.on('close', () => finish(null));
158
+ response.setEncoding('utf8');
159
+ response.on('data', (chunk: string) => { body += chunk; if (body.length > 4096) cancel(); });
160
+ response.on('end', () => {
161
+ try { finish(response.statusCode === 200 ? JSON.parse(body) as { boot?: unknown } : null); }
162
+ catch { finish(null); }
163
+ });
164
+ });
165
+ request.on('timeout', cancel);
166
+ request.on('error', () => finish(null));
167
+ request.on('close', () => { if (!responseStarted) finish(null); });
168
+ } catch { cancel(); }
138
169
  });
139
170
  }
140
171