@volter/world-runtime 3.0.25 → 3.0.27
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/local-branches.js +4 -0
- package/dist/src/served-world.d.ts +4 -0
- package/dist/src/served-world.js +57 -9
- package/package.json +4 -4
- package/src/local-branches.ts +4 -0
- package/src/served-world.ts +51 -10
|
@@ -84,6 +84,10 @@ export class LocalBranches {
|
|
|
84
84
|
create: (at, ttl, _origin, parentKey, byKey) => this.create(parent, at, ttl, parentKey, byKey),
|
|
85
85
|
remove: async (name) => { const w = this.o.worlds.get(name); if (!w || this.record(w)?.from !== parent)
|
|
86
86
|
return false; await this.remove(name); return true; },
|
|
87
|
+
board: async (name, frame) => {
|
|
88
|
+
const w = this.o.worlds.get(name);
|
|
89
|
+
return w && this.record(w)?.from === parent ? w.branchBoard(frame) : null;
|
|
90
|
+
},
|
|
87
91
|
// a branch of this parent only, served in this process beside it: its own content plane's pass
|
|
88
92
|
pagesPass: async (name, scope, land, grant) => {
|
|
89
93
|
const w = this.o.worlds.get(name);
|
|
@@ -63,6 +63,7 @@ export type MountedWorld = {
|
|
|
63
63
|
framer?: string;
|
|
64
64
|
asker?: string;
|
|
65
65
|
}) => string | null;
|
|
66
|
+
branchBoard: (frame?: string) => Promise<Response>;
|
|
66
67
|
boot: (url: string) => Promise<void>;
|
|
67
68
|
stop: () => Promise<void>;
|
|
68
69
|
};
|
|
@@ -208,6 +209,8 @@ export type BranchDoors = {
|
|
|
208
209
|
framer?: string;
|
|
209
210
|
asker?: string;
|
|
210
211
|
}) => Promise<string | null>;
|
|
212
|
+
/** the branch's own board, counting from its fork; a frame names its changes alone */
|
|
213
|
+
board?: (name: string, frame?: string) => Promise<Response | null>;
|
|
211
214
|
};
|
|
212
215
|
/** A World's browser-origin label, `<world>--<org>`, or null when its names cannot make one (a DNS
|
|
213
216
|
* label: lowercase letters, digits and single hyphens, 63 at most). A host maps a label back by the
|
|
@@ -444,6 +447,7 @@ export declare class WorldDoors {
|
|
|
444
447
|
* screen draws yet) and the changes not yet cut on the records it covers, counted by the request that made them, with
|
|
445
448
|
* when the first was made; and the boards as this World's people arranged them. A frame's changes themselves are read
|
|
446
449
|
* one frame at a time (`board/changes`). */
|
|
450
|
+
branchBoard(frame?: string): Promise<Response>;
|
|
447
451
|
private board;
|
|
448
452
|
/** One frame's changes not yet cut, newest first: each the writes one request made, as words read them (the operation,
|
|
449
453
|
* the record, its few naming fields), the frame named `<vendor>:<frame id>`. */
|
package/dist/src/served-world.js
CHANGED
|
@@ -18,7 +18,11 @@ import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant
|
|
|
18
18
|
// GET map | timeline | diff every twin and what it holds; the logs merged newest first; the changes since the base
|
|
19
19
|
// GET/PUT clock, POST clock/advance the frozen instant every twin stamps from, forward only
|
|
20
20
|
// GET/POST branches, DELETE branches/<org>/<world> the host's branches of this World ("as of" views)
|
|
21
|
+
// GET branches/<org>/<world>/board[/changes] a branch's pages and changes from its fork, through its host
|
|
22
|
+
// GET branches/<org>/<world>/pages?land=<path> a read pass to the branch's own content plane
|
|
23
|
+
// GET/PUT board, GET board/changes the pages and their changes; the boards as people arranged them
|
|
21
24
|
// GET history?at=<instant> each twin's history cut at an instant (where such a branch starts)
|
|
25
|
+
// GET/POST marks where the World's changes count from; POST { note? } marks it now
|
|
22
26
|
// GET /<org>/<world>/<vendor>/<path> the vendor's own screens, the pack's workspaces (its `GET /twin` lists them)
|
|
23
27
|
// Tokens ride `x-volter-token`: the token opens everything, the read token only reads — on the vendor
|
|
24
28
|
// wire, a twin that enforces it (`requestScopes: ['read']`) is handed the read token's other requests
|
|
@@ -33,7 +37,7 @@ import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass } from '
|
|
|
33
37
|
import { createHash } from 'node:crypto';
|
|
34
38
|
import { dirname, join, resolve } from 'node:path';
|
|
35
39
|
import { appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, changesetReadiness, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow, parentEntries, readBranchMeta, rebaseBranch, } from '@volter/world-core/runtime';
|
|
36
|
-
import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from "./changeset.js";
|
|
40
|
+
import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, listWorldMarks, markWorld, MARK_NOTE_MAX, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from "./changeset.js";
|
|
37
41
|
import { fetchFromOrigin } from "./origin.js";
|
|
38
42
|
import { CONSOLE_BASE, consoleRedirect, serveConsoleApart } from "./console-apart.js";
|
|
39
43
|
import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from "./configs.js";
|
|
@@ -215,6 +219,7 @@ export async function mountWorld(name, opts = {}) {
|
|
|
215
219
|
handle: (request) => world.handle(request),
|
|
216
220
|
site: (request, host) => world.site(request, host),
|
|
217
221
|
pagesPass: (scope, land, grant) => world.pagesPass(scope, land, grant),
|
|
222
|
+
branchBoard: (frame) => world.branchBoard(frame),
|
|
218
223
|
async boot(url) {
|
|
219
224
|
keep();
|
|
220
225
|
const port = Number(new URL(url).port) || 0;
|
|
@@ -1543,6 +1548,30 @@ export class WorldDoors {
|
|
|
1543
1548
|
views[vendor] = historyAtInstant(this.stateOf(vendor), instant, this.controlRoot(vendor)); });
|
|
1544
1549
|
return Response.json({ world: this.served, at: new Date(Date.parse(instant)).toISOString(), views });
|
|
1545
1550
|
}
|
|
1551
|
+
// THE WORLD'S MARKS (`GET marks`, `POST marks { note? }`): where its changes count from. A World seeded through its
|
|
1552
|
+
// doors (a hosted one has no seed run to land its starting data as already there) is marked once it is set up, so
|
|
1553
|
+
// what it starts with is not counted as its changes; the latest mark is the base of the diff, the board's Changes
|
|
1554
|
+
// and a new changeset, as `volter-world mark <world>` makes it on this machine
|
|
1555
|
+
if (kind === 'marks' && !name && request.method === 'GET')
|
|
1556
|
+
return Response.json({ world: this.served, marks: listWorldMarks(this.name, this.worldRoot) });
|
|
1557
|
+
if (kind === 'marks' && !name && request.method === 'POST') {
|
|
1558
|
+
const body = (await request.json().catch(() => ({})));
|
|
1559
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
1560
|
+
return Response.json({ error: 'a mark is sent as a JSON object' }, { status: 400 });
|
|
1561
|
+
if (body.note !== undefined && (typeof body.note !== 'string' || body.note.length > MARK_NOTE_MAX))
|
|
1562
|
+
return Response.json({ error: `a mark's note is a string of at most ${MARK_NOTE_MAX} characters` }, { status: 400 });
|
|
1563
|
+
const vendors = Object.keys(this.up().services);
|
|
1564
|
+
// taken while no twin writes, so it stands at one position of every log
|
|
1565
|
+
const { mark, passed } = withHistoryLocks(vendors.map((vendor) => worldPaths(this.stateOf(vendor), this.controlRoot(vendor)).dir), () => {
|
|
1566
|
+
const delta = diffWorld(this.name, { root: this.worldRoot });
|
|
1567
|
+
// what another World pushed here is in its changeset already: never this World's uncut changes
|
|
1568
|
+
const received = new Set(listWorldChangesets({ root: this.worldRoot, world: this.name }).filter((l) => l.changeset.pushedBy).flatMap((l) => l.changeset.actions.map((a) => a.action.id)));
|
|
1569
|
+
const passed = delta.actions.filter((a) => !received.has(a.action.id)).length;
|
|
1570
|
+
const mark = markWorld(this.name, { root: this.worldRoot, ...(typeof body.note === 'string' && body.note.trim() ? { note: body.note } : {}) });
|
|
1571
|
+
return { mark, passed };
|
|
1572
|
+
});
|
|
1573
|
+
return Response.json({ world: this.served, mark, passed }, { status: 201 });
|
|
1574
|
+
}
|
|
1546
1575
|
if (kind === 'map' && !name && request.method === 'GET')
|
|
1547
1576
|
return Response.json(await this.map());
|
|
1548
1577
|
if (kind === 'board' && !name && request.method === 'GET')
|
|
@@ -2096,6 +2125,16 @@ export class WorldDoors {
|
|
|
2096
2125
|
* and removes them; a host that makes none answers 404, and a view offers no "as of". */
|
|
2097
2126
|
async branchesDoor(request, url, rest, via) {
|
|
2098
2127
|
const branches = this.host.branches;
|
|
2128
|
+
if (rest[2] === 'board' && request.method === 'GET' && (rest.length === 3 || (rest.length === 4 && rest[3] === 'changes'))) {
|
|
2129
|
+
if (!branches)
|
|
2130
|
+
return Response.json({ error: `this host cannot read branches of ${this.served}` }, { status: 501 });
|
|
2131
|
+
const name = `${decodeURIComponent(rest[0])}/${decodeURIComponent(rest[1])}`;
|
|
2132
|
+
if (!(await branches.list()).some((b) => b.name === name))
|
|
2133
|
+
return Response.json({ error: `no branch ${name} of ${this.served}` }, { status: 404 });
|
|
2134
|
+
if (!branches.board)
|
|
2135
|
+
return Response.json({ error: `this host cannot read ${name}'s board` }, { status: 501 });
|
|
2136
|
+
return (await branches.board(name, rest[3] === 'changes' ? url.searchParams.get('frame') ?? '' : undefined)) ?? Response.json({ error: `no branch ${name} of ${this.served}` }, { status: 404 });
|
|
2137
|
+
}
|
|
2099
2138
|
if (!branches)
|
|
2100
2139
|
return Response.json({ error: `this host makes no branches of ${this.served}` }, { status: 404 });
|
|
2101
2140
|
if (rest.length === 0 && request.method === 'GET') {
|
|
@@ -2230,18 +2269,26 @@ export class WorldDoors {
|
|
|
2230
2269
|
* screen draws yet) and the changes not yet cut on the records it covers, counted by the request that made them, with
|
|
2231
2270
|
* when the first was made; and the boards as this World's people arranged them. A frame's changes themselves are read
|
|
2232
2271
|
* one frame at a time (`board/changes`). */
|
|
2233
|
-
async
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2272
|
+
async branchBoard(frame) {
|
|
2273
|
+
const url = new URL(this.host.browserOrigin?.() ?? 'http://world.local');
|
|
2274
|
+
if (frame !== undefined) {
|
|
2275
|
+
url.searchParams.set('frame', frame);
|
|
2276
|
+
return this.boardChanges(url, true);
|
|
2277
|
+
}
|
|
2278
|
+
return Response.json(await this.board(url, true));
|
|
2279
|
+
}
|
|
2280
|
+
async board(url, fork = false) {
|
|
2281
|
+
const { frames, delta } = await this.boardFrames(url, fork);
|
|
2282
|
+
// the World's last cut, or the branch's whole ledger when its parent is looking at it
|
|
2236
2283
|
return { world: this.served, base: { id: delta.base.id, at: delta.base.createdAt ?? null }, frames: frames.map(({ frame, mine }) => ({ ...frame, changes: new Set(mine.map(actOf)).size, since: mine.map((c) => c.action.occurredAt).sort()[0] ?? null })), ...this.savedBoards() };
|
|
2237
2284
|
}
|
|
2238
2285
|
/** One frame's changes not yet cut, newest first: each the writes one request made, as words read them (the operation,
|
|
2239
2286
|
* the record, its few naming fields), the frame named `<vendor>:<frame id>`. */
|
|
2240
|
-
async boardChanges(url) {
|
|
2287
|
+
async boardChanges(url, fork = false) {
|
|
2241
2288
|
const key = url.searchParams.get('frame') ?? '';
|
|
2242
2289
|
if (!key)
|
|
2243
2290
|
return Response.json({ error: 'board/changes?frame=<vendor>:<frame id>' }, { status: 400 });
|
|
2244
|
-
const found = (await this.boardFrames(url)).frames.find(({ frame }) => `${frame.vendor}:${frame.id}` === key);
|
|
2291
|
+
const found = (await this.boardFrames(url, fork)).frames.find(({ frame }) => `${frame.vendor}:${frame.id}` === key);
|
|
2245
2292
|
if (!found)
|
|
2246
2293
|
return Response.json({ error: `no frame ${key} on ${this.served}'s board` }, { status: 404 });
|
|
2247
2294
|
const acts = new Map();
|
|
@@ -2259,7 +2306,7 @@ export class WorldDoors {
|
|
|
2259
2306
|
return Response.json({ frame: key, total: all.length, since: found.mine.map((c) => c.action.occurredAt).sort()[0] ?? null, acts: all.slice(0, 100) });
|
|
2260
2307
|
}
|
|
2261
2308
|
/** Every twin's frames, where each opens here, and each with the changes not yet cut that are its own. */
|
|
2262
|
-
async boardFrames(url) {
|
|
2309
|
+
async boardFrames(url, fork = false) {
|
|
2263
2310
|
const claimed = new Map((await this.claimedHosts(url.origin)).map(({ vendor, hosts }) => [vendor, new Set(hosts)]));
|
|
2264
2311
|
const sitesAt = this.host.sitesTemplate?.() ?? null;
|
|
2265
2312
|
// a site private to the World's people (a `{site}` template: a hosted World) opens through this World's own origin,
|
|
@@ -2275,9 +2322,10 @@ export class WorldDoors {
|
|
|
2275
2322
|
const own = this.host.browserOrigin?.() ?? null;
|
|
2276
2323
|
return own ? `${own}/-/${this.served}/sites/go?${new URLSearchParams({ host: hostname, land: rest }).toString()}` : null;
|
|
2277
2324
|
};
|
|
2278
|
-
|
|
2325
|
+
// inherited history is the parent log: the branch's own ledger starts at its fork, even after a cut
|
|
2326
|
+
const delta = diffWorld(this.name, { root: this.worldRoot, ...(fork ? { base: 'world-boot' } : {}) });
|
|
2279
2327
|
const received = new Set(listWorldChangesets({ root: this.worldRoot, world: this.name }).filter((l) => l.changeset.pushedBy).flatMap((l) => l.changeset.actions.map((a) => a.action.id)));
|
|
2280
|
-
const changes = delta.actions.filter((a) => !received.has(a.action.id));
|
|
2328
|
+
const changes = fork ? delta.actions : delta.actions.filter((a) => !received.has(a.action.id));
|
|
2281
2329
|
const frames = [];
|
|
2282
2330
|
for (const vendor of Object.keys(this.up().services)) {
|
|
2283
2331
|
const twin = this.host.twinFetch(vendor);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.27",
|
|
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",
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
"node": ">=22.3"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@volter/world-core": "3.0.
|
|
72
|
-
"@volter/world-console": "3.0.
|
|
71
|
+
"@volter/world-core": "3.0.27",
|
|
72
|
+
"@volter/world-console": "3.0.22"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"@electric-sql/pglite": "0.5.8",
|
|
76
76
|
"@electric-sql/pglite-pgvector": "0.0.9",
|
|
77
77
|
"@volter/world-access": "3.0.1",
|
|
78
|
-
"@volter/world-core": "3.0.
|
|
78
|
+
"@volter/world-core": "3.0.27",
|
|
79
79
|
"pg-gateway": "0.3.0-beta.4",
|
|
80
80
|
"smol-toml": "^1.8.0",
|
|
81
81
|
"yaml": "2.9.0"
|
package/src/local-branches.ts
CHANGED
|
@@ -93,6 +93,10 @@ export class LocalBranches {
|
|
|
93
93
|
list: async () => this.list(parent),
|
|
94
94
|
create: (at, ttl, _origin, parentKey, byKey) => this.create(parent, at, ttl, parentKey, byKey),
|
|
95
95
|
remove: async (name) => { const w = this.o.worlds.get(name); if (!w || this.record(w)?.from !== parent) return false; await this.remove(name); return true; },
|
|
96
|
+
board: async (name, frame) => {
|
|
97
|
+
const w = this.o.worlds.get(name);
|
|
98
|
+
return w && this.record(w)?.from === parent ? w.branchBoard(frame) : null;
|
|
99
|
+
},
|
|
96
100
|
// a branch of this parent only, served in this process beside it: its own content plane's pass
|
|
97
101
|
pagesPass: async (name, scope, land, grant) => {
|
|
98
102
|
const w = this.o.worlds.get(name);
|
package/src/served-world.ts
CHANGED
|
@@ -18,7 +18,11 @@ import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant
|
|
|
18
18
|
// GET map | timeline | diff every twin and what it holds; the logs merged newest first; the changes since the base
|
|
19
19
|
// GET/PUT clock, POST clock/advance the frozen instant every twin stamps from, forward only
|
|
20
20
|
// GET/POST branches, DELETE branches/<org>/<world> the host's branches of this World ("as of" views)
|
|
21
|
+
// GET branches/<org>/<world>/board[/changes] a branch's pages and changes from its fork, through its host
|
|
22
|
+
// GET branches/<org>/<world>/pages?land=<path> a read pass to the branch's own content plane
|
|
23
|
+
// GET/PUT board, GET board/changes the pages and their changes; the boards as people arranged them
|
|
21
24
|
// GET history?at=<instant> each twin's history cut at an instant (where such a branch starts)
|
|
25
|
+
// GET/POST marks where the World's changes count from; POST { note? } marks it now
|
|
22
26
|
// GET /<org>/<world>/<vendor>/<path> the vendor's own screens, the pack's workspaces (its `GET /twin` lists them)
|
|
23
27
|
// Tokens ride `x-volter-token`: the token opens everything, the read token only reads — on the vendor
|
|
24
28
|
// wire, a twin that enforces it (`requestScopes: ['read']`) is handed the read token's other requests
|
|
@@ -36,7 +40,7 @@ import {
|
|
|
36
40
|
appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, changesetReadiness, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow,
|
|
37
41
|
parentEntries, readBranchMeta, rebaseBranch, type Changeset, type Entry, type Receipt, type RootConfig,
|
|
38
42
|
} from '@volter/world-core/runtime';
|
|
39
|
-
import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from './changeset.ts';
|
|
43
|
+
import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, listWorldMarks, markWorld, MARK_NOTE_MAX, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from './changeset.ts';
|
|
40
44
|
import { fetchFromOrigin } from './origin.ts';
|
|
41
45
|
import { CONSOLE_BASE, consoleRedirect, serveConsoleApart, type ConsoleMount } from './console-apart.ts';
|
|
42
46
|
import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from './configs.ts';
|
|
@@ -159,7 +163,7 @@ function configRefFor(worldRoot: string, name: string): string {
|
|
|
159
163
|
/** A world mounted by a host. `token`/`readToken` read the CURRENT tokens (rotate changes them);
|
|
160
164
|
* `twins()` names the world's twins once booted; `rotate()` mints both tokens anew — the old die
|
|
161
165
|
* with the call — and keeps them beside the world like `boot` does. */
|
|
162
|
-
export type MountedWorld = { name: string; served: string; root: string; readonly token: string; readonly readToken: string; twins: () => string[]; rotate: () => { token: string; readToken: string }; handle: (request: Request) => Promise<Response>; /** a hostname this World's twins answer, as a browser reads it (siteOfHost) */ site: (request: Request, host: string) => Promise<Response>; /** WorldDoors.pagesPass */ pagesPass: (scope: 'read' | 'write', land: string, grant?: { who?: string; framer?: string; asker?: string }) => string | null; boot: (url: string) => Promise<void>; stop: () => Promise<void> };
|
|
166
|
+
export type MountedWorld = { name: string; served: string; root: string; readonly token: string; readonly readToken: string; twins: () => string[]; rotate: () => { token: string; readToken: string }; handle: (request: Request) => Promise<Response>; /** a hostname this World's twins answer, as a browser reads it (siteOfHost) */ site: (request: Request, host: string) => Promise<Response>; /** WorldDoors.pagesPass */ pagesPass: (scope: 'read' | 'write', land: string, grant?: { who?: string; framer?: string; asker?: string }) => string | null; branchBoard: (frame?: string) => Promise<Response>; boot: (url: string) => Promise<void>; stop: () => Promise<void> };
|
|
163
167
|
/** A browser session a World keeps: its scope, when it ends, and the person a pass named, when one did. */
|
|
164
168
|
/** What a content-plane pass, and the session it opens, carry: its scope; the control session that asked (the content
|
|
165
169
|
* session ends with it; none when the World's token asked); the person; the console that frames it; and whether it
|
|
@@ -224,6 +228,7 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
|
|
|
224
228
|
handle: (request) => world.handle(request),
|
|
225
229
|
site: (request, host) => world.site(request, host),
|
|
226
230
|
pagesPass: (scope, land, grant) => world.pagesPass(scope, land, grant),
|
|
231
|
+
branchBoard: (frame) => world.branchBoard(frame),
|
|
227
232
|
async boot(url) {
|
|
228
233
|
keep();
|
|
229
234
|
const port = Number(new URL(url).port) || 0;
|
|
@@ -395,6 +400,8 @@ export type BranchDoors = {
|
|
|
395
400
|
/** the address that opens a branch's content plane with a one-time pass at `scope`, landing on `land` (a path of the
|
|
396
401
|
* branch): how its parent's console frames the branch's pages beside its own (the comparison) */
|
|
397
402
|
pagesPass?: (name: string, scope: 'read' | 'write', land: string, grant?: { who?: string; framer?: string; asker?: string }) => Promise<string | null>;
|
|
403
|
+
/** the branch's own board, counting from its fork; a frame names its changes alone */
|
|
404
|
+
board?: (name: string, frame?: string) => Promise<Response | null>;
|
|
398
405
|
};
|
|
399
406
|
|
|
400
407
|
/** A World's browser-origin label, `<world>--<org>`, or null when its names cannot make one (a DNS
|
|
@@ -1340,6 +1347,27 @@ export class WorldDoors {
|
|
|
1340
1347
|
withHistoryLocks(vendors.map((vendor) => worldPaths(this.stateOf(vendor), this.controlRoot(vendor)).dir), () => { for (const vendor of vendors) views[vendor] = historyAtInstant(this.stateOf(vendor), instant, this.controlRoot(vendor)); });
|
|
1341
1348
|
return Response.json({ world: this.served, at: new Date(Date.parse(instant)).toISOString(), views });
|
|
1342
1349
|
}
|
|
1350
|
+
// THE WORLD'S MARKS (`GET marks`, `POST marks { note? }`): where its changes count from. A World seeded through its
|
|
1351
|
+
// doors (a hosted one has no seed run to land its starting data as already there) is marked once it is set up, so
|
|
1352
|
+
// what it starts with is not counted as its changes; the latest mark is the base of the diff, the board's Changes
|
|
1353
|
+
// and a new changeset, as `volter-world mark <world>` makes it on this machine
|
|
1354
|
+
if (kind === 'marks' && !name && request.method === 'GET') return Response.json({ world: this.served, marks: listWorldMarks(this.name, this.worldRoot) });
|
|
1355
|
+
if (kind === 'marks' && !name && request.method === 'POST') {
|
|
1356
|
+
const body = (await request.json().catch(() => ({}))) as { note?: unknown };
|
|
1357
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return Response.json({ error: 'a mark is sent as a JSON object' }, { status: 400 });
|
|
1358
|
+
if (body.note !== undefined && (typeof body.note !== 'string' || body.note.length > MARK_NOTE_MAX)) return Response.json({ error: `a mark's note is a string of at most ${MARK_NOTE_MAX} characters` }, { status: 400 });
|
|
1359
|
+
const vendors = Object.keys(this.up().services);
|
|
1360
|
+
// taken while no twin writes, so it stands at one position of every log
|
|
1361
|
+
const { mark, passed } = withHistoryLocks(vendors.map((vendor) => worldPaths(this.stateOf(vendor), this.controlRoot(vendor)).dir), () => {
|
|
1362
|
+
const delta = diffWorld(this.name, { root: this.worldRoot });
|
|
1363
|
+
// what another World pushed here is in its changeset already: never this World's uncut changes
|
|
1364
|
+
const received = new Set(listWorldChangesets({ root: this.worldRoot, world: this.name }).filter((l) => l.changeset.pushedBy).flatMap((l) => l.changeset.actions.map((a) => a.action.id)));
|
|
1365
|
+
const passed = delta.actions.filter((a) => !received.has(a.action.id)).length;
|
|
1366
|
+
const mark = markWorld(this.name, { root: this.worldRoot, ...(typeof body.note === 'string' && body.note.trim() ? { note: body.note } : {}) });
|
|
1367
|
+
return { mark, passed };
|
|
1368
|
+
});
|
|
1369
|
+
return Response.json({ world: this.served, mark, passed }, { status: 201 });
|
|
1370
|
+
}
|
|
1343
1371
|
if (kind === 'map' && !name && request.method === 'GET') return Response.json(await this.map());
|
|
1344
1372
|
if (kind === 'board' && !name && request.method === 'GET') return Response.json(await this.board(url));
|
|
1345
1373
|
if (kind === 'board' && name === 'changes' && !verb && request.method === 'GET') return await this.boardChanges(url);
|
|
@@ -1771,6 +1799,13 @@ export class WorldDoors {
|
|
|
1771
1799
|
* and removes them; a host that makes none answers 404, and a view offers no "as of". */
|
|
1772
1800
|
private async branchesDoor(request: Request, url: URL, rest: string[], via: 'token' | 'key' | 'session' | null): Promise<Response> {
|
|
1773
1801
|
const branches = this.host.branches;
|
|
1802
|
+
if (rest[2] === 'board' && request.method === 'GET' && (rest.length === 3 || (rest.length === 4 && rest[3] === 'changes'))) {
|
|
1803
|
+
if (!branches) return Response.json({ error: `this host cannot read branches of ${this.served}` }, { status: 501 });
|
|
1804
|
+
const name = `${decodeURIComponent(rest[0]!)}/${decodeURIComponent(rest[1]!)}`;
|
|
1805
|
+
if (!(await branches.list()).some((b) => b.name === name)) return Response.json({ error: `no branch ${name} of ${this.served}` }, { status: 404 });
|
|
1806
|
+
if (!branches.board) return Response.json({ error: `this host cannot read ${name}'s board` }, { status: 501 });
|
|
1807
|
+
return (await branches.board(name, rest[3] === 'changes' ? url.searchParams.get('frame') ?? '' : undefined)) ?? Response.json({ error: `no branch ${name} of ${this.served}` }, { status: 404 });
|
|
1808
|
+
}
|
|
1774
1809
|
if (!branches) return Response.json({ error: `this host makes no branches of ${this.served}` }, { status: 404 });
|
|
1775
1810
|
if (rest.length === 0 && request.method === 'GET') {
|
|
1776
1811
|
// beside the host's snapshots, the World's own NAMED branches (`volter world branch`): one runs at a time, and
|
|
@@ -1877,17 +1912,22 @@ export class WorldDoors {
|
|
|
1877
1912
|
* screen draws yet) and the changes not yet cut on the records it covers, counted by the request that made them, with
|
|
1878
1913
|
* when the first was made; and the boards as this World's people arranged them. A frame's changes themselves are read
|
|
1879
1914
|
* one frame at a time (`board/changes`). */
|
|
1880
|
-
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1915
|
+
async branchBoard(frame?: string): Promise<Response> {
|
|
1916
|
+
const url = new URL(this.host.browserOrigin?.() ?? 'http://world.local');
|
|
1917
|
+
if (frame !== undefined) { url.searchParams.set('frame', frame); return this.boardChanges(url, true); }
|
|
1918
|
+
return Response.json(await this.board(url, true));
|
|
1919
|
+
}
|
|
1920
|
+
private async board(url: URL, fork = false): Promise<unknown> {
|
|
1921
|
+
const { frames, delta } = await this.boardFrames(url, fork);
|
|
1922
|
+
// the World's last cut, or the branch's whole ledger when its parent is looking at it
|
|
1883
1923
|
return { world: this.served, base: { id: delta.base.id, at: delta.base.createdAt ?? null }, frames: frames.map(({ frame, mine }) => ({ ...frame, changes: new Set(mine.map(actOf)).size, since: mine.map((c) => c.action.occurredAt).sort()[0] ?? null })), ...this.savedBoards() };
|
|
1884
1924
|
}
|
|
1885
1925
|
/** One frame's changes not yet cut, newest first: each the writes one request made, as words read them (the operation,
|
|
1886
1926
|
* the record, its few naming fields), the frame named `<vendor>:<frame id>`. */
|
|
1887
|
-
private async boardChanges(url: URL): Promise<Response> {
|
|
1927
|
+
private async boardChanges(url: URL, fork = false): Promise<Response> {
|
|
1888
1928
|
const key = url.searchParams.get('frame') ?? '';
|
|
1889
1929
|
if (!key) return Response.json({ error: 'board/changes?frame=<vendor>:<frame id>' }, { status: 400 });
|
|
1890
|
-
const found = (await this.boardFrames(url)).frames.find(({ frame }) => `${frame.vendor}:${frame.id}` === key);
|
|
1930
|
+
const found = (await this.boardFrames(url, fork)).frames.find(({ frame }) => `${frame.vendor}:${frame.id}` === key);
|
|
1891
1931
|
if (!found) return Response.json({ error: `no frame ${key} on ${this.served}'s board` }, { status: 404 });
|
|
1892
1932
|
const acts = new Map<string, { at: string; writes: number; entries: Array<{ op: string; operation?: string; subject: { type: string; id: string }; fields: Record<string, string> }> }>();
|
|
1893
1933
|
for (const c of found.mine) {
|
|
@@ -1902,7 +1942,7 @@ export class WorldDoors {
|
|
|
1902
1942
|
return Response.json({ frame: key, total: all.length, since: found.mine.map((c) => c.action.occurredAt).sort()[0] ?? null, acts: all.slice(0, 100) });
|
|
1903
1943
|
}
|
|
1904
1944
|
/** Every twin's frames, where each opens here, and each with the changes not yet cut that are its own. */
|
|
1905
|
-
private async boardFrames(url: URL): Promise<{ delta: ReturnType<typeof diffWorld>; frames: Array<{ frame: Record<string, unknown> & { vendor: string; id: string }; mine: Array<ReturnType<typeof diffWorld>['actions'][number]> }> }> {
|
|
1945
|
+
private async boardFrames(url: URL, fork = false): Promise<{ delta: ReturnType<typeof diffWorld>; frames: Array<{ frame: Record<string, unknown> & { vendor: string; id: string }; mine: Array<ReturnType<typeof diffWorld>['actions'][number]> }> }> {
|
|
1906
1946
|
const claimed = new Map((await this.claimedHosts(url.origin)).map(({ vendor, hosts }) => [vendor, new Set(hosts)]));
|
|
1907
1947
|
const sitesAt = this.host.sitesTemplate?.() ?? null;
|
|
1908
1948
|
// a site private to the World's people (a `{site}` template: a hosted World) opens through this World's own origin,
|
|
@@ -1915,9 +1955,10 @@ export class WorldDoors {
|
|
|
1915
1955
|
const own = this.host.browserOrigin?.() ?? null;
|
|
1916
1956
|
return own ? `${own}/-/${this.served}/sites/go?${new URLSearchParams({ host: hostname, land: rest }).toString()}` : null;
|
|
1917
1957
|
};
|
|
1918
|
-
|
|
1958
|
+
// inherited history is the parent log: the branch's own ledger starts at its fork, even after a cut
|
|
1959
|
+
const delta = diffWorld(this.name, { root: this.worldRoot, ...(fork ? { base: 'world-boot' } : {}) });
|
|
1919
1960
|
const received = new Set(listWorldChangesets({ root: this.worldRoot, world: this.name }).filter((l) => l.changeset.pushedBy).flatMap((l) => l.changeset.actions.map((a) => a.action.id)));
|
|
1920
|
-
const changes = delta.actions.filter((a) => !received.has(a.action.id));
|
|
1961
|
+
const changes = fork ? delta.actions : delta.actions.filter((a) => !received.has(a.action.id));
|
|
1921
1962
|
const frames: Array<{ frame: Record<string, unknown> & { vendor: string; id: string }; mine: typeof changes }> = [];
|
|
1922
1963
|
for (const vendor of Object.keys(this.up().services)) {
|
|
1923
1964
|
const twin = this.host.twinFetch(vendor);
|