@volter/world-runtime 3.0.26 → 3.0.28

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.
@@ -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>`. */
@@ -18,6 +18,9 @@ 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)
22
25
  // GET/POST marks where the World's changes count from; POST { note? } marks it now
23
26
  // GET /<org>/<world>/<vendor>/<path> the vendor's own screens, the pack's workspaces (its `GET /twin` lists them)
@@ -216,6 +219,7 @@ export async function mountWorld(name, opts = {}) {
216
219
  handle: (request) => world.handle(request),
217
220
  site: (request, host) => world.site(request, host),
218
221
  pagesPass: (scope, land, grant) => world.pagesPass(scope, land, grant),
222
+ branchBoard: (frame) => world.branchBoard(frame),
219
223
  async boot(url) {
220
224
  keep();
221
225
  const port = Number(new URL(url).port) || 0;
@@ -2121,6 +2125,16 @@ export class WorldDoors {
2121
2125
  * and removes them; a host that makes none answers 404, and a view offers no "as of". */
2122
2126
  async branchesDoor(request, url, rest, via) {
2123
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
+ }
2124
2138
  if (!branches)
2125
2139
  return Response.json({ error: `this host makes no branches of ${this.served}` }, { status: 404 });
2126
2140
  if (rest.length === 0 && request.method === 'GET') {
@@ -2255,18 +2269,26 @@ export class WorldDoors {
2255
2269
  * screen draws yet) and the changes not yet cut on the records it covers, counted by the request that made them, with
2256
2270
  * when the first was made; and the boards as this World's people arranged them. A frame's changes themselves are read
2257
2271
  * one frame at a time (`board/changes`). */
2258
- async board(url) {
2259
- const { frames, delta } = await this.boardFrames(url);
2260
- // the changes are counted from the World's base: what was last cut or received, and when
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
2261
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() };
2262
2284
  }
2263
2285
  /** One frame's changes not yet cut, newest first: each the writes one request made, as words read them (the operation,
2264
2286
  * the record, its few naming fields), the frame named `<vendor>:<frame id>`. */
2265
- async boardChanges(url) {
2287
+ async boardChanges(url, fork = false) {
2266
2288
  const key = url.searchParams.get('frame') ?? '';
2267
2289
  if (!key)
2268
2290
  return Response.json({ error: 'board/changes?frame=<vendor>:<frame id>' }, { status: 400 });
2269
- 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);
2270
2292
  if (!found)
2271
2293
  return Response.json({ error: `no frame ${key} on ${this.served}'s board` }, { status: 404 });
2272
2294
  const acts = new Map();
@@ -2284,7 +2306,7 @@ export class WorldDoors {
2284
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) });
2285
2307
  }
2286
2308
  /** Every twin's frames, where each opens here, and each with the changes not yet cut that are its own. */
2287
- async boardFrames(url) {
2309
+ async boardFrames(url, fork = false) {
2288
2310
  const claimed = new Map((await this.claimedHosts(url.origin)).map(({ vendor, hosts }) => [vendor, new Set(hosts)]));
2289
2311
  const sitesAt = this.host.sitesTemplate?.() ?? null;
2290
2312
  // a site private to the World's people (a `{site}` template: a hosted World) opens through this World's own origin,
@@ -2300,9 +2322,10 @@ export class WorldDoors {
2300
2322
  const own = this.host.browserOrigin?.() ?? null;
2301
2323
  return own ? `${own}/-/${this.served}/sites/go?${new URLSearchParams({ host: hostname, land: rest }).toString()}` : null;
2302
2324
  };
2303
- const delta = diffWorld(this.name, { root: this.worldRoot });
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' } : {}) });
2304
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)));
2305
- 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));
2306
2329
  const frames = [];
2307
2330
  for (const vendor of Object.keys(this.up().services)) {
2308
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.26",
3
+ "version": "3.0.28",
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.26",
72
- "@volter/world-console": "3.0.21"
71
+ "@volter/world-core": "3.0.28",
72
+ "@volter/world-console": "3.0.23"
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.26",
78
+ "@volter/world-core": "3.0.28",
79
79
  "pg-gateway": "0.3.0-beta.4",
80
80
  "smol-toml": "^1.8.0",
81
81
  "yaml": "2.9.0"
@@ -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);
@@ -18,6 +18,9 @@ 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)
22
25
  // GET/POST marks where the World's changes count from; POST { note? } marks it now
23
26
  // GET /<org>/<world>/<vendor>/<path> the vendor's own screens, the pack's workspaces (its `GET /twin` lists them)
@@ -160,7 +163,7 @@ function configRefFor(worldRoot: string, name: string): string {
160
163
  /** A world mounted by a host. `token`/`readToken` read the CURRENT tokens (rotate changes them);
161
164
  * `twins()` names the world's twins once booted; `rotate()` mints both tokens anew — the old die
162
165
  * with the call — and keeps them beside the world like `boot` does. */
163
- 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> };
164
167
  /** A browser session a World keeps: its scope, when it ends, and the person a pass named, when one did. */
165
168
  /** What a content-plane pass, and the session it opens, carry: its scope; the control session that asked (the content
166
169
  * session ends with it; none when the World's token asked); the person; the console that frames it; and whether it
@@ -225,6 +228,7 @@ export async function mountWorld(name: string, opts: { /** the platforms whose p
225
228
  handle: (request) => world.handle(request),
226
229
  site: (request, host) => world.site(request, host),
227
230
  pagesPass: (scope, land, grant) => world.pagesPass(scope, land, grant),
231
+ branchBoard: (frame) => world.branchBoard(frame),
228
232
  async boot(url) {
229
233
  keep();
230
234
  const port = Number(new URL(url).port) || 0;
@@ -396,6 +400,8 @@ export type BranchDoors = {
396
400
  /** the address that opens a branch's content plane with a one-time pass at `scope`, landing on `land` (a path of the
397
401
  * branch): how its parent's console frames the branch's pages beside its own (the comparison) */
398
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>;
399
405
  };
400
406
 
401
407
  /** A World's browser-origin label, `<world>--<org>`, or null when its names cannot make one (a DNS
@@ -1793,6 +1799,13 @@ export class WorldDoors {
1793
1799
  * and removes them; a host that makes none answers 404, and a view offers no "as of". */
1794
1800
  private async branchesDoor(request: Request, url: URL, rest: string[], via: 'token' | 'key' | 'session' | null): Promise<Response> {
1795
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
+ }
1796
1809
  if (!branches) return Response.json({ error: `this host makes no branches of ${this.served}` }, { status: 404 });
1797
1810
  if (rest.length === 0 && request.method === 'GET') {
1798
1811
  // beside the host's snapshots, the World's own NAMED branches (`volter world branch`): one runs at a time, and
@@ -1899,17 +1912,22 @@ export class WorldDoors {
1899
1912
  * screen draws yet) and the changes not yet cut on the records it covers, counted by the request that made them, with
1900
1913
  * when the first was made; and the boards as this World's people arranged them. A frame's changes themselves are read
1901
1914
  * one frame at a time (`board/changes`). */
1902
- private async board(url: URL): Promise<unknown> {
1903
- const { frames, delta } = await this.boardFrames(url);
1904
- // the changes are counted from the World's base: what was last cut or received, and when
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
1905
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() };
1906
1924
  }
1907
1925
  /** One frame's changes not yet cut, newest first: each the writes one request made, as words read them (the operation,
1908
1926
  * the record, its few naming fields), the frame named `<vendor>:<frame id>`. */
1909
- private async boardChanges(url: URL): Promise<Response> {
1927
+ private async boardChanges(url: URL, fork = false): Promise<Response> {
1910
1928
  const key = url.searchParams.get('frame') ?? '';
1911
1929
  if (!key) return Response.json({ error: 'board/changes?frame=<vendor>:<frame id>' }, { status: 400 });
1912
- 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);
1913
1931
  if (!found) return Response.json({ error: `no frame ${key} on ${this.served}'s board` }, { status: 404 });
1914
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> }> }>();
1915
1933
  for (const c of found.mine) {
@@ -1924,7 +1942,7 @@ export class WorldDoors {
1924
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) });
1925
1943
  }
1926
1944
  /** Every twin's frames, where each opens here, and each with the changes not yet cut that are its own. */
1927
- 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]> }> }> {
1928
1946
  const claimed = new Map((await this.claimedHosts(url.origin)).map(({ vendor, hosts }) => [vendor, new Set(hosts)]));
1929
1947
  const sitesAt = this.host.sitesTemplate?.() ?? null;
1930
1948
  // a site private to the World's people (a `{site}` template: a hosted World) opens through this World's own origin,
@@ -1937,9 +1955,10 @@ export class WorldDoors {
1937
1955
  const own = this.host.browserOrigin?.() ?? null;
1938
1956
  return own ? `${own}/-/${this.served}/sites/go?${new URLSearchParams({ host: hostname, land: rest }).toString()}` : null;
1939
1957
  };
1940
- const delta = diffWorld(this.name, { root: this.worldRoot });
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' } : {}) });
1941
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)));
1942
- 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));
1943
1962
  const frames: Array<{ frame: Record<string, unknown> & { vendor: string; id: string }; mine: typeof changes }> = [];
1944
1963
  for (const vendor of Object.keys(this.up().services)) {
1945
1964
  const twin = this.host.twinFetch(vendor);