@volter/world-runtime 2.0.14 → 2.0.15

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.
@@ -55,22 +55,26 @@ export async function branchWorld(base, name, options = {}) {
55
55
  catch {
56
56
  return false;
57
57
  } })();
58
+ const began = Date.now();
58
59
  let instance;
59
60
  try {
60
61
  instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, envFile });
61
62
  }
62
63
  catch (error) {
63
- let recorded = false;
64
+ // only a record made during this call is ours to remove, and it is removed by its own createdAt, as the fork's
65
+ // cleanup below does: a concurrent `branch` of the same name that recorded first keeps its World
66
+ let recorded = null;
64
67
  if (!existed) {
65
68
  try {
66
- statusWorld(name, root);
67
- recorded = true;
69
+ const made = statusWorld(name, root).createdAt;
70
+ if (Date.parse(made) >= began)
71
+ recorded = made;
68
72
  }
69
73
  catch { /* the boot recorded nothing */ }
70
74
  }
71
75
  if (recorded) {
72
76
  try {
73
- await downWorld(name, root, { purge: true });
77
+ await downWorld(name, root, { purge: true, expectedCreatedAt: recorded });
74
78
  }
75
79
  catch (cleanup) {
76
80
  throw new Error(`${error instanceof Error ? error.message : String(error)} (and the half-made branch ${name} could not be removed: ${cleanup instanceof Error ? cleanup.message : String(cleanup)})`);
@@ -166,11 +166,32 @@ export async function mountWorld(name, opts = {}) {
166
166
  keep();
167
167
  const port = Number(new URL(url).port) || 0;
168
168
  writeFileSync(record, `${JSON.stringify({ name: served, url, base: `${url}/${served}`, port, pid: process.pid, startedAt: new Date().toISOString() }, null, 2)}\n`);
169
- const booted = await upWorld(configRef, { name, root: worldRoot, keepState: true });
170
- instance = booted;
171
- // a root set in the config while the world was stopped reaches the twins' state now, where the deploy reads it
172
- materializeRoots(loadWorldConfig(configRef, worldRoot).config, booted.dirs.data, worldRoot);
173
- timers = await scheduleRefreshes(name, worldRoot, booted);
169
+ // a boot that fails leaves nothing claimed: no serve record naming this process (a later mount of this World would
170
+ // refuse as "already served"), and no twins it started; twins another owner runs were never ours to stop
171
+ let booted = null;
172
+ try {
173
+ booted = await upWorld(configRef, { name, root: worldRoot, keepState: true });
174
+ instance = booted;
175
+ // a root set in the config while the world was stopped reaches the twins' state now, where the deploy reads it
176
+ materializeRoots(loadWorldConfig(configRef, worldRoot).config, booted.dirs.data, worldRoot);
177
+ timers = await scheduleRefreshes(name, worldRoot, booted);
178
+ }
179
+ catch (error) {
180
+ for (const t of timers)
181
+ clearInterval(t);
182
+ timers = [];
183
+ if (booted) {
184
+ try {
185
+ await downWorld(name, worldRoot);
186
+ }
187
+ catch { /* the boot's own error is the one reported */ }
188
+ }
189
+ try {
190
+ rmSync(record, { force: true });
191
+ }
192
+ catch { /* already gone */ }
193
+ throw error;
194
+ }
174
195
  },
175
196
  async stop() {
176
197
  for (const t of timers)
@@ -1438,7 +1459,7 @@ export class WorldDoors {
1438
1459
  named = listWorlds(this.worldRoot).map((w) => ({ name: w.name, running: w.running }));
1439
1460
  }
1440
1461
  catch { /* a World with no local branches */ }
1441
- return Response.json({ world: this.served, branches: await branches.list(), checkedOut: this.name, named });
1462
+ return Response.json({ world: this.served, branches: await branches.list(), checkedOut: this.name, named, canServe: this.host.serveBranch !== undefined });
1442
1463
  }
1443
1464
  // SERVE ANOTHER NAMED BRANCH here (`POST branches/<branch>/serve`): only who may write the World switches what it serves
1444
1465
  if (rest.length === 2 && rest[1] === 'serve' && request.method === 'POST') {
@@ -55,13 +55,24 @@ export async function serveWorldView(name, opts = {}) {
55
55
  switching = true;
56
56
  const from = world.name;
57
57
  try {
58
- await world.stop();
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
+ }
59
69
  let next;
60
70
  try {
61
71
  next = await mountWorld(branch, mountOpts);
62
72
  await next.boot(url);
63
73
  }
64
74
  catch (error) {
75
+ // the target's failed boot released its claim (served-world.ts boot), so the branch it replaced mounts again
65
76
  const back = await mountWorld(from, mountOpts);
66
77
  await back.boot(url);
67
78
  worlds.set(back.served, back);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
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.8",
71
- "@volter/world-console": "2.0.8"
70
+ "@volter/world-core": "2.0.9",
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.8",
77
+ "@volter/world-core": "2.0.9",
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.8"
87
+ "@volter/twin-mongodb": "0.1.9"
88
88
  }
89
89
  }
package/src/branch.ts CHANGED
@@ -51,14 +51,17 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
51
51
  // a branch that fails to boot (a twin whose mounted version breaks the config's pin) leaves no half-made branch behind:
52
52
  // its record without a fork would check out later as an empty World
53
53
  const existed = (() => { try { statusWorld(name, root); return true; } catch { return false; } })();
54
+ const began = Date.now();
54
55
  let instance: WorldInstance;
55
56
  try {
56
57
  instance = await upWorld(existsSync(from.configPath) ? from.configPath : from.config, { name, root, mode: from.mode, envFile });
57
58
  } catch (error) {
58
- let recorded = false;
59
- if (!existed) { try { statusWorld(name, root); recorded = true; } catch { /* the boot recorded nothing */ } }
59
+ // only a record made during this call is ours to remove, and it is removed by its own createdAt, as the fork's
60
+ // cleanup below does: a concurrent `branch` of the same name that recorded first keeps its World
61
+ let recorded: string | null = null;
62
+ if (!existed) { try { const made = statusWorld(name, root).createdAt; if (Date.parse(made) >= began) recorded = made; } catch { /* the boot recorded nothing */ } }
60
63
  if (recorded) {
61
- try { await downWorld(name, root, { purge: true }); } catch (cleanup) {
64
+ try { await downWorld(name, root, { purge: true, expectedCreatedAt: recorded }); } catch (cleanup) {
62
65
  throw new Error(`${error instanceof Error ? error.message : String(error)} (and the half-made branch ${name} could not be removed: ${cleanup instanceof Error ? cleanup.message : String(cleanup)})`);
63
66
  }
64
67
  }
@@ -171,11 +171,22 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
171
171
  keep();
172
172
  const port = Number(new URL(url).port) || 0;
173
173
  writeFileSync(record, `${JSON.stringify({ name: served, url, base: `${url}/${served}`, port, pid: process.pid, startedAt: new Date().toISOString() }, null, 2)}\n`);
174
- const booted = await upWorld(configRef, { name, root: worldRoot, keepState: true });
175
- instance = booted;
176
- // a root set in the config while the world was stopped reaches the twins' state now, where the deploy reads it
177
- materializeRoots(loadWorldConfig(configRef, worldRoot).config, booted.dirs.data, worldRoot);
178
- timers = await scheduleRefreshes(name, worldRoot, booted);
174
+ // a boot that fails leaves nothing claimed: no serve record naming this process (a later mount of this World would
175
+ // refuse as "already served"), and no twins it started; twins another owner runs were never ours to stop
176
+ let booted: WorldInstance | null = null;
177
+ try {
178
+ booted = await upWorld(configRef, { name, root: worldRoot, keepState: true });
179
+ instance = booted;
180
+ // a root set in the config while the world was stopped reaches the twins' state now, where the deploy reads it
181
+ materializeRoots(loadWorldConfig(configRef, worldRoot).config, booted.dirs.data, worldRoot);
182
+ timers = await scheduleRefreshes(name, worldRoot, booted);
183
+ } catch (error) {
184
+ for (const t of timers) clearInterval(t);
185
+ timers = [];
186
+ if (booted) { try { await downWorld(name, worldRoot); } catch { /* the boot's own error is the one reported */ } }
187
+ try { rmSync(record, { force: true }); } catch { /* already gone */ }
188
+ throw error;
189
+ }
179
190
  },
180
191
  async stop() {
181
192
  for (const t of timers) clearInterval(t);
@@ -1234,7 +1245,7 @@ export class WorldDoors {
1234
1245
  // `checkedOut` is the one this serve holds, so a page never names the World for the branch it shows
1235
1246
  let named: Array<{ name: string; running: boolean }> = [];
1236
1247
  try { named = listWorlds(this.worldRoot).map((w) => ({ name: w.name, running: w.running })); } catch { /* a World with no local branches */ }
1237
- return Response.json({ world: this.served, branches: await branches.list(), checkedOut: this.name, named });
1248
+ return Response.json({ world: this.served, branches: await branches.list(), checkedOut: this.name, named, canServe: this.host.serveBranch !== undefined });
1238
1249
  }
1239
1250
  // SERVE ANOTHER NAMED BRANCH here (`POST branches/<branch>/serve`): only who may write the World switches what it serves
1240
1251
  if (rest.length === 2 && rest[1] === 'serve' && request.method === 'POST') {
package/src/world-view.ts CHANGED
@@ -55,12 +55,17 @@ export async function serveWorldView(name: string, opts: { root?: string; port?:
55
55
  switching = true;
56
56
  const from = world.name;
57
57
  try {
58
- await world.stop();
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
+ }
59
63
  let next: MountedWorld;
60
64
  try {
61
65
  next = await mountWorld(branch, mountOpts);
62
66
  await next.boot(url);
63
67
  } catch (error) {
68
+ // the target's failed boot released its claim (served-world.ts boot), so the branch it replaced mounts again
64
69
  const back = await mountWorld(from, mountOpts);
65
70
  await back.boot(url);
66
71
  worlds.set(back.served, back); world = back;