@openfairygui/backend 0.2.0-alpha.9 → 0.2.0
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 +29 -8
- package/dist/index.cjs +1633 -27
- package/dist/index.d.cts +516 -4
- package/dist/index.d.mts +516 -4
- package/dist/index.mjs +1629 -23
- package/dist/node.cjs +1603 -9
- package/dist/node.d.cts +507 -2
- package/dist/node.d.mts +507 -2
- package/dist/node.mjs +1601 -6
- package/package.json +7 -6
- package/src/index.ts +18 -17
- package/src/node.ts +24 -8
- package/src/runtime/capabilities.ts +126 -0
- package/src/runtime/contracts.ts +518 -0
- package/src/runtime.ts +49 -623
- package/src/services/authoring-service.ts +242 -99
- package/src/services/cache-service.ts +1 -2
- package/src/services/context.ts +17 -8
- package/src/services/event-service.ts +1 -2
- package/src/services/job-service.ts +4 -5
- package/src/services/read-service.ts +1 -2
- package/src/services/runtime-service.ts +133 -27
- package/src/services/session-project-writer.ts +69 -0
- package/src/services/session-utils.ts +6 -3
- package/src/storage.ts +67 -27
- package/dist/runtime-DFatY9W0.mjs +0 -1417
- package/dist/runtime-GKzsXJdO.cjs +0 -1441
- package/dist/runtime-GyNVxAQ0.d.mts +0 -494
- package/dist/runtime-Jec6FcF5.d.cts +0 -494
- package/src/services/snapshot-utils.ts +0 -43
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,1584 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, commitUamProjectSourcePaths, liftDocumentToUamProject, materializeUamProject, normalizeUamProject, staleBranchDirectories, staleResourceFolders, staleSourceFiles, validateUamProject } from "@openfairygui/core/uam";
|
|
2
|
+
import { applyUamTransactionAppAsync } from "@openfairygui/functions/uam";
|
|
3
|
+
import { ProjectReader, ProjectWriter } from "@openfairygui/core/project-io";
|
|
4
|
+
//#region src/contracts.ts
|
|
5
|
+
const BACKEND_CONTRACT_VERSION = "1.1.0-p2";
|
|
6
|
+
const BACKEND_CAPABILITY_SCHEMA_VERSION = 2;
|
|
7
|
+
const BACKEND_COMPATIBILITY_POLICY = {
|
|
8
|
+
incompatibleChange: "requires contractVersion bump",
|
|
9
|
+
capabilitySchemaChange: "requires capabilitySchemaVersion bump",
|
|
10
|
+
additiveChange: "allowed without breaking existing consumers"
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/path-policy.ts
|
|
14
|
+
function normalizeComparablePath(value) {
|
|
15
|
+
const normalized = value.replace(/[/\\]+$/, "").replace(/\\/g, "/");
|
|
16
|
+
const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
|
|
17
|
+
const drivePrefix = driveMatch?.[1].toLowerCase() ?? "";
|
|
18
|
+
const remainder = driveMatch ? driveMatch[2] ?? "" : normalized;
|
|
19
|
+
const hasRoot = driveMatch ? true : remainder.startsWith("/");
|
|
20
|
+
const rawSegments = remainder.split("/").filter((segment) => segment.length > 0);
|
|
21
|
+
const segments = [];
|
|
22
|
+
for (const segment of rawSegments) {
|
|
23
|
+
if (segment === ".") continue;
|
|
24
|
+
if (segment === "..") {
|
|
25
|
+
if (segments.length > 0 && segments[segments.length - 1] !== "..") segments.pop();
|
|
26
|
+
else if (!hasRoot) segments.push("..");
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
segments.push(segment);
|
|
30
|
+
}
|
|
31
|
+
const joined = segments.join("/");
|
|
32
|
+
return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function createRuntimePathPolicy() {
|
|
35
|
+
return {
|
|
36
|
+
canonicalization: "realpath+normalized-casefold",
|
|
37
|
+
sessionIdentity: "project-root",
|
|
38
|
+
saveTarget: "opened-project-only",
|
|
39
|
+
outputTargets: "deferred",
|
|
40
|
+
workspaceBoundary: "project-root-only"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function resolveFairyPath(fileSystem, input) {
|
|
44
|
+
const resolvedInput = fileSystem.resolve(input);
|
|
45
|
+
const stat = await fileSystem.stat(resolvedInput);
|
|
46
|
+
if (stat.isFile() && resolvedInput.endsWith(".fairy")) return await fileSystem.resolvePath(resolvedInput);
|
|
47
|
+
if (stat.isDirectory()) {
|
|
48
|
+
const fairyFiles = (await fileSystem.readdir(resolvedInput)).filter((entry) => entry.endsWith(".fairy"));
|
|
49
|
+
if (fairyFiles.length === 1) return await fileSystem.resolvePath(fileSystem.join(resolvedInput, fairyFiles[0]));
|
|
50
|
+
if (fairyFiles.length > 1) throw new Error(`Multiple .fairy files found in ${resolvedInput}: ${fairyFiles.join(", ")}`);
|
|
51
|
+
throw new Error(`No .fairy file found in ${resolvedInput}`);
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Input is not a .fairy file or directory: ${resolvedInput}`);
|
|
54
|
+
}
|
|
55
|
+
async function resolveCanonicalProjectRoot(fileSystem, input) {
|
|
56
|
+
const fairyPath = await resolveFairyPath(fileSystem, input);
|
|
57
|
+
const canonicalProjectPath = await fileSystem.resolvePath(fileSystem.dirname(fairyPath));
|
|
58
|
+
return {
|
|
59
|
+
fairyPath,
|
|
60
|
+
canonicalProjectPath,
|
|
61
|
+
canonicalPathKey: normalizeComparablePath(canonicalProjectPath)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function validateSaveTarget(fileSystem, openedFairyPath, targetPath) {
|
|
65
|
+
if (!targetPath) return null;
|
|
66
|
+
const attemptedPath = await fileSystem.resolvePath(fileSystem.resolve(targetPath));
|
|
67
|
+
const allowedPath = await fileSystem.resolvePath(openedFairyPath);
|
|
68
|
+
if (normalizeComparablePath(attemptedPath) === normalizeComparablePath(allowedPath)) return null;
|
|
69
|
+
return {
|
|
70
|
+
code: "path_policy_violation",
|
|
71
|
+
message: `Save target is restricted to the originally opened project file: ${allowedPath}`,
|
|
72
|
+
policy: "save_target",
|
|
73
|
+
attemptedPath,
|
|
74
|
+
allowedPath
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/services/context.ts
|
|
79
|
+
function diagnosticFromError(error) {
|
|
80
|
+
return {
|
|
81
|
+
code: error.code,
|
|
82
|
+
message: error.message,
|
|
83
|
+
severity: "error"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function randomId$1() {
|
|
87
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
88
|
+
}
|
|
89
|
+
function createMeta(stage, startedAt, options) {
|
|
90
|
+
return {
|
|
91
|
+
requestId: options?.requestId ?? randomId$1(),
|
|
92
|
+
sessionId: options?.sessionId,
|
|
93
|
+
revision: options?.revision,
|
|
94
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
95
|
+
warnings: options?.warnings ?? [],
|
|
96
|
+
diagnostics: options?.diagnostics ?? [],
|
|
97
|
+
stage,
|
|
98
|
+
contractVersion: BACKEND_CONTRACT_VERSION,
|
|
99
|
+
capabilitySchemaVersion: 2
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function success(stage, startedAt, data, options) {
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
meta: createMeta(stage, startedAt, options),
|
|
106
|
+
data
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function failure(stage, startedAt, error, session, options) {
|
|
110
|
+
const diagnostics = options?.diagnostics ?? [diagnosticFromError(error)];
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
meta: createMeta(stage, startedAt, {
|
|
114
|
+
...options,
|
|
115
|
+
diagnostics
|
|
116
|
+
}),
|
|
117
|
+
error,
|
|
118
|
+
session
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/services/session-project-writer.ts
|
|
123
|
+
function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
124
|
+
async function trackWrite(targetPath, write) {
|
|
125
|
+
try {
|
|
126
|
+
const result = await write();
|
|
127
|
+
writtenPaths.push(targetPath);
|
|
128
|
+
return result;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
failedPaths.push(targetPath);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
readFile: (path) => fileSystem.readFile(path),
|
|
136
|
+
readFileRaw: (path) => fileSystem.readFileRaw(path),
|
|
137
|
+
writeFile: (path, content) => trackWrite(path, async () => {
|
|
138
|
+
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
139
|
+
await fileSystem.writeFile(path, content);
|
|
140
|
+
}),
|
|
141
|
+
writeFileRaw: (path, data) => trackWrite(path, async () => {
|
|
142
|
+
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
143
|
+
await fileSystem.writeFileRaw(path, data);
|
|
144
|
+
}),
|
|
145
|
+
mkdir: (path) => fileSystem.mkdir(path, { recursive: true }),
|
|
146
|
+
readdir: (path) => fileSystem.readdir(path),
|
|
147
|
+
async exists(path) {
|
|
148
|
+
try {
|
|
149
|
+
await fileSystem.stat(path);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
join: (...paths) => fileSystem.join(...paths),
|
|
156
|
+
dirname: (path) => fileSystem.dirname(path),
|
|
157
|
+
unlink: (path) => trackWrite(path, () => fileSystem.unlink(path)),
|
|
158
|
+
rmdir: (path) => trackWrite(path, () => fileSystem.rmdir(path))
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function writeSessionProject(input) {
|
|
162
|
+
await new ProjectWriter(createWriterFileSystem(input.fileSystem, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, {
|
|
163
|
+
staleSourceFiles: input.staleSourceFiles,
|
|
164
|
+
staleResourceFolders: input.staleResourceFolders,
|
|
165
|
+
staleBranchDirectories: input.staleBranchDirectories
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/services/session-utils.ts
|
|
170
|
+
function toSessionSnapshot(session, capabilities) {
|
|
171
|
+
return {
|
|
172
|
+
sessionId: session.sessionId,
|
|
173
|
+
canonicalProjectPath: session.canonicalProjectPath,
|
|
174
|
+
revision: session.revision,
|
|
175
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
176
|
+
dirty: session.dirty,
|
|
177
|
+
uamFidelity: session.uamFidelity,
|
|
178
|
+
lockHeld: session.lockHeld,
|
|
179
|
+
capabilities: structuredClone(capabilities)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function createSessionNotFoundError(sessionId) {
|
|
183
|
+
return {
|
|
184
|
+
code: "session_not_found",
|
|
185
|
+
message: `Session was not found: ${sessionId}`,
|
|
186
|
+
sessionId
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function createStaleWriteError(session, expectedRevision) {
|
|
190
|
+
return {
|
|
191
|
+
code: "stale_write",
|
|
192
|
+
message: `Expected revision ${expectedRevision} does not match current revision ${session.revision}.`,
|
|
193
|
+
sessionId: session.sessionId,
|
|
194
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
195
|
+
expectedRevision,
|
|
196
|
+
actualRevision: session.revision
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/services/authoring-service.ts
|
|
201
|
+
function sourceFileKey(source) {
|
|
202
|
+
return [
|
|
203
|
+
source.branch,
|
|
204
|
+
source.packageName,
|
|
205
|
+
source.path,
|
|
206
|
+
source.fileName
|
|
207
|
+
].join("\0");
|
|
208
|
+
}
|
|
209
|
+
function resourceFolderKey(folder) {
|
|
210
|
+
return [
|
|
211
|
+
folder.branch,
|
|
212
|
+
folder.packageName,
|
|
213
|
+
folder.path
|
|
214
|
+
].join("\0");
|
|
215
|
+
}
|
|
216
|
+
function branchDirectoryKey(directory) {
|
|
217
|
+
return [directory.branch, directory.packageName ?? ""].join("\0");
|
|
218
|
+
}
|
|
219
|
+
function recordStaleProjectFiles(session, previousProject, nextProject) {
|
|
220
|
+
if (!session.fileSystem) return;
|
|
221
|
+
for (const source of staleSourceFiles(previousProject, nextProject)) session.pendingStaleSourceFiles.set(sourceFileKey(source), source);
|
|
222
|
+
for (const source of staleSourceFiles(nextProject, previousProject)) session.pendingStaleSourceFiles.delete(sourceFileKey(source));
|
|
223
|
+
for (const folder of staleResourceFolders(previousProject, nextProject)) session.pendingStaleResourceFolders.set(resourceFolderKey(folder), folder);
|
|
224
|
+
for (const folder of staleResourceFolders(nextProject, previousProject)) session.pendingStaleResourceFolders.delete(resourceFolderKey(folder));
|
|
225
|
+
for (const directory of staleBranchDirectories(previousProject, nextProject)) session.pendingStaleBranchDirectories.set(branchDirectoryKey(directory), directory);
|
|
226
|
+
for (const directory of staleBranchDirectories(nextProject, previousProject)) session.pendingStaleBranchDirectories.delete(branchDirectoryKey(directory));
|
|
227
|
+
}
|
|
228
|
+
function toBackendDiagnostics(error) {
|
|
229
|
+
return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
|
|
230
|
+
code: error.code,
|
|
231
|
+
message: error.message,
|
|
232
|
+
severity: "error",
|
|
233
|
+
operationKind: error.operationKind,
|
|
234
|
+
opIndex: error.opIndex,
|
|
235
|
+
opId: error.opId
|
|
236
|
+
}];
|
|
237
|
+
}
|
|
238
|
+
function createCapabilityUnavailableError$1(message) {
|
|
239
|
+
return {
|
|
240
|
+
code: "capability_unavailable",
|
|
241
|
+
message,
|
|
242
|
+
capability: "fileSystem",
|
|
243
|
+
requiredAdapter: "BackendFileSystem"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function createUamFidelityUnsupportedError(session) {
|
|
247
|
+
return {
|
|
248
|
+
code: "uam_fidelity_unsupported",
|
|
249
|
+
message: "The source project contains formal properties that the current UAM cannot preserve.",
|
|
250
|
+
sessionId: session.sessionId,
|
|
251
|
+
canonicalPathKey: session.canonicalPathKey
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function validationDiagnostics(sessionProject) {
|
|
255
|
+
return validateUamProject(sessionProject).map((issue) => ({
|
|
256
|
+
code: "materialize_validation_failed",
|
|
257
|
+
message: issue.message,
|
|
258
|
+
severity: "error",
|
|
259
|
+
path: issue.path,
|
|
260
|
+
operationKind: "materializeSession"
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
function toMaterializeSnapshot(session, capabilities, input) {
|
|
264
|
+
return {
|
|
265
|
+
...toSessionSnapshot(session, capabilities),
|
|
266
|
+
mode: "fullProject",
|
|
267
|
+
reason: input.reason,
|
|
268
|
+
materializeRevision: session.revision,
|
|
269
|
+
saveRevision: session.lastSavedRevision,
|
|
270
|
+
writtenPaths: [...input.writtenPaths],
|
|
271
|
+
skippedPaths: [...input.skippedPaths],
|
|
272
|
+
diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function storageCanonicalTarget(input) {
|
|
276
|
+
const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || ".");
|
|
277
|
+
return {
|
|
278
|
+
fileSystem: input.fileSystem,
|
|
279
|
+
fairyPath: input.fairyPath,
|
|
280
|
+
canonicalProjectPath,
|
|
281
|
+
canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function detachSharedByteViews(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
285
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
286
|
+
seen.add(value);
|
|
287
|
+
for (const [key, child] of Object.entries(value)) {
|
|
288
|
+
if (child instanceof Uint8Array) {
|
|
289
|
+
if (typeof SharedArrayBuffer !== "undefined" && child.buffer instanceof SharedArrayBuffer) value[key] = new Uint8Array(child);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
detachSharedByteViews(child, seen);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
var AuthoringService = class {
|
|
296
|
+
sessionOperations = /* @__PURE__ */ new Map();
|
|
297
|
+
constructor(context, cacheService, eventService) {
|
|
298
|
+
this.context = context;
|
|
299
|
+
this.cacheService = cacheService;
|
|
300
|
+
this.eventService = eventService;
|
|
301
|
+
}
|
|
302
|
+
async runSessionExclusive(sessionId, operation) {
|
|
303
|
+
const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
|
|
304
|
+
let release = () => void 0;
|
|
305
|
+
const current = new Promise((resolve) => {
|
|
306
|
+
release = resolve;
|
|
307
|
+
});
|
|
308
|
+
const tail = previous.then(() => current);
|
|
309
|
+
this.sessionOperations.set(sessionId, tail);
|
|
310
|
+
await previous;
|
|
311
|
+
try {
|
|
312
|
+
return await operation();
|
|
313
|
+
} finally {
|
|
314
|
+
release();
|
|
315
|
+
if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async applyTransaction(input) {
|
|
319
|
+
const queuedInput = structuredClone(input);
|
|
320
|
+
detachSharedByteViews(queuedInput);
|
|
321
|
+
return this.runSessionExclusive(queuedInput.sessionId, () => this.applyTransactionExclusive(queuedInput));
|
|
322
|
+
}
|
|
323
|
+
async applyTransactionExclusive(input) {
|
|
324
|
+
const startedAt = Date.now();
|
|
325
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
326
|
+
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
327
|
+
if (input.expectedRevision !== session.revision) {
|
|
328
|
+
this.eventService.emit({
|
|
329
|
+
kind: "transaction.rejected",
|
|
330
|
+
sessionId: session.sessionId,
|
|
331
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
332
|
+
revision: session.revision
|
|
333
|
+
});
|
|
334
|
+
return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
|
|
335
|
+
sessionId: session.sessionId,
|
|
336
|
+
revision: session.revision
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
const result = await applyUamTransactionAppAsync({
|
|
340
|
+
project: session.project,
|
|
341
|
+
operations: input.operations
|
|
342
|
+
});
|
|
343
|
+
if (this.context.sessions.get(input.sessionId) !== session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
344
|
+
if (result.ok === false) {
|
|
345
|
+
const diagnostics = toBackendDiagnostics(result.error);
|
|
346
|
+
this.eventService.emit({
|
|
347
|
+
kind: "transaction.rejected",
|
|
348
|
+
sessionId: session.sessionId,
|
|
349
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
350
|
+
revision: session.revision,
|
|
351
|
+
diagnostics
|
|
352
|
+
});
|
|
353
|
+
return failure("authoring", startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
|
|
354
|
+
sessionId: session.sessionId,
|
|
355
|
+
revision: session.revision,
|
|
356
|
+
diagnostics
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
recordStaleProjectFiles(session, session.project, result.project);
|
|
360
|
+
session.project = result.project;
|
|
361
|
+
session.revision += 1;
|
|
362
|
+
session.dirty = true;
|
|
363
|
+
const cacheEntry = this.cacheService.invalidateSession(session);
|
|
364
|
+
this.eventService.emit({
|
|
365
|
+
kind: "transaction.applied",
|
|
366
|
+
sessionId: session.sessionId,
|
|
367
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
368
|
+
revision: session.revision
|
|
369
|
+
});
|
|
370
|
+
this.eventService.emit({
|
|
371
|
+
kind: "cache.invalidated",
|
|
372
|
+
sessionId: session.sessionId,
|
|
373
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
374
|
+
revision: session.revision,
|
|
375
|
+
cacheRevision: cacheEntry.revision
|
|
376
|
+
});
|
|
377
|
+
return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
378
|
+
sessionId: session.sessionId,
|
|
379
|
+
revision: session.revision
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async saveSession(input) {
|
|
383
|
+
if (input.force === true || input.mode === "materializeCleanSession") return this.materializeSession({
|
|
384
|
+
sessionId: input.sessionId,
|
|
385
|
+
expectedRevision: input.expectedRevision,
|
|
386
|
+
targetPath: input.targetPath,
|
|
387
|
+
fileSystem: input.fileSystem,
|
|
388
|
+
mode: "fullProject",
|
|
389
|
+
reason: "force_save"
|
|
390
|
+
});
|
|
391
|
+
return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
|
|
392
|
+
}
|
|
393
|
+
async saveSessionExclusive(input) {
|
|
394
|
+
const startedAt = Date.now();
|
|
395
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
396
|
+
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
397
|
+
const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
|
|
398
|
+
if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
|
|
399
|
+
sessionId: session.sessionId,
|
|
400
|
+
revision: session.revision
|
|
401
|
+
});
|
|
402
|
+
if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
|
|
403
|
+
sessionId: session.sessionId,
|
|
404
|
+
revision: session.revision
|
|
405
|
+
});
|
|
406
|
+
const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
|
|
407
|
+
if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
|
|
408
|
+
sessionId: session.sessionId,
|
|
409
|
+
revision: session.revision
|
|
410
|
+
});
|
|
411
|
+
if (!session.dirty) return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
412
|
+
sessionId: session.sessionId,
|
|
413
|
+
revision: session.revision
|
|
414
|
+
});
|
|
415
|
+
if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
|
|
416
|
+
sessionId: session.sessionId,
|
|
417
|
+
revision: session.revision
|
|
418
|
+
});
|
|
419
|
+
const committedPaths = [];
|
|
420
|
+
const failedPaths = [];
|
|
421
|
+
this.eventService.emit({
|
|
422
|
+
kind: "save.started",
|
|
423
|
+
sessionId: session.sessionId,
|
|
424
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
425
|
+
revision: session.revision
|
|
426
|
+
});
|
|
427
|
+
try {
|
|
428
|
+
await writeSessionProject({
|
|
429
|
+
fileSystem,
|
|
430
|
+
document: materializeUamProject(session.project),
|
|
431
|
+
fairyPath: session.fairyPath,
|
|
432
|
+
staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
|
|
433
|
+
staleResourceFolders: [...session.pendingStaleResourceFolders.values()],
|
|
434
|
+
staleBranchDirectories: [...session.pendingStaleBranchDirectories.values()],
|
|
435
|
+
writtenPaths: committedPaths,
|
|
436
|
+
failedPaths
|
|
437
|
+
});
|
|
438
|
+
session.fileSystem ??= fileSystem;
|
|
439
|
+
session.pendingStaleSourceFiles.clear();
|
|
440
|
+
session.pendingStaleResourceFolders.clear();
|
|
441
|
+
session.pendingStaleBranchDirectories.clear();
|
|
442
|
+
commitUamProjectSourcePaths(session.project);
|
|
443
|
+
session.lastSavedRevision = session.revision;
|
|
444
|
+
session.dirty = false;
|
|
445
|
+
const cacheEntry = this.cacheService.refreshSession(session);
|
|
446
|
+
this.eventService.emit({
|
|
447
|
+
kind: "save.completed",
|
|
448
|
+
sessionId: session.sessionId,
|
|
449
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
450
|
+
revision: session.revision
|
|
451
|
+
});
|
|
452
|
+
this.eventService.emit({
|
|
453
|
+
kind: "cache.updated",
|
|
454
|
+
sessionId: session.sessionId,
|
|
455
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
456
|
+
revision: session.revision,
|
|
457
|
+
cacheRevision: cacheEntry.revision
|
|
458
|
+
});
|
|
459
|
+
return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
460
|
+
sessionId: session.sessionId,
|
|
461
|
+
revision: session.revision
|
|
462
|
+
});
|
|
463
|
+
} catch (error) {
|
|
464
|
+
this.cacheService.invalidateSession(session);
|
|
465
|
+
this.eventService.emit({
|
|
466
|
+
kind: "save.failed",
|
|
467
|
+
sessionId: session.sessionId,
|
|
468
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
469
|
+
revision: session.revision
|
|
470
|
+
});
|
|
471
|
+
return failure("authoring", startedAt, {
|
|
472
|
+
code: "save_partial_failure",
|
|
473
|
+
message: error instanceof Error ? error.message : String(error),
|
|
474
|
+
sessionId: session.sessionId,
|
|
475
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
476
|
+
attemptedRevision: session.revision,
|
|
477
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
478
|
+
committedPaths,
|
|
479
|
+
failedPaths,
|
|
480
|
+
diskMayBePartiallyUpdated: true
|
|
481
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
482
|
+
sessionId: session.sessionId,
|
|
483
|
+
revision: session.revision
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async materializeSession(input) {
|
|
488
|
+
return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
|
|
489
|
+
}
|
|
490
|
+
async materializeSessionExclusive(input) {
|
|
491
|
+
const startedAt = Date.now();
|
|
492
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
493
|
+
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
494
|
+
if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
|
|
495
|
+
sessionId: session.sessionId,
|
|
496
|
+
revision: session.revision
|
|
497
|
+
});
|
|
498
|
+
const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
|
|
499
|
+
const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
|
|
500
|
+
if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("materializeSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
|
|
501
|
+
sessionId: session.sessionId,
|
|
502
|
+
revision: session.revision
|
|
503
|
+
});
|
|
504
|
+
const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
|
|
505
|
+
const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
|
|
506
|
+
if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
|
|
507
|
+
sessionId: session.sessionId,
|
|
508
|
+
revision: session.revision
|
|
509
|
+
});
|
|
510
|
+
if (storageTarget) {
|
|
511
|
+
const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
|
|
512
|
+
if (holderSessionId && holderSessionId !== session.sessionId) return failure("authoring", startedAt, {
|
|
513
|
+
code: "lock_conflict",
|
|
514
|
+
kind: "in_process_session_exists",
|
|
515
|
+
message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
|
|
516
|
+
canonicalPathKey: storageTarget.canonicalPathKey,
|
|
517
|
+
holderSessionId
|
|
518
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
519
|
+
sessionId: session.sessionId,
|
|
520
|
+
revision: session.revision
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
|
|
524
|
+
sessionId: session.sessionId,
|
|
525
|
+
revision: session.revision
|
|
526
|
+
});
|
|
527
|
+
const diagnostics = validationDiagnostics(session.project);
|
|
528
|
+
if (diagnostics.length > 0) return failure("authoring", startedAt, {
|
|
529
|
+
code: "materialize_validation_failed",
|
|
530
|
+
message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
|
|
531
|
+
sessionId: session.sessionId,
|
|
532
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
533
|
+
issueCount: diagnostics.length,
|
|
534
|
+
diagnostics
|
|
535
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
536
|
+
sessionId: session.sessionId,
|
|
537
|
+
revision: session.revision,
|
|
538
|
+
diagnostics
|
|
539
|
+
});
|
|
540
|
+
let document;
|
|
541
|
+
try {
|
|
542
|
+
document = materializeUamProject(session.project);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
const diagnosticsFromError = [{
|
|
545
|
+
code: "materialize_validation_failed",
|
|
546
|
+
message: error instanceof Error ? error.message : String(error),
|
|
547
|
+
severity: "error",
|
|
548
|
+
operationKind: "materializeSession"
|
|
549
|
+
}];
|
|
550
|
+
return failure("authoring", startedAt, {
|
|
551
|
+
code: "materialize_validation_failed",
|
|
552
|
+
message: error instanceof Error ? error.message : String(error),
|
|
553
|
+
sessionId: session.sessionId,
|
|
554
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
555
|
+
issueCount: diagnosticsFromError.length,
|
|
556
|
+
diagnostics: diagnosticsFromError
|
|
557
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
558
|
+
sessionId: session.sessionId,
|
|
559
|
+
revision: session.revision,
|
|
560
|
+
diagnostics: diagnosticsFromError
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const writtenPaths = [];
|
|
564
|
+
const failedPaths = [];
|
|
565
|
+
const skippedPaths = [];
|
|
566
|
+
this.eventService.emit({
|
|
567
|
+
kind: "save.started",
|
|
568
|
+
sessionId: session.sessionId,
|
|
569
|
+
canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
|
|
570
|
+
revision: session.revision
|
|
571
|
+
});
|
|
572
|
+
try {
|
|
573
|
+
const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
|
|
574
|
+
await writeSessionProject({
|
|
575
|
+
fileSystem,
|
|
576
|
+
document,
|
|
577
|
+
fairyPath,
|
|
578
|
+
staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
|
|
579
|
+
staleResourceFolders: isSessionStorageTarget ? [...session.pendingStaleResourceFolders.values()] : [],
|
|
580
|
+
staleBranchDirectories: isSessionStorageTarget ? [...session.pendingStaleBranchDirectories.values()] : [],
|
|
581
|
+
writtenPaths,
|
|
582
|
+
failedPaths
|
|
583
|
+
});
|
|
584
|
+
if (isSessionStorageTarget) {
|
|
585
|
+
session.pendingStaleSourceFiles.clear();
|
|
586
|
+
session.pendingStaleResourceFolders.clear();
|
|
587
|
+
session.pendingStaleBranchDirectories.clear();
|
|
588
|
+
}
|
|
589
|
+
if (storageTarget && !isSessionStorageTarget) {
|
|
590
|
+
session.pendingStaleSourceFiles.clear();
|
|
591
|
+
session.pendingStaleResourceFolders.clear();
|
|
592
|
+
session.pendingStaleBranchDirectories.clear();
|
|
593
|
+
}
|
|
594
|
+
if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
|
|
595
|
+
if (storageTarget) {
|
|
596
|
+
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
597
|
+
session.fileSystem = storageTarget.fileSystem;
|
|
598
|
+
session.fairyPath = storageTarget.fairyPath;
|
|
599
|
+
session.canonicalProjectPath = storageTarget.canonicalProjectPath;
|
|
600
|
+
session.canonicalPathKey = storageTarget.canonicalPathKey;
|
|
601
|
+
this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
|
|
602
|
+
}
|
|
603
|
+
session.lastSavedRevision = session.revision;
|
|
604
|
+
session.dirty = false;
|
|
605
|
+
const cacheEntry = this.cacheService.refreshSession(session);
|
|
606
|
+
this.eventService.emit({
|
|
607
|
+
kind: "save.completed",
|
|
608
|
+
sessionId: session.sessionId,
|
|
609
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
610
|
+
revision: session.revision
|
|
611
|
+
});
|
|
612
|
+
this.eventService.emit({
|
|
613
|
+
kind: "cache.updated",
|
|
614
|
+
sessionId: session.sessionId,
|
|
615
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
616
|
+
revision: session.revision,
|
|
617
|
+
cacheRevision: cacheEntry.revision
|
|
618
|
+
});
|
|
619
|
+
return success("authoring", startedAt, toMaterializeSnapshot(session, this.context.capabilities, {
|
|
620
|
+
reason: input.reason,
|
|
621
|
+
writtenPaths,
|
|
622
|
+
skippedPaths,
|
|
623
|
+
diagnostics: []
|
|
624
|
+
}), {
|
|
625
|
+
sessionId: session.sessionId,
|
|
626
|
+
revision: session.revision
|
|
627
|
+
});
|
|
628
|
+
} catch (error) {
|
|
629
|
+
const diagnosticsFromError = [{
|
|
630
|
+
code: "write_failed",
|
|
631
|
+
message: error instanceof Error ? error.message : String(error),
|
|
632
|
+
severity: "error",
|
|
633
|
+
path: failedPaths[0],
|
|
634
|
+
operationKind: "materializeSession"
|
|
635
|
+
}];
|
|
636
|
+
this.cacheService.invalidateSession(session);
|
|
637
|
+
this.eventService.emit({
|
|
638
|
+
kind: "save.failed",
|
|
639
|
+
sessionId: session.sessionId,
|
|
640
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
641
|
+
revision: session.revision,
|
|
642
|
+
diagnostics: diagnosticsFromError
|
|
643
|
+
});
|
|
644
|
+
return failure("authoring", startedAt, {
|
|
645
|
+
code: "write_failed",
|
|
646
|
+
message: error instanceof Error ? error.message : String(error),
|
|
647
|
+
sessionId: session.sessionId,
|
|
648
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
649
|
+
attemptedRevision: session.revision,
|
|
650
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
651
|
+
writtenPaths,
|
|
652
|
+
failedPaths,
|
|
653
|
+
skippedPaths,
|
|
654
|
+
diagnostics: diagnosticsFromError,
|
|
655
|
+
diskMayBePartiallyUpdated: true
|
|
656
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
657
|
+
sessionId: session.sessionId,
|
|
658
|
+
revision: session.revision,
|
|
659
|
+
diagnostics: diagnosticsFromError
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/services/cache-service.ts
|
|
666
|
+
function createCacheEntry(session, valid) {
|
|
667
|
+
return {
|
|
668
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
669
|
+
sessionId: session.sessionId,
|
|
670
|
+
revision: session.revision,
|
|
671
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
672
|
+
dirty: session.dirty,
|
|
673
|
+
valid,
|
|
674
|
+
indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
675
|
+
summary: {
|
|
676
|
+
packageCount: session.project.packages.length,
|
|
677
|
+
resourceCount: session.project.packages.reduce((total, pkg) => total + pkg.resources.length, 0),
|
|
678
|
+
diagnostics: []
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
var CacheService = class {
|
|
683
|
+
constructor(context) {
|
|
684
|
+
this.context = context;
|
|
685
|
+
}
|
|
686
|
+
getCacheSnapshot(input) {
|
|
687
|
+
const startedAt = Date.now();
|
|
688
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
689
|
+
if (!session || session.closed) return failure("read", startedAt, createSessionNotFoundError(input.sessionId));
|
|
690
|
+
const entry = this.context.cacheBySession.get(input.sessionId);
|
|
691
|
+
return success("read", startedAt, {
|
|
692
|
+
cacheRevision: entry?.revision ?? session.revision,
|
|
693
|
+
entries: entry ? [structuredClone(entry)] : []
|
|
694
|
+
}, {
|
|
695
|
+
sessionId: session.sessionId,
|
|
696
|
+
revision: session.revision
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
refreshSession(session) {
|
|
700
|
+
const entry = createCacheEntry(session, true);
|
|
701
|
+
this.context.cacheBySession.set(session.sessionId, entry);
|
|
702
|
+
return entry;
|
|
703
|
+
}
|
|
704
|
+
invalidateSession(session) {
|
|
705
|
+
const entry = createCacheEntry(session, false);
|
|
706
|
+
this.context.cacheBySession.set(session.sessionId, entry);
|
|
707
|
+
return entry;
|
|
708
|
+
}
|
|
709
|
+
removeSession(sessionId) {
|
|
710
|
+
this.context.cacheBySession.delete(sessionId);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region src/services/event-service.ts
|
|
715
|
+
const DEFAULT_EVENT_RETENTION_LIMIT = 1e3;
|
|
716
|
+
var EventService = class {
|
|
717
|
+
constructor(context) {
|
|
718
|
+
this.context = context;
|
|
719
|
+
}
|
|
720
|
+
emit(event) {
|
|
721
|
+
const emitted = {
|
|
722
|
+
...event,
|
|
723
|
+
sequence: this.context.nextEventSequence(),
|
|
724
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
725
|
+
diagnostics: event.diagnostics ?? []
|
|
726
|
+
};
|
|
727
|
+
const sessionId = event.sessionId;
|
|
728
|
+
if (!sessionId) return emitted;
|
|
729
|
+
const events = this.context.eventsBySession.get(sessionId) ?? [];
|
|
730
|
+
events.push(emitted);
|
|
731
|
+
while (events.length > DEFAULT_EVENT_RETENTION_LIMIT) events.shift();
|
|
732
|
+
this.context.eventsBySession.set(sessionId, events);
|
|
733
|
+
return emitted;
|
|
734
|
+
}
|
|
735
|
+
getEvents(input) {
|
|
736
|
+
const startedAt = Date.now();
|
|
737
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
738
|
+
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
739
|
+
const events = this.context.eventsBySession.get(input.sessionId) ?? [];
|
|
740
|
+
const oldestSequence = events[0]?.sequence ?? (events.length === 0 ? 1 : 0);
|
|
741
|
+
const currentSequence = events.at(-1)?.sequence ?? 0;
|
|
742
|
+
const after = input.after === void 0 ? 0 : Number(input.after);
|
|
743
|
+
if (!Number.isInteger(after) || after < 0) return failure("runtime", startedAt, {
|
|
744
|
+
code: "event_cursor_invalid",
|
|
745
|
+
message: `Invalid event cursor: ${input.after}`,
|
|
746
|
+
sessionId: input.sessionId,
|
|
747
|
+
after: String(input.after)
|
|
748
|
+
});
|
|
749
|
+
if (events.length > 0 && after !== 0 && after < oldestSequence - 1) return failure("runtime", startedAt, {
|
|
750
|
+
code: "event_cursor_invalid",
|
|
751
|
+
message: `Event cursor has expired: ${after}`,
|
|
752
|
+
sessionId: input.sessionId,
|
|
753
|
+
after: String(input.after)
|
|
754
|
+
});
|
|
755
|
+
if (after > currentSequence) return failure("runtime", startedAt, {
|
|
756
|
+
code: "event_cursor_invalid",
|
|
757
|
+
message: `Unknown event cursor: ${after}`,
|
|
758
|
+
sessionId: input.sessionId,
|
|
759
|
+
after: String(input.after)
|
|
760
|
+
});
|
|
761
|
+
const filtered = events.filter((event) => event.sequence > after);
|
|
762
|
+
const limit = input.limit === void 0 ? filtered.length : Math.max(0, input.limit);
|
|
763
|
+
return success("runtime", startedAt, {
|
|
764
|
+
events: filtered.slice(0, limit).map((event) => structuredClone(event)),
|
|
765
|
+
oldestSequence,
|
|
766
|
+
currentSequence,
|
|
767
|
+
cursorExpired: false
|
|
768
|
+
}, {
|
|
769
|
+
sessionId: session.sessionId,
|
|
770
|
+
revision: session.revision
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
removeSession(sessionId) {
|
|
774
|
+
this.context.eventsBySession.delete(sessionId);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/services/job-service.ts
|
|
779
|
+
const COMPLETED_JOB_RETENTION_LIMIT = 100;
|
|
780
|
+
const REFRESH_START_DELAY_MS = 0;
|
|
781
|
+
const REFRESH_COMPLETE_DELAY_MS = 50;
|
|
782
|
+
function isTerminal(status) {
|
|
783
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
784
|
+
}
|
|
785
|
+
var JobService = class {
|
|
786
|
+
constructor(context, cacheService, eventService) {
|
|
787
|
+
this.context = context;
|
|
788
|
+
this.cacheService = cacheService;
|
|
789
|
+
this.eventService = eventService;
|
|
790
|
+
}
|
|
791
|
+
cancellationRequests = /* @__PURE__ */ new Set();
|
|
792
|
+
getJobs(sessionId) {
|
|
793
|
+
return this.context.jobsBySession.get(sessionId) ?? [];
|
|
794
|
+
}
|
|
795
|
+
setJobs(sessionId, jobs) {
|
|
796
|
+
const retained = [];
|
|
797
|
+
let terminalCount = 0;
|
|
798
|
+
for (let index = jobs.length - 1; index >= 0; index -= 1) {
|
|
799
|
+
const job = jobs[index];
|
|
800
|
+
if (isTerminal(job.status)) {
|
|
801
|
+
if (terminalCount >= COMPLETED_JOB_RETENTION_LIMIT) continue;
|
|
802
|
+
terminalCount += 1;
|
|
803
|
+
}
|
|
804
|
+
retained.push(job);
|
|
805
|
+
}
|
|
806
|
+
this.context.jobsBySession.set(sessionId, retained.reverse());
|
|
807
|
+
}
|
|
808
|
+
refreshCache(input) {
|
|
809
|
+
const startedAt = Date.now();
|
|
810
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
811
|
+
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
812
|
+
const jobId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
813
|
+
const job = {
|
|
814
|
+
jobId,
|
|
815
|
+
kind: "cache.refresh",
|
|
816
|
+
status: "queued",
|
|
817
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
818
|
+
sessionId: session.sessionId,
|
|
819
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
820
|
+
revision: session.revision,
|
|
821
|
+
diagnostics: [],
|
|
822
|
+
progress: {
|
|
823
|
+
completed: 0,
|
|
824
|
+
total: 1,
|
|
825
|
+
message: input.reason ?? "manual"
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
const jobs = this.getJobs(session.sessionId);
|
|
829
|
+
jobs.push(job);
|
|
830
|
+
this.setJobs(session.sessionId, jobs);
|
|
831
|
+
this.eventService.emit({
|
|
832
|
+
kind: "job.created",
|
|
833
|
+
sessionId: session.sessionId,
|
|
834
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
835
|
+
revision: session.revision,
|
|
836
|
+
jobId
|
|
837
|
+
});
|
|
838
|
+
this.scheduleRefreshJob(session.sessionId, jobId);
|
|
839
|
+
return success("runtime", startedAt, structuredClone(job), {
|
|
840
|
+
sessionId: session.sessionId,
|
|
841
|
+
revision: session.revision
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
getJob(input) {
|
|
845
|
+
const startedAt = Date.now();
|
|
846
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
847
|
+
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
848
|
+
const job = this.getJobs(input.sessionId).find((item) => item.jobId === input.jobId);
|
|
849
|
+
if (!job) return failure("runtime", startedAt, {
|
|
850
|
+
code: "job_not_found",
|
|
851
|
+
message: `Job was not found: ${input.jobId}`,
|
|
852
|
+
sessionId: input.sessionId,
|
|
853
|
+
jobId: input.jobId
|
|
854
|
+
}, void 0, {
|
|
855
|
+
sessionId: session.sessionId,
|
|
856
|
+
revision: session.revision
|
|
857
|
+
});
|
|
858
|
+
return success("runtime", startedAt, structuredClone(job), {
|
|
859
|
+
sessionId: session.sessionId,
|
|
860
|
+
revision: session.revision
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
listJobs(input) {
|
|
864
|
+
const startedAt = Date.now();
|
|
865
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
866
|
+
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
867
|
+
let jobs = [...this.getJobs(input.sessionId)];
|
|
868
|
+
if (input.kind) jobs = jobs.filter((job) => job.kind === input.kind);
|
|
869
|
+
if (input.status) if (input.status === "active") jobs = jobs.filter((job) => !isTerminal(job.status));
|
|
870
|
+
else if (input.status === "terminal") jobs = jobs.filter((job) => isTerminal(job.status));
|
|
871
|
+
else jobs = jobs.filter((job) => job.status === input.status);
|
|
872
|
+
if (input.limit !== void 0) jobs = jobs.slice(0, Math.max(0, input.limit));
|
|
873
|
+
return success("runtime", startedAt, { jobs: jobs.map((job) => structuredClone(job)) }, {
|
|
874
|
+
sessionId: session.sessionId,
|
|
875
|
+
revision: session.revision
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
cancelJob(input) {
|
|
879
|
+
const startedAt = Date.now();
|
|
880
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
881
|
+
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
882
|
+
const job = this.getJobs(input.sessionId).find((item) => item.jobId === input.jobId);
|
|
883
|
+
if (!job) return failure("runtime", startedAt, {
|
|
884
|
+
code: "job_not_found",
|
|
885
|
+
message: `Job was not found: ${input.jobId}`,
|
|
886
|
+
sessionId: input.sessionId,
|
|
887
|
+
jobId: input.jobId
|
|
888
|
+
}, void 0, {
|
|
889
|
+
sessionId: session.sessionId,
|
|
890
|
+
revision: session.revision
|
|
891
|
+
});
|
|
892
|
+
if (isTerminal(job.status)) return failure("runtime", startedAt, {
|
|
893
|
+
code: "job_not_cancellable",
|
|
894
|
+
message: `Job is already terminal: ${input.jobId}`,
|
|
895
|
+
sessionId: input.sessionId,
|
|
896
|
+
jobId: input.jobId,
|
|
897
|
+
status: job.status
|
|
898
|
+
}, void 0, {
|
|
899
|
+
sessionId: session.sessionId,
|
|
900
|
+
revision: session.revision
|
|
901
|
+
});
|
|
902
|
+
this.cancellationRequests.add(job.jobId);
|
|
903
|
+
this.eventService.emit({
|
|
904
|
+
kind: "job.cancelRequested",
|
|
905
|
+
sessionId: input.sessionId,
|
|
906
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
907
|
+
revision: session.revision,
|
|
908
|
+
jobId: job.jobId
|
|
909
|
+
});
|
|
910
|
+
const cancelled = this.cancelRefreshJob(session.sessionId, job);
|
|
911
|
+
return success("runtime", startedAt, structuredClone(cancelled), {
|
|
912
|
+
sessionId: session.sessionId,
|
|
913
|
+
revision: session.revision
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
removeSession(sessionId) {
|
|
917
|
+
for (const job of this.getJobs(sessionId)) this.cancellationRequests.delete(job.jobId);
|
|
918
|
+
this.context.jobsBySession.delete(sessionId);
|
|
919
|
+
}
|
|
920
|
+
replaceJob(sessionId, nextJob) {
|
|
921
|
+
const jobs = this.getJobs(sessionId).map((job) => job.jobId === nextJob.jobId ? nextJob : job);
|
|
922
|
+
this.setJobs(sessionId, jobs);
|
|
923
|
+
}
|
|
924
|
+
scheduleRefreshJob(sessionId, jobId) {
|
|
925
|
+
setTimeout(() => this.startRefreshJob(sessionId, jobId), REFRESH_START_DELAY_MS);
|
|
926
|
+
}
|
|
927
|
+
startRefreshJob(sessionId, jobId) {
|
|
928
|
+
const session = this.context.sessions.get(sessionId);
|
|
929
|
+
if (!session || session.closed) return;
|
|
930
|
+
const job = this.getJobs(sessionId).find((item) => item.jobId === jobId);
|
|
931
|
+
if (!job || isTerminal(job.status)) return;
|
|
932
|
+
if (this.cancellationRequests.has(jobId)) {
|
|
933
|
+
this.cancelRefreshJob(sessionId, job);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const running = {
|
|
937
|
+
...job,
|
|
938
|
+
status: "running",
|
|
939
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
940
|
+
progress: {
|
|
941
|
+
completed: 0,
|
|
942
|
+
total: 1,
|
|
943
|
+
message: "refreshing cache"
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
this.replaceJob(sessionId, running);
|
|
947
|
+
this.eventService.emit({
|
|
948
|
+
kind: "job.started",
|
|
949
|
+
sessionId,
|
|
950
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
951
|
+
revision: session.revision,
|
|
952
|
+
jobId
|
|
953
|
+
});
|
|
954
|
+
this.eventService.emit({
|
|
955
|
+
kind: "job.progress",
|
|
956
|
+
sessionId,
|
|
957
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
958
|
+
revision: session.revision,
|
|
959
|
+
jobId,
|
|
960
|
+
payload: running.progress
|
|
961
|
+
});
|
|
962
|
+
setTimeout(() => this.completeRefreshJob(sessionId, jobId), REFRESH_COMPLETE_DELAY_MS);
|
|
963
|
+
}
|
|
964
|
+
completeRefreshJob(sessionId, jobId) {
|
|
965
|
+
const session = this.context.sessions.get(sessionId);
|
|
966
|
+
if (!session || session.closed) return;
|
|
967
|
+
const job = this.getJobs(sessionId).find((item) => item.jobId === jobId);
|
|
968
|
+
if (!job || isTerminal(job.status)) return;
|
|
969
|
+
if (this.cancellationRequests.has(jobId)) {
|
|
970
|
+
this.cancelRefreshJob(sessionId, job);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
const entry = this.cacheService.refreshSession(session);
|
|
975
|
+
const completed = {
|
|
976
|
+
...job,
|
|
977
|
+
status: "completed",
|
|
978
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
979
|
+
cacheRevision: entry.revision,
|
|
980
|
+
progress: {
|
|
981
|
+
completed: 1,
|
|
982
|
+
total: 1,
|
|
983
|
+
message: "cache refreshed"
|
|
984
|
+
},
|
|
985
|
+
result: { cacheRevision: entry.revision }
|
|
986
|
+
};
|
|
987
|
+
this.replaceJob(sessionId, completed);
|
|
988
|
+
this.eventService.emit({
|
|
989
|
+
kind: "job.completed",
|
|
990
|
+
sessionId,
|
|
991
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
992
|
+
revision: session.revision,
|
|
993
|
+
cacheRevision: entry.revision,
|
|
994
|
+
jobId
|
|
995
|
+
});
|
|
996
|
+
this.eventService.emit({
|
|
997
|
+
kind: "cache.updated",
|
|
998
|
+
sessionId,
|
|
999
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
1000
|
+
revision: session.revision,
|
|
1001
|
+
cacheRevision: entry.revision
|
|
1002
|
+
});
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
const failed = {
|
|
1005
|
+
...job,
|
|
1006
|
+
status: "failed",
|
|
1007
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1008
|
+
error: {
|
|
1009
|
+
code: "cache_refresh_failed",
|
|
1010
|
+
message: error instanceof Error ? error.message : "Cache refresh failed",
|
|
1011
|
+
sessionId,
|
|
1012
|
+
jobId
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
this.replaceJob(sessionId, failed);
|
|
1016
|
+
this.eventService.emit({
|
|
1017
|
+
kind: "job.failed",
|
|
1018
|
+
sessionId,
|
|
1019
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
1020
|
+
revision: session.revision,
|
|
1021
|
+
jobId,
|
|
1022
|
+
diagnostics: failed.diagnostics
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
cancelRefreshJob(sessionId, job) {
|
|
1027
|
+
const session = this.context.sessions.get(sessionId);
|
|
1028
|
+
const cancelled = {
|
|
1029
|
+
...job,
|
|
1030
|
+
status: "cancelled",
|
|
1031
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1032
|
+
error: {
|
|
1033
|
+
code: "job_cancelled",
|
|
1034
|
+
message: `Job was cancelled: ${job.jobId}`,
|
|
1035
|
+
sessionId,
|
|
1036
|
+
jobId: job.jobId
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
this.replaceJob(sessionId, cancelled);
|
|
1040
|
+
this.cancellationRequests.delete(job.jobId);
|
|
1041
|
+
this.eventService.emit({
|
|
1042
|
+
kind: "job.cancelled",
|
|
1043
|
+
sessionId,
|
|
1044
|
+
canonicalPathKey: session?.canonicalPathKey ?? job.canonicalPathKey,
|
|
1045
|
+
revision: session?.revision ?? job.revision,
|
|
1046
|
+
jobId: job.jobId
|
|
1047
|
+
});
|
|
1048
|
+
return cancelled;
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/services/read-service.ts
|
|
1053
|
+
var ReadService = class {
|
|
1054
|
+
constructor(context) {
|
|
1055
|
+
this.context = context;
|
|
1056
|
+
}
|
|
1057
|
+
getCapabilities() {
|
|
1058
|
+
return success("read", Date.now(), structuredClone(this.context.capabilities));
|
|
1059
|
+
}
|
|
1060
|
+
getSession(input) {
|
|
1061
|
+
const startedAt = Date.now();
|
|
1062
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
1063
|
+
if (!session || session.closed) return failure("read", startedAt, createSessionNotFoundError(input.sessionId));
|
|
1064
|
+
return success("read", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
1065
|
+
sessionId: session.sessionId,
|
|
1066
|
+
revision: session.revision
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
//#endregion
|
|
1071
|
+
//#region src/services/runtime-service.ts
|
|
1072
|
+
function randomId() {
|
|
1073
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1074
|
+
}
|
|
1075
|
+
function createCapabilityUnavailableError(capability) {
|
|
1076
|
+
const artifactCapability = capability === "artifact.publish" || capability === "artifact.restore";
|
|
1077
|
+
return {
|
|
1078
|
+
code: "capability_unavailable",
|
|
1079
|
+
message: artifactCapability ? `${capability} requires the Node bridge boundary exposed by @openfairygui/backend/node.` : `${capability} requires an injected BackendFileSystem adapter.`,
|
|
1080
|
+
capability,
|
|
1081
|
+
requiredAdapter: capability === "fileSystem" ? "BackendFileSystem" : void 0,
|
|
1082
|
+
requiredHost: artifactCapability ? "node" : void 0,
|
|
1083
|
+
bridgeBoundary: artifactCapability ? "external-bridge" : void 0
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
function createProjectReaderFileSystem(fileSystem) {
|
|
1087
|
+
return {
|
|
1088
|
+
readFile(filePath) {
|
|
1089
|
+
return fileSystem.readFile(filePath);
|
|
1090
|
+
},
|
|
1091
|
+
readFileRaw(filePath) {
|
|
1092
|
+
return fileSystem.readFileRaw(filePath);
|
|
1093
|
+
},
|
|
1094
|
+
writeFile(filePath, content) {
|
|
1095
|
+
return fileSystem.writeFile(filePath, content);
|
|
1096
|
+
},
|
|
1097
|
+
writeFileRaw(filePath, data) {
|
|
1098
|
+
return fileSystem.writeFileRaw(filePath, data);
|
|
1099
|
+
},
|
|
1100
|
+
async mkdir(dirPath) {
|
|
1101
|
+
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
1102
|
+
},
|
|
1103
|
+
readdir(dirPath) {
|
|
1104
|
+
return fileSystem.readdir(dirPath);
|
|
1105
|
+
},
|
|
1106
|
+
async exists(filePath) {
|
|
1107
|
+
try {
|
|
1108
|
+
await fileSystem.stat(filePath);
|
|
1109
|
+
return true;
|
|
1110
|
+
} catch {
|
|
1111
|
+
return false;
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
join(...paths) {
|
|
1115
|
+
return fileSystem.join(...paths);
|
|
1116
|
+
},
|
|
1117
|
+
dirname(filePath) {
|
|
1118
|
+
return fileSystem.dirname(filePath);
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function createCaptureFileSystem(files, directories) {
|
|
1123
|
+
const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
1124
|
+
return {
|
|
1125
|
+
async readFile(filePath) {
|
|
1126
|
+
const value = files.get(normalize(filePath));
|
|
1127
|
+
if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
|
|
1128
|
+
return value;
|
|
1129
|
+
},
|
|
1130
|
+
async readFileRaw(filePath) {
|
|
1131
|
+
const value = files.get(normalize(filePath));
|
|
1132
|
+
if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
|
|
1133
|
+
return value.slice();
|
|
1134
|
+
},
|
|
1135
|
+
async writeFile(filePath, content) {
|
|
1136
|
+
files.set(normalize(filePath), content);
|
|
1137
|
+
},
|
|
1138
|
+
async writeFileRaw(filePath, data) {
|
|
1139
|
+
files.set(normalize(filePath), data.slice());
|
|
1140
|
+
},
|
|
1141
|
+
async mkdir(dirPath) {
|
|
1142
|
+
directories.add(normalize(dirPath));
|
|
1143
|
+
},
|
|
1144
|
+
async readdir() {
|
|
1145
|
+
return [];
|
|
1146
|
+
},
|
|
1147
|
+
async exists(filePath) {
|
|
1148
|
+
return files.has(normalize(filePath));
|
|
1149
|
+
},
|
|
1150
|
+
join(...paths) {
|
|
1151
|
+
return normalize(paths.filter(Boolean).join("/"));
|
|
1152
|
+
},
|
|
1153
|
+
dirname(filePath) {
|
|
1154
|
+
const normalized = normalize(filePath);
|
|
1155
|
+
const separator = normalized.lastIndexOf("/");
|
|
1156
|
+
return separator < 0 ? "" : normalized.slice(0, separator);
|
|
1157
|
+
},
|
|
1158
|
+
async unlink(filePath) {
|
|
1159
|
+
files.delete(normalize(filePath));
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function capturedFilesEqual(left, right) {
|
|
1164
|
+
if (left.size !== right.size) return false;
|
|
1165
|
+
for (const [filePath, leftValue] of left) {
|
|
1166
|
+
const rightValue = right.get(filePath);
|
|
1167
|
+
if (typeof leftValue === "string") {
|
|
1168
|
+
if (leftValue !== rightValue) return false;
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
|
|
1172
|
+
for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
|
|
1173
|
+
}
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
function capturedDirectoriesEqual(left, right) {
|
|
1177
|
+
return left.size === right.size && [...left].every((directory) => right.has(directory));
|
|
1178
|
+
}
|
|
1179
|
+
async function hasFullUamFidelity(document, project) {
|
|
1180
|
+
const sourceFiles = /* @__PURE__ */ new Map();
|
|
1181
|
+
const materializedFiles = /* @__PURE__ */ new Map();
|
|
1182
|
+
const sourceDirectories = /* @__PURE__ */ new Set();
|
|
1183
|
+
const materializedDirectories = /* @__PURE__ */ new Set();
|
|
1184
|
+
try {
|
|
1185
|
+
await Promise.all([new ProjectWriter(createCaptureFileSystem(sourceFiles, sourceDirectories)).write(document, "Project.fairy"), new ProjectWriter(createCaptureFileSystem(materializedFiles, materializedDirectories)).write(materializeUamProject(project), "Project.fairy")]);
|
|
1186
|
+
} catch {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
return capturedFilesEqual(sourceFiles, materializedFiles) && capturedDirectoriesEqual(sourceDirectories, materializedDirectories);
|
|
1190
|
+
}
|
|
1191
|
+
var RuntimeService = class {
|
|
1192
|
+
constructor(context, cacheService, eventService, jobService) {
|
|
1193
|
+
this.context = context;
|
|
1194
|
+
this.cacheService = cacheService;
|
|
1195
|
+
this.eventService = eventService;
|
|
1196
|
+
this.jobService = jobService;
|
|
1197
|
+
}
|
|
1198
|
+
async openSession(input) {
|
|
1199
|
+
const startedAt = Date.now();
|
|
1200
|
+
if (!this.context.fileSystem) return failure("runtime", startedAt, createCapabilityUnavailableError("fileSystem"));
|
|
1201
|
+
const fileSystem = this.context.fileSystem;
|
|
1202
|
+
const { fairyPath, canonicalProjectPath, canonicalPathKey } = await resolveCanonicalProjectRoot(fileSystem, input.projectPath);
|
|
1203
|
+
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
1204
|
+
const lockFilePath = fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
|
|
1205
|
+
if (existingSessionId) return failure("runtime", startedAt, {
|
|
1206
|
+
code: "lock_conflict",
|
|
1207
|
+
kind: "in_process_session_exists",
|
|
1208
|
+
message: `Project is already open in this backend runtime: ${canonicalProjectPath}`,
|
|
1209
|
+
canonicalPathKey,
|
|
1210
|
+
holderSessionId: existingSessionId,
|
|
1211
|
+
lockFilePath
|
|
1212
|
+
});
|
|
1213
|
+
let sessionLock = null;
|
|
1214
|
+
try {
|
|
1215
|
+
sessionLock = await fileSystem.acquireSessionLock(lockFilePath);
|
|
1216
|
+
await sessionLock.writeMetadata(JSON.stringify(this.context.host?.lockMetadata?.({
|
|
1217
|
+
canonicalPathKey,
|
|
1218
|
+
canonicalProjectPath,
|
|
1219
|
+
lockFilePath
|
|
1220
|
+
}) ?? {
|
|
1221
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1222
|
+
canonicalPathKey
|
|
1223
|
+
}));
|
|
1224
|
+
const document = await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
|
|
1225
|
+
const project = liftDocumentToUamProject(document);
|
|
1226
|
+
const sessionId = randomId();
|
|
1227
|
+
const session = {
|
|
1228
|
+
sessionId,
|
|
1229
|
+
fairyPath,
|
|
1230
|
+
canonicalProjectPath,
|
|
1231
|
+
canonicalPathKey,
|
|
1232
|
+
lockFilePath,
|
|
1233
|
+
sessionLock,
|
|
1234
|
+
fileSystem,
|
|
1235
|
+
project,
|
|
1236
|
+
uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
|
|
1237
|
+
revision: 0,
|
|
1238
|
+
lastSavedRevision: 0,
|
|
1239
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
1240
|
+
pendingStaleResourceFolders: /* @__PURE__ */ new Map(),
|
|
1241
|
+
pendingStaleBranchDirectories: /* @__PURE__ */ new Map(),
|
|
1242
|
+
dirty: false,
|
|
1243
|
+
lockHeld: true,
|
|
1244
|
+
closed: false
|
|
1245
|
+
};
|
|
1246
|
+
this.context.sessions.set(sessionId, session);
|
|
1247
|
+
this.context.sessionsByPath.set(canonicalPathKey, sessionId);
|
|
1248
|
+
this.cacheService.refreshSession(session);
|
|
1249
|
+
this.eventService.emit({
|
|
1250
|
+
kind: "session.opened",
|
|
1251
|
+
sessionId,
|
|
1252
|
+
canonicalPathKey,
|
|
1253
|
+
revision: session.revision
|
|
1254
|
+
});
|
|
1255
|
+
return success("runtime", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
1256
|
+
sessionId: session.sessionId,
|
|
1257
|
+
revision: session.revision
|
|
1258
|
+
});
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (sessionLock) await sessionLock.release().catch(() => void 0);
|
|
1261
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") return failure("runtime", startedAt, {
|
|
1262
|
+
code: "lock_conflict",
|
|
1263
|
+
kind: "advisory_lock_conflict",
|
|
1264
|
+
message: `Advisory lock already exists for project: ${canonicalProjectPath}`,
|
|
1265
|
+
canonicalPathKey,
|
|
1266
|
+
lockFilePath
|
|
1267
|
+
});
|
|
1268
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOTSUP") return failure("runtime", startedAt, {
|
|
1269
|
+
...createCapabilityUnavailableError("fileSystem"),
|
|
1270
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1271
|
+
});
|
|
1272
|
+
throw error;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
openProjectSession(input) {
|
|
1276
|
+
const startedAt = Date.now();
|
|
1277
|
+
const sessionId = input.sessionId ?? randomId();
|
|
1278
|
+
const storage = input.storage;
|
|
1279
|
+
const memoryProjectPath = `memory://${sessionId}`;
|
|
1280
|
+
const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
|
|
1281
|
+
const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
|
|
1282
|
+
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
1283
|
+
if (existingSessionId) return failure("runtime", startedAt, {
|
|
1284
|
+
code: "lock_conflict",
|
|
1285
|
+
kind: "in_process_session_exists",
|
|
1286
|
+
message: `Project is already open in this backend runtime: ${canonicalProjectPath}`,
|
|
1287
|
+
canonicalPathKey,
|
|
1288
|
+
holderSessionId: existingSessionId
|
|
1289
|
+
});
|
|
1290
|
+
const session = {
|
|
1291
|
+
sessionId,
|
|
1292
|
+
fairyPath: storage?.fairyPath ?? canonicalProjectPath,
|
|
1293
|
+
canonicalProjectPath,
|
|
1294
|
+
canonicalPathKey,
|
|
1295
|
+
lockFilePath: "",
|
|
1296
|
+
sessionLock: null,
|
|
1297
|
+
fileSystem: storage?.fileSystem,
|
|
1298
|
+
project: normalizeUamProject(input.project),
|
|
1299
|
+
uamFidelity: "full",
|
|
1300
|
+
revision: 0,
|
|
1301
|
+
lastSavedRevision: 0,
|
|
1302
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
1303
|
+
pendingStaleResourceFolders: /* @__PURE__ */ new Map(),
|
|
1304
|
+
pendingStaleBranchDirectories: /* @__PURE__ */ new Map(),
|
|
1305
|
+
dirty: false,
|
|
1306
|
+
lockHeld: false,
|
|
1307
|
+
closed: false
|
|
1308
|
+
};
|
|
1309
|
+
this.context.sessions.set(sessionId, session);
|
|
1310
|
+
this.context.sessionsByPath.set(canonicalPathKey, sessionId);
|
|
1311
|
+
this.cacheService.refreshSession(session);
|
|
1312
|
+
this.eventService.emit({
|
|
1313
|
+
kind: "session.opened",
|
|
1314
|
+
sessionId,
|
|
1315
|
+
canonicalPathKey,
|
|
1316
|
+
revision: session.revision
|
|
1317
|
+
});
|
|
1318
|
+
return success("runtime", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
1319
|
+
sessionId: session.sessionId,
|
|
1320
|
+
revision: session.revision
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
async closeSession(input) {
|
|
1324
|
+
const startedAt = Date.now();
|
|
1325
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
1326
|
+
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
1327
|
+
this.eventService.emit({
|
|
1328
|
+
kind: "session.closeRequested",
|
|
1329
|
+
sessionId: session.sessionId,
|
|
1330
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
1331
|
+
revision: session.revision
|
|
1332
|
+
});
|
|
1333
|
+
await session.sessionLock?.release().catch(() => void 0);
|
|
1334
|
+
session.sessionLock = null;
|
|
1335
|
+
session.lockHeld = false;
|
|
1336
|
+
session.closed = true;
|
|
1337
|
+
this.context.sessions.delete(session.sessionId);
|
|
1338
|
+
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
1339
|
+
this.cacheService.removeSession(session.sessionId);
|
|
1340
|
+
this.jobService.removeSession(session.sessionId);
|
|
1341
|
+
this.eventService.emit({
|
|
1342
|
+
kind: "session.closed",
|
|
1343
|
+
sessionId: session.sessionId,
|
|
1344
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
1345
|
+
revision: session.revision
|
|
1346
|
+
});
|
|
1347
|
+
this.eventService.removeSession(session.sessionId);
|
|
1348
|
+
return success("runtime", startedAt, {
|
|
1349
|
+
sessionId: session.sessionId,
|
|
1350
|
+
closed: true
|
|
1351
|
+
}, {
|
|
1352
|
+
sessionId: session.sessionId,
|
|
1353
|
+
revision: session.revision
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
//#endregion
|
|
1358
|
+
//#region src/services/artifact-service.ts
|
|
1359
|
+
function createArtifactCapabilities() {
|
|
1360
|
+
const bridge = {
|
|
1361
|
+
available: false,
|
|
1362
|
+
requiredHost: "node",
|
|
1363
|
+
executionBoundary: "external-bridge",
|
|
1364
|
+
bridgeEntrypoint: "@openfairygui/backend/node",
|
|
1365
|
+
reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
|
|
1366
|
+
};
|
|
1367
|
+
return {
|
|
1368
|
+
publish: false,
|
|
1369
|
+
restore: false,
|
|
1370
|
+
status: "bridge-required",
|
|
1371
|
+
publishBridge: bridge,
|
|
1372
|
+
restoreBridge: bridge
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
//#endregion
|
|
1376
|
+
//#region src/runtime/capabilities.ts
|
|
1377
|
+
const BACKEND_METHODS = [
|
|
1378
|
+
"getCapabilities",
|
|
1379
|
+
"openSession",
|
|
1380
|
+
"openProjectSession",
|
|
1381
|
+
"getSession",
|
|
1382
|
+
"applyTransaction",
|
|
1383
|
+
"saveSession",
|
|
1384
|
+
"materializeSession",
|
|
1385
|
+
"closeSession",
|
|
1386
|
+
"getEvents",
|
|
1387
|
+
"getJob",
|
|
1388
|
+
"listJobs",
|
|
1389
|
+
"cancelJob",
|
|
1390
|
+
"getCacheSnapshot",
|
|
1391
|
+
"refreshCache"
|
|
1392
|
+
];
|
|
1393
|
+
const ARTIFACT_BRIDGE_CAPABILITY = {
|
|
1394
|
+
available: false,
|
|
1395
|
+
requiredHost: "node",
|
|
1396
|
+
executionBoundary: "external-bridge",
|
|
1397
|
+
bridgeEntrypoint: "@openfairygui/backend/node",
|
|
1398
|
+
reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
|
|
1399
|
+
};
|
|
1400
|
+
function createCapabilities() {
|
|
1401
|
+
return {
|
|
1402
|
+
contractVersion: BACKEND_CONTRACT_VERSION,
|
|
1403
|
+
capabilitySchemaVersion: 2,
|
|
1404
|
+
transactionKernelOwner: "@openfairygui/core",
|
|
1405
|
+
appSeamOwner: "@openfairygui/functions",
|
|
1406
|
+
runtimeOwner: "@openfairygui/backend",
|
|
1407
|
+
methods: BACKEND_METHODS,
|
|
1408
|
+
read: {
|
|
1409
|
+
capabilitySnapshot: true,
|
|
1410
|
+
sessionSnapshot: true
|
|
1411
|
+
},
|
|
1412
|
+
authoring: {
|
|
1413
|
+
applyTransaction: true,
|
|
1414
|
+
saveSession: true,
|
|
1415
|
+
resourceKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.resourceKinds],
|
|
1416
|
+
nodeKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.nodeKinds],
|
|
1417
|
+
gearKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.gearKinds],
|
|
1418
|
+
transactionScope: {
|
|
1419
|
+
resourceKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.resourceKinds],
|
|
1420
|
+
nodeKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.nodeKinds],
|
|
1421
|
+
gearKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.gearKinds]
|
|
1422
|
+
},
|
|
1423
|
+
unsupported: ["artifact.publish", "artifact.restore"]
|
|
1424
|
+
},
|
|
1425
|
+
artifact: createArtifactCapabilities(),
|
|
1426
|
+
manifest: {
|
|
1427
|
+
browserSafe: true,
|
|
1428
|
+
rootEntrypoint: "@openfairygui/backend",
|
|
1429
|
+
nodeEntrypoint: "@openfairygui/backend/node",
|
|
1430
|
+
adapters: {
|
|
1431
|
+
fileSystem: {
|
|
1432
|
+
injected: true,
|
|
1433
|
+
requiredFor: [
|
|
1434
|
+
"openSession",
|
|
1435
|
+
"saveSession",
|
|
1436
|
+
"materializeSession"
|
|
1437
|
+
]
|
|
1438
|
+
},
|
|
1439
|
+
projectStorage: {
|
|
1440
|
+
injected: true,
|
|
1441
|
+
browserSafe: true,
|
|
1442
|
+
requiredFor: [
|
|
1443
|
+
"openProjectSession.writeback",
|
|
1444
|
+
"saveSession",
|
|
1445
|
+
"materializeSession"
|
|
1446
|
+
],
|
|
1447
|
+
adapterFactory: "createBackendStorageFileSystem"
|
|
1448
|
+
},
|
|
1449
|
+
host: {
|
|
1450
|
+
injected: true,
|
|
1451
|
+
requiredFor: ["advisoryLockMetadata"]
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
executionBoundaries: {
|
|
1455
|
+
projectSession: "in-process-browser-safe",
|
|
1456
|
+
fileBackedSession: "adapter-backed",
|
|
1457
|
+
artifactPublish: ARTIFACT_BRIDGE_CAPABILITY,
|
|
1458
|
+
artifactRestore: ARTIFACT_BRIDGE_CAPABILITY
|
|
1459
|
+
},
|
|
1460
|
+
diagnostics: {
|
|
1461
|
+
stableCodes: true,
|
|
1462
|
+
errorDiagnosticMirror: true
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
compatibilityPolicy: BACKEND_COMPATIBILITY_POLICY,
|
|
1466
|
+
runtime: {
|
|
1467
|
+
sessionRuntime: true,
|
|
1468
|
+
advisoryLocking: true,
|
|
1469
|
+
coordinatedSave: true,
|
|
1470
|
+
atomicSave: false,
|
|
1471
|
+
staleRevisionProtection: true,
|
|
1472
|
+
pathPolicy: createRuntimePathPolicy(),
|
|
1473
|
+
events: {
|
|
1474
|
+
polling: true,
|
|
1475
|
+
subscriptions: false,
|
|
1476
|
+
retentionLimit: 1e3,
|
|
1477
|
+
sequenceScope: "runtime"
|
|
1478
|
+
},
|
|
1479
|
+
jobs: {
|
|
1480
|
+
inMemory: true,
|
|
1481
|
+
cooperativeCancel: true,
|
|
1482
|
+
persistent: false,
|
|
1483
|
+
supportedKinds: ["cache.refresh"],
|
|
1484
|
+
artifactJobs: false,
|
|
1485
|
+
completedRetentionLimit: 100
|
|
1486
|
+
},
|
|
1487
|
+
cache: {
|
|
1488
|
+
derivedReadOnly: true,
|
|
1489
|
+
keyedBy: "canonicalPathKey",
|
|
1490
|
+
sourceOfTruth: false,
|
|
1491
|
+
refreshMethod: "refreshCache"
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
//#endregion
|
|
1497
|
+
//#region src/runtime.ts
|
|
1498
|
+
var BackendRuntime = class {
|
|
1499
|
+
fileSystem;
|
|
1500
|
+
capabilities;
|
|
1501
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1502
|
+
sessionsByPath = /* @__PURE__ */ new Map();
|
|
1503
|
+
eventsBySession = /* @__PURE__ */ new Map();
|
|
1504
|
+
jobsBySession = /* @__PURE__ */ new Map();
|
|
1505
|
+
cacheBySession = /* @__PURE__ */ new Map();
|
|
1506
|
+
eventSequence = 0;
|
|
1507
|
+
context;
|
|
1508
|
+
readService;
|
|
1509
|
+
runtimeService;
|
|
1510
|
+
authoringService;
|
|
1511
|
+
cacheService;
|
|
1512
|
+
eventService;
|
|
1513
|
+
jobService;
|
|
1514
|
+
constructor(options = {}) {
|
|
1515
|
+
this.fileSystem = options.fileSystem;
|
|
1516
|
+
this.capabilities = createCapabilities();
|
|
1517
|
+
this.context = {
|
|
1518
|
+
fileSystem: this.fileSystem,
|
|
1519
|
+
host: options.host,
|
|
1520
|
+
capabilities: this.capabilities,
|
|
1521
|
+
sessions: this.sessions,
|
|
1522
|
+
sessionsByPath: this.sessionsByPath,
|
|
1523
|
+
eventsBySession: this.eventsBySession,
|
|
1524
|
+
jobsBySession: this.jobsBySession,
|
|
1525
|
+
cacheBySession: this.cacheBySession,
|
|
1526
|
+
nextEventSequence: () => {
|
|
1527
|
+
this.eventSequence += 1;
|
|
1528
|
+
return this.eventSequence;
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
this.readService = new ReadService(this.context);
|
|
1532
|
+
this.eventService = new EventService(this.context);
|
|
1533
|
+
this.cacheService = new CacheService(this.context);
|
|
1534
|
+
this.jobService = new JobService(this.context, this.cacheService, this.eventService);
|
|
1535
|
+
this.runtimeService = new RuntimeService(this.context, this.cacheService, this.eventService, this.jobService);
|
|
1536
|
+
this.authoringService = new AuthoringService(this.context, this.cacheService, this.eventService);
|
|
1537
|
+
}
|
|
1538
|
+
getCapabilities() {
|
|
1539
|
+
return this.readService.getCapabilities();
|
|
1540
|
+
}
|
|
1541
|
+
async openSession(input) {
|
|
1542
|
+
return this.runtimeService.openSession(input);
|
|
1543
|
+
}
|
|
1544
|
+
openProjectSession(input) {
|
|
1545
|
+
return this.runtimeService.openProjectSession(input);
|
|
1546
|
+
}
|
|
1547
|
+
getSession(input) {
|
|
1548
|
+
return this.readService.getSession(input);
|
|
1549
|
+
}
|
|
1550
|
+
async applyTransaction(input) {
|
|
1551
|
+
return this.authoringService.applyTransaction(input);
|
|
1552
|
+
}
|
|
1553
|
+
async saveSession(input) {
|
|
1554
|
+
return this.authoringService.saveSession(input);
|
|
1555
|
+
}
|
|
1556
|
+
async materializeSession(input) {
|
|
1557
|
+
return this.authoringService.materializeSession(input);
|
|
1558
|
+
}
|
|
1559
|
+
async closeSession(input) {
|
|
1560
|
+
return this.runtimeService.closeSession(input);
|
|
1561
|
+
}
|
|
1562
|
+
getEvents(input) {
|
|
1563
|
+
return this.eventService.getEvents(input);
|
|
1564
|
+
}
|
|
1565
|
+
getJob(input) {
|
|
1566
|
+
return this.jobService.getJob(input);
|
|
1567
|
+
}
|
|
1568
|
+
listJobs(input) {
|
|
1569
|
+
return this.jobService.listJobs(input);
|
|
1570
|
+
}
|
|
1571
|
+
cancelJob(input) {
|
|
1572
|
+
return this.jobService.cancelJob(input);
|
|
1573
|
+
}
|
|
1574
|
+
getCacheSnapshot(input) {
|
|
1575
|
+
return this.cacheService.getCacheSnapshot(input);
|
|
1576
|
+
}
|
|
1577
|
+
refreshCache(input) {
|
|
1578
|
+
return this.jobService.refreshCache(input);
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
//#endregion
|
|
2
1582
|
//#region src/storage.ts
|
|
3
1583
|
var StorageFileStat = class {
|
|
4
1584
|
constructor(kind) {
|
|
@@ -16,6 +1596,42 @@ function createPathError(code, message) {
|
|
|
16
1596
|
error.code = code;
|
|
17
1597
|
return error;
|
|
18
1598
|
}
|
|
1599
|
+
function getWebLockManager() {
|
|
1600
|
+
if (typeof navigator === "undefined") return null;
|
|
1601
|
+
const lockManager = navigator.locks;
|
|
1602
|
+
return lockManager && typeof lockManager.request === "function" ? lockManager : null;
|
|
1603
|
+
}
|
|
1604
|
+
function acquireWebSessionLock(lockManager, lockName) {
|
|
1605
|
+
return new Promise((resolve, reject) => {
|
|
1606
|
+
let releasePlatformLock = () => void 0;
|
|
1607
|
+
const held = new Promise((release) => {
|
|
1608
|
+
releasePlatformLock = release;
|
|
1609
|
+
});
|
|
1610
|
+
lockManager.request(lockName, {
|
|
1611
|
+
mode: "exclusive",
|
|
1612
|
+
ifAvailable: true
|
|
1613
|
+
}, async (lock) => {
|
|
1614
|
+
if (!lock) {
|
|
1615
|
+
reject(createPathError("EEXIST", `Browser session lock is already held: ${lockName}`));
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
let released = false;
|
|
1619
|
+
resolve({
|
|
1620
|
+
writeMetadata() {
|
|
1621
|
+
return Promise.resolve();
|
|
1622
|
+
},
|
|
1623
|
+
release() {
|
|
1624
|
+
if (!released) {
|
|
1625
|
+
released = true;
|
|
1626
|
+
releasePlatformLock();
|
|
1627
|
+
}
|
|
1628
|
+
return Promise.resolve();
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
await held;
|
|
1632
|
+
}).catch(reject);
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
19
1635
|
function normalizeStoragePath(value) {
|
|
20
1636
|
const normalized = value.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
21
1637
|
const absolute = normalized.startsWith("/");
|
|
@@ -70,7 +1686,8 @@ async function inferStat(storage, filePath) {
|
|
|
70
1686
|
}
|
|
71
1687
|
}
|
|
72
1688
|
function createBackendStorageFileSystem(storage) {
|
|
73
|
-
|
|
1689
|
+
if (typeof storage.unlink !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide unlink() for project resource lifecycle writes.");
|
|
1690
|
+
if (typeof storage.rmdir !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide rmdir() for project resource folder lifecycle writes.");
|
|
74
1691
|
const fileSystem = {
|
|
75
1692
|
stat(filePath) {
|
|
76
1693
|
return inferStat(storage, fileSystem.resolve(filePath));
|
|
@@ -106,29 +1723,18 @@ function createBackendStorageFileSystem(storage) {
|
|
|
106
1723
|
const resolved = fileSystem.resolve(filePath);
|
|
107
1724
|
return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
|
|
108
1725
|
},
|
|
109
|
-
async
|
|
110
|
-
const resolved = fileSystem.resolve(
|
|
111
|
-
if (storage.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
async writeFile(content) {
|
|
117
|
-
if (closed) throw createPathError("EBADF", `Storage lock handle is closed: ${resolved}`);
|
|
118
|
-
await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
|
|
119
|
-
await storage.writeFile(resolved, content);
|
|
120
|
-
},
|
|
121
|
-
async close() {
|
|
122
|
-
closed = true;
|
|
123
|
-
lockedPaths.delete(resolved);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
1726
|
+
async acquireSessionLock(lockPath) {
|
|
1727
|
+
const resolved = fileSystem.resolve(lockPath);
|
|
1728
|
+
if (storage.acquireSessionLock) return storage.acquireSessionLock(resolved);
|
|
1729
|
+
const lockManager = getWebLockManager();
|
|
1730
|
+
if (!lockManager) throw createPathError("ENOTSUP", "Browser openSession requires Web Locks or BackendAsyncStorageAdapter.acquireSessionLock().");
|
|
1731
|
+
return acquireWebSessionLock(lockManager, `@openfairygui/backend:${resolved}`);
|
|
126
1732
|
},
|
|
127
1733
|
unlink(filePath) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
1734
|
+
return storage.unlink(fileSystem.resolve(filePath));
|
|
1735
|
+
},
|
|
1736
|
+
rmdir(dirPath) {
|
|
1737
|
+
return storage.rmdir(fileSystem.resolve(dirPath));
|
|
132
1738
|
},
|
|
133
1739
|
join(...paths) {
|
|
134
1740
|
return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
|