@volter/world-runtime 2.0.40 → 2.0.41
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 +5 -3
- package/dist/src/origin.js +15 -0
- package/dist/src/runtime.js +26 -0
- package/dist/src/served-world.d.ts +4 -0
- package/dist/src/served-world.js +31 -2
- package/package.json +4 -4
- package/src/branch.ts +3 -1
- package/src/origin.ts +11 -0
- package/src/runtime.ts +21 -0
- package/src/served-world.ts +30 -2
package/dist/src/branch.js
CHANGED
|
@@ -84,10 +84,12 @@ export async function branchWorld(base, name, options = {}) {
|
|
|
84
84
|
}
|
|
85
85
|
const forked = {};
|
|
86
86
|
try {
|
|
87
|
-
|
|
87
|
+
// the branch's origin is its base's, or none: never one a stopped World of the same name left
|
|
88
|
+
if (from.origin)
|
|
88
89
|
instance.origin = { ...from.origin };
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
else
|
|
91
|
+
delete instance.origin;
|
|
92
|
+
saveWorldInstance(instance);
|
|
91
93
|
for (const { service, fromRoot, states } of source) {
|
|
92
94
|
const toRoot = join(instance.dirs.data, service);
|
|
93
95
|
for (const { state, generation, view, position: pos } of states) {
|
package/dist/src/origin.js
CHANGED
|
@@ -8,6 +8,7 @@ import { join, resolve } from 'node:path';
|
|
|
8
8
|
import { getActiveWorldStore, readBranchMeta, withAncestryLock, writeBranchMeta } from '@volter/world-core';
|
|
9
9
|
import { saveWorldInstance, statusWorld } from "./runtime.js";
|
|
10
10
|
import { fetchReady, isPathRemote, localWorld, TOKEN_HEADER } from "./served-world.js";
|
|
11
|
+
import { loadWorldConfig, writeWorldConfig } from "./configs.js";
|
|
11
12
|
function originOf(instance, options) {
|
|
12
13
|
const url = options.url ?? instance.origin?.url;
|
|
13
14
|
const namespace = options.namespace ?? instance.origin?.namespace;
|
|
@@ -132,6 +133,20 @@ export async function fetchFromOrigin(name, options) {
|
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
saveWorldInstance({ ...instance, origin: { ...origin, ...(options.full ? { clonedAt: at_ } : instance.origin?.clonedAt ? { clonedAt: instance.origin.clonedAt } : {}), fetchedAt: at_ } });
|
|
136
|
+
// a clone names its remote where `volter remote add` does (world.json, as git config holds a remote): one record of
|
|
137
|
+
// it, which every restart carries (runtime's namesOrigin) and `remote remove` ends. Only the World the config is
|
|
138
|
+
// for writes it: a runtime branch boots from its base's config, whose remote stays the base's
|
|
139
|
+
if (options.full && instance.name === instance.config) {
|
|
140
|
+
try {
|
|
141
|
+
const { path: configPath, config } = loadWorldConfig(instance.configPath, root);
|
|
142
|
+
const named = isPathRemote(origin.url) ? origin.url : `${origin.url}/${origin.namespace}`;
|
|
143
|
+
if (config.remotes?.origin !== named)
|
|
144
|
+
writeWorldConfig(configPath, { ...config, remotes: { ...(config.remotes ?? {}), origin: named } });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.error(`[origin] ${name}: the remote was not named in its config: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
135
150
|
return { world: name, origin, full: options.full ?? false, appended, behind, snapshots };
|
|
136
151
|
}
|
|
137
152
|
export function worldOrigin(name, root) {
|
package/dist/src/runtime.js
CHANGED
|
@@ -728,6 +728,22 @@ function writePidsFromInstance(instance, fresh = []) {
|
|
|
728
728
|
].filter((pid) => Number.isInteger(pid) && pid > 0))];
|
|
729
729
|
writeOwnedPids(instance.root, instance.name, pids, fresh);
|
|
730
730
|
}
|
|
731
|
+
/** Whether a world.json remote (`https://<host>/<org>/<world>`, or a world's directory, relative to the World's root) is
|
|
732
|
+
* the recorded origin, read as the CLI's parseOriginUrl reads it (the URL's origin, then its two path parts). */
|
|
733
|
+
function namesOrigin(remote, origin, worldRoot) {
|
|
734
|
+
if (!remote)
|
|
735
|
+
return false;
|
|
736
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(remote))
|
|
737
|
+
return resolve(worldRoot, remote) === resolve(worldRoot, origin.url);
|
|
738
|
+
try {
|
|
739
|
+
const parsed = new URL(remote);
|
|
740
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
741
|
+
return parts.length === 2 && parsed.origin === new URL(origin.url).origin && parts.join('/') === origin.namespace;
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
731
747
|
export function saveWorldInstance(instance) {
|
|
732
748
|
writeTextAtomic(instanceFile(instance.root, instance.name), `${JSON.stringify(instance, null, 2)}\n`);
|
|
733
749
|
}
|
|
@@ -1483,6 +1499,7 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1483
1499
|
const pidsFile = join(instance, 'pids');
|
|
1484
1500
|
const envFile = resolve(root, options.envFile ?? join(instance, 'world.env'));
|
|
1485
1501
|
const instanceFile = join(instance, 'instance.json');
|
|
1502
|
+
let carriedOrigin;
|
|
1486
1503
|
const priorOutputInstance = existsSync(instanceFile) ? JSON.parse(readFileSync(instanceFile, 'utf8')) : undefined;
|
|
1487
1504
|
const priorOutput = priorOutputInstance?.name === name && resolve(priorOutputInstance.root) === root
|
|
1488
1505
|
? { path: resolve(priorOutputInstance.envFile), contents: envFileContents(priorOutputInstance.env) } : undefined;
|
|
@@ -1536,6 +1553,14 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1536
1553
|
|| !Number.isFinite(Date.parse(receipt.stoppedAt))) {
|
|
1537
1554
|
throw new Error(`World "${name}" has no verified teardown receipt for its prior instance; use its owning runtime to complete cleanup before replacement`);
|
|
1538
1555
|
}
|
|
1556
|
+
// the origin belongs to the World, not to its running state (git: a remote outlives every checkout): an up of the
|
|
1557
|
+
// same config carries the one the last left while world.json's `origin` remote still names it (`remote remove`
|
|
1558
|
+
// ends it there; an instance under another World's config, a runtime branch, has only its own record); its cursors
|
|
1559
|
+
// only when its state (the mirror they count) is kept, as reset drops them
|
|
1560
|
+
if (prior.origin && prior.config === loaded.config.id && (name !== loaded.config.id || namesOrigin(loaded.config.remotes?.origin, prior.origin, root))) {
|
|
1561
|
+
carriedOrigin = options.keepState ? prior.origin
|
|
1562
|
+
: { url: prior.origin.url, namespace: prior.origin.namespace, ...(prior.origin.clonedAt ? { clonedAt: prior.origin.clonedAt } : {}) };
|
|
1563
|
+
}
|
|
1539
1564
|
}
|
|
1540
1565
|
assertLifecycleStartable(root, name);
|
|
1541
1566
|
beginWorldLifecycle(root, name, createdAt, options.owner);
|
|
@@ -1643,6 +1668,7 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1643
1668
|
...(loaded.config.actors ? { actors: loaded.config.actors } : {}),
|
|
1644
1669
|
...(loaded.config.fixtures ? { fixtures: loaded.config.fixtures } : {}),
|
|
1645
1670
|
lifecycle: { owner: options.owner, log: lifecycleLogPath(root, name) },
|
|
1671
|
+
...(carriedOrigin ? { origin: carriedOrigin } : {}),
|
|
1646
1672
|
};
|
|
1647
1673
|
saveWorldInstance(worldInstance);
|
|
1648
1674
|
const cancelled = new AbortController();
|
|
@@ -268,6 +268,10 @@ export declare class WorldDoors {
|
|
|
268
268
|
/** What this World issued, for world-access to decide a grant against: its two tokens, its named keys, its sessions. */
|
|
269
269
|
private issued;
|
|
270
270
|
private credential;
|
|
271
|
+
/** Each changeset's actions' receipts, from the landed copies on each twin's log: a deploy appends one per entry it
|
|
272
|
+
* performed, and a push one per entry it landed at the remote (`landed` while the remote waits, beside the
|
|
273
|
+
* changeset's own `applied`). */
|
|
274
|
+
private deployedOf;
|
|
271
275
|
/** Who made a request, as this World names it: `key:<name>` for a named key, `person:<who>` for a browser's session
|
|
272
276
|
* (`person` when it names no one), `token` / `token:read` for the World's own tokens; null when it presented nothing
|
|
273
277
|
* this World issued. */
|
package/dist/src/served-world.js
CHANGED
|
@@ -658,6 +658,33 @@ export class WorldDoors {
|
|
|
658
658
|
return { scope: grant.scope, via: 'session', token: null, session: grant.session, ...(grant.who ? { who: grant.who } : {}) };
|
|
659
659
|
return { scope: grant.scope, via: grant.via === 'key' ? 'key' : 'token', token: grant.presented ?? null };
|
|
660
660
|
}
|
|
661
|
+
/** Each changeset's actions' receipts, from the landed copies on each twin's log: a deploy appends one per entry it
|
|
662
|
+
* performed, and a push one per entry it landed at the remote (`landed` while the remote waits, beside the
|
|
663
|
+
* changeset's own `applied`). */
|
|
664
|
+
deployedOf(changesets) {
|
|
665
|
+
// by the state each action was written in (a twin may hold several), and a settled receipt wins over a provisional
|
|
666
|
+
// `landed` one whichever comes later in the log (a push's local copy, then the remote's deploy fetched in)
|
|
667
|
+
const landed = new Map();
|
|
668
|
+
const of = (service, state) => {
|
|
669
|
+
const at = `${service}\u0000${state}`;
|
|
670
|
+
let held = landed.get(at);
|
|
671
|
+
if (!held) {
|
|
672
|
+
held = new Map();
|
|
673
|
+
if (this.up().services[service]) {
|
|
674
|
+
for (const e of parentEntries(state, this.controlRoot(service))) {
|
|
675
|
+
if (!e.landsId || !e.receipt)
|
|
676
|
+
continue;
|
|
677
|
+
const before = held.get(e.landsId);
|
|
678
|
+
if (!before || before.status === 'landed' || e.receipt.status !== 'landed')
|
|
679
|
+
held.set(e.landsId, e.receipt);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
landed.set(at, held);
|
|
683
|
+
}
|
|
684
|
+
return held;
|
|
685
|
+
};
|
|
686
|
+
return changesets.map((c) => c.actions.flatMap((a) => { const r = of(a.service, a.stateService ?? this.stateOf(a.service)).get(a.action.id); return r ? [{ service: a.service, actionId: a.action.id, ...r }] : []; }));
|
|
687
|
+
}
|
|
661
688
|
/** Who made a request, as this World names it: `key:<name>` for a named key, `person:<who>` for a browser's session
|
|
662
689
|
* (`person` when it names no one), `token` / `token:read` for the World's own tokens; null when it presented nothing
|
|
663
690
|
* this World issued. */
|
|
@@ -1198,9 +1225,11 @@ export class WorldDoors {
|
|
|
1198
1225
|
// with the body's hash as it stands, so a reader sees an approval or a verification made on other bytes as stale
|
|
1199
1226
|
if (name) {
|
|
1200
1227
|
const l = findWorldChangeset(name, { root: this.worldRoot, world: this.name });
|
|
1201
|
-
return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset) });
|
|
1228
|
+
return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset), deployed: this.deployedOf([l.changeset])[0] });
|
|
1202
1229
|
}
|
|
1203
|
-
|
|
1230
|
+
const all = listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => l.changeset);
|
|
1231
|
+
const deployed = this.deployedOf(all);
|
|
1232
|
+
return Response.json({ changesets: all.map((c, i) => ({ ...c, currentHash: changesetContentHash(c), deployed: deployed[i] })) });
|
|
1204
1233
|
}
|
|
1205
1234
|
if (kind === 'changesets' && name && verb === 'verify' && request.method === 'POST') {
|
|
1206
1235
|
const outcome = await this.serialized('changesets', () => verifyWorldChangeset(name, { root: this.worldRoot, world: this.name, ephemeral: true }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.41",
|
|
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.
|
|
70
|
+
"@volter/world-core": "2.0.23",
|
|
71
71
|
"@volter/world-console": "2.0.13"
|
|
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.2",
|
|
77
|
-
"@volter/world-core": "2.0.
|
|
77
|
+
"@volter/world-core": "2.0.23",
|
|
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.23"
|
|
88
88
|
}
|
|
89
89
|
}
|
package/src/branch.ts
CHANGED
|
@@ -69,7 +69,9 @@ export async function branchWorld(base: string, name: string, options: BranchOpt
|
|
|
69
69
|
}
|
|
70
70
|
const forked: Record<string, string[]> = {};
|
|
71
71
|
try {
|
|
72
|
-
|
|
72
|
+
// the branch's origin is its base's, or none: never one a stopped World of the same name left
|
|
73
|
+
if (from.origin) instance.origin = { ...from.origin }; else delete instance.origin;
|
|
74
|
+
saveWorldInstance(instance);
|
|
73
75
|
for (const { service, fromRoot, states } of source) {
|
|
74
76
|
const toRoot = join(instance.dirs.data, service);
|
|
75
77
|
for (const { state, generation, view, position: pos } of states) {
|
package/src/origin.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { appendOriginEntry, appendParentEntry, branchEntries, getActiveWorldStor
|
|
|
9
9
|
import { saveWorldInstance, statusWorld } from './runtime.ts';
|
|
10
10
|
import type { WorldInstance } from './schema.ts';
|
|
11
11
|
import { fetchReady, isPathRemote, localWorld, TOKEN_HEADER } from './served-world.ts';
|
|
12
|
+
import { loadWorldConfig, writeWorldConfig } from './configs.ts';
|
|
12
13
|
|
|
13
14
|
export type FetchOptions = { root?: string; url?: string; namespace?: string; key: string; full?: boolean; services?: string[]; views?: Record<string, HistoryReference>; now?: Date };
|
|
14
15
|
export type FetchOutcome = {
|
|
@@ -126,6 +127,16 @@ export async function fetchFromOrigin(name: string, options: FetchOptions): Prom
|
|
|
126
127
|
|
|
127
128
|
}
|
|
128
129
|
saveWorldInstance({ ...instance, origin: { ...origin, ...(options.full ? { clonedAt: at_ } : instance.origin?.clonedAt ? { clonedAt: instance.origin.clonedAt } : {}), fetchedAt: at_ } });
|
|
130
|
+
// a clone names its remote where `volter remote add` does (world.json, as git config holds a remote): one record of
|
|
131
|
+
// it, which every restart carries (runtime's namesOrigin) and `remote remove` ends. Only the World the config is
|
|
132
|
+
// for writes it: a runtime branch boots from its base's config, whose remote stays the base's
|
|
133
|
+
if (options.full && instance.name === instance.config) {
|
|
134
|
+
try {
|
|
135
|
+
const { path: configPath, config } = loadWorldConfig(instance.configPath, root);
|
|
136
|
+
const named = isPathRemote(origin.url) ? origin.url : `${origin.url}/${origin.namespace}`;
|
|
137
|
+
if (config.remotes?.origin !== named) writeWorldConfig(configPath, { ...config, remotes: { ...(config.remotes ?? {}), origin: named } });
|
|
138
|
+
} catch (error) { console.error(`[origin] ${name}: the remote was not named in its config: ${(error as Error).message}`); }
|
|
139
|
+
}
|
|
129
140
|
return { world: name, origin, full: options.full ?? false, appended, behind, snapshots };
|
|
130
141
|
}
|
|
131
142
|
|
package/src/runtime.ts
CHANGED
|
@@ -757,6 +757,17 @@ function writePidsFromInstance(instance: WorldInstance, fresh: number[] = []): v
|
|
|
757
757
|
writeOwnedPids(instance.root, instance.name, pids, fresh);
|
|
758
758
|
}
|
|
759
759
|
|
|
760
|
+
/** Whether a world.json remote (`https://<host>/<org>/<world>`, or a world's directory, relative to the World's root) is
|
|
761
|
+
* the recorded origin, read as the CLI's parseOriginUrl reads it (the URL's origin, then its two path parts). */
|
|
762
|
+
function namesOrigin(remote: string | undefined, origin: { url: string; namespace: string }, worldRoot: string): boolean {
|
|
763
|
+
if (!remote) return false;
|
|
764
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(remote)) return resolve(worldRoot, remote) === resolve(worldRoot, origin.url);
|
|
765
|
+
try {
|
|
766
|
+
const parsed = new URL(remote); const parts = parsed.pathname.split('/').filter(Boolean);
|
|
767
|
+
return parts.length === 2 && parsed.origin === new URL(origin.url).origin && parts.join('/') === origin.namespace;
|
|
768
|
+
} catch { return false; }
|
|
769
|
+
}
|
|
770
|
+
|
|
760
771
|
export function saveWorldInstance(instance: WorldInstance): void {
|
|
761
772
|
writeTextAtomic(instanceFile(instance.root, instance.name), `${JSON.stringify(instance, null, 2)}\n`);
|
|
762
773
|
}
|
|
@@ -1483,6 +1494,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
1483
1494
|
const pidsFile = join(instance, 'pids');
|
|
1484
1495
|
const envFile = resolve(root, options.envFile ?? join(instance, 'world.env'));
|
|
1485
1496
|
const instanceFile = join(instance, 'instance.json');
|
|
1497
|
+
let carriedOrigin: WorldInstance['origin'] | undefined;
|
|
1486
1498
|
const priorOutputInstance = existsSync(instanceFile) ? JSON.parse(readFileSync(instanceFile, 'utf8')) as WorldInstance : undefined;
|
|
1487
1499
|
const priorOutput = priorOutputInstance?.name === name && resolve(priorOutputInstance.root) === root
|
|
1488
1500
|
? { path: resolve(priorOutputInstance.envFile), contents: envFileContents(priorOutputInstance.env) } : undefined;
|
|
@@ -1537,6 +1549,14 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
1537
1549
|
|| !Number.isFinite(Date.parse(receipt.stoppedAt))) {
|
|
1538
1550
|
throw new Error(`World "${name}" has no verified teardown receipt for its prior instance; use its owning runtime to complete cleanup before replacement`);
|
|
1539
1551
|
}
|
|
1552
|
+
// the origin belongs to the World, not to its running state (git: a remote outlives every checkout): an up of the
|
|
1553
|
+
// same config carries the one the last left while world.json's `origin` remote still names it (`remote remove`
|
|
1554
|
+
// ends it there; an instance under another World's config, a runtime branch, has only its own record); its cursors
|
|
1555
|
+
// only when its state (the mirror they count) is kept, as reset drops them
|
|
1556
|
+
if (prior.origin && prior.config === loaded.config.id && (name !== loaded.config.id || namesOrigin(loaded.config.remotes?.origin, prior.origin, root))) {
|
|
1557
|
+
carriedOrigin = options.keepState ? prior.origin
|
|
1558
|
+
: { url: prior.origin.url, namespace: prior.origin.namespace, ...(prior.origin.clonedAt ? { clonedAt: prior.origin.clonedAt } : {}) };
|
|
1559
|
+
}
|
|
1540
1560
|
}
|
|
1541
1561
|
assertLifecycleStartable(root, name);
|
|
1542
1562
|
beginWorldLifecycle(root, name, createdAt, options.owner);
|
|
@@ -1622,6 +1642,7 @@ export async function upWorld(configId: string, options: UpWorldOptions = {}, ab
|
|
|
1622
1642
|
...(loaded.config.actors ? { actors: loaded.config.actors } : {}),
|
|
1623
1643
|
...(loaded.config.fixtures ? { fixtures: loaded.config.fixtures } : {}),
|
|
1624
1644
|
lifecycle: { owner: options.owner, log: lifecycleLogPath(root, name) },
|
|
1645
|
+
...(carriedOrigin ? { origin: carriedOrigin } : {}),
|
|
1625
1646
|
};
|
|
1626
1647
|
saveWorldInstance(worldInstance);
|
|
1627
1648
|
const cancelled = new AbortController();
|
package/src/served-world.ts
CHANGED
|
@@ -595,6 +595,32 @@ export class WorldDoors {
|
|
|
595
595
|
if (grant.via === 'session') return { scope: grant.scope, via: 'session', token: null, session: grant.session!, ...(grant.who ? { who: grant.who } : {}) };
|
|
596
596
|
return { scope: grant.scope, via: grant.via === 'key' ? 'key' : 'token', token: grant.presented ?? null };
|
|
597
597
|
}
|
|
598
|
+
/** Each changeset's actions' receipts, from the landed copies on each twin's log: a deploy appends one per entry it
|
|
599
|
+
* performed, and a push one per entry it landed at the remote (`landed` while the remote waits, beside the
|
|
600
|
+
* changeset's own `applied`). */
|
|
601
|
+
private deployedOf(changesets: Changeset[]): Array<Array<{ service: string; actionId: string } & Receipt>> {
|
|
602
|
+
// by the state each action was written in (a twin may hold several), and a settled receipt wins over a provisional
|
|
603
|
+
// `landed` one whichever comes later in the log (a push's local copy, then the remote's deploy fetched in)
|
|
604
|
+
const landed = new Map<string, Map<string, Receipt>>();
|
|
605
|
+
const of = (service: string, state: string): Map<string, Receipt> => {
|
|
606
|
+
const at = `${service}\u0000${state}`;
|
|
607
|
+
let held = landed.get(at);
|
|
608
|
+
if (!held) {
|
|
609
|
+
held = new Map();
|
|
610
|
+
if (this.up().services[service]) {
|
|
611
|
+
for (const e of parentEntries(state, this.controlRoot(service))) {
|
|
612
|
+
if (!e.landsId || !e.receipt) continue;
|
|
613
|
+
const before = held.get(e.landsId);
|
|
614
|
+
if (!before || before.status === 'landed' || e.receipt.status !== 'landed') held.set(e.landsId, e.receipt);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
landed.set(at, held);
|
|
618
|
+
}
|
|
619
|
+
return held;
|
|
620
|
+
};
|
|
621
|
+
return changesets.map((c) => c.actions.flatMap((a) => { const r = of(a.service, a.stateService ?? this.stateOf(a.service)).get(a.action.id); return r ? [{ service: a.service, actionId: a.action.id, ...r }] : []; }));
|
|
622
|
+
}
|
|
623
|
+
|
|
598
624
|
/** Who made a request, as this World names it: `key:<name>` for a named key, `person:<who>` for a browser's session
|
|
599
625
|
* (`person` when it names no one), `token` / `token:read` for the World's own tokens; null when it presented nothing
|
|
600
626
|
* this World issued. */
|
|
@@ -1040,8 +1066,10 @@ export class WorldDoors {
|
|
|
1040
1066
|
}
|
|
1041
1067
|
if (kind === 'changesets' && request.method === 'GET') {
|
|
1042
1068
|
// with the body's hash as it stands, so a reader sees an approval or a verification made on other bytes as stale
|
|
1043
|
-
if (name) { const l = findWorldChangeset(name, { root: this.worldRoot, world: this.name }); return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset) }); }
|
|
1044
|
-
|
|
1069
|
+
if (name) { const l = findWorldChangeset(name, { root: this.worldRoot, world: this.name }); return Response.json({ ...l.changeset, currentHash: changesetContentHash(l.changeset), deployed: this.deployedOf([l.changeset])[0] }); }
|
|
1070
|
+
const all = listWorldChangesets({ root: this.worldRoot, world: this.name }).map((l) => l.changeset);
|
|
1071
|
+
const deployed = this.deployedOf(all);
|
|
1072
|
+
return Response.json({ changesets: all.map((c, i) => ({ ...c, currentHash: changesetContentHash(c), deployed: deployed[i] })) });
|
|
1045
1073
|
}
|
|
1046
1074
|
if (kind === 'changesets' && name && verb === 'verify' && request.method === 'POST') {
|
|
1047
1075
|
const outcome = await this.serialized('changesets', () => verifyWorldChangeset(name, { root: this.worldRoot, world: this.name, ephemeral: true }));
|