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