narraeon 0.6.0 → 0.6.2
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/README.md +11 -0
- package/README.zh-CN.md +8 -0
- package/dist/node/protocol/v1.js +12 -2
- package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
- package/dist/node/runtime/V1Runtime.js +5 -5
- package/dist/node/runtime/archive/WorldArchives.js +14 -46
- package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
- package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +25 -2
- package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
- package/dist/node/runtime/config/dataRootLock.js +108 -0
- package/dist/node/runtime/content/ContentWorkspace.js +0 -10
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +7 -44
- package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
- package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
- package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +32 -207
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +29 -239
- package/dist/node/runtime/play/PlayCallChain.js +338 -366
- package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
- package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
- package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +39 -7
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +7 -16
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +14 -6
- package/dist/node/runtime/setting/SettingImprovementSession.js +39 -25
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
- package/dist/node/runtime/world/FileNativeWorldStore.js +230 -781
- package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
- package/dist/node/runtime/world/WorldActivity.js +14 -22
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
- package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
- package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
- package/dist/node/runtime/world/WorldQueue.js +46 -0
- package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +1 -13
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +29 -2
- package/dist/node/server/mobile.js +52 -0
- package/dist/node/server/startWebServer.js +15 -8
- package/dist/web/assets/index-CXnFaYK9.js +1323 -0
- package/dist/web/assets/{index-BJ-1Mb0e.css → index-LPMV0pl3.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
- package/dist/web/assets/index-BgBL8G-A.js +0 -1323
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { captureNarrativeOrigin, snapshotBackground, isHistoryBackground, } from "../history/HistoryBackground.js";
|
|
2
|
-
import { FileNativeArtifactStore } from "../artifact/FileNativeArtifactStore.js";
|
|
2
|
+
import { FileNativeArtifactStore, forkArtifactClosure, } from "../artifact/FileNativeArtifactStore.js";
|
|
3
|
+
import { migrateLegacyArtifactStore } from "../artifact/LegacyArtifactStoreMigration.js";
|
|
3
4
|
import { WorldExtensionRequests } from "../extension/WorldExtensionRequests.js";
|
|
4
5
|
import { WorldExtensionControls, extensionDefinitions, } from "../extension/WorldExtensionControls.js";
|
|
5
6
|
import { assertSameGameplay, readGameplay, } from "../../protocol/gameplay.js";
|
|
@@ -7,7 +8,7 @@ import { HistoryQuery } from "../history/HistoryQuery.js";
|
|
|
7
8
|
import { exportStory } from "../history/StoryExport.js";
|
|
8
9
|
import { PackageScriptPermissions } from "../extension/PackageScriptPermissions.js";
|
|
9
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import {
|
|
11
|
+
import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
|
|
11
12
|
import { dirname, join, resolve } from "node:path";
|
|
12
13
|
import { isDeepStrictEqual } from "node:util";
|
|
13
14
|
import { inspectContentPackageCurrentTree, isContentPackageWorldDocumentPath, } from "../content/FileNativeContentTree.js";
|
|
@@ -17,10 +18,11 @@ import { WorldDocumentStore } from "./WorldDocumentStore.js";
|
|
|
17
18
|
import { readWorldActivity, updateWorldActivity, } from "./WorldActivity.js";
|
|
18
19
|
import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore.js";
|
|
19
20
|
import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
|
|
20
|
-
import { FileNativeWorldStorageMigrator } from "./FileNativeWorldStorageMigrator.js";
|
|
21
|
-
import {
|
|
21
|
+
import { FileNativeWorldStorageMigrator, UnsupportedLegacyStorageError, } from "./FileNativeWorldStorageMigrator.js";
|
|
22
|
+
import { WorldAuthorityError, WorldLedgerAuthority, } from "./WorldLedgerAuthority.js";
|
|
23
|
+
import { forgetLedger, rewindTo } from "./WorldTurnLedger.js";
|
|
22
24
|
import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
|
|
23
|
-
import {
|
|
25
|
+
import { projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
|
|
24
26
|
const publicationFile = "publication.json";
|
|
25
27
|
const localMetadataFile = "local.json";
|
|
26
28
|
export class FileNativeWorldCreationError extends Error {
|
|
@@ -37,11 +39,21 @@ export class FileNativeWorldNotFoundError extends Error {
|
|
|
37
39
|
this.name = "FileNativeWorldNotFoundError";
|
|
38
40
|
}
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Completed operations kept per world for same-process duplicate guards. A
|
|
44
|
+
* pending player-revision intent resumes against its own world's entry, so
|
|
45
|
+
* retention is per world: another world's traffic cannot evict it.
|
|
46
|
+
*/
|
|
47
|
+
const RETAINED_OPERATION_OUTCOMES_PER_WORLD = 256;
|
|
40
48
|
export class FileNativeWorldStore {
|
|
41
49
|
extensionRequests;
|
|
42
50
|
#documentMaintenanceCache = new Map();
|
|
43
51
|
#worldsRoot;
|
|
44
|
-
|
|
52
|
+
// Process-local duplicate guard keyed by operation ID (ADR-0044): entries are
|
|
53
|
+
// not persisted, so a restart forgets them and a repeated operation redoes.
|
|
54
|
+
// Completed entries are bounded per world; in-progress entries are not.
|
|
55
|
+
#operationOutcomes = new Map();
|
|
56
|
+
#retainedOperationIds = new Map();
|
|
45
57
|
#promptCompiler;
|
|
46
58
|
#activeControlUsers = new Map();
|
|
47
59
|
#openingWorldRevisions = new Set();
|
|
@@ -56,23 +68,26 @@ export class FileNativeWorldStore {
|
|
|
56
68
|
const root = resolve(dataRoot);
|
|
57
69
|
this.#worldsRoot = join(root, "worlds-file-native");
|
|
58
70
|
this.extensionRequests = new WorldExtensionRequests(this.#worldsRoot);
|
|
59
|
-
this.#operationsRoot = join(root, "operations");
|
|
60
71
|
this.#promptCompiler =
|
|
61
72
|
options.promptCompiler ?? new FileNativePromptCompiler();
|
|
62
73
|
this.operations = new FileNativeWorldOperationCoordinator(root);
|
|
63
74
|
this.playTimeline = new FileNativePlayTimelineStore(root);
|
|
64
75
|
this.playAdvances = new FileNativePlayAdvanceStore(root);
|
|
65
|
-
this.#storageMigrator = new FileNativeWorldStorageMigrator(root
|
|
76
|
+
this.#storageMigrator = new FileNativeWorldStorageMigrator(root);
|
|
66
77
|
}
|
|
67
78
|
/**
|
|
68
|
-
* Upgrade
|
|
69
|
-
* play-timeline behavior crosses its seam. Concurrent callers share one
|
|
79
|
+
* Upgrade an Authority V3 world to the current ledger layout before current
|
|
80
|
+
* Authority or play-timeline behavior crosses its seam. Concurrent callers share one
|
|
70
81
|
* in-process promise; the durable world lock serializes processes.
|
|
71
82
|
*/
|
|
72
83
|
async ensureCurrentStorage(worldId) {
|
|
73
84
|
assertIdentity(worldId, "World ID");
|
|
74
85
|
if (this.#currentStorage.has(worldId))
|
|
75
86
|
return;
|
|
87
|
+
// A shared migration may be queued behind this caller; when the caller
|
|
88
|
+
// already holds the world, run the migration inline instead of awaiting it.
|
|
89
|
+
if (this.operations.isWorldHeld(worldId))
|
|
90
|
+
return this.#ensureCurrentStorageSerialized(worldId);
|
|
76
91
|
const existing = this.#storageMigrations.get(worldId);
|
|
77
92
|
if (existing !== undefined)
|
|
78
93
|
return existing;
|
|
@@ -89,7 +104,7 @@ export class FileNativeWorldStore {
|
|
|
89
104
|
async #ensureCurrentStorageSerialized(worldId) {
|
|
90
105
|
try {
|
|
91
106
|
const root = join(this.#worldsRoot, worldId);
|
|
92
|
-
const authority = new
|
|
107
|
+
const authority = new WorldLedgerAuthority(root);
|
|
93
108
|
if (await authority.exists()) {
|
|
94
109
|
await authority.readHead();
|
|
95
110
|
this.#currentStorage.add(worldId);
|
|
@@ -97,6 +112,7 @@ export class FileNativeWorldStore {
|
|
|
97
112
|
}
|
|
98
113
|
await this.operations.withWorldAuthorityLock(worldId, async () => {
|
|
99
114
|
await this.#storageMigrator.migrate(worldId);
|
|
115
|
+
await migrateLegacyArtifactStore(dirname(this.#worldsRoot), worldId);
|
|
100
116
|
});
|
|
101
117
|
this.#currentStorage.add(worldId);
|
|
102
118
|
}
|
|
@@ -114,7 +130,9 @@ export class FileNativeWorldStore {
|
|
|
114
130
|
}
|
|
115
131
|
if (error instanceof WorldOperationBusyError)
|
|
116
132
|
throw new FileNativeWorldCreationError("operation_conflict", "World storage is being upgraded by another durable operation", { cause: error });
|
|
117
|
-
|
|
133
|
+
if (error instanceof UnsupportedLegacyStorageError)
|
|
134
|
+
throw new FileNativeWorldCreationError("world_corrupt", "This world was created by v0.2.0 or earlier; open it once with v0.3.0–v0.6.0 before upgrading", { cause: error });
|
|
135
|
+
throw new FileNativeWorldCreationError("world_corrupt", "World storage could not be migrated to the current format", error instanceof Error ? { cause: error } : undefined);
|
|
118
136
|
}
|
|
119
137
|
}
|
|
120
138
|
async gameplay(worldId) {
|
|
@@ -150,7 +168,7 @@ export class FileNativeWorldStore {
|
|
|
150
168
|
const root = join(this.#worldsRoot, worldId);
|
|
151
169
|
const publication = await readPublicationAt(root);
|
|
152
170
|
const title = await readWorldTitleAt(root, publication);
|
|
153
|
-
const endpoint = await new
|
|
171
|
+
const endpoint = await new WorldLedgerAuthority(root).recoverHeadResult();
|
|
154
172
|
const history = new HistoryQuery(worldId, endpoint.history.map(({ messageId, role, exactText }) => ({
|
|
155
173
|
id: messageId,
|
|
156
174
|
role,
|
|
@@ -200,8 +218,14 @@ export class FileNativeWorldStore {
|
|
|
200
218
|
throw error;
|
|
201
219
|
}
|
|
202
220
|
return this.operations.withExclusiveWorldStateMutation(worldId, () => this.operations.withWorldAuthorityLock(worldId, async () => {
|
|
221
|
+
forgetLedger(root);
|
|
203
222
|
await rm(root, { recursive: true, force: true });
|
|
223
|
+
// 删除期间的并发读取可能重新填充缓存,删除后再失效一次。
|
|
224
|
+
forgetLedger(root);
|
|
204
225
|
this.#currentStorage.delete(worldId);
|
|
226
|
+
for (const operationId of this.#retainedOperationIds.get(worldId) ?? [])
|
|
227
|
+
this.#operationOutcomes.delete(operationId);
|
|
228
|
+
this.#retainedOperationIds.delete(worldId);
|
|
205
229
|
return { deleted: true };
|
|
206
230
|
}));
|
|
207
231
|
}
|
|
@@ -298,9 +322,7 @@ export class FileNativeWorldStore {
|
|
|
298
322
|
const preview = this.#preview(boundInput, worldId);
|
|
299
323
|
const finalRoot = join(this.#worldsRoot, worldId);
|
|
300
324
|
const stagingRoot = join(this.#worldsRoot, `.staging-${worldId}-${randomUUID()}`);
|
|
301
|
-
const operationPath = this.#operationPath(input.operationId);
|
|
302
325
|
await mkdir(this.#worldsRoot, { recursive: true, mode: 0o700 });
|
|
303
|
-
await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
|
|
304
326
|
await rm(stagingRoot, { recursive: true, force: true });
|
|
305
327
|
try {
|
|
306
328
|
const openingMessage = {
|
|
@@ -334,19 +356,7 @@ export class FileNativeWorldStore {
|
|
|
334
356
|
worldId,
|
|
335
357
|
history: genesis.history,
|
|
336
358
|
});
|
|
337
|
-
await new
|
|
338
|
-
operationId: input.operationId,
|
|
339
|
-
state,
|
|
340
|
-
history: [openingMessage],
|
|
341
|
-
additionalMaterials: initialMaterials,
|
|
342
|
-
});
|
|
343
|
-
await writeJson(join(stagingRoot, "runtime", "materialized-head.json"), {
|
|
344
|
-
schemaVersion: 3,
|
|
345
|
-
head: "genesis",
|
|
346
|
-
sequence: 0,
|
|
347
|
-
commitDigest: null,
|
|
348
|
-
...backgroundCount([openingMessage]),
|
|
349
|
-
});
|
|
359
|
+
await new WorldLedgerAuthority(stagingRoot).initialize();
|
|
350
360
|
await writeJson(join(stagingRoot, "runtime", "additional-materials.json"), { head: "genesis", items: initialMaterials });
|
|
351
361
|
const publication = {
|
|
352
362
|
schemaVersion: 1,
|
|
@@ -358,12 +368,7 @@ export class FileNativeWorldStore {
|
|
|
358
368
|
};
|
|
359
369
|
await writeJson(join(stagingRoot, publicationFile), publication);
|
|
360
370
|
await updateWorldActivity(stagingRoot, "genesis", [openingMessage], Date.now());
|
|
361
|
-
await durableTree(stagingRoot);
|
|
362
|
-
crashAtFileNativeWorldCreationEdge("after_staging");
|
|
363
371
|
await rename(stagingRoot, finalRoot);
|
|
364
|
-
await syncDirectory(this.#worldsRoot);
|
|
365
|
-
crashAtFileNativeWorldCreationEdge("after_atomic_publish");
|
|
366
|
-
await publishOperation(operationPath, publication);
|
|
367
372
|
return {
|
|
368
373
|
outcome: "created",
|
|
369
374
|
world: await this.#currentSummary(publication),
|
|
@@ -418,12 +423,8 @@ export class FileNativeWorldStore {
|
|
|
418
423
|
* must never pair a mutable state tree with a different response head.
|
|
419
424
|
*/
|
|
420
425
|
async renderPlayerViewsAtHead(worldId, head) {
|
|
421
|
-
const root = join(this.#worldsRoot, worldId);
|
|
422
|
-
const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
|
|
423
426
|
const [state, control] = await Promise.all([
|
|
424
|
-
|
|
425
|
-
? readTree(join(root, "state"))
|
|
426
|
-
: this.recoverEndpoint(worldId, head).then(({ state }) => state),
|
|
427
|
+
this.recoverEndpoint(worldId, head).then(({ state }) => state),
|
|
427
428
|
this.readSurface(worldId, "control"),
|
|
428
429
|
]);
|
|
429
430
|
const source = control.find(({ path }) => path === "player-views.yaml");
|
|
@@ -437,7 +438,7 @@ export class FileNativeWorldStore {
|
|
|
437
438
|
await this.ensureCurrentStorage(worldId);
|
|
438
439
|
const root = join(this.#worldsRoot, worldId);
|
|
439
440
|
await readPublicationAt(root);
|
|
440
|
-
return (await new
|
|
441
|
+
return (await new WorldLedgerAuthority(root).readHead()).head;
|
|
441
442
|
}
|
|
442
443
|
async withArchiveExport(worldId, action) {
|
|
443
444
|
assertIdentity(worldId, "World ID");
|
|
@@ -469,7 +470,7 @@ export class FileNativeWorldStore {
|
|
|
469
470
|
await this.ensureCurrentStorage(worldId);
|
|
470
471
|
const root = join(this.#worldsRoot, worldId);
|
|
471
472
|
await readPublicationAt(root);
|
|
472
|
-
return new
|
|
473
|
+
return new WorldLedgerAuthority(root).currentOperationId();
|
|
473
474
|
}
|
|
474
475
|
async saveControlDraft(worldId, files) {
|
|
475
476
|
assertIdentity(worldId, "World ID");
|
|
@@ -547,7 +548,6 @@ export class FileNativeWorldStore {
|
|
|
547
548
|
if (currentFingerprint === nextFingerprint) {
|
|
548
549
|
await rm(stagingRoot, { recursive: true, force: true });
|
|
549
550
|
await rm(previousPath, { recursive: true, force: true });
|
|
550
|
-
await syncDirectory(runtimeRoot);
|
|
551
551
|
return { fingerprint: nextFingerprint };
|
|
552
552
|
}
|
|
553
553
|
const previous = await readTree(previousPath);
|
|
@@ -556,16 +556,11 @@ export class FileNativeWorldStore {
|
|
|
556
556
|
fingerprint(previous) === input.expectedFingerprint) {
|
|
557
557
|
if (fingerprint(staged) === nextFingerprint) {
|
|
558
558
|
await rename(stagedPath, currentPath);
|
|
559
|
-
await syncDirectory(root);
|
|
560
|
-
await syncDirectory(stagingRoot);
|
|
561
559
|
await rm(previousPath, { recursive: true, force: true });
|
|
562
560
|
await rm(stagingRoot, { recursive: true, force: true });
|
|
563
|
-
await syncDirectory(runtimeRoot);
|
|
564
561
|
return { fingerprint: nextFingerprint };
|
|
565
562
|
}
|
|
566
563
|
await rename(previousPath, currentPath);
|
|
567
|
-
await syncDirectory(root);
|
|
568
|
-
await syncDirectory(runtimeRoot);
|
|
569
564
|
current = await readTree(currentPath);
|
|
570
565
|
currentFingerprint = fingerprint(current);
|
|
571
566
|
}
|
|
@@ -575,16 +570,10 @@ export class FileNativeWorldStore {
|
|
|
575
570
|
await rm(stagingRoot, { recursive: true, force: true });
|
|
576
571
|
await rm(previousPath, { recursive: true, force: true });
|
|
577
572
|
await writeSurface(stagingRoot, "control", input.files);
|
|
578
|
-
await durableTree(stagingRoot);
|
|
579
573
|
await rename(currentPath, previousPath);
|
|
580
|
-
await syncDirectory(root);
|
|
581
|
-
await syncDirectory(runtimeRoot);
|
|
582
574
|
await rename(stagedPath, currentPath);
|
|
583
|
-
await syncDirectory(root);
|
|
584
|
-
await syncDirectory(stagingRoot);
|
|
585
575
|
await rm(previousPath, { recursive: true, force: true });
|
|
586
576
|
await rm(stagingRoot, { recursive: true, force: true });
|
|
587
|
-
await syncDirectory(runtimeRoot);
|
|
588
577
|
return { fingerprint: nextFingerprint };
|
|
589
578
|
});
|
|
590
579
|
}
|
|
@@ -709,52 +698,22 @@ export class FileNativeWorldStore {
|
|
|
709
698
|
}
|
|
710
699
|
try {
|
|
711
700
|
return await this.operations.withWorldAuthorityLock(worldId, async () => {
|
|
712
|
-
const
|
|
713
|
-
const
|
|
714
|
-
const checkpoint = await readMaterializedCheckpoint(root);
|
|
715
|
-
const cachedBackgrounds = await readTree(join(root, "runtime", "history-backgrounds"));
|
|
716
|
-
if (!sameMaterializedEndpoint(checkpoint, authority) ||
|
|
717
|
-
(checkpoint?.historyBackgroundCount ?? 0) !== cachedBackgrounds.length) {
|
|
718
|
-
const recovered = await authorityStore.recover();
|
|
719
|
-
let operation = null;
|
|
720
|
-
if (authority.operationId !== null && authority.sequence > 0) {
|
|
721
|
-
const candidate = await this.getOperationOutcome(authority.operationId);
|
|
722
|
-
if (isCommittedOutcome(candidate)) {
|
|
723
|
-
if (candidate.worldId !== worldId ||
|
|
724
|
-
candidate.head !== authority.head ||
|
|
725
|
-
candidate.commitDigest !== authority.commitDigest)
|
|
726
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Current Authority head is bound to a different operation receipt");
|
|
727
|
-
operation = candidate;
|
|
728
|
-
}
|
|
729
|
-
else if (candidate.outcome !== "not_started") {
|
|
730
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Current Authority head has no recoverable acceptance receipt");
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
await materializeRecoveredEndpoint(root, authority, recovered);
|
|
734
|
-
if (operation !== null)
|
|
735
|
-
await publishJson(this.#operationOutcomePath(authority.operationId), { ...operation, outcome: "committed" });
|
|
736
|
-
}
|
|
737
|
-
const [state, control, history, materials, backgrounds] = await Promise.all([
|
|
738
|
-
readTree(join(root, "state")),
|
|
739
|
-
readTree(join(root, "control")),
|
|
740
|
-
readTree(join(root, "history")),
|
|
741
|
-
readOptionalJson(join(root, "runtime", "additional-materials.json")),
|
|
742
|
-
readTree(join(root, "runtime", "history-backgrounds")),
|
|
743
|
-
]);
|
|
744
|
-
if ((materials?.head ?? "genesis") !== authority.head)
|
|
745
|
-
throw new FileNativeWorldCreationError("inconsistent_materialization", "Materialized additional materials do not match the current Authority endpoint");
|
|
701
|
+
const recovered = await new WorldLedgerAuthority(root).recover();
|
|
702
|
+
const control = await readTree(join(root, "control"));
|
|
746
703
|
return {
|
|
747
704
|
worldId,
|
|
748
|
-
parentHead:
|
|
749
|
-
files: [...state, ...control].reduce((files, { path, contents }, index) => {
|
|
750
|
-
files[`${index < state.length ? "state" : "control"}/${path}`] =
|
|
751
|
-
contents;
|
|
705
|
+
parentHead: recovered.head,
|
|
706
|
+
files: [...recovered.state, ...control].reduce((files, { path, contents }, index) => {
|
|
707
|
+
files[`${index < recovered.state.length ? "state" : "control"}/${path}`] = contents;
|
|
752
708
|
return files;
|
|
753
709
|
}, {}),
|
|
754
|
-
additionalMaterials:
|
|
755
|
-
history:
|
|
756
|
-
|
|
757
|
-
|
|
710
|
+
additionalMaterials: recovered.additionalMaterials,
|
|
711
|
+
history: Object.fromEntries(recovered.history.map(({ messageId, exactText }) => [
|
|
712
|
+
messageId,
|
|
713
|
+
exactText,
|
|
714
|
+
])),
|
|
715
|
+
historyBackgrounds: historyBackgroundRecord(recovered.history),
|
|
716
|
+
narrativeCheckpoint: recovered.narrativeCheckpoint,
|
|
758
717
|
};
|
|
759
718
|
});
|
|
760
719
|
}
|
|
@@ -767,7 +726,7 @@ export class FileNativeWorldStore {
|
|
|
767
726
|
/**
|
|
768
727
|
* Bind an immutable historical endpoint with the world's current control
|
|
769
728
|
* surface. This is the same semantic input as a fresh context started while
|
|
770
|
-
* that endpoint was current, without
|
|
729
|
+
* that endpoint was current, without rewinding today's head.
|
|
771
730
|
*/
|
|
772
731
|
async bindPlayCallChainAt(worldId, head) {
|
|
773
732
|
assertIdentity(worldId, "World ID");
|
|
@@ -791,34 +750,29 @@ export class FileNativeWorldStore {
|
|
|
791
750
|
])),
|
|
792
751
|
};
|
|
793
752
|
}
|
|
794
|
-
|
|
753
|
+
reserveOperation(operationId) {
|
|
795
754
|
assertIdentity(operationId, "operation ID");
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
755
|
+
if (this.#operationOutcomes.has(operationId))
|
|
756
|
+
return Promise.resolve(false);
|
|
757
|
+
this.#operationOutcomes.set(operationId, { phase: "in_progress" });
|
|
758
|
+
return Promise.resolve(true);
|
|
759
|
+
}
|
|
760
|
+
#rememberOperationOutcome(worldId, operationId, outcome) {
|
|
761
|
+
this.#operationOutcomes.set(operationId, { phase: "done", outcome });
|
|
762
|
+
const retained = this.#retainedOperationIds.get(worldId) ?? [];
|
|
763
|
+
retained.push(operationId);
|
|
764
|
+
while (retained.length > RETAINED_OPERATION_OUTCOMES_PER_WORLD) {
|
|
765
|
+
const evicted = retained.shift();
|
|
766
|
+
if (evicted !== undefined)
|
|
767
|
+
this.#operationOutcomes.delete(evicted);
|
|
808
768
|
}
|
|
769
|
+
this.#retainedOperationIds.set(worldId, retained);
|
|
809
770
|
}
|
|
810
|
-
|
|
771
|
+
releaseOperationReservation(operationId) {
|
|
811
772
|
assertIdentity(operationId, "operation ID");
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
return;
|
|
816
|
-
if (!isRecord(current) ||
|
|
817
|
-
!hasExactKeys(current, ["outcome"]) ||
|
|
818
|
-
current.outcome !== "in_progress")
|
|
819
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Operation reservation to release has an invalid shape");
|
|
820
|
-
await rm(path, { force: true });
|
|
821
|
-
await syncDirectory(this.#operationsRoot);
|
|
773
|
+
if (this.#operationOutcomes.get(operationId)?.phase === "in_progress")
|
|
774
|
+
this.#operationOutcomes.delete(operationId);
|
|
775
|
+
return Promise.resolve();
|
|
822
776
|
}
|
|
823
777
|
async recoverEndpoint(worldId, head) {
|
|
824
778
|
assertIdentity(worldId, "World ID");
|
|
@@ -826,7 +780,7 @@ export class FileNativeWorldStore {
|
|
|
826
780
|
const root = join(this.#worldsRoot, worldId);
|
|
827
781
|
await readPublicationAt(root);
|
|
828
782
|
try {
|
|
829
|
-
const recovered = await new
|
|
783
|
+
const recovered = await new WorldLedgerAuthority(root).recover(head);
|
|
830
784
|
return {
|
|
831
785
|
worldId,
|
|
832
786
|
head: recovered.head,
|
|
@@ -842,25 +796,6 @@ export class FileNativeWorldStore {
|
|
|
842
796
|
throw new FileNativeWorldCreationError("world_corrupt", `Cannot recover immutable Authority endpoint${head === undefined ? "" : `: ${head}`}`, error instanceof Error ? { cause: error } : undefined);
|
|
843
797
|
}
|
|
844
798
|
}
|
|
845
|
-
async repairMaterialization(worldId) {
|
|
846
|
-
assertIdentity(worldId, "World ID");
|
|
847
|
-
await this.ensureCurrentStorage(worldId);
|
|
848
|
-
try {
|
|
849
|
-
const operationId = await this.operations.withExclusiveWorldStateMutation(worldId, async () => {
|
|
850
|
-
await this.bindPlayCallChain(worldId);
|
|
851
|
-
const root = join(this.#worldsRoot, worldId);
|
|
852
|
-
return (await new FileNativeAuthorityV3(root).readHead()).operationId;
|
|
853
|
-
});
|
|
854
|
-
return operationId === null
|
|
855
|
-
? { outcome: "not_started" }
|
|
856
|
-
: this.getOperationOutcome(operationId);
|
|
857
|
-
}
|
|
858
|
-
catch (error) {
|
|
859
|
-
if (error instanceof WorldOperationBusyError)
|
|
860
|
-
throw new FileNativeWorldCreationError("operation_conflict", "This world has a durable state operation and cannot repair materialization yet");
|
|
861
|
-
throw error;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
799
|
async deriveWorld(input) {
|
|
865
800
|
assertIdentity(input.operationId, "Fork operation ID");
|
|
866
801
|
assertIdentity(input.sourceWorldId, "Source world ID");
|
|
@@ -878,37 +813,17 @@ export class FileNativeWorldStore {
|
|
|
878
813
|
const target = input.targetDataRoot === undefined
|
|
879
814
|
? this
|
|
880
815
|
: new FileNativeWorldStore(input.targetDataRoot);
|
|
881
|
-
const operationPath = input.identityPrefix === "import"
|
|
882
|
-
? join(target.#operationsRoot, `import-${operationDigest(input.operationId)}.json`)
|
|
883
|
-
: target.#derivationOperationPath(input.operationId);
|
|
884
816
|
const requestFingerprint = derivationRequestFingerprint(input);
|
|
885
817
|
const worldId = `world-${operationDigest(`${input.identityPrefix ?? "derive"}:${input.operationId}`).slice(0, 24)}`;
|
|
886
818
|
const finalRoot = input.publish === false
|
|
887
819
|
? resolve(input.targetWorldRoot)
|
|
888
820
|
: join(target.#worldsRoot, worldId);
|
|
889
|
-
const previous = input.publish === false
|
|
890
|
-
? null
|
|
891
|
-
: await readOptionalJson(operationPath);
|
|
892
|
-
if (previous !== null) {
|
|
893
|
-
await target.ensureCurrentStorage(previous.worldId);
|
|
894
|
-
if (!(await derivationPublicationMatches(join(target.#worldsRoot, previous.worldId), previous, input, requestFingerprint)))
|
|
895
|
-
throw new FileNativeWorldCreationError("operation_conflict", "The same fork operation ID is bound to a different fork payload");
|
|
896
|
-
return {
|
|
897
|
-
outcome: "derived",
|
|
898
|
-
world: await target.#currentSummary(previous),
|
|
899
|
-
worldRoot: finalRoot,
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
821
|
const alreadyPublished = await readOptionalJson(join(finalRoot, publicationFile));
|
|
903
822
|
if (alreadyPublished !== null) {
|
|
904
823
|
if (input.publish !== false)
|
|
905
824
|
await target.ensureCurrentStorage(alreadyPublished.worldId);
|
|
906
825
|
if (!(await derivationPublicationMatches(finalRoot, alreadyPublished, input, requestFingerprint)))
|
|
907
826
|
throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
|
|
908
|
-
if (input.publish !== false) {
|
|
909
|
-
await mkdir(target.#operationsRoot, { recursive: true, mode: 0o700 });
|
|
910
|
-
await publishJson(operationPath, alreadyPublished);
|
|
911
|
-
}
|
|
912
827
|
return {
|
|
913
828
|
outcome: "derived",
|
|
914
829
|
world: await readWorldSummaryAt(finalRoot, alreadyPublished),
|
|
@@ -920,43 +835,31 @@ export class FileNativeWorldStore {
|
|
|
920
835
|
const sourceRoot = join(this.#worldsRoot, input.sourceWorldId);
|
|
921
836
|
const sourcePublication = await readPublicationAt(sourceRoot);
|
|
922
837
|
const sourceSummary = await readWorldSummaryAt(sourceRoot, sourcePublication);
|
|
923
|
-
const sourceAuthority = new
|
|
838
|
+
const sourceAuthority = new WorldLedgerAuthority(sourceRoot);
|
|
924
839
|
const sourceHead = input.sourceHead === "current"
|
|
925
840
|
? (await sourceAuthority.readHead()).head
|
|
926
841
|
: input.sourceHead;
|
|
927
|
-
if (input.requireMaterialized) {
|
|
928
|
-
const authorityHead = await sourceAuthority.readHead();
|
|
929
|
-
const materialized = await readMaterializedCheckpoint(sourceRoot);
|
|
930
|
-
if (materialized?.head !== authorityHead.head ||
|
|
931
|
-
materialized.commitDigest !== authorityHead.commitDigest)
|
|
932
|
-
throw new FileNativeWorldCreationError("inconsistent_materialization", "Please repair the world before exporting a backup");
|
|
933
|
-
}
|
|
934
842
|
const [sourceGenesis, control] = await Promise.all([
|
|
935
843
|
readGenesisForFork(sourceRoot, input.sourceWorldId),
|
|
936
844
|
readTree(join(sourceRoot, "control")),
|
|
937
845
|
]);
|
|
938
846
|
const staging = join(dirname(finalRoot), `.staging-${worldId}-${randomUUID()}`);
|
|
939
847
|
await mkdir(dirname(finalRoot), { recursive: true, mode: 0o700 });
|
|
940
|
-
if (input.publish !== false)
|
|
941
|
-
await mkdir(target.#operationsRoot, { recursive: true, mode: 0o700 });
|
|
942
848
|
try {
|
|
943
|
-
await
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
849
|
+
await copyForkClosure(sourceRoot, staging);
|
|
850
|
+
await rewindTo(staging, headPosition(sourceHead));
|
|
851
|
+
await forkArtifactClosure(staging, {
|
|
852
|
+
sourceWorldId: input.sourceWorldId,
|
|
853
|
+
targetWorldId: worldId,
|
|
854
|
+
restoredHead: sourceHead,
|
|
948
855
|
});
|
|
949
|
-
|
|
950
|
-
await
|
|
951
|
-
await
|
|
856
|
+
// 分叉不信任来源的投影文件:当前文件由副本上的账本端点重建。
|
|
857
|
+
await new WorldLedgerAuthority(staging).materializeCurrentFiles();
|
|
858
|
+
const selected = await new WorldLedgerAuthority(staging).recover();
|
|
952
859
|
await PackageScriptPermissions.write(staging, input.resetPackageScriptPermissions
|
|
953
860
|
? []
|
|
954
861
|
: await PackageScriptPermissions.read(sourceRoot));
|
|
955
862
|
await WorldExtensionControls.copy(sourceRoot, staging);
|
|
956
|
-
await mkdir(join(staging, "history"), { recursive: true });
|
|
957
|
-
for (const file of historySurfaceFiles(selected.history))
|
|
958
|
-
await writeIdempotentText(join(staging, "history", file.path), file.contents);
|
|
959
|
-
await replaceMaterializedTree(join(staging, "runtime", "history-backgrounds"), projectHistoryBackgrounds(selected.history));
|
|
960
863
|
const genesis = {
|
|
961
864
|
schemaVersion: 1,
|
|
962
865
|
type: "file_native_genesis",
|
|
@@ -974,17 +877,6 @@ export class FileNativeWorldStore {
|
|
|
974
877
|
worldId,
|
|
975
878
|
history: genesis.history,
|
|
976
879
|
});
|
|
977
|
-
await writeJson(join(staging, "runtime", "additional-materials.json"), {
|
|
978
|
-
head: sourceHead,
|
|
979
|
-
items: selected.additionalMaterials,
|
|
980
|
-
});
|
|
981
|
-
await writeJson(join(staging, "runtime", "materialized-head.json"), {
|
|
982
|
-
schemaVersion: 3,
|
|
983
|
-
head: sourceHead,
|
|
984
|
-
sequence: clonedHead.sequence,
|
|
985
|
-
commitDigest: clonedHead.commitDigest,
|
|
986
|
-
...backgroundCount(selected.history),
|
|
987
|
-
});
|
|
988
880
|
await input.stageTarget?.({
|
|
989
881
|
sourceHead,
|
|
990
882
|
targetWorldRoot: staging,
|
|
@@ -1009,7 +901,6 @@ export class FileNativeWorldStore {
|
|
|
1009
901
|
await writeJson(join(staging, "runtime", "archive-import.json"), {
|
|
1010
902
|
schemaVersion: 1,
|
|
1011
903
|
});
|
|
1012
|
-
crashAtFileNativeAuthorityEdge("derivation_after_target_staging");
|
|
1013
904
|
const publication = {
|
|
1014
905
|
schemaVersion: 1,
|
|
1015
906
|
worldId,
|
|
@@ -1020,13 +911,8 @@ export class FileNativeWorldStore {
|
|
|
1020
911
|
};
|
|
1021
912
|
await writeJson(join(staging, publicationFile), publication);
|
|
1022
913
|
await updateWorldActivity(staging, sourceHead, selected.history, Date.now());
|
|
1023
|
-
|
|
1024
|
-
crashAtFileNativeAuthorityEdge("derivation_before_publish");
|
|
914
|
+
forgetLedger(finalRoot);
|
|
1025
915
|
await rename(staging, finalRoot);
|
|
1026
|
-
await syncDirectory(dirname(finalRoot));
|
|
1027
|
-
crashAtFileNativeAuthorityEdge("derivation_after_publish");
|
|
1028
|
-
if (input.publish !== false)
|
|
1029
|
-
await publishJson(operationPath, publication);
|
|
1030
916
|
return {
|
|
1031
917
|
outcome: "derived",
|
|
1032
918
|
world: await readWorldSummaryAt(finalRoot, publication),
|
|
@@ -1039,8 +925,6 @@ export class FileNativeWorldStore {
|
|
|
1039
925
|
if (published !== null) {
|
|
1040
926
|
if (!(await derivationPublicationMatches(finalRoot, published, input, requestFingerprint)))
|
|
1041
927
|
throw new FileNativeWorldCreationError("operation_conflict", "Published fork world does not match the operation payload");
|
|
1042
|
-
if (input.publish !== false)
|
|
1043
|
-
await publishJson(operationPath, published);
|
|
1044
928
|
return {
|
|
1045
929
|
outcome: "derived",
|
|
1046
930
|
world: await readWorldSummaryAt(finalRoot, published),
|
|
@@ -1059,142 +943,53 @@ export class FileNativeWorldStore {
|
|
|
1059
943
|
throw error;
|
|
1060
944
|
}
|
|
1061
945
|
}
|
|
1062
|
-
async failOperation(operationId, outcome = "failed") {
|
|
1063
|
-
const current = await this.getOperationOutcome(operationId);
|
|
1064
|
-
if (isCommittedOutcome(current))
|
|
1065
|
-
return;
|
|
1066
|
-
await writeJson(this.#operationOutcomePath(operationId), { outcome });
|
|
1067
|
-
}
|
|
1068
946
|
async getOperationOutcome(operationId) {
|
|
1069
|
-
return this.#readOperationOutcome(operationId);
|
|
1070
|
-
}
|
|
1071
|
-
async #readOperationOutcome(operationId) {
|
|
1072
947
|
assertIdentity(operationId, "operation ID");
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
await this.ensureCurrentStorage(value.worldId);
|
|
1082
|
-
value = await readOptionalJson(this.#operationOutcomePath(operationId));
|
|
1083
|
-
}
|
|
1084
|
-
if (value === null)
|
|
1085
|
-
directRecord = null;
|
|
1086
|
-
else {
|
|
1087
|
-
assertOperationOutcomeIntegrity(value);
|
|
1088
|
-
directRecord = value;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
catch (error) {
|
|
1092
|
-
if (error instanceof FileNativeWorldCreationError)
|
|
1093
|
-
throw error;
|
|
1094
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome has an invalid structure", error instanceof Error ? { cause: error } : undefined);
|
|
1095
|
-
}
|
|
1096
|
-
if (directRecord?.outcome === "acceptance_prepared")
|
|
1097
|
-
return resolvePreparedOperationOutcome(this.#worldsRoot, operationId, directRecord);
|
|
1098
|
-
const direct = directRecord === null ? null : projectOperationOutcome(directRecord);
|
|
1099
|
-
if (direct?.outcome === "committed" ||
|
|
1100
|
-
direct?.outcome === "committed_materialization_pending") {
|
|
1101
|
-
const worldRoot = join(this.#worldsRoot, direct.worldId);
|
|
1102
|
-
const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, direct);
|
|
1103
|
-
const materialized = await readMaterializedCheckpoint(worldRoot);
|
|
1104
|
-
if (materialized !== null && materialized.sequence >= commit.sequence)
|
|
1105
|
-
return { ...direct, outcome: "committed" };
|
|
1106
|
-
await assertPendingMaterializationCompatible(worldRoot, direct);
|
|
1107
|
-
return { ...direct, outcome: "committed_materialization_pending" };
|
|
1108
|
-
}
|
|
1109
|
-
return direct?.outcome === "in_progress"
|
|
1110
|
-
? { outcome: "in_progress" }
|
|
1111
|
-
: (direct ?? { outcome: "not_started" });
|
|
948
|
+
const entry = this.#operationOutcomes.get(operationId);
|
|
949
|
+
if (entry === undefined)
|
|
950
|
+
return { outcome: "not_started" };
|
|
951
|
+
if (entry.phase === "in_progress")
|
|
952
|
+
return { outcome: "in_progress" };
|
|
953
|
+
const outcome = structuredClone(entry.outcome);
|
|
954
|
+
await assertCommittedAuthorityOutcomeMatches(join(this.#worldsRoot, outcome.worldId), outcome);
|
|
955
|
+
return outcome;
|
|
1112
956
|
}
|
|
1113
957
|
/**
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
* snapshot and appends the replacement player text in one acceptance step.
|
|
958
|
+
* Roll the world back to a player message's logical parent endpoint before an
|
|
959
|
+
* edited replacement is submitted (ADR-0045). Rewinding to the current head is
|
|
960
|
+
* a no-op, so the edit's steps can repeat safely.
|
|
1118
961
|
*/
|
|
1119
|
-
async
|
|
1120
|
-
assertIdentity(
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
!/^commit:[1-9][0-9]*$/u.test(input.replacesHead))
|
|
1125
|
-
throw new TypeError("Timeline-revision endpoint is invalid");
|
|
1126
|
-
if (input.replacementText.trim() === "")
|
|
1127
|
-
throw new TypeError("Edited player message cannot be empty");
|
|
1128
|
-
if (!/^sha256:[a-f0-9]{64}$/u.test(input.requestFingerprint))
|
|
1129
|
-
throw new TypeError("Timeline-revision request fingerprint is invalid");
|
|
1130
|
-
await this.ensureCurrentStorage(input.worldId);
|
|
962
|
+
async rewindPlayerRevision(worldId, restoresHead) {
|
|
963
|
+
assertIdentity(worldId, "World ID");
|
|
964
|
+
if (!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(restoresHead))
|
|
965
|
+
throw new TypeError("Player-revision endpoint is invalid");
|
|
966
|
+
await this.ensureCurrentStorage(worldId);
|
|
1131
967
|
try {
|
|
1132
|
-
|
|
968
|
+
await this.operations.withExclusiveWorldStateMutation(worldId, () => rewindTo(join(this.#worldsRoot, worldId), headPosition(restoresHead)));
|
|
1133
969
|
}
|
|
1134
970
|
catch (error) {
|
|
1135
971
|
if (error instanceof WorldOperationBusyError)
|
|
1136
|
-
throw new FileNativeWorldCreationError("operation_conflict", "
|
|
972
|
+
throw new FileNativeWorldCreationError("operation_conflict", "Player edits are locked while a world revision is open");
|
|
1137
973
|
throw error;
|
|
1138
974
|
}
|
|
1139
975
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
return existing;
|
|
1158
|
-
}
|
|
1159
|
-
const root = join(this.#worldsRoot, input.worldId);
|
|
1160
|
-
const authorityStore = new FileNativeAuthorityV3(root);
|
|
1161
|
-
const [replaced, replacementMaterials] = await Promise.all([
|
|
1162
|
-
authorityStore.commitAt(input.replacesHead),
|
|
1163
|
-
authorityStore.recoverMaterials(input.restoresHead),
|
|
1164
|
-
]);
|
|
1165
|
-
const logicalParent = replaced?.timelineParent.head;
|
|
1166
|
-
if (replaced === null ||
|
|
1167
|
-
!replaced.historyAppend.some(({ role }) => role === "player") ||
|
|
1168
|
-
logicalParent !== input.restoresHead)
|
|
1169
|
-
throw new FileNativeWorldCreationError("operation_conflict", "Selected player message does not match the logical parent endpoint to restore");
|
|
1170
|
-
const binding = await this.bindPlayCallChainAt(input.worldId, input.restoresHead);
|
|
1171
|
-
const snapshot = WorldDocumentStore.open({
|
|
1172
|
-
layout: "world_state",
|
|
1173
|
-
files: Object.entries(binding.files).map(([path, contents]) => ({
|
|
1174
|
-
path,
|
|
1175
|
-
contents,
|
|
1176
|
-
})),
|
|
1177
|
-
});
|
|
1178
|
-
const background = snapshotBackground(captureNarrativeOrigin(snapshot, 1).value);
|
|
1179
|
-
return this.#commitHistoryChange({
|
|
1180
|
-
operationId: input.operationId,
|
|
1181
|
-
worldId: input.worldId,
|
|
1182
|
-
parentHead: input.expectedCurrentHead,
|
|
1183
|
-
historyAppend: [
|
|
1184
|
-
{
|
|
1185
|
-
role: "player",
|
|
1186
|
-
exactText: input.replacementText,
|
|
1187
|
-
...backgroundField(background),
|
|
1188
|
-
},
|
|
1189
|
-
],
|
|
1190
|
-
nextMaterials: worldNeutralMaterials(input.worldId, replacementMaterials),
|
|
1191
|
-
stateChanges: [],
|
|
1192
|
-
mode: "timeline_revision",
|
|
1193
|
-
timelineRevision: {
|
|
1194
|
-
restoresHead: input.restoresHead,
|
|
1195
|
-
replacesHead: input.replacesHead,
|
|
1196
|
-
requestFingerprint: input.requestFingerprint,
|
|
1197
|
-
},
|
|
976
|
+
/** The in-progress player edit record; written before the first world change. */
|
|
977
|
+
async readPlayerRevisionIntent(worldId) {
|
|
978
|
+
assertIdentity(worldId, "World ID");
|
|
979
|
+
return readOptionalJsonFile(playerRevisionIntentPath(this.#worldsRoot, worldId));
|
|
980
|
+
}
|
|
981
|
+
async writePlayerRevisionIntent(worldId, intent) {
|
|
982
|
+
assertIdentity(worldId, "World ID");
|
|
983
|
+
const path = playerRevisionIntentPath(this.#worldsRoot, worldId);
|
|
984
|
+
await mkdir(dirname(path), { recursive: true });
|
|
985
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
986
|
+
await writeFile(temporary, `${JSON.stringify(intent)}\n`, "utf8");
|
|
987
|
+
await rename(temporary, path);
|
|
988
|
+
}
|
|
989
|
+
async clearPlayerRevisionIntent(worldId) {
|
|
990
|
+
assertIdentity(worldId, "World ID");
|
|
991
|
+
await rm(playerRevisionIntentPath(this.#worldsRoot, worldId), {
|
|
992
|
+
force: true,
|
|
1198
993
|
});
|
|
1199
994
|
}
|
|
1200
995
|
async commitCorrection(input) {
|
|
@@ -1262,83 +1057,51 @@ export class FileNativeWorldStore {
|
|
|
1262
1057
|
}
|
|
1263
1058
|
}
|
|
1264
1059
|
async #commitHistoryChange(input) {
|
|
1265
|
-
const existing =
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const fact = await
|
|
1270
|
-
if (fact.digest !==
|
|
1060
|
+
const existing = this.#operationOutcomes.get(input.operationId);
|
|
1061
|
+
if (existing?.phase === "done") {
|
|
1062
|
+
const outcome = existing.outcome;
|
|
1063
|
+
assertSameOperationOutcome(outcome, input);
|
|
1064
|
+
const fact = await new WorldLedgerAuthority(join(this.#worldsRoot, outcome.worldId)).authorityFactAt(outcome.head);
|
|
1065
|
+
if (fact.digest !== outcome.commitDigest ||
|
|
1271
1066
|
!authorityCommitMatchesInput(fact.commit, input))
|
|
1272
1067
|
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
|
|
1273
|
-
return
|
|
1274
|
-
}
|
|
1275
|
-
const prepared = existing.outcome === "in_progress"
|
|
1276
|
-
? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
|
|
1277
|
-
: null;
|
|
1278
|
-
if (prepared !== null) {
|
|
1279
|
-
const comparable = {
|
|
1280
|
-
...structuredClone(prepared),
|
|
1281
|
-
outcome: "committed_materialization_pending",
|
|
1282
|
-
};
|
|
1283
|
-
assertSameOperationOutcome(comparable, input);
|
|
1068
|
+
return outcome;
|
|
1284
1069
|
}
|
|
1285
|
-
if (existing
|
|
1286
|
-
!(existing.outcome === "in_progress" &&
|
|
1287
|
-
(input.operationReserved === true || prepared !== null)))
|
|
1070
|
+
if (existing?.phase === "in_progress" && input.operationReserved !== true)
|
|
1288
1071
|
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
|
|
1289
1072
|
const root = join(this.#worldsRoot, input.worldId);
|
|
1290
1073
|
try {
|
|
1291
1074
|
return await this.operations.withWorldAuthorityLock(input.worldId, async () => {
|
|
1292
|
-
const concurrent =
|
|
1293
|
-
if (
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1075
|
+
const concurrent = this.#operationOutcomes.get(input.operationId);
|
|
1076
|
+
if (concurrent?.phase === "done") {
|
|
1077
|
+
const outcome = concurrent.outcome;
|
|
1078
|
+
assertSameOperationOutcome(outcome, input);
|
|
1079
|
+
const known = await new WorldLedgerAuthority(join(this.#worldsRoot, outcome.worldId)).authorityFactAt(outcome.head);
|
|
1080
|
+
if (known.digest !== outcome.commitDigest ||
|
|
1297
1081
|
!authorityCommitMatchesInput(known.commit, input))
|
|
1298
1082
|
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is bound to a different complete commit payload");
|
|
1299
|
-
return
|
|
1083
|
+
return outcome;
|
|
1300
1084
|
}
|
|
1301
|
-
|
|
1302
|
-
? await readAcceptancePreparedOutcome(this.#operationOutcomePath(input.operationId))
|
|
1303
|
-
: null;
|
|
1304
|
-
if (concurrentPrepared !== null)
|
|
1305
|
-
assertSameOperationOutcome({
|
|
1306
|
-
...structuredClone(concurrentPrepared),
|
|
1307
|
-
outcome: "committed_materialization_pending",
|
|
1308
|
-
}, input);
|
|
1309
|
-
else if (concurrent.outcome === "in_progress" &&
|
|
1085
|
+
if (concurrent?.phase === "in_progress" &&
|
|
1310
1086
|
input.operationReserved !== true)
|
|
1311
1087
|
throw new FileNativeWorldCreationError("operation_conflict", "The same play operation ID is occupied by another durable operation");
|
|
1312
|
-
const authorityStore = new
|
|
1088
|
+
const authorityStore = new WorldLedgerAuthority(root);
|
|
1313
1089
|
const authority = await authorityStore.readHead();
|
|
1314
|
-
if (authority.head !== input.parentHead)
|
|
1090
|
+
if (authority.head !== input.parentHead)
|
|
1315
1091
|
throw new FileNativeWorldCreationError("operation_conflict", "The parent endpoint of the play operation has changed");
|
|
1316
|
-
}
|
|
1317
|
-
const materialized = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
|
|
1318
|
-
if ((materialized?.head ?? "genesis") !== authority.head)
|
|
1319
|
-
throw new FileNativeWorldCreationError("operation_conflict", "The world has accepted but unrepaired materialization; new competing writes are forbidden");
|
|
1320
1092
|
const [basisState, storedControl] = await Promise.all([
|
|
1321
|
-
authorityStore.recoverState(
|
|
1093
|
+
authorityStore.recoverState(authority.head),
|
|
1322
1094
|
readTree(join(root, "control")),
|
|
1323
1095
|
]);
|
|
1324
1096
|
const candidateState = new Map(basisState.map((file) => [file.path, file.contents]));
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
for (const change of input.stateChanges) {
|
|
1334
|
-
assertRelativePath(change.relativePath);
|
|
1335
|
-
const previous = candidateState.get(change.relativePath);
|
|
1336
|
-
const previousHash = previous === undefined ? null : sha256(previous);
|
|
1337
|
-
if (previousHash !== change.expectedPreviousHash ||
|
|
1338
|
-
sha256(change.canonicalNextBytes) !== change.nextHash)
|
|
1339
|
-
throw new FileNativeWorldCreationError("operation_conflict", `State-change hash conflicts: ${change.relativePath}`);
|
|
1340
|
-
candidateState.set(change.relativePath, change.canonicalNextBytes);
|
|
1341
|
-
}
|
|
1097
|
+
for (const change of input.stateChanges) {
|
|
1098
|
+
assertRelativePath(change.relativePath);
|
|
1099
|
+
const previous = candidateState.get(change.relativePath);
|
|
1100
|
+
const previousHash = previous === undefined ? null : sha256(previous);
|
|
1101
|
+
if (previousHash !== change.expectedPreviousHash ||
|
|
1102
|
+
sha256(change.canonicalNextBytes) !== change.nextHash)
|
|
1103
|
+
throw new FileNativeWorldCreationError("operation_conflict", `State-change hash conflicts: ${change.relativePath}`);
|
|
1104
|
+
candidateState.set(change.relativePath, change.canonicalNextBytes);
|
|
1342
1105
|
}
|
|
1343
1106
|
const inspection = inspectContentPackageCurrentTree([
|
|
1344
1107
|
...[...candidateState].map(([path, contents]) => ({
|
|
@@ -1352,10 +1115,7 @@ export class FileNativeWorldStore {
|
|
|
1352
1115
|
], { requireOpening: false });
|
|
1353
1116
|
if (inspection.status !== "usable")
|
|
1354
1117
|
throw new FileNativeWorldCreationError("candidate_validation_failed", `Candidate world failed mechanical validation: ${inspection.issues.map(({ message }) => message).join("; ")}`);
|
|
1355
|
-
|
|
1356
|
-
await authorityStore.discardUnacceptedNextEpoch();
|
|
1357
|
-
crashAtFileNativeAuthorityEdge("before_commit_acceptance");
|
|
1358
|
-
const preparedAppend = await authorityStore.prepareAppend({
|
|
1118
|
+
const { commit, commitDigest } = await authorityStore.appendCommit({
|
|
1359
1119
|
...(input.playContext === undefined
|
|
1360
1120
|
? {}
|
|
1361
1121
|
: { playContext: input.playContext }),
|
|
@@ -1364,12 +1124,9 @@ export class FileNativeWorldStore {
|
|
|
1364
1124
|
: { worldClock: input.worldClock }),
|
|
1365
1125
|
operationId: input.operationId,
|
|
1366
1126
|
parentHead: input.parentHead,
|
|
1367
|
-
...(input.timelineRevision === undefined
|
|
1368
|
-
? {}
|
|
1369
|
-
: { timelineParentHead: input.timelineRevision.restoresHead }),
|
|
1370
1127
|
mode: input.mode,
|
|
1371
1128
|
historyAppend: input.historyAppend,
|
|
1372
|
-
stateChanges:
|
|
1129
|
+
stateChanges: input.stateChanges,
|
|
1373
1130
|
nextMaterials: input.nextMaterials,
|
|
1374
1131
|
...(input.narrativeCheckpoint === undefined
|
|
1375
1132
|
? {}
|
|
@@ -1380,57 +1137,27 @@ export class FileNativeWorldStore {
|
|
|
1380
1137
|
corrects: input.parentHead,
|
|
1381
1138
|
}
|
|
1382
1139
|
: {}),
|
|
1383
|
-
...(input.timelineRevision === undefined
|
|
1384
|
-
? {}
|
|
1385
|
-
: { timelineRevision: input.timelineRevision }),
|
|
1386
1140
|
});
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if (acceptedPreparation !== null &&
|
|
1390
|
-
acceptedPreparation.commitDigest !== preparedAppend.commitDigest)
|
|
1391
|
-
throw new FileNativeWorldCreationError("operation_conflict", "The prepared Authority fact does not match the reserved operation");
|
|
1392
|
-
const { commit } = preparedAppend;
|
|
1393
|
-
const pending = {
|
|
1394
|
-
outcome: "committed_materialization_pending",
|
|
1141
|
+
const outcome = {
|
|
1142
|
+
outcome: "committed",
|
|
1395
1143
|
worldId: input.worldId,
|
|
1396
1144
|
parentHead: input.parentHead,
|
|
1397
1145
|
head: commit.head,
|
|
1398
|
-
commitDigest
|
|
1146
|
+
commitDigest,
|
|
1399
1147
|
historyAppend: structuredClone(input.historyAppend),
|
|
1400
1148
|
nextAdditionalMaterials: structuredClone(input.nextMaterials),
|
|
1401
1149
|
mode: input.mode,
|
|
1402
1150
|
};
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
crashAtFileNativeAuthorityEdge("after_acceptance_prepared");
|
|
1408
|
-
await authorityStore.publishPrepared(preparedAppend);
|
|
1409
|
-
const acceptedAt = Date.now();
|
|
1410
|
-
crashAtFileNativeAuthorityEdge("after_commit_acceptance");
|
|
1411
|
-
await publishJson(this.#operationOutcomePath(input.operationId), pending);
|
|
1412
|
-
try {
|
|
1413
|
-
const recovered = await authorityStore.recover(commit.head);
|
|
1414
|
-
await assertPendingMaterializationCompatible(root, pending);
|
|
1415
|
-
await materializeRecoveredEndpoint(root, preparedAppend.nextHead, recovered, acceptedAt);
|
|
1416
|
-
crashAtFileNativeAuthorityEdge("after_materialization");
|
|
1417
|
-
const outcome = { ...pending, outcome: "committed" };
|
|
1418
|
-
await publishJson(this.#operationOutcomePath(input.operationId), outcome);
|
|
1419
|
-
return outcome;
|
|
1420
|
-
}
|
|
1421
|
-
catch (error) {
|
|
1422
|
-
if (error instanceof FileNativeWorldCreationError &&
|
|
1423
|
-
(error.code === "world_corrupt" ||
|
|
1424
|
-
error.code === "inconsistent_materialization"))
|
|
1425
|
-
throw error;
|
|
1426
|
-
return pending;
|
|
1427
|
-
}
|
|
1151
|
+
this.#rememberOperationOutcome(input.worldId, input.operationId, outcome);
|
|
1152
|
+
const recovered = await authorityStore.recover(commit.head);
|
|
1153
|
+
await updateWorldActivity(root, commit.head, recovered.history, Date.now());
|
|
1154
|
+
return outcome;
|
|
1428
1155
|
});
|
|
1429
1156
|
}
|
|
1430
1157
|
catch (error) {
|
|
1431
1158
|
if (error instanceof WorldOperationBusyError)
|
|
1432
1159
|
throw new FileNativeWorldCreationError("operation_conflict", "World Authority commit is being serialized by another process");
|
|
1433
|
-
if (error instanceof
|
|
1160
|
+
if (error instanceof WorldAuthorityError)
|
|
1434
1161
|
throw new FileNativeWorldCreationError(error.code === "corrupt" ? "world_corrupt" : "operation_conflict", error.message, { cause: error });
|
|
1435
1162
|
throw error;
|
|
1436
1163
|
}
|
|
@@ -1440,7 +1167,7 @@ export class FileNativeWorldStore {
|
|
|
1440
1167
|
await this.ensureCurrentStorage(worldId);
|
|
1441
1168
|
const root = join(this.#worldsRoot, worldId);
|
|
1442
1169
|
await readPublicationAt(root);
|
|
1443
|
-
const authority = await new
|
|
1170
|
+
const authority = await new WorldLedgerAuthority(root).readHistory();
|
|
1444
1171
|
return structuredClone(authority);
|
|
1445
1172
|
}
|
|
1446
1173
|
async inspectDocumentMaintenance(worldId, head) {
|
|
@@ -1463,7 +1190,7 @@ export class FileNativeWorldStore {
|
|
|
1463
1190
|
await this.ensureCurrentStorage(worldId);
|
|
1464
1191
|
const root = join(this.#worldsRoot, worldId);
|
|
1465
1192
|
await readPublicationAt(root);
|
|
1466
|
-
const authority = new
|
|
1193
|
+
const authority = new WorldLedgerAuthority(root);
|
|
1467
1194
|
const selectedHead = head ?? (await authority.readHead()).head;
|
|
1468
1195
|
let cache = this.#documentMaintenanceCache.get(worldId);
|
|
1469
1196
|
if (cache === undefined) {
|
|
@@ -1501,49 +1228,25 @@ export class FileNativeWorldStore {
|
|
|
1501
1228
|
};
|
|
1502
1229
|
}
|
|
1503
1230
|
async #readPublication(operationId) {
|
|
1231
|
+
// The world root's own publication record is the only creation receipt.
|
|
1232
|
+
const worldId = `world-${operationDigest(operationId).slice(0, 24)}`;
|
|
1504
1233
|
let publication;
|
|
1505
1234
|
try {
|
|
1506
|
-
publication = await
|
|
1235
|
+
publication = await readPublicationAt(join(this.#worldsRoot, worldId));
|
|
1507
1236
|
}
|
|
1508
1237
|
catch (error) {
|
|
1509
|
-
if (isNodeError(error) && error.code === "ENOENT")
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
publication = await readPublicationAt(join(this.#worldsRoot, worldId));
|
|
1513
|
-
}
|
|
1514
|
-
catch (worldError) {
|
|
1515
|
-
if (isNodeError(worldError) && worldError.code === "ENOENT")
|
|
1516
|
-
return null;
|
|
1517
|
-
throw worldError;
|
|
1518
|
-
}
|
|
1519
|
-
if (publication.operationId !== operationId) {
|
|
1520
|
-
throw new FileNativeWorldCreationError("operation_conflict", "Deterministic world identity is occupied by another creation operation");
|
|
1521
|
-
}
|
|
1522
|
-
await mkdir(this.#operationsRoot, { recursive: true, mode: 0o700 });
|
|
1523
|
-
await publishOperation(this.#operationPath(operationId), publication);
|
|
1524
|
-
}
|
|
1525
|
-
else {
|
|
1526
|
-
throw error;
|
|
1527
|
-
}
|
|
1238
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
1239
|
+
return null;
|
|
1240
|
+
throw error;
|
|
1528
1241
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Creation operation does not match the world publication record");
|
|
1242
|
+
if (publication.operationId !== operationId) {
|
|
1243
|
+
throw new FileNativeWorldCreationError("operation_conflict", "Deterministic world identity is occupied by another creation operation");
|
|
1532
1244
|
}
|
|
1533
1245
|
return publication;
|
|
1534
1246
|
}
|
|
1535
1247
|
#currentSummary(publication) {
|
|
1536
1248
|
return readWorldSummaryAt(join(this.#worldsRoot, publication.worldId), publication);
|
|
1537
1249
|
}
|
|
1538
|
-
#operationPath(operationId) {
|
|
1539
|
-
return join(this.#operationsRoot, `${operationDigest(operationId)}.json`);
|
|
1540
|
-
}
|
|
1541
|
-
#operationOutcomePath(operationId) {
|
|
1542
|
-
return join(this.#operationsRoot, `outcome-${operationDigest(operationId)}.json`);
|
|
1543
|
-
}
|
|
1544
|
-
#derivationOperationPath(operationId) {
|
|
1545
|
-
return join(this.#operationsRoot, `derive-${operationDigest(operationId)}.json`);
|
|
1546
|
-
}
|
|
1547
1250
|
#preview(input, worldId) {
|
|
1548
1251
|
const openingMessage = genesisOpeningMessage(openingText(input.packageFiles));
|
|
1549
1252
|
return this.#promptCompiler.preview({
|
|
@@ -1681,12 +1384,6 @@ function contentPackageCreationFingerprint(title, files) {
|
|
|
1681
1384
|
function operationDigest(operationId) {
|
|
1682
1385
|
return createHash("sha256").update(operationId).digest("hex");
|
|
1683
1386
|
}
|
|
1684
|
-
async function publishOperation(path, value) {
|
|
1685
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1686
|
-
await writeJson(temporary, value);
|
|
1687
|
-
await rename(temporary, path);
|
|
1688
|
-
await syncDirectory(dirname(path));
|
|
1689
|
-
}
|
|
1690
1387
|
function isRecord(value) {
|
|
1691
1388
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1692
1389
|
}
|
|
@@ -1699,93 +1396,14 @@ function hasRequiredAndOptionalKeys(value, required, optional) {
|
|
|
1699
1396
|
return (required.every((key) => Object.hasOwn(value, key)) &&
|
|
1700
1397
|
Object.keys(value).every((key) => allowed.has(key)));
|
|
1701
1398
|
}
|
|
1702
|
-
function assertOperationOutcomeIntegrity(value) {
|
|
1703
|
-
if (!isRecord(value) || typeof value.outcome !== "string")
|
|
1704
|
-
throw corruptOperationOutcome();
|
|
1705
|
-
if (value.outcome === "in_progress") {
|
|
1706
|
-
if (!hasExactKeys(value, ["outcome"]))
|
|
1707
|
-
throw corruptOperationOutcome();
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
if (value.outcome === "failed" || value.outcome === "cancelled") {
|
|
1711
|
-
if (!hasExactKeys(value, ["outcome"]))
|
|
1712
|
-
throw corruptOperationOutcome();
|
|
1713
|
-
return;
|
|
1714
|
-
}
|
|
1715
|
-
if (value.outcome === "acceptance_prepared" ||
|
|
1716
|
-
value.outcome === "committed" ||
|
|
1717
|
-
value.outcome === "committed_materialization_pending") {
|
|
1718
|
-
if (!hasExactKeys(value, [
|
|
1719
|
-
"outcome",
|
|
1720
|
-
"worldId",
|
|
1721
|
-
"parentHead",
|
|
1722
|
-
"head",
|
|
1723
|
-
"commitDigest",
|
|
1724
|
-
"historyAppend",
|
|
1725
|
-
"nextAdditionalMaterials",
|
|
1726
|
-
"mode",
|
|
1727
|
-
]) ||
|
|
1728
|
-
typeof value.worldId !== "string" ||
|
|
1729
|
-
value.worldId.length === 0 ||
|
|
1730
|
-
typeof value.parentHead !== "string" ||
|
|
1731
|
-
!/^(?:genesis|commit:[1-9][0-9]*)$/u.test(value.parentHead) ||
|
|
1732
|
-
typeof value.head !== "string" ||
|
|
1733
|
-
!/^commit:[1-9][0-9]*$/u.test(value.head) ||
|
|
1734
|
-
typeof value.commitDigest !== "string" ||
|
|
1735
|
-
!/^[a-f0-9]{64}$/u.test(value.commitDigest) ||
|
|
1736
|
-
!Array.isArray(value.historyAppend) ||
|
|
1737
|
-
!value.historyAppend.every(isPlayHistoryAppendInput) ||
|
|
1738
|
-
!Array.isArray(value.nextAdditionalMaterials) ||
|
|
1739
|
-
!value.nextAdditionalMaterials.every(isAuthorityMaterialSelection) ||
|
|
1740
|
-
!(value.mode === "play" ||
|
|
1741
|
-
value.mode === "correction" ||
|
|
1742
|
-
value.mode === "timeline_revision"))
|
|
1743
|
-
throw corruptOperationOutcome();
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
throw corruptOperationOutcome();
|
|
1747
|
-
}
|
|
1748
|
-
function projectOperationOutcome(value) {
|
|
1749
|
-
return structuredClone(value);
|
|
1750
|
-
}
|
|
1751
|
-
async function readAcceptancePreparedOutcome(path) {
|
|
1752
|
-
const value = await readOptionalJson(path);
|
|
1753
|
-
if (value === null)
|
|
1754
|
-
return null;
|
|
1755
|
-
assertOperationOutcomeIntegrity(value);
|
|
1756
|
-
if (value.outcome !== "acceptance_prepared")
|
|
1757
|
-
return null;
|
|
1758
|
-
return structuredClone(value);
|
|
1759
|
-
}
|
|
1760
|
-
async function resolvePreparedOperationOutcome(worldsRoot, operationId, value) {
|
|
1761
|
-
const prepared = value;
|
|
1762
|
-
const worldRoot = join(worldsRoot, prepared.worldId);
|
|
1763
|
-
const authorityStore = new FileNativeAuthorityV3(worldRoot);
|
|
1764
|
-
const authority = await authorityStore.readHead();
|
|
1765
|
-
if (authority.head === prepared.parentHead)
|
|
1766
|
-
return { outcome: "in_progress" };
|
|
1767
|
-
if (authority.operationId !== operationId)
|
|
1768
|
-
throw corruptOperationOutcome();
|
|
1769
|
-
const commit = await assertCommittedAuthorityOutcomeMatches(worldRoot, prepared);
|
|
1770
|
-
if (authority.sequence < commit.sequence)
|
|
1771
|
-
throw corruptOperationOutcome();
|
|
1772
|
-
const materialized = await readMaterializedCheckpoint(worldRoot);
|
|
1773
|
-
const outcome = materialized !== null && materialized.sequence >= commit.sequence
|
|
1774
|
-
? "committed"
|
|
1775
|
-
: "committed_materialization_pending";
|
|
1776
|
-
const resolved = { ...structuredClone(prepared), outcome };
|
|
1777
|
-
if (outcome === "committed_materialization_pending")
|
|
1778
|
-
await assertPendingMaterializationCompatible(worldRoot, resolved);
|
|
1779
|
-
return resolved;
|
|
1780
|
-
}
|
|
1781
1399
|
function corruptOperationOutcome() {
|
|
1782
1400
|
return new FileNativeWorldCreationError("world_corrupt", "Authority operation outcome structure or operation identity is invalid");
|
|
1783
1401
|
}
|
|
1784
1402
|
async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
|
|
1785
|
-
const fact = await new
|
|
1403
|
+
const fact = await new WorldLedgerAuthority(worldRoot).authorityFactAt(outcome.head);
|
|
1786
1404
|
const { commit } = fact;
|
|
1787
1405
|
if (fact.digest !== outcome.commitDigest ||
|
|
1788
|
-
commit.
|
|
1406
|
+
commit.parentHead !== outcome.parentHead ||
|
|
1789
1407
|
commit.head !== outcome.head ||
|
|
1790
1408
|
commit.mode !== outcome.mode ||
|
|
1791
1409
|
!isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
|
|
@@ -1797,143 +1415,6 @@ async function assertCommittedAuthorityOutcomeMatches(worldRoot, outcome) {
|
|
|
1797
1415
|
throw corruptOperationOutcome();
|
|
1798
1416
|
return commit;
|
|
1799
1417
|
}
|
|
1800
|
-
async function assertPendingMaterializationCompatible(worldRoot, outcome) {
|
|
1801
|
-
const authority = new FileNativeAuthorityV3(worldRoot);
|
|
1802
|
-
await assertCommittedAuthorityOutcomeMatches(worldRoot, outcome);
|
|
1803
|
-
const [parent, target, state, history] = await Promise.all([
|
|
1804
|
-
authority.recover(outcome.parentHead),
|
|
1805
|
-
authority.recover(outcome.head),
|
|
1806
|
-
readTree(join(worldRoot, "state")),
|
|
1807
|
-
readTree(join(worldRoot, "history")),
|
|
1808
|
-
]);
|
|
1809
|
-
assertProjectionBetweenEndpoints("state", state, parent.state, target.state);
|
|
1810
|
-
assertHistoryProjectionBetweenEndpoints(history, historySurfaceFiles(parent.history), historySurfaceFiles(target.history));
|
|
1811
|
-
const existingMaterials = await readOptionalJson(join(worldRoot, "runtime", "additional-materials.json"));
|
|
1812
|
-
if (existingMaterials !== null &&
|
|
1813
|
-
!((existingMaterials.head === parent.head &&
|
|
1814
|
-
isDeepStrictEqual(existingMaterials.items, parent.additionalMaterials)) ||
|
|
1815
|
-
(existingMaterials.head === target.head &&
|
|
1816
|
-
isDeepStrictEqual(existingMaterials.items, target.additionalMaterials))))
|
|
1817
|
-
throw new FileNativeWorldCreationError("inconsistent_materialization", "Additional-material list content conflicts at the same endpoint");
|
|
1818
|
-
}
|
|
1819
|
-
function assertProjectionBetweenEndpoints(surface, current, parent, target) {
|
|
1820
|
-
const parentFiles = new Map(parent.map((file) => [file.path, file.contents]));
|
|
1821
|
-
const targetFiles = new Map(target.map((file) => [file.path, file.contents]));
|
|
1822
|
-
for (const file of current) {
|
|
1823
|
-
if (file.contents !== parentFiles.get(file.path) &&
|
|
1824
|
-
file.contents !== targetFiles.get(file.path))
|
|
1825
|
-
throw new FileNativeWorldCreationError("inconsistent_materialization", `World ${surface} projection conflicts with both accepted endpoints: ${file.path}`);
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
function assertHistoryProjectionBetweenEndpoints(current, parent, target) {
|
|
1829
|
-
const byMessageId = (files) => new Map(files.map((file) => [
|
|
1830
|
-
fileNativeHistoryMessageIdFromProjectionPath(file.path),
|
|
1831
|
-
file.contents,
|
|
1832
|
-
]));
|
|
1833
|
-
const parentFiles = byMessageId(parent);
|
|
1834
|
-
const targetFiles = byMessageId(target);
|
|
1835
|
-
const seen = new Set();
|
|
1836
|
-
for (const file of current) {
|
|
1837
|
-
const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
|
|
1838
|
-
if (messageId === null ||
|
|
1839
|
-
seen.has(messageId) ||
|
|
1840
|
-
(file.contents !== parentFiles.get(messageId) &&
|
|
1841
|
-
file.contents !== targetFiles.get(messageId)))
|
|
1842
|
-
throw new FileNativeWorldCreationError("inconsistent_materialization", `World history projection conflicts with both accepted endpoints: ${file.path}`);
|
|
1843
|
-
seen.add(messageId);
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
function isAuthorityMaterialSelection(value) {
|
|
1847
|
-
if (!isRecord(value))
|
|
1848
|
-
return false;
|
|
1849
|
-
if (value.kind === "document")
|
|
1850
|
-
return (hasExactKeys(value, ["kind", "document"]) &&
|
|
1851
|
-
typeof value.document === "string");
|
|
1852
|
-
if (value.kind === "node")
|
|
1853
|
-
return (hasExactKeys(value, ["kind", "document", "locator"]) &&
|
|
1854
|
-
typeof value.document === "string" &&
|
|
1855
|
-
isAuthorityMaterialLocator(value.locator));
|
|
1856
|
-
if (value.kind === "history_message")
|
|
1857
|
-
return (hasExactKeys(value, ["kind", "message"]) &&
|
|
1858
|
-
typeof value.message === "string");
|
|
1859
|
-
return (value.kind === "history_commit" &&
|
|
1860
|
-
hasExactKeys(value, ["kind", "commit"]) &&
|
|
1861
|
-
typeof value.commit === "string");
|
|
1862
|
-
}
|
|
1863
|
-
function isAuthorityMaterialLocator(value) {
|
|
1864
|
-
return (isRecord(value) &&
|
|
1865
|
-
hasRequiredAndOptionalKeys(value, [], ["yaml", "markdown"]) &&
|
|
1866
|
-
(value.yaml === undefined ||
|
|
1867
|
-
(Array.isArray(value.yaml) &&
|
|
1868
|
-
value.yaml.every((segment) => typeof segment === "string" || Number.isSafeInteger(segment)))) &&
|
|
1869
|
-
(value.markdown === undefined ||
|
|
1870
|
-
(Array.isArray(value.markdown) &&
|
|
1871
|
-
value.markdown.every((segment) => typeof segment === "string"))));
|
|
1872
|
-
}
|
|
1873
|
-
async function publishJson(path, value) {
|
|
1874
|
-
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1875
|
-
await writeJson(temporary, value);
|
|
1876
|
-
await rename(temporary, path);
|
|
1877
|
-
await syncDirectory(dirname(path));
|
|
1878
|
-
}
|
|
1879
|
-
async function readMaterializedCheckpoint(root) {
|
|
1880
|
-
const value = await readOptionalJson(join(root, "runtime", "materialized-head.json"));
|
|
1881
|
-
if (value === null)
|
|
1882
|
-
return null;
|
|
1883
|
-
if (!isRecord(value) ||
|
|
1884
|
-
!hasRequiredAndOptionalKeys(value, ["schemaVersion", "head", "sequence", "commitDigest"], ["historyBackgroundCount"]) ||
|
|
1885
|
-
(value.historyBackgroundCount !== undefined &&
|
|
1886
|
-
(!Number.isSafeInteger(value.historyBackgroundCount) ||
|
|
1887
|
-
Number(value.historyBackgroundCount) < 1)) ||
|
|
1888
|
-
value.schemaVersion !== 3 ||
|
|
1889
|
-
typeof value.head !== "string" ||
|
|
1890
|
-
!Number.isSafeInteger(value.sequence) ||
|
|
1891
|
-
Number(value.sequence) < 0 ||
|
|
1892
|
-
(value.commitDigest !== null &&
|
|
1893
|
-
(typeof value.commitDigest !== "string" ||
|
|
1894
|
-
!/^[a-f0-9]{64}$/u.test(value.commitDigest))) ||
|
|
1895
|
-
(Number(value.sequence) === 0
|
|
1896
|
-
? value.head !== "genesis" || value.commitDigest !== null
|
|
1897
|
-
: value.head !== `commit:${String(value.sequence)}` ||
|
|
1898
|
-
value.commitDigest === null))
|
|
1899
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Materialized world checkpoint has an invalid shape");
|
|
1900
|
-
return value;
|
|
1901
|
-
}
|
|
1902
|
-
function sameMaterializedEndpoint(checkpoint, authority) {
|
|
1903
|
-
return (checkpoint !== null &&
|
|
1904
|
-
checkpoint.head === authority.head &&
|
|
1905
|
-
checkpoint.sequence === authority.sequence &&
|
|
1906
|
-
checkpoint.commitDigest === authority.commitDigest);
|
|
1907
|
-
}
|
|
1908
|
-
function isPlayHistoryAppendInput(value) {
|
|
1909
|
-
return (isRecord(value) &&
|
|
1910
|
-
hasRequiredAndOptionalKeys(value, ["role", "exactText"], ["background"]) &&
|
|
1911
|
-
(value.background === undefined ||
|
|
1912
|
-
(isHistoryBackground(value.background) &&
|
|
1913
|
-
(value.role !== "player" || value.background.kind === "snapshot"))) &&
|
|
1914
|
-
(value.role === "player" || value.role === "narrator") &&
|
|
1915
|
-
typeof value.exactText === "string");
|
|
1916
|
-
}
|
|
1917
|
-
async function materializeRecoveredEndpoint(root, authority, endpoint, lastActivityAt = null) {
|
|
1918
|
-
if (authority.head !== endpoint.head)
|
|
1919
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Recovered endpoint does not match the Authority head to materialize");
|
|
1920
|
-
await replaceMaterializedTree(join(root, "state"), endpoint.state);
|
|
1921
|
-
crashAtFileNativeAuthorityEdge("after_state_materialization");
|
|
1922
|
-
await replaceMaterializedTree(join(root, "history"), historySurfaceFiles(endpoint.history));
|
|
1923
|
-
await replaceMaterializedTree(join(root, "runtime", "history-backgrounds"), projectHistoryBackgrounds(endpoint.history));
|
|
1924
|
-
await publishJson(join(root, "runtime", "additional-materials.json"), {
|
|
1925
|
-
head: authority.head,
|
|
1926
|
-
items: endpoint.additionalMaterials,
|
|
1927
|
-
});
|
|
1928
|
-
await publishJson(join(root, "runtime", "materialized-head.json"), {
|
|
1929
|
-
schemaVersion: 3,
|
|
1930
|
-
head: authority.head,
|
|
1931
|
-
sequence: authority.sequence,
|
|
1932
|
-
commitDigest: authority.commitDigest,
|
|
1933
|
-
...backgroundCount(endpoint.history),
|
|
1934
|
-
});
|
|
1935
|
-
await updateWorldActivity(root, authority.head, endpoint.history, lastActivityAt);
|
|
1936
|
-
}
|
|
1937
1418
|
async function replaceMaterializedTree(root, files) {
|
|
1938
1419
|
const next = new Map();
|
|
1939
1420
|
for (const file of files) {
|
|
@@ -1953,34 +1434,19 @@ async function replaceMaterializedTree(root, files) {
|
|
|
1953
1434
|
continue;
|
|
1954
1435
|
const path = join(root, file.path);
|
|
1955
1436
|
await rm(path, { force: true });
|
|
1956
|
-
await syncDirectory(dirname(path));
|
|
1957
1437
|
}
|
|
1958
1438
|
}
|
|
1959
1439
|
async function publishText(path, contents) {
|
|
1960
1440
|
await mkdir(dirname(path), { recursive: true });
|
|
1961
1441
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1962
1442
|
await writeFile(temporary, contents, "utf8");
|
|
1963
|
-
await syncFile(temporary);
|
|
1964
1443
|
await rename(temporary, path);
|
|
1965
|
-
await syncDirectory(dirname(path));
|
|
1966
1444
|
}
|
|
1967
1445
|
function historySurfaceFiles(messages) {
|
|
1968
1446
|
return projectFileNativeHistorySurface(messages, (messageId) => {
|
|
1969
1447
|
throw new FileNativeWorldCreationError("world_corrupt", `History-message identity cannot form a materialization path: ${messageId}`);
|
|
1970
1448
|
});
|
|
1971
1449
|
}
|
|
1972
|
-
function materializedHistoryRecord(files) {
|
|
1973
|
-
const result = {};
|
|
1974
|
-
for (const file of files) {
|
|
1975
|
-
const messageId = fileNativeHistoryMessageIdFromProjectionPath(file.path);
|
|
1976
|
-
if (messageId === null)
|
|
1977
|
-
throw new FileNativeWorldCreationError("world_corrupt", `Materialized history path is invalid: ${file.path}`);
|
|
1978
|
-
if (messageId in result)
|
|
1979
|
-
throw new FileNativeWorldCreationError("world_corrupt", `Materialized history contains a duplicate message: ${messageId}`);
|
|
1980
|
-
result[messageId] = file.contents;
|
|
1981
|
-
}
|
|
1982
|
-
return result;
|
|
1983
|
-
}
|
|
1984
1450
|
async function writeIdempotentText(path, contents) {
|
|
1985
1451
|
try {
|
|
1986
1452
|
const existing = await readFile(path, "utf8");
|
|
@@ -1992,15 +1458,9 @@ async function writeIdempotentText(path, contents) {
|
|
|
1992
1458
|
throw error;
|
|
1993
1459
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1994
1460
|
await writeFile(temporary, contents, "utf8");
|
|
1995
|
-
await syncFile(temporary);
|
|
1996
1461
|
await rename(temporary, path);
|
|
1997
|
-
await syncDirectory(dirname(path));
|
|
1998
1462
|
}
|
|
1999
1463
|
}
|
|
2000
|
-
function isCommittedOutcome(outcome) {
|
|
2001
|
-
return (outcome.outcome === "committed" ||
|
|
2002
|
-
outcome.outcome === "committed_materialization_pending");
|
|
2003
|
-
}
|
|
2004
1464
|
function assertSameOperationOutcome(outcome, input) {
|
|
2005
1465
|
if (outcome.worldId !== input.worldId ||
|
|
2006
1466
|
outcome.parentHead !== input.parentHead ||
|
|
@@ -2037,8 +1497,7 @@ function authorityCommitMatchesInput(commit, input) {
|
|
|
2037
1497
|
const candidate = input.stateChanges[index];
|
|
2038
1498
|
if (candidate === undefined)
|
|
2039
1499
|
return false;
|
|
2040
|
-
|
|
2041
|
-
const expected = {
|
|
1500
|
+
return isDeepStrictEqual(stored, {
|
|
2042
1501
|
kind: candidate.kind,
|
|
2043
1502
|
documentId: candidate.documentId,
|
|
2044
1503
|
stableShortRef: candidate.stableShortRef,
|
|
@@ -2046,10 +1505,7 @@ function authorityCommitMatchesInput(commit, input) {
|
|
|
2046
1505
|
codec: candidate.codec,
|
|
2047
1506
|
expectedPreviousHash: candidate.expectedPreviousHash,
|
|
2048
1507
|
nextHash: candidate.nextHash,
|
|
2049
|
-
};
|
|
2050
|
-
return (nextBlob.epoch === commit.sequence &&
|
|
2051
|
-
`sha256:${nextBlob.digest}` === candidate.nextHash &&
|
|
2052
|
-
isDeepStrictEqual(metadata, expected));
|
|
1508
|
+
});
|
|
2053
1509
|
});
|
|
2054
1510
|
const expectedCorrection = input.mode === "correction"
|
|
2055
1511
|
? input.stateChanges.map(({ documentId }) => documentId)
|
|
@@ -2057,9 +1513,7 @@ function authorityCommitMatchesInput(commit, input) {
|
|
|
2057
1513
|
return (commit.mode === input.mode &&
|
|
2058
1514
|
commit.playContext === input.playContext &&
|
|
2059
1515
|
commit.worldClock === input.worldClock &&
|
|
2060
|
-
commit.
|
|
2061
|
-
commit.timelineParent.head ===
|
|
2062
|
-
(input.timelineRevision?.restoresHead ?? input.parentHead) &&
|
|
1516
|
+
commit.parentHead === input.parentHead &&
|
|
2063
1517
|
isDeepStrictEqual(commit.historyAppend.map(({ role, exactText, background }) => ({
|
|
2064
1518
|
role,
|
|
2065
1519
|
exactText,
|
|
@@ -2067,12 +1521,48 @@ function authorityCommitMatchesInput(commit, input) {
|
|
|
2067
1521
|
})), input.historyAppend) &&
|
|
2068
1522
|
stateChangesMatch &&
|
|
2069
1523
|
isDeepStrictEqual(commit.nextAdditionalMaterials, input.nextMaterials) &&
|
|
2070
|
-
isDeepStrictEqual(commit.timelineRevision, input.timelineRevision) &&
|
|
2071
1524
|
isDeepStrictEqual(commit.narrativeCheckpoint, input.narrativeCheckpoint) &&
|
|
2072
1525
|
isDeepStrictEqual(commit.correctionTargets, expectedCorrection) &&
|
|
2073
1526
|
commit.corrects ===
|
|
2074
1527
|
(input.mode === "correction" ? input.parentHead : undefined));
|
|
2075
1528
|
}
|
|
1529
|
+
function headPosition(head) {
|
|
1530
|
+
if (head === "genesis")
|
|
1531
|
+
return 0;
|
|
1532
|
+
const match = /^commit:([1-9][0-9]*)$/u.exec(head);
|
|
1533
|
+
if (match === null)
|
|
1534
|
+
throw new TypeError(`Endpoint is invalid: ${head}`);
|
|
1535
|
+
return Number.parseInt(match[1], 10);
|
|
1536
|
+
}
|
|
1537
|
+
// 分叉只复制一个世界的闭包;回退与身份重分配由调用方在副本上完成。
|
|
1538
|
+
const forkClosureEntries = [
|
|
1539
|
+
"state",
|
|
1540
|
+
"control",
|
|
1541
|
+
"history",
|
|
1542
|
+
join("runtime", "head.json"),
|
|
1543
|
+
join("runtime", "turns"),
|
|
1544
|
+
join("runtime", "history-backgrounds"),
|
|
1545
|
+
join("runtime", "additional-materials.json"),
|
|
1546
|
+
join("runtime", "genesis.json"),
|
|
1547
|
+
join("runtime", "play-genesis-timeline.json"),
|
|
1548
|
+
"artifacts",
|
|
1549
|
+
];
|
|
1550
|
+
async function copyForkClosure(sourceRoot, target) {
|
|
1551
|
+
await mkdir(join(target, "runtime"), { recursive: true, mode: 0o700 });
|
|
1552
|
+
for (const entry of forkClosureEntries) {
|
|
1553
|
+
const source = join(sourceRoot, entry);
|
|
1554
|
+
try {
|
|
1555
|
+
await stat(source);
|
|
1556
|
+
}
|
|
1557
|
+
catch (error) {
|
|
1558
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
1559
|
+
continue;
|
|
1560
|
+
throw error;
|
|
1561
|
+
}
|
|
1562
|
+
await mkdir(dirname(join(target, entry)), { recursive: true, mode: 0o700 });
|
|
1563
|
+
await cp(source, join(target, entry), { recursive: true });
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
2076
1566
|
function openWorldDocumentSnapshot(files) {
|
|
2077
1567
|
return WorldDocumentStore.open({
|
|
2078
1568
|
layout: "world_state",
|
|
@@ -2089,36 +1579,11 @@ async function writeJson(path, value) {
|
|
|
2089
1579
|
encoding: "utf8",
|
|
2090
1580
|
mode: 0o600,
|
|
2091
1581
|
});
|
|
2092
|
-
await syncFile(path);
|
|
2093
1582
|
}
|
|
2094
|
-
async function
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
}
|
|
2099
|
-
finally {
|
|
2100
|
-
await handle.close();
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
async function syncDirectory(path) {
|
|
2104
|
-
const handle = await open(path, "r");
|
|
2105
|
-
try {
|
|
2106
|
-
await handle.sync();
|
|
2107
|
-
}
|
|
2108
|
-
finally {
|
|
2109
|
-
await handle.close();
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
async function durableTree(root) {
|
|
2113
|
-
const entries = await readdir(root, { withFileTypes: true });
|
|
2114
|
-
for (const entry of entries) {
|
|
2115
|
-
const path = join(root, entry.name);
|
|
2116
|
-
if (entry.isDirectory())
|
|
2117
|
-
await durableTree(path);
|
|
2118
|
-
else if (entry.isFile())
|
|
2119
|
-
await syncFile(path);
|
|
2120
|
-
}
|
|
2121
|
-
await syncDirectory(root);
|
|
1583
|
+
async function publishJson(path, value) {
|
|
1584
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1585
|
+
await writeJson(temporary, value);
|
|
1586
|
+
await rename(temporary, path);
|
|
2122
1587
|
}
|
|
2123
1588
|
async function readGenesisForFork(worldRoot, worldId) {
|
|
2124
1589
|
const value = await readJson(join(worldRoot, "runtime", "genesis.json"));
|
|
@@ -2191,15 +1656,15 @@ async function readPublicationAt(root) {
|
|
|
2191
1656
|
async function readWorldSummaryAt(root, publication, serializeRebuild) {
|
|
2192
1657
|
const published = publication ?? (await readPublicationAt(root));
|
|
2193
1658
|
const gameplay = await readWorldGameplayAt(root);
|
|
2194
|
-
const
|
|
2195
|
-
|
|
1659
|
+
const activity = await readWorldActivity(root, serializeRebuild);
|
|
1660
|
+
return {
|
|
2196
1661
|
...toSummary(published, gameplay),
|
|
2197
1662
|
title: await readWorldTitleAt(root, published),
|
|
2198
|
-
lastActivityAt,
|
|
2199
|
-
excerpt,
|
|
2200
|
-
playerTurns,
|
|
1663
|
+
lastActivityAt: activity.lastActivityAt,
|
|
1664
|
+
excerpt: activity.excerpt,
|
|
1665
|
+
playerTurns: activity.playerTurns,
|
|
1666
|
+
...(activity.pendingUpgrade === true ? { pendingUpgrade: true } : {}),
|
|
2201
1667
|
};
|
|
2202
|
-
return summary;
|
|
2203
1668
|
}
|
|
2204
1669
|
async function readWorldTitleAt(root, published) {
|
|
2205
1670
|
const metadata = await readOptionalJson(join(root, localMetadataFile));
|
|
@@ -2318,36 +1783,20 @@ function assertRelativePath(path) {
|
|
|
2318
1783
|
function isNodeError(error) {
|
|
2319
1784
|
return error instanceof Error && "code" in error;
|
|
2320
1785
|
}
|
|
2321
|
-
function crashAtFileNativeWorldCreationEdge(edge) {
|
|
2322
|
-
if (process.env.NODE_ENV === "test" &&
|
|
2323
|
-
process.env
|
|
2324
|
-
.NARRAEON_INTERNAL_TEST_CRASH_AT_FILE_NATIVE_WORLD_CREATION_EDGE === edge) {
|
|
2325
|
-
process.kill(process.pid, "SIGKILL");
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
function crashAtFileNativeAuthorityEdge(edge) {
|
|
2329
|
-
if (process.env.NODE_ENV === "test" &&
|
|
2330
|
-
process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_FILE_NATIVE_AUTHORITY_EDGE ===
|
|
2331
|
-
edge) {
|
|
2332
|
-
process.kill(process.pid, "SIGKILL");
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
1786
|
import { readDocumentMaintenanceFacts, } from "./WorldDocumentMaintenance.js";
|
|
2336
1787
|
function backgroundField(background) {
|
|
2337
1788
|
return background === undefined ? {} : { background };
|
|
2338
1789
|
}
|
|
2339
|
-
function
|
|
2340
|
-
return
|
|
2341
|
-
const id = path.replace(/\.json$/u, "");
|
|
2342
|
-
const value = JSON.parse(contents);
|
|
2343
|
-
if (path !== `${id}.json` ||
|
|
2344
|
-
!Object.hasOwn(history, id) ||
|
|
2345
|
-
!isHistoryBackground(value))
|
|
2346
|
-
throw new FileNativeWorldCreationError("world_corrupt", "Materialized history background is invalid or unreachable");
|
|
2347
|
-
return [id, value];
|
|
2348
|
-
}));
|
|
1790
|
+
function playerRevisionIntentPath(worldsRoot, worldId) {
|
|
1791
|
+
return join(worldsRoot, worldId, "runtime", "player-revision-intent.json");
|
|
2349
1792
|
}
|
|
2350
|
-
function
|
|
2351
|
-
|
|
2352
|
-
|
|
1793
|
+
async function readOptionalJsonFile(path) {
|
|
1794
|
+
try {
|
|
1795
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
1796
|
+
}
|
|
1797
|
+
catch (error) {
|
|
1798
|
+
if (error.code === "ENOENT")
|
|
1799
|
+
return null;
|
|
1800
|
+
throw error;
|
|
1801
|
+
}
|
|
2353
1802
|
}
|