narraeon 0.7.1 → 0.7.4
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/node/protocol/v1.js +8 -2
- package/dist/node/runtime/V1Runtime.js +104 -71
- package/dist/node/runtime/archive/WorldArchives.js +17 -5
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +59 -12
- package/dist/node/runtime/config/dataRootCleanup.js +52 -0
- package/dist/node/runtime/content/ContentWorkspace.js +2 -2
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +34 -2
- package/dist/node/runtime/model/FileNativeModelAdapters.js +106 -18
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +70 -3
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +236 -26
- package/dist/node/runtime/play/PlayCallChain.js +172 -54
- package/dist/node/runtime/play/PlayPromptRun.js +157 -3
- package/dist/node/runtime/play/PlayTraceRetention.js +2 -2
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +10 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +11 -4
- package/dist/node/runtime/world/WorldLedgerAuthority.js +13 -10
- package/dist/node/runtime/world/WorldOperationCoordinator.js +4 -0
- package/dist/node/runtime/world/WorldQueue.js +14 -1
- package/dist/node/runtime/world/WorldTurnLedger.js +48 -20
- package/dist/node/server/createServer.js +44 -15
- package/dist/node/server/startWebServer.js +2 -5
- package/dist/web/assets/index-9N9kO8kY.js +1408 -0
- package/dist/web/assets/index-BjxBYYQi.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/node/runtime/config/dataRootLock.js +0 -108
- package/dist/web/assets/index-BUCslLEK.css +0 -1
- package/dist/web/assets/index-DkamBH6c.js +0 -1408
|
@@ -20,7 +20,7 @@ import { FileNativePlayTimelineStore } from "../play/FileNativePlayTimelineStore
|
|
|
20
20
|
import { FileNativePlayAdvanceStore } from "../play/FileNativePlayAdvanceStore.js";
|
|
21
21
|
import { FileNativeWorldStorageMigrator, UnsupportedLegacyStorageError, } from "./FileNativeWorldStorageMigrator.js";
|
|
22
22
|
import { WorldAuthorityError, WorldLedgerAuthority, } from "./WorldLedgerAuthority.js";
|
|
23
|
-
import { forgetLedger, rewindTo } from "./WorldTurnLedger.js";
|
|
23
|
+
import { forgetLedger, rewindTo, WorldTurnLedgerError, } from "./WorldTurnLedger.js";
|
|
24
24
|
import { FileNativeWorldOperationCoordinator, WorldOperationBusyError, } from "./WorldOperationCoordinator.js";
|
|
25
25
|
import { projectFileNativeHistorySurface, projectHistoryBackgrounds, historyBackgroundRecord, } from "./FileNativeHistoryProjection.js";
|
|
26
26
|
const publicationFile = "publication.json";
|
|
@@ -105,7 +105,9 @@ export class FileNativeWorldStore {
|
|
|
105
105
|
try {
|
|
106
106
|
const root = join(this.#worldsRoot, worldId);
|
|
107
107
|
const authority = new WorldLedgerAuthority(root);
|
|
108
|
-
if (await authority.exists())
|
|
108
|
+
if ((await authority.exists()) &&
|
|
109
|
+
(await this.playTimeline.layoutCurrent(worldId)) &&
|
|
110
|
+
(await this.playAdvances.layoutCurrent(worldId))) {
|
|
109
111
|
await authority.readHead();
|
|
110
112
|
this.#currentStorage.add(worldId);
|
|
111
113
|
return;
|
|
@@ -465,12 +467,14 @@ export class FileNativeWorldStore {
|
|
|
465
467
|
assertIdentity(worldId, "World ID");
|
|
466
468
|
await rm(join(this.#worldsRoot, worldId, "runtime", "archive-import.json"), { force: true });
|
|
467
469
|
}
|
|
468
|
-
|
|
470
|
+
/** 端点名称与它的操作标识取自同一次读取,回退不会让两者分属不同端点。 */
|
|
471
|
+
async currentEndpoint(worldId) {
|
|
469
472
|
assertIdentity(worldId, "World ID");
|
|
470
473
|
await this.ensureCurrentStorage(worldId);
|
|
471
474
|
const root = join(this.#worldsRoot, worldId);
|
|
472
475
|
await readPublicationAt(root);
|
|
473
|
-
|
|
476
|
+
const { head, operationId } = await new WorldLedgerAuthority(root).readHead();
|
|
477
|
+
return { head, operationId };
|
|
474
478
|
}
|
|
475
479
|
async saveControlDraft(worldId, files) {
|
|
476
480
|
assertIdentity(worldId, "World ID");
|
|
@@ -793,6 +797,9 @@ export class FileNativeWorldStore {
|
|
|
793
797
|
catch (error) {
|
|
794
798
|
if (error instanceof FileNativeWorldCreationError)
|
|
795
799
|
throw error;
|
|
800
|
+
// 端点在读取途中被回退丢弃不是损坏:调用方重新取得当前端点即可。
|
|
801
|
+
if (error instanceof WorldTurnLedgerError && error.code === "discarded")
|
|
802
|
+
throw new FileNativeWorldCreationError("endpoint_discarded", `Authority endpoint was rewound away${head === undefined ? "" : `: ${head}`}`, { cause: error });
|
|
796
803
|
throw new FileNativeWorldCreationError("world_corrupt", `Cannot recover immutable Authority endpoint${head === undefined ? "" : `: ${head}`}`, error instanceof Error ? { cause: error } : undefined);
|
|
797
804
|
}
|
|
798
805
|
}
|
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { projectFileNativeHistorySurface, projectHistoryBackgrounds, } from "./FileNativeHistoryProjection.js";
|
|
5
|
-
import { appendTurn, initializeLedger, listTurns, readFileAt, readHead as readLedgerHead, readTreeAt, readTurnDigest, resolveEndpoint, } from "./WorldTurnLedger.js";
|
|
5
|
+
import { appendTurn, initializeLedger, listTurns, readEndpoint, readFileAt, readHead as readLedgerHead, readTreeAt, readTurnDigest, resolveEndpoint, } from "./WorldTurnLedger.js";
|
|
6
6
|
export class WorldAuthorityError extends Error {
|
|
7
7
|
code;
|
|
8
8
|
constructor(code, message) {
|
|
@@ -56,28 +56,31 @@ export class WorldLedgerAuthority {
|
|
|
56
56
|
async initialize() {
|
|
57
57
|
await initializeLedger(this.#root);
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
59
|
+
/** 当前端点:逻辑位置(回退后即被恢复的轮)及其元数据,三者取自同一份账本视图。 */
|
|
60
60
|
async readHead() {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
61
|
+
const endpoint = await readEndpoint(this.#root);
|
|
62
|
+
const { sequence } = endpoint;
|
|
63
63
|
return {
|
|
64
|
-
head: headName(
|
|
65
|
-
sequence
|
|
66
|
-
operationId: await this.#operationIdAt(
|
|
67
|
-
commitDigest:
|
|
64
|
+
head: headName(sequence),
|
|
65
|
+
sequence,
|
|
66
|
+
operationId: await this.#operationIdAt(sequence, endpoint.record),
|
|
67
|
+
commitDigest: endpoint.digest,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
async currentOperationId() {
|
|
71
71
|
return (await this.readHead()).operationId;
|
|
72
72
|
}
|
|
73
|
-
async #operationIdAt(position) {
|
|
73
|
+
async #operationIdAt(position, record) {
|
|
74
74
|
if (position === 0) {
|
|
75
75
|
const genesis = await readJsonOrNull(join(this.#root, genesisFile));
|
|
76
76
|
return typeof genesis?.operationId === "string"
|
|
77
77
|
? genesis.operationId
|
|
78
78
|
: null;
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
const commit = record?.payload;
|
|
81
|
+
if (commit?.sequence !== position)
|
|
82
|
+
throw new WorldAuthorityError("corrupt", `第 ${position} 轮缺少提交元数据`);
|
|
83
|
+
return commit.operationId;
|
|
81
84
|
}
|
|
82
85
|
async #positionOf(head) {
|
|
83
86
|
if (head === undefined)
|
|
@@ -154,6 +154,10 @@ export class FileNativeWorldOperationCoordinator {
|
|
|
154
154
|
isWorldHeld(worldId) {
|
|
155
155
|
return this.#queue.isHeld(worldId);
|
|
156
156
|
}
|
|
157
|
+
/** World operations for this world that are running or waiting for their turn. */
|
|
158
|
+
pendingWorldOperations(worldId) {
|
|
159
|
+
return this.#queue.pendingCount(worldId);
|
|
160
|
+
}
|
|
157
161
|
#releaseClaim(handle) {
|
|
158
162
|
const current = this.#claims.get(handle.worldId);
|
|
159
163
|
if (current?.operationId === handle.operationId &&
|
|
@@ -11,6 +11,7 @@ import { resolve } from "node:path";
|
|
|
11
11
|
*/
|
|
12
12
|
export class WorldQueue {
|
|
13
13
|
#tails = new Map();
|
|
14
|
+
#pending = new Map();
|
|
14
15
|
#held = new AsyncLocalStorage();
|
|
15
16
|
run(worldId, action) {
|
|
16
17
|
const held = this.#held.getStore();
|
|
@@ -19,8 +20,9 @@ export class WorldQueue {
|
|
|
19
20
|
const previous = this.#tails.get(worldId) ?? Promise.resolve();
|
|
20
21
|
const nextHeld = new Set(held ?? []);
|
|
21
22
|
nextHeld.add(worldId);
|
|
23
|
+
this.#pending.set(worldId, this.pendingCount(worldId) + 1);
|
|
22
24
|
const result = previous.then(() => this.#held.run(nextHeld, action));
|
|
23
|
-
const tail = result.then(() =>
|
|
25
|
+
const tail = result.then(() => this.#finish(worldId), () => this.#finish(worldId));
|
|
24
26
|
this.#tails.set(worldId, tail);
|
|
25
27
|
void tail.then(() => {
|
|
26
28
|
if (this.#tails.get(worldId) === tail)
|
|
@@ -28,10 +30,21 @@ export class WorldQueue {
|
|
|
28
30
|
});
|
|
29
31
|
return result;
|
|
30
32
|
}
|
|
33
|
+
/** Operations for worldId that are running or waiting for their turn. */
|
|
34
|
+
pendingCount(worldId) {
|
|
35
|
+
return this.#pending.get(worldId) ?? 0;
|
|
36
|
+
}
|
|
31
37
|
/** True while the current async context is inside an operation for worldId. */
|
|
32
38
|
isHeld(worldId) {
|
|
33
39
|
return this.#held.getStore()?.has(worldId) ?? false;
|
|
34
40
|
}
|
|
41
|
+
#finish(worldId) {
|
|
42
|
+
const remaining = this.pendingCount(worldId) - 1;
|
|
43
|
+
if (remaining > 0)
|
|
44
|
+
this.#pending.set(worldId, remaining);
|
|
45
|
+
else
|
|
46
|
+
this.#pending.delete(worldId);
|
|
47
|
+
}
|
|
35
48
|
}
|
|
36
49
|
const queuesByDataRoot = new Map();
|
|
37
50
|
/** The queue shared by every store that changes a world under one data root. */
|
|
@@ -78,22 +78,32 @@ async function readAllRecords(root) {
|
|
|
78
78
|
return [];
|
|
79
79
|
throw error;
|
|
80
80
|
}
|
|
81
|
-
const
|
|
81
|
+
const turns = [];
|
|
82
82
|
for (const name of names) {
|
|
83
83
|
if (!name.endsWith(".json"))
|
|
84
84
|
continue;
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// 回退在发布端点之后才删除被丢弃的轮记录:读到一半消失的记录一定已被丢弃,跳过即可。
|
|
86
|
+
// 端点若在读取期间变化,loadView 会重新读取。
|
|
87
|
+
const text = await readTextOrNull(join(root, turnsDirectory, name));
|
|
88
|
+
if (text === null)
|
|
89
|
+
continue;
|
|
90
|
+
turns.push({
|
|
91
|
+
record: JSON.parse(text),
|
|
92
|
+
digest: createHash("sha256").update(text).digest("hex"),
|
|
93
|
+
});
|
|
87
94
|
}
|
|
88
|
-
return
|
|
95
|
+
return turns.sort((left, right) => left.record.sequence - right.record.sequence);
|
|
89
96
|
}
|
|
90
97
|
// 被某个 rewind 丢弃的轮是 (restores, rewind) 之间的位置;它们可能仍在磁盘上,
|
|
91
98
|
// 直到清理完成,但读取时一律忽略。
|
|
92
|
-
function viewOf(head,
|
|
99
|
+
function viewOf(head, turns) {
|
|
93
100
|
const byPosition = new Map();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
const digests = new Map();
|
|
102
|
+
for (const { record, digest } of turns) {
|
|
103
|
+
if (record.sequence > head)
|
|
104
|
+
continue;
|
|
105
|
+
byPosition.set(record.sequence, record);
|
|
106
|
+
digests.set(record.sequence, digest);
|
|
97
107
|
}
|
|
98
108
|
const discarded = new Set();
|
|
99
109
|
for (const record of byPosition.values()) {
|
|
@@ -103,21 +113,28 @@ function viewOf(head, records) {
|
|
|
103
113
|
discarded.add(position);
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
|
-
return { head, byPosition, discarded };
|
|
116
|
+
return { head, byPosition, digests, discarded };
|
|
107
117
|
}
|
|
108
118
|
// 进程内视图缓存,按端点失效。一个数据目录只被一个进程操作(ADR-0044),
|
|
109
119
|
// 所以 head 相同即记录相同:任何改变可见记录的写入都会先推进 head。
|
|
110
120
|
// 只有 beginTurn 与 recoverIncomplete 会在 head 之上写入或删除记录,它们不影响视图
|
|
111
121
|
// (视图只包含序号不大于 head 的记录,且 recoverIncomplete 本身会在结束时失效缓存)。
|
|
112
122
|
const viewCache = new Map();
|
|
123
|
+
// 一次读取只解析一个视图。回退先发布新端点、再删除被丢弃的轮记录:读完记录后端点仍未变化,
|
|
124
|
+
// 说明读到的记录与这个端点一致;端点变了就用新端点重新读取。
|
|
113
125
|
async function loadView(root) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
for (;;) {
|
|
127
|
+
const head = await readHead(root);
|
|
128
|
+
const cached = viewCache.get(resolve(root));
|
|
129
|
+
if (cached?.head === head)
|
|
130
|
+
return cached;
|
|
131
|
+
const turns = await readAllRecords(root);
|
|
132
|
+
if ((await readHead(root)) !== head)
|
|
133
|
+
continue;
|
|
134
|
+
const view = viewOf(head, turns);
|
|
135
|
+
viewCache.set(resolve(root), view);
|
|
136
|
+
return view;
|
|
137
|
+
}
|
|
121
138
|
}
|
|
122
139
|
// 世界目录被删除、归档导入或分叉发布到已存在路径之前调用:下一次读取从磁盘重建视图。
|
|
123
140
|
export function forgetLedger(root) {
|
|
@@ -133,7 +150,7 @@ function resolvePosition(view, sequence) {
|
|
|
133
150
|
if (position === 0)
|
|
134
151
|
return 0;
|
|
135
152
|
if (view.discarded.has(position)) {
|
|
136
|
-
throw new WorldTurnLedgerError("
|
|
153
|
+
throw new WorldTurnLedgerError("discarded", `端点 ${sequence} 已被回退丢弃`);
|
|
137
154
|
}
|
|
138
155
|
const record = view.byPosition.get(position);
|
|
139
156
|
if (!record) {
|
|
@@ -217,8 +234,9 @@ export async function appendTurn(root, input) {
|
|
|
217
234
|
// 被丢弃的轮记录(读取时已忽略)在这里清理。返回是否回滚了未完成的轮。
|
|
218
235
|
export async function recoverIncomplete(root) {
|
|
219
236
|
const head = await readHead(root);
|
|
220
|
-
const
|
|
221
|
-
const incomplete =
|
|
237
|
+
const turns = await readAllRecords(root);
|
|
238
|
+
const incomplete = turns
|
|
239
|
+
.map(({ record }) => record)
|
|
222
240
|
.filter((record) => record.sequence > head)
|
|
223
241
|
.sort((left, right) => right.sequence - left.sequence);
|
|
224
242
|
for (const record of incomplete) {
|
|
@@ -234,7 +252,7 @@ export async function recoverIncomplete(root) {
|
|
|
234
252
|
}
|
|
235
253
|
await rm(join(root, turnFileName(record.sequence)), { force: true });
|
|
236
254
|
}
|
|
237
|
-
const view = viewOf(head,
|
|
255
|
+
const view = viewOf(head, turns);
|
|
238
256
|
for (const position of view.discarded) {
|
|
239
257
|
await rm(join(root, turnFileName(position)), { force: true });
|
|
240
258
|
}
|
|
@@ -259,6 +277,16 @@ export async function listTurns(root) {
|
|
|
259
277
|
export async function resolveEndpoint(root, sequence) {
|
|
260
278
|
return resolvePosition(await loadView(root), sequence);
|
|
261
279
|
}
|
|
280
|
+
// 当前端点的逻辑位置、它的轮记录与记录摘要,三者来自同一个视图。
|
|
281
|
+
export async function readEndpoint(root) {
|
|
282
|
+
const view = await loadView(root);
|
|
283
|
+
const sequence = resolvePosition(view, view.head);
|
|
284
|
+
return {
|
|
285
|
+
sequence,
|
|
286
|
+
record: view.byPosition.get(sequence) ?? null,
|
|
287
|
+
digest: view.digests.get(sequence) ?? null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
262
290
|
// 第 sequence 轮记录文件的 sha256;0 轮没有记录,返回 null。
|
|
263
291
|
export async function readTurnDigest(root, sequence) {
|
|
264
292
|
if (sequence === 0)
|
|
@@ -91,6 +91,7 @@ export async function createServer(input) {
|
|
|
91
91
|
return await input.runtime.worldArchives.upload(request.body);
|
|
92
92
|
}
|
|
93
93
|
catch (error) {
|
|
94
|
+
await input.runtime.recordArchiveFailure({ type: "world.archive.upload" }, error);
|
|
94
95
|
if (error instanceof WorldArchiveError)
|
|
95
96
|
return reply
|
|
96
97
|
.status(error.code === "limit_exceeded" ? 413 : 400)
|
|
@@ -98,6 +99,21 @@ export async function createServer(input) {
|
|
|
98
99
|
throw error;
|
|
99
100
|
}
|
|
100
101
|
});
|
|
102
|
+
// The browser checks this before it starts the one-time download, so a
|
|
103
|
+
// missing or failing export is reported on the page instead of a blank file.
|
|
104
|
+
archives.get("/api/runtime/v1/world-archives/:archiveId/check", async (request, reply) => {
|
|
105
|
+
try {
|
|
106
|
+
await input.runtime.worldArchives.checkDownload(request.params.archiveId);
|
|
107
|
+
return reply.status(204).send();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
await input.runtime.recordArchiveFailure({
|
|
111
|
+
type: "world.archive.download",
|
|
112
|
+
archiveId: request.params.archiveId,
|
|
113
|
+
}, error);
|
|
114
|
+
return archiveDownloadFailure(reply, error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
101
117
|
archives.get("/api/runtime/v1/world-archives/:archiveId", async (request, reply) => {
|
|
102
118
|
try {
|
|
103
119
|
const archive = await input.runtime.worldArchives.download(request.params.archiveId);
|
|
@@ -111,20 +127,11 @@ export async function createServer(input) {
|
|
|
111
127
|
.send(archive.stream);
|
|
112
128
|
}
|
|
113
129
|
catch (error) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"code" in error &&
|
|
120
|
-
error.code === "ENOENT")
|
|
121
|
-
return reply.status(404).send({
|
|
122
|
-
error: {
|
|
123
|
-
code: "invalid_archive",
|
|
124
|
-
message: "The archive download is unavailable",
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
throw error;
|
|
130
|
+
await input.runtime.recordArchiveFailure({
|
|
131
|
+
type: "world.archive.download",
|
|
132
|
+
archiveId: request.params.archiveId,
|
|
133
|
+
}, error);
|
|
134
|
+
return archiveDownloadFailure(reply, error);
|
|
128
135
|
}
|
|
129
136
|
});
|
|
130
137
|
done();
|
|
@@ -160,11 +167,19 @@ export async function createServer(input) {
|
|
|
160
167
|
}
|
|
161
168
|
catch (error) {
|
|
162
169
|
if (error instanceof V1ProtocolError) {
|
|
163
|
-
|
|
170
|
+
// 端点已被回退丢弃或内容包已被其他操作修改:客户端应重新读取,不是服务端错误。
|
|
171
|
+
const status = error.code === "endpoint_discarded" ||
|
|
172
|
+
error.code === "content_conflict"
|
|
173
|
+
? 409
|
|
174
|
+
: 400;
|
|
175
|
+
return reply.status(status).send({
|
|
164
176
|
protocol: "narraeon.runtime/v1",
|
|
165
177
|
error: {
|
|
166
178
|
code: error.code,
|
|
167
179
|
message: error.message,
|
|
180
|
+
...(error.details === undefined
|
|
181
|
+
? {}
|
|
182
|
+
: { details: error.details }),
|
|
168
183
|
},
|
|
169
184
|
});
|
|
170
185
|
}
|
|
@@ -180,6 +195,20 @@ export async function createServer(input) {
|
|
|
180
195
|
server.get("/index.html", index);
|
|
181
196
|
return server;
|
|
182
197
|
}
|
|
198
|
+
function archiveDownloadFailure(reply, error) {
|
|
199
|
+
if (error instanceof WorldArchiveError)
|
|
200
|
+
return reply
|
|
201
|
+
.status(400)
|
|
202
|
+
.send({ error: { code: error.code, message: error.message } });
|
|
203
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
204
|
+
return reply.status(404).send({
|
|
205
|
+
error: {
|
|
206
|
+
code: "invalid_archive",
|
|
207
|
+
message: "The archive download is unavailable",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
183
212
|
function acceptsPlayCallChainStream(request) {
|
|
184
213
|
const accept = request.headers.accept;
|
|
185
214
|
return (typeof accept === "string" &&
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { prepareDataRoot } from "../runtime/config/dataRootCleanup.js";
|
|
2
2
|
import { createRuntime } from "./createRuntime.js";
|
|
3
3
|
import { createServer } from "./createServer.js";
|
|
4
4
|
export const defaultWebPort = 4317;
|
|
5
5
|
export const defaultWebHost = "127.0.0.1";
|
|
6
6
|
export const containerWebHost = "0.0.0.0";
|
|
7
7
|
export async function startWebServer(input) {
|
|
8
|
-
|
|
8
|
+
await prepareDataRoot(input.paths.dataRoot);
|
|
9
9
|
let server;
|
|
10
10
|
try {
|
|
11
11
|
const runtime = await createRuntime(input.paths);
|
|
@@ -15,15 +15,12 @@ export async function startWebServer(input) {
|
|
|
15
15
|
port: input.port,
|
|
16
16
|
...(input.logger === undefined ? {} : { logger: input.logger }),
|
|
17
17
|
});
|
|
18
|
-
server.addHook("onClose", () => lock.release());
|
|
19
18
|
await server.listen({ host: input.host, port: input.port });
|
|
20
19
|
return server;
|
|
21
20
|
}
|
|
22
21
|
catch (error) {
|
|
23
22
|
if (server !== undefined)
|
|
24
23
|
await server.close().catch(() => undefined);
|
|
25
|
-
else
|
|
26
|
-
await lock.release();
|
|
27
24
|
throw error;
|
|
28
25
|
}
|
|
29
26
|
}
|