@volter/world-runtime 2.0.15 → 2.0.16

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.
@@ -156,7 +156,8 @@ export async function mountWorld(name, opts = {}) {
156
156
  const world = new WorldDoors(name, worldRoot, configRef, served, host, held('token') ?? mintToken('tok_'), held('token.read') ?? mintToken('tok_r_'));
157
157
  const keep = () => { writeSecret(join(worldRoot, stateDirName(), 'token'), world.token); writeSecret(join(worldRoot, stateDirName(), 'token.read'), world.readToken); };
158
158
  let timers = [];
159
- return {
159
+ let stopping = null;
160
+ const mounted = {
160
161
  name, served, root: worldRoot,
161
162
  get token() { return world.token; }, get readToken() { return world.readToken; },
162
163
  twins: () => world.twins(),
@@ -193,18 +194,40 @@ export async function mountWorld(name, opts = {}) {
193
194
  throw error;
194
195
  }
195
196
  },
196
- async stop() {
197
- for (const t of timers)
198
- clearInterval(t);
199
- try {
200
- rmSync(record, { force: true });
201
- }
202
- catch { /* already gone */ }
203
- const result = await downWorld(name, worldRoot);
204
- if (result.externalErrors.length)
205
- throw new Error(`World "${name}" shutdown incomplete; instance and reservation retained: ${result.externalErrors.join('; ')}`);
197
+ stop() {
198
+ // one stop at a time: a second caller (the view shutting down while a switch stops this World) shares the first's
199
+ stopping ??= stopOnce().finally(() => { stopping = null; });
200
+ return stopping;
206
201
  },
207
202
  };
203
+ async function stopOnce() {
204
+ // refreshes stop first, so none writes into the World while its twins stop; the serve record goes only once they are
205
+ // down. A stop that fails keeps the record (a checkout under the serve still refuses) and re-arms the refreshes:
206
+ // refused before anything stopped, the World is served as it was; failed part way, its record names what remains
207
+ for (const t of timers)
208
+ clearInterval(t);
209
+ timers = [];
210
+ let result;
211
+ try {
212
+ result = await downWorld(name, worldRoot);
213
+ if (result.externalErrors.length)
214
+ throw new Error(`World "${name}" shutdown incomplete; instance and reservation retained: ${result.externalErrors.join('; ')}`);
215
+ }
216
+ catch (error) {
217
+ if (instance) {
218
+ try {
219
+ timers = await scheduleRefreshes(name, worldRoot, instance);
220
+ }
221
+ catch { /* the stop's error is the one reported */ }
222
+ }
223
+ throw error;
224
+ }
225
+ try {
226
+ rmSync(record, { force: true });
227
+ }
228
+ catch { /* already gone */ }
229
+ }
230
+ return mounted;
208
231
  }
209
232
  /** A local World's mirrors: each pack's builders, found once per process, and each client built once. */
210
233
  const localMirrors = new Map();
@@ -55,17 +55,8 @@ export async function serveWorldView(name, opts = {}) {
55
55
  switching = true;
56
56
  const from = world.name;
57
57
  try {
58
- // a stop that fails part way brings this branch back as it was before the error is reported
59
- try {
60
- await world.stop();
61
- }
62
- catch (error) {
63
- try {
64
- await world.boot(url);
65
- }
66
- catch { /* the stop's own error is the one reported */ }
67
- throw error;
68
- }
58
+ // a stop that is refused or fails leaves this branch served as it was (served-world.ts stop): the switch just fails
59
+ await world.stop();
69
60
  let next;
70
61
  try {
71
62
  next = await mountWorld(branch, mountOpts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.15",
3
+ "version": "2.0.16",
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",
@@ -67,14 +67,14 @@
67
67
  "node": ">=22.3"
68
68
  },
69
69
  "peerDependencies": {
70
- "@volter/world-core": "2.0.9",
70
+ "@volter/world-core": "2.0.10",
71
71
  "@volter/world-console": "2.0.9"
72
72
  },
73
73
  "dependencies": {
74
74
  "@electric-sql/pglite": "0.5.8",
75
75
  "@electric-sql/pglite-pgvector": "0.0.9",
76
76
  "@volter/world-access": "2.0.1",
77
- "@volter/world-core": "2.0.9",
77
+ "@volter/world-core": "2.0.10",
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.9"
87
+ "@volter/twin-mongodb": "0.1.10"
88
88
  }
89
89
  }
@@ -161,7 +161,8 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
161
161
  const world = new WorldDoors(name, worldRoot, configRef, served, host, held('token') ?? mintToken('tok_'), held('token.read') ?? mintToken('tok_r_'));
162
162
  const keep = (): void => { writeSecret(join(worldRoot, stateDirName(), 'token'), world.token); writeSecret(join(worldRoot, stateDirName(), 'token.read'), world.readToken); };
163
163
  let timers: ReturnType<typeof setInterval>[] = [];
164
- return {
164
+ let stopping: Promise<void> | null = null;
165
+ const mounted: MountedWorld = {
165
166
  name, served, root: worldRoot,
166
167
  get token() { return world.token; }, get readToken() { return world.readToken; },
167
168
  twins: () => world.twins(),
@@ -188,13 +189,29 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
188
189
  throw error;
189
190
  }
190
191
  },
191
- async stop() {
192
- for (const t of timers) clearInterval(t);
193
- try { rmSync(record, { force: true }); } catch { /* already gone */ }
194
- const result = await downWorld(name, worldRoot);
195
- if (result.externalErrors.length) throw new Error(`World "${name}" shutdown incomplete; instance and reservation retained: ${result.externalErrors.join('; ')}`);
192
+ stop() {
193
+ // one stop at a time: a second caller (the view shutting down while a switch stops this World) shares the first's
194
+ stopping ??= stopOnce().finally(() => { stopping = null; });
195
+ return stopping;
196
196
  },
197
197
  };
198
+ async function stopOnce(): Promise<void> {
199
+ // refreshes stop first, so none writes into the World while its twins stop; the serve record goes only once they are
200
+ // down. A stop that fails keeps the record (a checkout under the serve still refuses) and re-arms the refreshes:
201
+ // refused before anything stopped, the World is served as it was; failed part way, its record names what remains
202
+ for (const t of timers) clearInterval(t);
203
+ timers = [];
204
+ let result: Awaited<ReturnType<typeof downWorld>>;
205
+ try {
206
+ result = await downWorld(name, worldRoot);
207
+ if (result.externalErrors.length) throw new Error(`World "${name}" shutdown incomplete; instance and reservation retained: ${result.externalErrors.join('; ')}`);
208
+ } catch (error) {
209
+ if (instance) { try { timers = await scheduleRefreshes(name, worldRoot, instance); } catch { /* the stop's error is the one reported */ } }
210
+ throw error;
211
+ }
212
+ try { rmSync(record, { force: true }); } catch { /* already gone */ }
213
+ }
214
+ return mounted;
198
215
  }
199
216
 
200
217
  /** A local World's mirrors: each pack's builders, found once per process, and each client built once. */
package/src/world-view.ts CHANGED
@@ -55,11 +55,8 @@ export async function serveWorldView(name: string, opts: { root?: string; port?:
55
55
  switching = true;
56
56
  const from = world.name;
57
57
  try {
58
- // a stop that fails part way brings this branch back as it was before the error is reported
59
- try { await world.stop(); } catch (error) {
60
- try { await world.boot(url); } catch { /* the stop's own error is the one reported */ }
61
- throw error;
62
- }
58
+ // a stop that is refused or fails leaves this branch served as it was (served-world.ts stop): the switch just fails
59
+ await world.stop();
63
60
  let next: MountedWorld;
64
61
  try {
65
62
  next = await mountWorld(branch, mountOpts);