@volter/world-runtime 3.0.24 → 3.0.26

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.
@@ -19,6 +19,7 @@ import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant
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
21
  // GET history?at=<instant> each twin's history cut at an instant (where such a branch starts)
22
+ // GET/POST marks where the World's changes count from; POST { note? } marks it now
22
23
  // GET /<org>/<world>/<vendor>/<path> the vendor's own screens, the pack's workspaces (its `GET /twin` lists them)
23
24
  // Tokens ride `x-volter-token`: the token opens everything, the read token only reads — on the vendor
24
25
  // wire, a twin that enforces it (`requestScopes: ['read']`) is handed the read token's other requests
@@ -33,7 +34,7 @@ import { grantOf, presentedSecrets, sameSecret, spentPasses, verifyPass } from '
33
34
  import { createHash } from 'node:crypto';
34
35
  import { dirname, join, resolve } from 'node:path';
35
36
  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";
37
+ import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, listWorldMarks, markWorld, MARK_NOTE_MAX, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from "./changeset.js";
37
38
  import { fetchFromOrigin } from "./origin.js";
38
39
  import { CONSOLE_BASE, consoleRedirect, serveConsoleApart } from "./console-apart.js";
39
40
  import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from "./configs.js";
@@ -1543,6 +1544,30 @@ export class WorldDoors {
1543
1544
  views[vendor] = historyAtInstant(this.stateOf(vendor), instant, this.controlRoot(vendor)); });
1544
1545
  return Response.json({ world: this.served, at: new Date(Date.parse(instant)).toISOString(), views });
1545
1546
  }
1547
+ // THE WORLD'S MARKS (`GET marks`, `POST marks { note? }`): where its changes count from. A World seeded through its
1548
+ // doors (a hosted one has no seed run to land its starting data as already there) is marked once it is set up, so
1549
+ // what it starts with is not counted as its changes; the latest mark is the base of the diff, the board's Changes
1550
+ // and a new changeset, as `volter-world mark <world>` makes it on this machine
1551
+ if (kind === 'marks' && !name && request.method === 'GET')
1552
+ return Response.json({ world: this.served, marks: listWorldMarks(this.name, this.worldRoot) });
1553
+ if (kind === 'marks' && !name && request.method === 'POST') {
1554
+ const body = (await request.json().catch(() => ({})));
1555
+ if (!body || typeof body !== 'object' || Array.isArray(body))
1556
+ return Response.json({ error: 'a mark is sent as a JSON object' }, { status: 400 });
1557
+ if (body.note !== undefined && (typeof body.note !== 'string' || body.note.length > MARK_NOTE_MAX))
1558
+ return Response.json({ error: `a mark's note is a string of at most ${MARK_NOTE_MAX} characters` }, { status: 400 });
1559
+ const vendors = Object.keys(this.up().services);
1560
+ // taken while no twin writes, so it stands at one position of every log
1561
+ const { mark, passed } = withHistoryLocks(vendors.map((vendor) => worldPaths(this.stateOf(vendor), this.controlRoot(vendor)).dir), () => {
1562
+ const delta = diffWorld(this.name, { root: this.worldRoot });
1563
+ // what another World pushed here is in its changeset already: never this World's uncut changes
1564
+ 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)));
1565
+ const passed = delta.actions.filter((a) => !received.has(a.action.id)).length;
1566
+ const mark = markWorld(this.name, { root: this.worldRoot, ...(typeof body.note === 'string' && body.note.trim() ? { note: body.note } : {}) });
1567
+ return { mark, passed };
1568
+ });
1569
+ return Response.json({ world: this.served, mark, passed }, { status: 201 });
1570
+ }
1546
1571
  if (kind === 'map' && !name && request.method === 'GET')
1547
1572
  return Response.json(await this.map());
1548
1573
  if (kind === 'board' && !name && request.method === 'GET')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-runtime",
3
- "version": "3.0.24",
3
+ "version": "3.0.26",
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.24",
72
- "@volter/world-console": "3.0.19"
71
+ "@volter/world-core": "3.0.26",
72
+ "@volter/world-console": "3.0.21"
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.24",
78
+ "@volter/world-core": "3.0.26",
79
79
  "pg-gateway": "0.3.0-beta.4",
80
80
  "smol-toml": "^1.8.0",
81
81
  "yaml": "2.9.0"
@@ -19,6 +19,7 @@ import { getActiveWorldStore, withHistoryLocks, captureHistory, historyAtInstant
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
21
  // GET history?at=<instant> each twin's history cut at an instant (where such a branch starts)
22
+ // GET/POST marks where the World's changes count from; POST { note? } marks it now
22
23
  // GET /<org>/<world>/<vendor>/<path> the vendor's own screens, the pack's workspaces (its `GET /twin` lists them)
23
24
  // Tokens ride `x-volter-token`: the token opens everything, the read token only reads — on the vendor
24
25
  // wire, a twin that enforces it (`requestScopes: ['read']`) is handed the read token's other requests
@@ -36,7 +37,7 @@ import {
36
37
  appendActionIfAbsent, branchEntries, branchLogPath, branchMetaPath, changesetContentHash, changesetReadiness, confirmAction, parentLogPath, readTree, stateDirName, wholeLog, worldNow,
37
38
  parentEntries, readBranchMeta, rebaseBranch, type Changeset, type Entry, type Receipt, type RootConfig,
38
39
  } from '@volter/world-core/runtime';
39
- import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from './changeset.ts';
40
+ import { approveWorldChangeset, createWorldChangeset, diffWorld, findWorldChangeset, listWorldChangesets, listWorldMarks, markWorld, MARK_NOTE_MAX, pushWorldChangeset, verifyWorldChangeset, worldChangesetsDir } from './changeset.ts';
40
41
  import { fetchFromOrigin } from './origin.ts';
41
42
  import { CONSOLE_BASE, consoleRedirect, serveConsoleApart, type ConsoleMount } from './console-apart.ts';
42
43
  import { loadWorldConfig, worldChangesetRules, writeWorldConfig } from './configs.ts';
@@ -1340,6 +1341,27 @@ export class WorldDoors {
1340
1341
  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
1342
  return Response.json({ world: this.served, at: new Date(Date.parse(instant)).toISOString(), views });
1342
1343
  }
1344
+ // THE WORLD'S MARKS (`GET marks`, `POST marks { note? }`): where its changes count from. A World seeded through its
1345
+ // doors (a hosted one has no seed run to land its starting data as already there) is marked once it is set up, so
1346
+ // what it starts with is not counted as its changes; the latest mark is the base of the diff, the board's Changes
1347
+ // and a new changeset, as `volter-world mark <world>` makes it on this machine
1348
+ if (kind === 'marks' && !name && request.method === 'GET') return Response.json({ world: this.served, marks: listWorldMarks(this.name, this.worldRoot) });
1349
+ if (kind === 'marks' && !name && request.method === 'POST') {
1350
+ const body = (await request.json().catch(() => ({}))) as { note?: unknown };
1351
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return Response.json({ error: 'a mark is sent as a JSON object' }, { status: 400 });
1352
+ 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 });
1353
+ const vendors = Object.keys(this.up().services);
1354
+ // taken while no twin writes, so it stands at one position of every log
1355
+ const { mark, passed } = withHistoryLocks(vendors.map((vendor) => worldPaths(this.stateOf(vendor), this.controlRoot(vendor)).dir), () => {
1356
+ const delta = diffWorld(this.name, { root: this.worldRoot });
1357
+ // what another World pushed here is in its changeset already: never this World's uncut changes
1358
+ 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)));
1359
+ const passed = delta.actions.filter((a) => !received.has(a.action.id)).length;
1360
+ const mark = markWorld(this.name, { root: this.worldRoot, ...(typeof body.note === 'string' && body.note.trim() ? { note: body.note } : {}) });
1361
+ return { mark, passed };
1362
+ });
1363
+ return Response.json({ world: this.served, mark, passed }, { status: 201 });
1364
+ }
1343
1365
  if (kind === 'map' && !name && request.method === 'GET') return Response.json(await this.map());
1344
1366
  if (kind === 'board' && !name && request.method === 'GET') return Response.json(await this.board(url));
1345
1367
  if (kind === 'board' && name === 'changes' && !verb && request.method === 'GET') return await this.boardChanges(url);