@openfairygui/backend 0.2.0-alpha.3 → 0.2.0-alpha.30
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 +48 -2
- package/dist/index.cjs +150 -1
- package/dist/index.d.cts +34 -2
- package/dist/index.d.mts +34 -2
- package/dist/index.mjs +150 -2
- package/dist/node.cjs +4 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.mjs +4 -1
- package/dist/{runtime-IrXqqmko.d.cts → runtime-BQOEw22M.d.mts} +85 -10
- package/dist/{runtime-jKuQmcqM.cjs → runtime-CcBLI-J7.cjs} +523 -86
- package/dist/{runtime-BG5qOVkJ.mjs → runtime-NiBgr7PF.mjs} +524 -87
- package/dist/{runtime-u1Bq_ysz.d.mts → runtime-Wu6vGZ-j.d.cts} +86 -11
- package/package.json +5 -4
- package/src/contracts.ts +8 -0
- package/src/index.ts +25 -12
- package/src/node.ts +3 -0
- package/src/runtime/capabilities.ts +126 -0
- package/src/runtime/contracts.ts +513 -0
- package/src/runtime.ts +67 -551
- package/src/services/authoring-service.ts +559 -80
- package/src/services/context.ts +14 -8
- package/src/services/runtime-service.ts +122 -12
- package/src/services/session-project-writer.ts +67 -0
- package/src/services/session-utils.ts +5 -1
- package/src/storage.ts +201 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require("./node.cjs");
|
|
2
2
|
let _openfairygui_core_uam = require("@openfairygui/core/uam");
|
|
3
|
-
let _openfairygui_core_project_io = require("@openfairygui/core/project-io");
|
|
4
3
|
let _openfairygui_functions_uam = require("@openfairygui/functions/uam");
|
|
4
|
+
let _openfairygui_core_project_io = require("@openfairygui/core/project-io");
|
|
5
5
|
//#region src/contracts.ts
|
|
6
6
|
const BACKEND_CONTRACT_VERSION = "1.1.0-p2";
|
|
7
7
|
const BACKEND_CAPABILITY_SCHEMA_VERSION = 2;
|
|
@@ -76,24 +76,6 @@ async function validateSaveTarget(fileSystem, openedFairyPath, targetPath) {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
//#endregion
|
|
79
|
-
//#region src/services/artifact-service.ts
|
|
80
|
-
function createArtifactCapabilities() {
|
|
81
|
-
const bridge = {
|
|
82
|
-
available: false,
|
|
83
|
-
requiredHost: "node",
|
|
84
|
-
executionBoundary: "external-bridge",
|
|
85
|
-
bridgeEntrypoint: "@openfairygui/backend/node",
|
|
86
|
-
reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
|
|
87
|
-
};
|
|
88
|
-
return {
|
|
89
|
-
publish: false,
|
|
90
|
-
restore: false,
|
|
91
|
-
status: "bridge-required",
|
|
92
|
-
publishBridge: bridge,
|
|
93
|
-
restoreBridge: bridge
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
//#endregion
|
|
97
79
|
//#region src/services/context.ts
|
|
98
80
|
function diagnosticFromError(error) {
|
|
99
81
|
return {
|
|
@@ -138,6 +120,52 @@ function failure(stage, startedAt, error, session, options) {
|
|
|
138
120
|
};
|
|
139
121
|
}
|
|
140
122
|
//#endregion
|
|
123
|
+
//#region src/services/session-project-writer.ts
|
|
124
|
+
function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
|
|
125
|
+
async function trackWrite(targetPath, write) {
|
|
126
|
+
try {
|
|
127
|
+
const result = await write();
|
|
128
|
+
writtenPaths.push(targetPath);
|
|
129
|
+
return result;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
failedPaths.push(targetPath);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
readFile: (path) => fileSystem.readFile(path),
|
|
137
|
+
readFileRaw: (path) => fileSystem.readFileRaw(path),
|
|
138
|
+
writeFile: (path, content) => trackWrite(path, async () => {
|
|
139
|
+
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
140
|
+
await fileSystem.writeFile(path, content);
|
|
141
|
+
}),
|
|
142
|
+
writeFileRaw: (path, data) => trackWrite(path, async () => {
|
|
143
|
+
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
144
|
+
await fileSystem.writeFileRaw(path, data);
|
|
145
|
+
}),
|
|
146
|
+
mkdir: (path) => fileSystem.mkdir(path, { recursive: true }),
|
|
147
|
+
readdir: (path) => fileSystem.readdir(path),
|
|
148
|
+
async exists(path) {
|
|
149
|
+
try {
|
|
150
|
+
await fileSystem.stat(path);
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
join: (...paths) => fileSystem.join(...paths),
|
|
157
|
+
dirname: (path) => fileSystem.dirname(path),
|
|
158
|
+
unlink: (path) => trackWrite(path, () => fileSystem.unlink(path)),
|
|
159
|
+
rmdir: (path) => trackWrite(path, () => fileSystem.rmdir(path))
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function writeSessionProject(input) {
|
|
163
|
+
await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(input.fileSystem, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, {
|
|
164
|
+
staleSourceFiles: input.staleSourceFiles,
|
|
165
|
+
staleResourceFolders: input.staleResourceFolders
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
141
169
|
//#region src/services/snapshot-utils.ts
|
|
142
170
|
function cloneJsonValue(value) {
|
|
143
171
|
if (value === void 0 || value === null) return value;
|
|
@@ -180,6 +208,7 @@ function toSessionSnapshot(session, capabilities) {
|
|
|
180
208
|
revision: session.revision,
|
|
181
209
|
lastSavedRevision: session.lastSavedRevision,
|
|
182
210
|
dirty: session.dirty,
|
|
211
|
+
uamFidelity: session.uamFidelity,
|
|
183
212
|
lockHeld: session.lockHeld,
|
|
184
213
|
capabilities: cloneCapabilitiesSnapshot(capabilities)
|
|
185
214
|
};
|
|
@@ -203,65 +232,161 @@ function createStaleWriteError(session, expectedRevision) {
|
|
|
203
232
|
}
|
|
204
233
|
//#endregion
|
|
205
234
|
//#region src/services/authoring-service.ts
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
235
|
+
function projectSourceFiles(project) {
|
|
236
|
+
const result = /* @__PURE__ */ new Map();
|
|
237
|
+
for (const pkg of project.packages) {
|
|
238
|
+
result.set(`${pkg.id}/package.xml`, {
|
|
239
|
+
packageName: pkg.name,
|
|
240
|
+
branch: "",
|
|
241
|
+
path: "",
|
|
242
|
+
fileName: "package.xml"
|
|
243
|
+
});
|
|
244
|
+
const branches = /* @__PURE__ */ new Set();
|
|
245
|
+
for (const folder of pkg.folders) if (folder.branch) branches.add(folder.branch);
|
|
246
|
+
for (const resource of pkg.resources) {
|
|
247
|
+
if (resource.branch) branches.add(resource.branch);
|
|
248
|
+
const fileName = resource.kind === "component" ? `${resource.name}.xml` : resource.fileName ?? (resource.kind === "image" ? "" : resource.file) ?? "";
|
|
249
|
+
if (!fileName) continue;
|
|
250
|
+
result.set(`${pkg.id}/${resource.id}`, {
|
|
251
|
+
packageName: pkg.name,
|
|
252
|
+
branch: resource.branch,
|
|
253
|
+
path: resource.path,
|
|
254
|
+
fileName
|
|
255
|
+
});
|
|
215
256
|
}
|
|
257
|
+
for (const branch of branches) result.set(`${pkg.id}/branch/${branch}`, {
|
|
258
|
+
packageName: pkg.name,
|
|
259
|
+
branch,
|
|
260
|
+
path: "",
|
|
261
|
+
fileName: "package_branch.xml"
|
|
262
|
+
});
|
|
216
263
|
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
function sourceFileKey(source) {
|
|
267
|
+
return [
|
|
268
|
+
source.branch,
|
|
269
|
+
source.packageName,
|
|
270
|
+
source.path,
|
|
271
|
+
source.fileName
|
|
272
|
+
].join("\0");
|
|
273
|
+
}
|
|
274
|
+
function projectResourceFolders(project) {
|
|
275
|
+
const result = /* @__PURE__ */ new Map();
|
|
276
|
+
for (const pkg of project.packages) for (const folder of pkg.folders) result.set(`${pkg.id}/${folder.branch}/${folder.path}`, {
|
|
277
|
+
packageName: pkg.name,
|
|
278
|
+
branch: folder.branch,
|
|
279
|
+
path: folder.path
|
|
280
|
+
});
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function resourceFolderKey(folder) {
|
|
284
|
+
return [
|
|
285
|
+
folder.branch,
|
|
286
|
+
folder.packageName,
|
|
287
|
+
folder.path
|
|
288
|
+
].join("\0");
|
|
289
|
+
}
|
|
290
|
+
function recordStaleProjectFiles(session, previousProject, nextProject) {
|
|
291
|
+
if (!session.fileSystem) return;
|
|
292
|
+
const previousSources = projectSourceFiles(previousProject);
|
|
293
|
+
const nextSourceKeys = new Set([...projectSourceFiles(nextProject).values()].map(sourceFileKey));
|
|
294
|
+
for (const source of previousSources.values()) {
|
|
295
|
+
const key = sourceFileKey(source);
|
|
296
|
+
if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
|
|
297
|
+
}
|
|
298
|
+
for (const key of nextSourceKeys) session.pendingStaleSourceFiles.delete(key);
|
|
299
|
+
const previousFolders = projectResourceFolders(previousProject);
|
|
300
|
+
const nextFolderKeys = new Set([...projectResourceFolders(nextProject).values()].map(resourceFolderKey));
|
|
301
|
+
for (const folder of previousFolders.values()) {
|
|
302
|
+
const key = resourceFolderKey(folder);
|
|
303
|
+
if (!nextFolderKeys.has(key)) session.pendingStaleResourceFolders.set(key, folder);
|
|
304
|
+
}
|
|
305
|
+
for (const key of nextFolderKeys) session.pendingStaleResourceFolders.delete(key);
|
|
306
|
+
}
|
|
307
|
+
function toBackendDiagnostics(error) {
|
|
308
|
+
return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
|
|
309
|
+
code: error.code,
|
|
310
|
+
message: error.message,
|
|
311
|
+
severity: "error",
|
|
312
|
+
operationKind: error.operationKind,
|
|
313
|
+
opIndex: error.opIndex,
|
|
314
|
+
opId: error.opId
|
|
315
|
+
}];
|
|
316
|
+
}
|
|
317
|
+
function createCapabilityUnavailableError$1(message) {
|
|
217
318
|
return {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
319
|
+
code: "capability_unavailable",
|
|
320
|
+
message,
|
|
321
|
+
capability: "fileSystem",
|
|
322
|
+
requiredAdapter: "BackendFileSystem"
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function createUamFidelityUnsupportedError(session) {
|
|
326
|
+
return {
|
|
327
|
+
code: "uam_fidelity_unsupported",
|
|
328
|
+
message: "The source project contains formal properties that the current UAM cannot preserve.",
|
|
329
|
+
sessionId: session.sessionId,
|
|
330
|
+
canonicalPathKey: session.canonicalPathKey
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function validationDiagnostics(sessionProject) {
|
|
334
|
+
return (0, _openfairygui_core_uam.validateUamProject)(sessionProject).map((issue) => ({
|
|
335
|
+
code: "materialize_validation_failed",
|
|
336
|
+
message: issue.message,
|
|
337
|
+
severity: "error",
|
|
338
|
+
path: issue.path,
|
|
339
|
+
operationKind: "materializeSession"
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
function toMaterializeSnapshot(session, capabilities, input) {
|
|
343
|
+
return {
|
|
344
|
+
...toSessionSnapshot(session, capabilities),
|
|
345
|
+
mode: "fullProject",
|
|
346
|
+
reason: input.reason,
|
|
347
|
+
materializeRevision: session.revision,
|
|
348
|
+
saveRevision: session.lastSavedRevision,
|
|
349
|
+
writtenPaths: [...input.writtenPaths],
|
|
350
|
+
skippedPaths: [...input.skippedPaths],
|
|
351
|
+
diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function storageCanonicalTarget(input) {
|
|
355
|
+
const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || ".");
|
|
356
|
+
return {
|
|
357
|
+
fileSystem: input.fileSystem,
|
|
358
|
+
fairyPath: input.fairyPath,
|
|
359
|
+
canonicalProjectPath,
|
|
360
|
+
canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath)
|
|
256
361
|
};
|
|
257
362
|
}
|
|
258
363
|
var AuthoringService = class {
|
|
364
|
+
sessionOperations = /* @__PURE__ */ new Map();
|
|
259
365
|
constructor(context, cacheService, eventService) {
|
|
260
366
|
this.context = context;
|
|
261
367
|
this.cacheService = cacheService;
|
|
262
368
|
this.eventService = eventService;
|
|
263
369
|
}
|
|
370
|
+
async runSessionExclusive(sessionId, operation) {
|
|
371
|
+
const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
|
|
372
|
+
let release = () => void 0;
|
|
373
|
+
const current = new Promise((resolve) => {
|
|
374
|
+
release = resolve;
|
|
375
|
+
});
|
|
376
|
+
const tail = previous.then(() => current);
|
|
377
|
+
this.sessionOperations.set(sessionId, tail);
|
|
378
|
+
await previous;
|
|
379
|
+
try {
|
|
380
|
+
return await operation();
|
|
381
|
+
} finally {
|
|
382
|
+
release();
|
|
383
|
+
if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
264
386
|
async applyTransaction(input) {
|
|
387
|
+
return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
|
|
388
|
+
}
|
|
389
|
+
async applyTransactionExclusive(input) {
|
|
265
390
|
const startedAt = Date.now();
|
|
266
391
|
const session = this.context.sessions.get(input.sessionId);
|
|
267
392
|
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
@@ -282,22 +407,21 @@ var AuthoringService = class {
|
|
|
282
407
|
operations: input.operations
|
|
283
408
|
});
|
|
284
409
|
if (result.ok === false) {
|
|
410
|
+
const diagnostics = toBackendDiagnostics(result.error);
|
|
285
411
|
this.eventService.emit({
|
|
286
412
|
kind: "transaction.rejected",
|
|
287
413
|
sessionId: session.sessionId,
|
|
288
414
|
canonicalPathKey: session.canonicalPathKey,
|
|
289
415
|
revision: session.revision,
|
|
290
|
-
diagnostics
|
|
291
|
-
code: result.error.code,
|
|
292
|
-
message: issue.message,
|
|
293
|
-
severity: "error"
|
|
294
|
-
})) ?? []
|
|
416
|
+
diagnostics
|
|
295
417
|
});
|
|
296
418
|
return failure("authoring", startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
|
|
297
419
|
sessionId: session.sessionId,
|
|
298
|
-
revision: session.revision
|
|
420
|
+
revision: session.revision,
|
|
421
|
+
diagnostics
|
|
299
422
|
});
|
|
300
423
|
}
|
|
424
|
+
recordStaleProjectFiles(session, session.project, result.project);
|
|
301
425
|
session.project = result.project;
|
|
302
426
|
session.revision += 1;
|
|
303
427
|
session.dirty = true;
|
|
@@ -321,15 +445,22 @@ var AuthoringService = class {
|
|
|
321
445
|
});
|
|
322
446
|
}
|
|
323
447
|
async saveSession(input) {
|
|
448
|
+
if (input.force === true || input.mode === "materializeCleanSession") return this.materializeSession({
|
|
449
|
+
sessionId: input.sessionId,
|
|
450
|
+
expectedRevision: input.expectedRevision,
|
|
451
|
+
targetPath: input.targetPath,
|
|
452
|
+
fileSystem: input.fileSystem,
|
|
453
|
+
mode: "fullProject",
|
|
454
|
+
reason: "force_save"
|
|
455
|
+
});
|
|
456
|
+
return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
|
|
457
|
+
}
|
|
458
|
+
async saveSessionExclusive(input) {
|
|
324
459
|
const startedAt = Date.now();
|
|
325
460
|
const session = this.context.sessions.get(input.sessionId);
|
|
326
461
|
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
message: "saveSession requires an injected BackendFileSystem adapter.",
|
|
330
|
-
capability: "fileSystem",
|
|
331
|
-
requiredAdapter: "BackendFileSystem"
|
|
332
|
-
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
462
|
+
const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
|
|
463
|
+
if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
|
|
333
464
|
sessionId: session.sessionId,
|
|
334
465
|
revision: session.revision
|
|
335
466
|
});
|
|
@@ -337,7 +468,6 @@ var AuthoringService = class {
|
|
|
337
468
|
sessionId: session.sessionId,
|
|
338
469
|
revision: session.revision
|
|
339
470
|
});
|
|
340
|
-
const fileSystem = this.context.fileSystem;
|
|
341
471
|
const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
|
|
342
472
|
if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
|
|
343
473
|
sessionId: session.sessionId,
|
|
@@ -347,6 +477,10 @@ var AuthoringService = class {
|
|
|
347
477
|
sessionId: session.sessionId,
|
|
348
478
|
revision: session.revision
|
|
349
479
|
});
|
|
480
|
+
if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
|
|
481
|
+
sessionId: session.sessionId,
|
|
482
|
+
revision: session.revision
|
|
483
|
+
});
|
|
350
484
|
const committedPaths = [];
|
|
351
485
|
const failedPaths = [];
|
|
352
486
|
this.eventService.emit({
|
|
@@ -356,7 +490,19 @@ var AuthoringService = class {
|
|
|
356
490
|
revision: session.revision
|
|
357
491
|
});
|
|
358
492
|
try {
|
|
359
|
-
await
|
|
493
|
+
await writeSessionProject({
|
|
494
|
+
fileSystem,
|
|
495
|
+
document: (0, _openfairygui_core_uam.materializeUamProject)(session.project),
|
|
496
|
+
fairyPath: session.fairyPath,
|
|
497
|
+
staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
|
|
498
|
+
staleResourceFolders: [...session.pendingStaleResourceFolders.values()],
|
|
499
|
+
writtenPaths: committedPaths,
|
|
500
|
+
failedPaths
|
|
501
|
+
});
|
|
502
|
+
session.fileSystem ??= fileSystem;
|
|
503
|
+
session.pendingStaleSourceFiles.clear();
|
|
504
|
+
session.pendingStaleResourceFolders.clear();
|
|
505
|
+
(0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
|
|
360
506
|
session.lastSavedRevision = session.revision;
|
|
361
507
|
session.dirty = false;
|
|
362
508
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
@@ -401,6 +547,179 @@ var AuthoringService = class {
|
|
|
401
547
|
});
|
|
402
548
|
}
|
|
403
549
|
}
|
|
550
|
+
async materializeSession(input) {
|
|
551
|
+
return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
|
|
552
|
+
}
|
|
553
|
+
async materializeSessionExclusive(input) {
|
|
554
|
+
const startedAt = Date.now();
|
|
555
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
556
|
+
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
557
|
+
if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
|
|
558
|
+
sessionId: session.sessionId,
|
|
559
|
+
revision: session.revision
|
|
560
|
+
});
|
|
561
|
+
const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
|
|
562
|
+
const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
|
|
563
|
+
if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("materializeSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
|
|
564
|
+
sessionId: session.sessionId,
|
|
565
|
+
revision: session.revision
|
|
566
|
+
});
|
|
567
|
+
const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
|
|
568
|
+
const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
|
|
569
|
+
if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
|
|
570
|
+
sessionId: session.sessionId,
|
|
571
|
+
revision: session.revision
|
|
572
|
+
});
|
|
573
|
+
if (storageTarget) {
|
|
574
|
+
const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
|
|
575
|
+
if (holderSessionId && holderSessionId !== session.sessionId) return failure("authoring", startedAt, {
|
|
576
|
+
code: "lock_conflict",
|
|
577
|
+
kind: "in_process_session_exists",
|
|
578
|
+
message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
|
|
579
|
+
canonicalPathKey: storageTarget.canonicalPathKey,
|
|
580
|
+
holderSessionId
|
|
581
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
582
|
+
sessionId: session.sessionId,
|
|
583
|
+
revision: session.revision
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
|
|
587
|
+
sessionId: session.sessionId,
|
|
588
|
+
revision: session.revision
|
|
589
|
+
});
|
|
590
|
+
const diagnostics = validationDiagnostics(session.project);
|
|
591
|
+
if (diagnostics.length > 0) return failure("authoring", startedAt, {
|
|
592
|
+
code: "materialize_validation_failed",
|
|
593
|
+
message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
|
|
594
|
+
sessionId: session.sessionId,
|
|
595
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
596
|
+
issueCount: diagnostics.length,
|
|
597
|
+
diagnostics
|
|
598
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
599
|
+
sessionId: session.sessionId,
|
|
600
|
+
revision: session.revision,
|
|
601
|
+
diagnostics
|
|
602
|
+
});
|
|
603
|
+
let document;
|
|
604
|
+
try {
|
|
605
|
+
document = (0, _openfairygui_core_uam.materializeUamProject)(session.project);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
const diagnosticsFromError = [{
|
|
608
|
+
code: "materialize_validation_failed",
|
|
609
|
+
message: error instanceof Error ? error.message : String(error),
|
|
610
|
+
severity: "error",
|
|
611
|
+
operationKind: "materializeSession"
|
|
612
|
+
}];
|
|
613
|
+
return failure("authoring", startedAt, {
|
|
614
|
+
code: "materialize_validation_failed",
|
|
615
|
+
message: error instanceof Error ? error.message : String(error),
|
|
616
|
+
sessionId: session.sessionId,
|
|
617
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
618
|
+
issueCount: diagnosticsFromError.length,
|
|
619
|
+
diagnostics: diagnosticsFromError
|
|
620
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
621
|
+
sessionId: session.sessionId,
|
|
622
|
+
revision: session.revision,
|
|
623
|
+
diagnostics: diagnosticsFromError
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
const writtenPaths = [];
|
|
627
|
+
const failedPaths = [];
|
|
628
|
+
const skippedPaths = [];
|
|
629
|
+
this.eventService.emit({
|
|
630
|
+
kind: "save.started",
|
|
631
|
+
sessionId: session.sessionId,
|
|
632
|
+
canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
|
|
633
|
+
revision: session.revision
|
|
634
|
+
});
|
|
635
|
+
try {
|
|
636
|
+
const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
|
|
637
|
+
await writeSessionProject({
|
|
638
|
+
fileSystem,
|
|
639
|
+
document,
|
|
640
|
+
fairyPath,
|
|
641
|
+
staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
|
|
642
|
+
staleResourceFolders: isSessionStorageTarget ? [...session.pendingStaleResourceFolders.values()] : [],
|
|
643
|
+
writtenPaths,
|
|
644
|
+
failedPaths
|
|
645
|
+
});
|
|
646
|
+
if (isSessionStorageTarget) {
|
|
647
|
+
session.pendingStaleSourceFiles.clear();
|
|
648
|
+
session.pendingStaleResourceFolders.clear();
|
|
649
|
+
}
|
|
650
|
+
if (storageTarget && !isSessionStorageTarget) {
|
|
651
|
+
session.pendingStaleSourceFiles.clear();
|
|
652
|
+
session.pendingStaleResourceFolders.clear();
|
|
653
|
+
}
|
|
654
|
+
if (isSessionStorageTarget || storageTarget) (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
|
|
655
|
+
if (storageTarget) {
|
|
656
|
+
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
657
|
+
session.fileSystem = storageTarget.fileSystem;
|
|
658
|
+
session.fairyPath = storageTarget.fairyPath;
|
|
659
|
+
session.canonicalProjectPath = storageTarget.canonicalProjectPath;
|
|
660
|
+
session.canonicalPathKey = storageTarget.canonicalPathKey;
|
|
661
|
+
this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
|
|
662
|
+
}
|
|
663
|
+
session.lastSavedRevision = session.revision;
|
|
664
|
+
session.dirty = false;
|
|
665
|
+
const cacheEntry = this.cacheService.refreshSession(session);
|
|
666
|
+
this.eventService.emit({
|
|
667
|
+
kind: "save.completed",
|
|
668
|
+
sessionId: session.sessionId,
|
|
669
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
670
|
+
revision: session.revision
|
|
671
|
+
});
|
|
672
|
+
this.eventService.emit({
|
|
673
|
+
kind: "cache.updated",
|
|
674
|
+
sessionId: session.sessionId,
|
|
675
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
676
|
+
revision: session.revision,
|
|
677
|
+
cacheRevision: cacheEntry.revision
|
|
678
|
+
});
|
|
679
|
+
return success("authoring", startedAt, toMaterializeSnapshot(session, this.context.capabilities, {
|
|
680
|
+
reason: input.reason,
|
|
681
|
+
writtenPaths,
|
|
682
|
+
skippedPaths,
|
|
683
|
+
diagnostics: []
|
|
684
|
+
}), {
|
|
685
|
+
sessionId: session.sessionId,
|
|
686
|
+
revision: session.revision
|
|
687
|
+
});
|
|
688
|
+
} catch (error) {
|
|
689
|
+
const diagnosticsFromError = [{
|
|
690
|
+
code: "write_failed",
|
|
691
|
+
message: error instanceof Error ? error.message : String(error),
|
|
692
|
+
severity: "error",
|
|
693
|
+
path: failedPaths[0],
|
|
694
|
+
operationKind: "materializeSession"
|
|
695
|
+
}];
|
|
696
|
+
this.cacheService.invalidateSession(session);
|
|
697
|
+
this.eventService.emit({
|
|
698
|
+
kind: "save.failed",
|
|
699
|
+
sessionId: session.sessionId,
|
|
700
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
701
|
+
revision: session.revision,
|
|
702
|
+
diagnostics: diagnosticsFromError
|
|
703
|
+
});
|
|
704
|
+
return failure("authoring", startedAt, {
|
|
705
|
+
code: "write_failed",
|
|
706
|
+
message: error instanceof Error ? error.message : String(error),
|
|
707
|
+
sessionId: session.sessionId,
|
|
708
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
709
|
+
attemptedRevision: session.revision,
|
|
710
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
711
|
+
writtenPaths,
|
|
712
|
+
failedPaths,
|
|
713
|
+
skippedPaths,
|
|
714
|
+
diagnostics: diagnosticsFromError,
|
|
715
|
+
diskMayBePartiallyUpdated: true
|
|
716
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
717
|
+
sessionId: session.sessionId,
|
|
718
|
+
revision: session.revision,
|
|
719
|
+
diagnostics: diagnosticsFromError
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
404
723
|
};
|
|
405
724
|
//#endregion
|
|
406
725
|
//#region src/services/cache-service.ts
|
|
@@ -859,6 +1178,75 @@ function createProjectReaderFileSystem(fileSystem) {
|
|
|
859
1178
|
}
|
|
860
1179
|
};
|
|
861
1180
|
}
|
|
1181
|
+
function createCaptureFileSystem(files, directories) {
|
|
1182
|
+
const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
1183
|
+
return {
|
|
1184
|
+
async readFile(filePath) {
|
|
1185
|
+
const value = files.get(normalize(filePath));
|
|
1186
|
+
if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
|
|
1187
|
+
return value;
|
|
1188
|
+
},
|
|
1189
|
+
async readFileRaw(filePath) {
|
|
1190
|
+
const value = files.get(normalize(filePath));
|
|
1191
|
+
if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
|
|
1192
|
+
return value.slice();
|
|
1193
|
+
},
|
|
1194
|
+
async writeFile(filePath, content) {
|
|
1195
|
+
files.set(normalize(filePath), content);
|
|
1196
|
+
},
|
|
1197
|
+
async writeFileRaw(filePath, data) {
|
|
1198
|
+
files.set(normalize(filePath), data.slice());
|
|
1199
|
+
},
|
|
1200
|
+
async mkdir(dirPath) {
|
|
1201
|
+
directories.add(normalize(dirPath));
|
|
1202
|
+
},
|
|
1203
|
+
async readdir() {
|
|
1204
|
+
return [];
|
|
1205
|
+
},
|
|
1206
|
+
async exists(filePath) {
|
|
1207
|
+
return files.has(normalize(filePath));
|
|
1208
|
+
},
|
|
1209
|
+
join(...paths) {
|
|
1210
|
+
return normalize(paths.filter(Boolean).join("/"));
|
|
1211
|
+
},
|
|
1212
|
+
dirname(filePath) {
|
|
1213
|
+
const normalized = normalize(filePath);
|
|
1214
|
+
const separator = normalized.lastIndexOf("/");
|
|
1215
|
+
return separator < 0 ? "" : normalized.slice(0, separator);
|
|
1216
|
+
},
|
|
1217
|
+
async unlink(filePath) {
|
|
1218
|
+
files.delete(normalize(filePath));
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
function capturedFilesEqual(left, right) {
|
|
1223
|
+
if (left.size !== right.size) return false;
|
|
1224
|
+
for (const [filePath, leftValue] of left) {
|
|
1225
|
+
const rightValue = right.get(filePath);
|
|
1226
|
+
if (typeof leftValue === "string") {
|
|
1227
|
+
if (leftValue !== rightValue) return false;
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
|
|
1231
|
+
for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
|
|
1232
|
+
}
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
function capturedDirectoriesEqual(left, right) {
|
|
1236
|
+
return left.size === right.size && [...left].every((directory) => right.has(directory));
|
|
1237
|
+
}
|
|
1238
|
+
async function hasFullUamFidelity(document, project) {
|
|
1239
|
+
const sourceFiles = /* @__PURE__ */ new Map();
|
|
1240
|
+
const materializedFiles = /* @__PURE__ */ new Map();
|
|
1241
|
+
const sourceDirectories = /* @__PURE__ */ new Set();
|
|
1242
|
+
const materializedDirectories = /* @__PURE__ */ new Set();
|
|
1243
|
+
try {
|
|
1244
|
+
await Promise.all([new _openfairygui_core_project_io.ProjectWriter(createCaptureFileSystem(sourceFiles, sourceDirectories)).write(document, "Project.fairy"), new _openfairygui_core_project_io.ProjectWriter(createCaptureFileSystem(materializedFiles, materializedDirectories)).write((0, _openfairygui_core_uam.materializeUamProject)(project), "Project.fairy")]);
|
|
1245
|
+
} catch {
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
return capturedFilesEqual(sourceFiles, materializedFiles) && capturedDirectoriesEqual(sourceDirectories, materializedDirectories);
|
|
1249
|
+
}
|
|
862
1250
|
var RuntimeService = class {
|
|
863
1251
|
constructor(context, cacheService, eventService, jobService) {
|
|
864
1252
|
this.context = context;
|
|
@@ -893,7 +1281,8 @@ var RuntimeService = class {
|
|
|
893
1281
|
canonicalPathKey
|
|
894
1282
|
}));
|
|
895
1283
|
await advisoryLock.close();
|
|
896
|
-
const
|
|
1284
|
+
const document = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
|
|
1285
|
+
const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
|
|
897
1286
|
const sessionId = randomId();
|
|
898
1287
|
const session = {
|
|
899
1288
|
sessionId,
|
|
@@ -901,9 +1290,13 @@ var RuntimeService = class {
|
|
|
901
1290
|
canonicalProjectPath,
|
|
902
1291
|
canonicalPathKey,
|
|
903
1292
|
lockFilePath,
|
|
1293
|
+
fileSystem,
|
|
904
1294
|
project,
|
|
1295
|
+
uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
|
|
905
1296
|
revision: 0,
|
|
906
1297
|
lastSavedRevision: 0,
|
|
1298
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
1299
|
+
pendingStaleResourceFolders: /* @__PURE__ */ new Map(),
|
|
907
1300
|
dirty: false,
|
|
908
1301
|
lockHeld: true,
|
|
909
1302
|
closed: false
|
|
@@ -939,8 +1332,10 @@ var RuntimeService = class {
|
|
|
939
1332
|
openProjectSession(input) {
|
|
940
1333
|
const startedAt = Date.now();
|
|
941
1334
|
const sessionId = input.sessionId ?? randomId();
|
|
942
|
-
const
|
|
943
|
-
const
|
|
1335
|
+
const storage = input.storage;
|
|
1336
|
+
const memoryProjectPath = `memory://${sessionId}`;
|
|
1337
|
+
const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
|
|
1338
|
+
const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
|
|
944
1339
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
945
1340
|
if (existingSessionId) return failure("runtime", startedAt, {
|
|
946
1341
|
code: "lock_conflict",
|
|
@@ -951,13 +1346,17 @@ var RuntimeService = class {
|
|
|
951
1346
|
});
|
|
952
1347
|
const session = {
|
|
953
1348
|
sessionId,
|
|
954
|
-
fairyPath: canonicalProjectPath,
|
|
1349
|
+
fairyPath: storage?.fairyPath ?? canonicalProjectPath,
|
|
955
1350
|
canonicalProjectPath,
|
|
956
1351
|
canonicalPathKey,
|
|
957
1352
|
lockFilePath: "",
|
|
1353
|
+
fileSystem: storage?.fileSystem,
|
|
958
1354
|
project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
|
|
1355
|
+
uamFidelity: "full",
|
|
959
1356
|
revision: 0,
|
|
960
1357
|
lastSavedRevision: 0,
|
|
1358
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
1359
|
+
pendingStaleResourceFolders: /* @__PURE__ */ new Map(),
|
|
961
1360
|
dirty: false,
|
|
962
1361
|
lockHeld: false,
|
|
963
1362
|
closed: false
|
|
@@ -1010,7 +1409,25 @@ var RuntimeService = class {
|
|
|
1010
1409
|
}
|
|
1011
1410
|
};
|
|
1012
1411
|
//#endregion
|
|
1013
|
-
//#region src/
|
|
1412
|
+
//#region src/services/artifact-service.ts
|
|
1413
|
+
function createArtifactCapabilities() {
|
|
1414
|
+
const bridge = {
|
|
1415
|
+
available: false,
|
|
1416
|
+
requiredHost: "node",
|
|
1417
|
+
executionBoundary: "external-bridge",
|
|
1418
|
+
bridgeEntrypoint: "@openfairygui/backend/node",
|
|
1419
|
+
reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
|
|
1420
|
+
};
|
|
1421
|
+
return {
|
|
1422
|
+
publish: false,
|
|
1423
|
+
restore: false,
|
|
1424
|
+
status: "bridge-required",
|
|
1425
|
+
publishBridge: bridge,
|
|
1426
|
+
restoreBridge: bridge
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
//#endregion
|
|
1430
|
+
//#region src/runtime/capabilities.ts
|
|
1014
1431
|
const BACKEND_METHODS = [
|
|
1015
1432
|
"getCapabilities",
|
|
1016
1433
|
"openSession",
|
|
@@ -1018,6 +1435,7 @@ const BACKEND_METHODS = [
|
|
|
1018
1435
|
"getSession",
|
|
1019
1436
|
"applyTransaction",
|
|
1020
1437
|
"saveSession",
|
|
1438
|
+
"materializeSession",
|
|
1021
1439
|
"closeSession",
|
|
1022
1440
|
"getEvents",
|
|
1023
1441
|
"getJob",
|
|
@@ -1066,7 +1484,21 @@ function createCapabilities() {
|
|
|
1066
1484
|
adapters: {
|
|
1067
1485
|
fileSystem: {
|
|
1068
1486
|
injected: true,
|
|
1069
|
-
requiredFor: [
|
|
1487
|
+
requiredFor: [
|
|
1488
|
+
"openSession",
|
|
1489
|
+
"saveSession",
|
|
1490
|
+
"materializeSession"
|
|
1491
|
+
]
|
|
1492
|
+
},
|
|
1493
|
+
projectStorage: {
|
|
1494
|
+
injected: true,
|
|
1495
|
+
browserSafe: true,
|
|
1496
|
+
requiredFor: [
|
|
1497
|
+
"openProjectSession.writeback",
|
|
1498
|
+
"saveSession",
|
|
1499
|
+
"materializeSession"
|
|
1500
|
+
],
|
|
1501
|
+
adapterFactory: "createBackendStorageFileSystem"
|
|
1070
1502
|
},
|
|
1071
1503
|
host: {
|
|
1072
1504
|
injected: true,
|
|
@@ -1115,6 +1547,8 @@ function createCapabilities() {
|
|
|
1115
1547
|
}
|
|
1116
1548
|
};
|
|
1117
1549
|
}
|
|
1550
|
+
//#endregion
|
|
1551
|
+
//#region src/runtime.ts
|
|
1118
1552
|
var BackendRuntime = class {
|
|
1119
1553
|
fileSystem;
|
|
1120
1554
|
capabilities;
|
|
@@ -1173,6 +1607,9 @@ var BackendRuntime = class {
|
|
|
1173
1607
|
async saveSession(input) {
|
|
1174
1608
|
return this.authoringService.saveSession(input);
|
|
1175
1609
|
}
|
|
1610
|
+
async materializeSession(input) {
|
|
1611
|
+
return this.authoringService.materializeSession(input);
|
|
1612
|
+
}
|
|
1176
1613
|
async closeSession(input) {
|
|
1177
1614
|
return this.runtimeService.closeSession(input);
|
|
1178
1615
|
}
|