@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.
- package/dist/src/branch.js +8 -4
- package/dist/src/served-world.js +27 -6
- package/dist/src/world-view.js +12 -1
- package/package.json +5 -5
- package/src/branch.ts +6 -3
- package/src/served-world.ts +17 -6
- package/src/world-view.ts +6 -1
package/dist/src/branch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)})`);
|
package/dist/src/served-world.js
CHANGED
|
@@ -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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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') {
|
package/dist/src/world-view.js
CHANGED
|
@@ -55,13 +55,24 @@ export async function serveWorldView(name, opts = {}) {
|
|
|
55
55
|
switching = true;
|
|
56
56
|
const from = world.name;
|
|
57
57
|
try {
|
|
58
|
-
|
|
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.
|
|
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.
|
|
71
|
-
"@volter/world-console": "2.0.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
59
|
-
|
|
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
|
}
|
package/src/served-world.ts
CHANGED
|
@@ -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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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;
|