@volter/world 2.0.0 → 2.0.1

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/cli.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 the twins your app needs, running together (the world of the current directory)\n volter world init [--name <world>] [--allow-unknown] detect the app's vendors, write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter world serve [--port <p>] [--console-port <c>] serve this world on a URL under /<org>/<world>/; prints the token and the console (its own origin, port p+1 by default)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n volter twin <vendor> mirror [--port <p>] the twin's UI mirror\n volter twin <vendor> conformance check the twin against the vendor's spec\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> --token <personal token> sign the CLI into the hosted platform (a personal token from the console)\n volter remote add <name> <url|path|org/world> [--token <token>] name a remote; org/world resolves through the platform you logged into\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n";
2
+ export declare const HELP = "volter \u2014 run your app against a world of twins\n\nworld \u2014 the twins your app needs, running together (the world of the current directory)\n volter world init [--name <world>] [--allow-unknown] detect the app's vendors, write .volter/world.json\n volter world up [--sandbox] [--no-seed] start the twins on this branch (loads the default data the first time)\n volter world run [--verbose] -- <command...> run the app or its tests inside the world\n volter world activate eval \"$(volter world activate)\": vendor CLIs and curl in this shell reach the twins\n volter world shell a subshell with the world active\n volter world init --bare <org>/<world> --twins a,b a world with no app: a shared world for a team, or one standing in for a vendor\n volter world serve [--port <p>] [--console-port <c>] serve this world on a URL under /<org>/<world>/; prints the token and the console (its own origin, port p+1 by default)\n volter world console [<https://host/org/world>] [--port <p>] the console on this machine for a World served elsewhere (default: this world's origin); needs @volter/world-console\n volter world status world, branch, origin, unpushed changes, what is running\n volter world log [--receipts] [--json] every write the app made; --receipts adds each change's receipt\n volter world diff [--json] what changed since the default data (or the last mark)\n volter world seed load the default data\n volter world reset back to the default data\n volter world down [--purge] stop the twins (--purge forgets this branch's state)\n volter world branch [<name>] list branches, or make one from here and check it out\n volter world checkout <name> switch branches\n volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins\n volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed\n volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)\n volter world fetch what the remote observed since the last fetch\n volter world origin where this world clones from and pushes to\n volter world changeset [<name>] -m \"<message>\" cut the unpushed changes into a reviewable changeset\n volter world pull [--token <token>] fetch, then move this branch onto what came in (git's pull)\n volter world push [<name>] [--token <token>] [--force] push to the remote, which deploys and answers with receipts\n volter world rebase [<changeset>] rebase this branch onto its moved base, or a changeset onto the moved mirror\n volter world verify <changeset> run the world's checks over a changeset and record the result\n volter world approve <changeset> --as <principal> sign a changeset's current hash\n volter world deploy [<changeset>] perform landed changes against each twin's root, by its policy\n\ntwin \u2014 one twin: in this world, or on its own\n volter twin <vendor> the twin's URL, root, deploy policy, whether a credential is sealed\n volter twin <vendor> root <url> [--deploy auto|gated|hold] [--scope <path>] the vendor's real account behind this twin (--none clears it)\n volter twin <vendor> credential seal a credential read from stdin beside the world; never readable back\n volter twin <vendor> refresh observe the root now\n volter twin <vendor> serve [--port <p>] [--read-only] serve the twin; your SDK talks to it at http://127.0.0.1:<p>\n volter twin <vendor> mirror [--port <p>] the twin's UI mirror\n volter twin <vendor> conformance check the twin against the vendor's spec\n\nremote \u2014 the worlds this one pushes to and fetches from, by name\n volter login <platform url> --token <personal token> sign the CLI into the hosted platform (a personal token from the console)\n volter remote add <name> <url|path|org/world> [--token <token>] name a remote; org/world resolves through the platform you logged into\n volter remote remove <name> forget it\n volter remote list every remote, with its URL and whether a token is stored\n (the hosting product's serve answers here for one release: volter remote serve)\n\n --world <dir> the world root, when the cwd is not inside it\n --branch <name> act on that branch instead of the checked-out one\n --json machine-readable output where offered\n";
package/dist/src/cli.js CHANGED
@@ -32,7 +32,7 @@ world — the twins your app needs, running together (the world of the current
32
32
  volter world branch [<name>] list branches, or make one from here and check it out
33
33
  volter world checkout <name> switch branches
34
34
  volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins
35
- volter world clock show | set <iso> | advance <N s|m|h|d> the world's clock: set, not observed
35
+ volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed
36
36
  volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)
37
37
  volter world fetch what the remote observed since the last fetch
38
38
  volter world origin where this world clones from and pushes to
@@ -313,12 +313,22 @@ async function worldCommand(verb, args) {
313
313
  out(world.advanceClock(arg));
314
314
  return;
315
315
  }
316
+ if (action === 'shift') {
317
+ if (!arg)
318
+ throw new Error('volter world clock shift <N s|m|h|d>');
319
+ out(world.shiftClock(arg));
320
+ return;
321
+ }
322
+ if (action === 'clear') {
323
+ out(world.clearClock());
324
+ return;
325
+ }
316
326
  if (action === undefined || action === 'show') {
317
327
  const c = world.clock();
318
- out(`${c.at}${c.frozen ? '' : ' (wall clock — not set)'}`);
328
+ out(`${c.at}${c.frozen ? '' : c.running ? ' (running)' : ' (wall clock — not set)'}`);
319
329
  return;
320
330
  }
321
- throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d>');
331
+ throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear');
322
332
  }
323
333
  case 'replay': {
324
334
  const name = pos[0];
@@ -6,8 +6,6 @@ export type WorldRef = {
6
6
  root?: string;
7
7
  };
8
8
  export type WorldMode = 'local' | 'share' | 'sealed';
9
- /** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
10
- /** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
11
9
  export declare class TwinLog {
12
10
  readonly service: string;
13
11
  readonly stateService: string;
@@ -164,15 +162,23 @@ export declare class World {
164
162
  isMain(): boolean;
165
163
  /** What this branch's changes are measured from, as `diff` says it: the branch point, origin, or the story. */
166
164
  diffBase(): string;
167
- /** The world's clock: the frozen instant every twin stamps from, or the wall clock when unset. */
165
+ /** The world's clock (world-clock.cjs): its time now, and whether it is frozen at an instant, running (moved with
166
+ * `shiftClock`), or the wall clock (unset). */
168
167
  clock(): {
169
168
  at: string;
170
169
  frozen: boolean;
170
+ running: boolean;
171
171
  };
172
- /** Set the world's clock to an instant; every twin stamps from it until it moves. */
172
+ /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
173
173
  setClock(iso: string): string;
174
- /** Move a set clock forward: `30d`, `12h`, `5m`, `90s`. */
174
+ /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
175
175
  advanceClock(by: string): string;
176
+ /** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
177
+ * running clock further. */
178
+ shiftClock(by: string): string;
179
+ /** Return the World to the machine's time. */
180
+ clearClock(): string;
181
+ private writeClock;
176
182
  /** Rebase this branch onto its base's current position, every twin; conflicts named per subject and field. */
177
183
  rebaseBranch(): Array<{
178
184
  service: string;
package/dist/src/world.js CHANGED
@@ -4,14 +4,22 @@
4
4
  // user reads. The `volter` command is one client of this class and adds nothing.
5
5
  import { rebaseChangeset, worldBootMarker, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources } from '@volter/world-core';
6
6
  import { spawnSync } from 'node:child_process';
7
+ import clockForm from '@volter/world-core/world-clock';
7
8
  import { activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedWorld, shellWorld, statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin, deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, findInstalledPackage, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom, materializeRoots } from '@volter/world-runtime';
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
9
10
  import { dirname, join, resolve } from 'node:path';
10
11
  import { requireToken, storeToken } from "./credentials.js";
11
12
  import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigRelative, worldEnvPath, worldSeedPath } from "./locate.js";
12
13
  import { parentEntries, rebaseBranch } from '@volter/world-core';
13
14
  /** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
14
15
  /** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
16
+ /** `30d`, `12h`, `5m`, `90s` in ms. */
17
+ function durationMs(by) {
18
+ const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
19
+ if (!m)
20
+ throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
21
+ return Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
22
+ }
15
23
  export class TwinLog {
16
24
  service;
17
25
  stateService;
@@ -248,9 +256,19 @@ export class World {
248
256
  async branch(name, opts = {}) {
249
257
  if (listWorlds(this.root).some((w) => w.name === name))
250
258
  throw new Error(`A branch "${name}" already exists — \`volter world checkout ${name}\` switches to it`);
251
- await branchWorld(this.name, name, { root: this.root, envFile: worldEnvPath(this.root), ...(opts.at ? { at: opts.at } : {}) });
252
- if (this.instance()?.running)
259
+ // this branch stops first (a managed database is copied from a stopped World, and the two would share its port),
260
+ // and comes back if the new one fails
261
+ const wasRunning = !!this.instance()?.running;
262
+ if (wasRunning)
253
263
  await downWorld(this.name, this.root);
264
+ try {
265
+ await branchWorld(this.name, name, { root: this.root, envFile: worldEnvPath(this.root), ...(opts.at ? { at: opts.at } : {}) });
266
+ }
267
+ catch (error) {
268
+ if (wasRunning)
269
+ await checkoutWorld(this.name, { root: this.root });
270
+ throw error;
271
+ }
254
272
  setCurrentBranch(this.root, name);
255
273
  return new World(name, this.root);
256
274
  }
@@ -283,31 +301,54 @@ export class World {
283
301
  return existsSync(worldSeedPath(this.root)) ? 'the story' : 'the default data';
284
302
  }
285
303
  // ── the clock ───────────────────────────────────────────────────────────────────────────
286
- /** The world's clock: the frozen instant every twin stamps from, or the wall clock when unset. */
304
+ /** The world's clock (world-clock.cjs): its time now, and whether it is frozen at an instant, running (moved with
305
+ * `shiftClock`), or the wall clock (unset). */
287
306
  clock() {
288
307
  const file = clockFile(this.root, this.name);
289
- return existsSync(file) ? { at: readFileSync(file, 'utf8').trim(), frozen: true } : { at: new Date().toISOString(), frozen: false };
308
+ if (!existsSync(file))
309
+ return { at: new Date().toISOString(), frozen: false, running: false };
310
+ const c = clockForm.parseClock(readFileSync(file, 'utf8'));
311
+ return { at: new Date(clockForm.clockNowMs(c, Date.now())).toISOString(), frozen: c.kind === 'frozen', running: c.kind === 'running' };
290
312
  }
291
- /** Set the world's clock to an instant; every twin stamps from it until it moves. */
313
+ /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
292
314
  setClock(iso) {
293
315
  const parsed = Date.parse(iso);
294
316
  if (Number.isNaN(parsed))
295
317
  throw new Error(`${JSON.stringify(iso)} is not an ISO-8601 instant`);
296
- const file = clockFile(this.root, this.name);
297
- mkdirSync(dirname(file), { recursive: true });
298
- writeFileSync(file, `${new Date(parsed).toISOString()}\n`);
318
+ this.writeClock({ kind: 'frozen', at: parsed });
299
319
  return new Date(parsed).toISOString();
300
320
  }
301
- /** Move a set clock forward: `30d`, `12h`, `5m`, `90s`. */
321
+ /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
302
322
  advanceClock(by) {
303
- const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
304
- if (!m)
305
- throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
306
- const current = this.clock();
307
- if (!current.frozen)
308
- throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect');
309
- const ms = Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
310
- return this.setClock(new Date(Date.parse(current.at) + ms).toISOString());
323
+ const file = clockFile(this.root, this.name);
324
+ if (!existsSync(file))
325
+ throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect (`volter world clock shift` moves a running World)');
326
+ const c = clockForm.parseClock(readFileSync(file, 'utf8'));
327
+ this.writeClock({ ...c, at: c.at + durationMs(by) });
328
+ return this.clock().at;
329
+ }
330
+ /** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
331
+ * running clock further. */
332
+ shiftClock(by) {
333
+ const file = clockFile(this.root, this.name);
334
+ const c = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
335
+ if (c?.kind === 'frozen')
336
+ throw new Error('the clock is frozen — `volter world clock advance` moves it');
337
+ const wall = Date.now();
338
+ this.writeClock(c ? { ...c, at: c.at + durationMs(by) } : { kind: 'running', at: wall + durationMs(by), since: wall });
339
+ return this.clock().at;
340
+ }
341
+ /** Return the World to the machine's time. */
342
+ clearClock() {
343
+ const file = clockFile(this.root, this.name);
344
+ if (existsSync(file))
345
+ rmSync(file);
346
+ return new Date().toISOString();
347
+ }
348
+ writeClock(c) {
349
+ const file = clockFile(this.root, this.name);
350
+ mkdirSync(dirname(file), { recursive: true });
351
+ writeFileSync(file, `${clockForm.formatClock(c)}\n`);
311
352
  }
312
353
  /** Rebase this branch onto its base's current position, every twin; conflicts named per subject and field. */
313
354
  rebaseBranch() { return this.repos().map((r) => ({ service: r.service, ...rebaseBranch(r.stateService, r.root) })); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Run your app against a world of twins. `World` and `Repo` — one method per verb: init, up, run, log, diff, reset, branch, checkout, clone, fetch, changeset, push — and the `volter` command, one client of them.",
5
5
  "keywords": [
6
6
  "volter",
@@ -13,6 +13,11 @@
13
13
  ],
14
14
  "author": "Volter (https://github.com/volter-ai)",
15
15
  "license": "Apache-2.0",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/volter-ai/twin.git",
19
+ "directory": "packages/cli"
20
+ },
16
21
  "publishConfig": {
17
22
  "access": "public"
18
23
  },
@@ -23,8 +28,8 @@
23
28
  "dist"
24
29
  ],
25
30
  "dependencies": {
26
- "@volter/world-core": "2.0.0",
27
- "@volter/world-runtime": "2.0.0"
31
+ "@volter/world-core": "2.0.1",
32
+ "@volter/world-runtime": "2.0.1"
28
33
  },
29
34
  "bin": {
30
35
  "volter": "dist/src/cli.js"
package/src/cli.ts CHANGED
@@ -34,7 +34,7 @@ world — the twins your app needs, running together (the world of the current
34
34
  volter world branch [<name>] list branches, or make one from here and check it out
35
35
  volter world checkout <name> switch branches
36
36
  volter world replay <changeset> --into <branch> feed a changeset's changes into another branch's twins
37
- volter world clock show | set <iso> | advance <N s|m|h|d> the world's clock: set, not observed
37
+ volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear the world's clock: set, not observed
38
38
  volter world clone <url> --token <token> [--admin-token <t>] connect this world to a remote and fetch its history (the admin token copies the whole tree at once)
39
39
  volter world fetch what the remote observed since the last fetch
40
40
  volter world origin where this world clones from and pushes to
@@ -248,8 +248,10 @@ async function worldCommand(verb: string, args: string[]): Promise<void> {
248
248
  const [action, arg] = pos;
249
249
  if (action === 'set') { if (!arg) throw new Error('volter world clock set <iso-8601>'); out(world.setClock(arg)); return; }
250
250
  if (action === 'advance') { if (!arg) throw new Error('volter world clock advance <N s|m|h|d>'); out(world.advanceClock(arg)); return; }
251
- if (action === undefined || action === 'show') { const c = world.clock(); out(`${c.at}${c.frozen ? '' : ' (wall clock — not set)'}`); return; }
252
- throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d>');
251
+ if (action === 'shift') { if (!arg) throw new Error('volter world clock shift <N s|m|h|d>'); out(world.shiftClock(arg)); return; }
252
+ if (action === 'clear') { out(world.clearClock()); return; }
253
+ if (action === undefined || action === 'show') { const c = world.clock(); out(`${c.at}${c.frozen ? '' : c.running ? ' (running)' : ' (wall clock — not set)'}`); return; }
254
+ throw new Error('volter world clock show | set <iso> | advance <N s|m|h|d> | shift <N s|m|h|d> | clear');
253
255
  }
254
256
  case 'replay': {
255
257
  const name = pos[0]; const into = value(args, '--into');
package/src/world.ts CHANGED
@@ -4,13 +4,14 @@
4
4
  // user reads. The `volter` command is one client of this class and adds nothing.
5
5
  import { rebaseChangeset, worldBootMarker, type Changeset, type ChangesetApplication, type LedgerDelta, type PerformAction, type RemoteExecute, type TwinAction, type WorldMarker, type ApplyReceipt, listActions, isTwinBookkeeping, pushablePendingActions, pendingActions, applyTwinWrite, twinResources, type TwinResource } from '@volter/world-core';
6
6
  import { spawnSync } from 'node:child_process';
7
+ import clockForm from '@volter/world-core/world-clock';
7
8
  import {
8
9
  activateScript, approveWorldChangeset, branchWorld, clockFile, checkoutWorld, createWorldChangeset, diffWorld, downWorld, fetchFromOrigin, findWorldChangeset, initWorld, listWorldChangesets, listWorldMarks, listWorlds, markWorld, pushWorldChangeset, rebaseWorldChangeset, replayWorldChangeset, resetWorld, runWithWorldEnv, seedWorld, shellWorld,
9
10
  statusWorld, statusWorldChangeset, upWorld, verifyWorldChangeset, worldLedgers, worldOrigin,
10
11
  deployWorld, loadWorldConfig, writeWorldConfig, readServeRecord, refreshTwin, serveWorld, findInstalledPackage, type ServedWorld, rootForControlRoot, sealTwinCredential, sealedCredentialInfo, setTwinRoot, credentialPayloadFrom,
11
12
  type DeployTwinOutcome, type MaterializedRoot,
12
13
  type ChangesetLocation, type InitOptions, type InitResult, type WorldInstance, materializeRoots } from '@volter/world-runtime';
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
15
  import { dirname, join, resolve } from 'node:path';
15
16
  import { requireToken, storeToken } from './credentials.ts';
16
17
  import { currentBranch, findWorldRoot, mainBranch, requireWorldRoot, setCurrentBranch, worldConfigRelative, worldEnvPath, worldSeedPath } from './locate.ts';
@@ -21,6 +22,13 @@ export type WorldMode = 'local' | 'share' | 'sealed';
21
22
 
22
23
  /** One change as `log` shows it: the write, and the receipt the vendor answered with once pushed. */
23
24
  /** A twin's branch log as the world reads it: the twin's name, the state service it records under, its control root. */
25
+ /** `30d`, `12h`, `5m`, `90s` in ms. */
26
+ function durationMs(by: string): number {
27
+ const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
28
+ if (!m) throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
29
+ return Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2] as 's' | 'm' | 'h' | 'd'];
30
+ }
31
+
24
32
  export class TwinLog {
25
33
  constructor(readonly service: string, readonly stateService: string, readonly root: string) {}
26
34
  /** this branch's own entries, bookkeeping aside */
@@ -233,8 +241,16 @@ export class World {
233
241
  */
234
242
  async branch(name: string, opts: { at?: { instant?: string; positions?: Record<string, number>; views?: Record<string, string> } } = {}): Promise<World> {
235
243
  if (listWorlds(this.root).some((w) => w.name === name)) throw new Error(`A branch "${name}" already exists — \`volter world checkout ${name}\` switches to it`);
236
- await branchWorld(this.name, name, { root: this.root, envFile: worldEnvPath(this.root), ...(opts.at ? { at: opts.at } : {}) });
237
- if (this.instance()?.running) await downWorld(this.name, this.root);
244
+ // this branch stops first (a managed database is copied from a stopped World, and the two would share its port),
245
+ // and comes back if the new one fails
246
+ const wasRunning = !!this.instance()?.running;
247
+ if (wasRunning) await downWorld(this.name, this.root);
248
+ try {
249
+ await branchWorld(this.name, name, { root: this.root, envFile: worldEnvPath(this.root), ...(opts.at ? { at: opts.at } : {}) });
250
+ } catch (error) {
251
+ if (wasRunning) await checkoutWorld(this.name, { root: this.root });
252
+ throw error;
253
+ }
238
254
  setCurrentBranch(this.root, name);
239
255
  return new World(name, this.root);
240
256
  }
@@ -261,28 +277,49 @@ export class World {
261
277
  }
262
278
 
263
279
  // ── the clock ───────────────────────────────────────────────────────────────────────────
264
- /** The world's clock: the frozen instant every twin stamps from, or the wall clock when unset. */
265
- clock(): { at: string; frozen: boolean } {
280
+ /** The world's clock (world-clock.cjs): its time now, and whether it is frozen at an instant, running (moved with
281
+ * `shiftClock`), or the wall clock (unset). */
282
+ clock(): { at: string; frozen: boolean; running: boolean } {
266
283
  const file = clockFile(this.root, this.name);
267
- return existsSync(file) ? { at: readFileSync(file, 'utf8').trim(), frozen: true } : { at: new Date().toISOString(), frozen: false };
284
+ if (!existsSync(file)) return { at: new Date().toISOString(), frozen: false, running: false };
285
+ const c = clockForm.parseClock(readFileSync(file, 'utf8'));
286
+ return { at: new Date(clockForm.clockNowMs(c, Date.now())).toISOString(), frozen: c.kind === 'frozen', running: c.kind === 'running' };
268
287
  }
269
- /** Set the world's clock to an instant; every twin stamps from it until it moves. */
288
+ /** Set the world's clock to an instant; every twin, and the World's application, stamps from it until it moves. */
270
289
  setClock(iso: string): string {
271
290
  const parsed = Date.parse(iso);
272
291
  if (Number.isNaN(parsed)) throw new Error(`${JSON.stringify(iso)} is not an ISO-8601 instant`);
273
- const file = clockFile(this.root, this.name);
274
- mkdirSync(dirname(file), { recursive: true });
275
- writeFileSync(file, `${new Date(parsed).toISOString()}\n`);
292
+ this.writeClock({ kind: 'frozen', at: parsed });
276
293
  return new Date(parsed).toISOString();
277
294
  }
278
- /** Move a set clock forward: `30d`, `12h`, `5m`, `90s`. */
295
+ /** Move a set clock forward, frozen or running: `30d`, `12h`, `5m`, `90s`. */
279
296
  advanceClock(by: string): string {
280
- const m = /^(\d+(?:\.\d+)?)(s|m|h|d)$/.exec(by.trim());
281
- if (!m) throw new Error(`${JSON.stringify(by)} is not <N>(s|m|h|d)`);
282
- const current = this.clock();
283
- if (!current.frozen) throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect');
284
- const ms = Number(m[1]) * { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2] as 's' | 'm' | 'h' | 'd'];
285
- return this.setClock(new Date(Date.parse(current.at) + ms).toISOString());
297
+ const file = clockFile(this.root, this.name);
298
+ if (!existsSync(file)) throw new Error('the clock is not set — `volter world clock set <iso>` first; advancing the wall clock would freeze time as a side effect (`volter world clock shift` moves a running World)');
299
+ const c = clockForm.parseClock(readFileSync(file, 'utf8'));
300
+ this.writeClock({ ...c, at: c.at + durationMs(by) });
301
+ return this.clock().at;
302
+ }
303
+ /** Move the World forward while its time keeps running (a World serving an application): from the wall clock, or a
304
+ * running clock further. */
305
+ shiftClock(by: string): string {
306
+ const file = clockFile(this.root, this.name);
307
+ const c = existsSync(file) ? clockForm.parseClock(readFileSync(file, 'utf8')) : null;
308
+ if (c?.kind === 'frozen') throw new Error('the clock is frozen — `volter world clock advance` moves it');
309
+ const wall = Date.now();
310
+ this.writeClock(c ? { ...c, at: c.at + durationMs(by) } : { kind: 'running', at: wall + durationMs(by), since: wall });
311
+ return this.clock().at;
312
+ }
313
+ /** Return the World to the machine's time. */
314
+ clearClock(): string {
315
+ const file = clockFile(this.root, this.name);
316
+ if (existsSync(file)) rmSync(file);
317
+ return new Date().toISOString();
318
+ }
319
+ private writeClock(c: Parameters<typeof clockForm.formatClock>[0]): void {
320
+ const file = clockFile(this.root, this.name);
321
+ mkdirSync(dirname(file), { recursive: true });
322
+ writeFileSync(file, `${clockForm.formatClock(c)}\n`);
286
323
  }
287
324
  /** Rebase this branch onto its base's current position, every twin; conflicts named per subject and field. */
288
325
  rebaseBranch(): Array<{ service: string } & ReturnType<typeof rebaseBranch>> { return this.repos().map((r) => ({ service: r.service, ...rebaseBranch(r.stateService, r.root) })); }