@openfairygui/backend 0.2.0-alpha.2 → 0.2.0-alpha.21
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 +146 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.mts +33 -2
- package/dist/index.mjs +146 -2
- package/dist/node.cjs +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.mjs +1 -1
- package/dist/{runtime-OO1sHuCl.d.mts → runtime-B4SwHTRC.d.mts} +89 -10
- package/dist/{runtime-qcSlD_9-.d.cts → runtime-BB_3Ws5y.d.cts} +90 -11
- package/dist/{runtime-BPWzx-Tv.mjs → runtime-CqGK_D9A.mjs} +483 -87
- package/dist/{runtime-BL9Pkfc7.cjs → runtime-D3bShb1X.cjs} +482 -86
- package/package.json +5 -4
- package/src/contracts.ts +8 -0
- package/src/index.ts +25 -12
- package/src/runtime/capabilities.ts +126 -0
- package/src/runtime/contracts.ts +512 -0
- package/src/runtime.ts +67 -540
- package/src/services/authoring-service.ts +520 -80
- package/src/services/context.ts +12 -8
- package/src/services/runtime-service.ts +108 -12
- package/src/services/session-project-writer.ts +64 -0
- package/src/services/session-utils.ts +5 -1
- package/src/storage.ts +194 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { UAM_SUPPORTED_MATERIALIZATION_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,48 @@ 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
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function writeSessionProject(input) {
|
|
161
|
+
await new ProjectWriter(createWriterFileSystem(input.fileSystem, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, { staleSourceFiles: input.staleSourceFiles });
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
140
164
|
//#region src/services/snapshot-utils.ts
|
|
141
165
|
function cloneJsonValue(value) {
|
|
142
166
|
if (value === void 0 || value === null) return value;
|
|
@@ -179,6 +203,7 @@ function toSessionSnapshot(session, capabilities) {
|
|
|
179
203
|
revision: session.revision,
|
|
180
204
|
lastSavedRevision: session.lastSavedRevision,
|
|
181
205
|
dirty: session.dirty,
|
|
206
|
+
uamFidelity: session.uamFidelity,
|
|
182
207
|
lockHeld: session.lockHeld,
|
|
183
208
|
capabilities: cloneCapabilitiesSnapshot(capabilities)
|
|
184
209
|
};
|
|
@@ -202,65 +227,137 @@ function createStaleWriteError(session, expectedRevision) {
|
|
|
202
227
|
}
|
|
203
228
|
//#endregion
|
|
204
229
|
//#region src/services/authoring-service.ts
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
function projectSourceFiles(project) {
|
|
231
|
+
const result = /* @__PURE__ */ new Map();
|
|
232
|
+
for (const pkg of project.packages) {
|
|
233
|
+
result.set(`${pkg.id}/package.xml`, {
|
|
234
|
+
packageName: pkg.name,
|
|
235
|
+
branch: "",
|
|
236
|
+
path: "",
|
|
237
|
+
fileName: "package.xml"
|
|
238
|
+
});
|
|
239
|
+
const branches = /* @__PURE__ */ new Set();
|
|
240
|
+
for (const resource of pkg.resources) {
|
|
241
|
+
if (resource.branch) branches.add(resource.branch);
|
|
242
|
+
const fileName = resource.kind === "component" ? `${resource.name}.xml` : resource.fileName ?? resource.file ?? "";
|
|
243
|
+
if (!fileName) continue;
|
|
244
|
+
result.set(`${pkg.id}/${resource.id}`, {
|
|
245
|
+
packageName: pkg.name,
|
|
246
|
+
branch: resource.branch,
|
|
247
|
+
path: resource.path,
|
|
248
|
+
fileName
|
|
249
|
+
});
|
|
214
250
|
}
|
|
251
|
+
for (const branch of branches) result.set(`${pkg.id}/branch/${branch}`, {
|
|
252
|
+
packageName: pkg.name,
|
|
253
|
+
branch,
|
|
254
|
+
path: "",
|
|
255
|
+
fileName: "package_branch.xml"
|
|
256
|
+
});
|
|
215
257
|
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
function sourceFileKey(source) {
|
|
261
|
+
return [
|
|
262
|
+
source.branch,
|
|
263
|
+
source.packageName,
|
|
264
|
+
source.path,
|
|
265
|
+
source.fileName
|
|
266
|
+
].join("\0");
|
|
267
|
+
}
|
|
268
|
+
function recordStaleProjectFiles(session, previousProject, nextProject) {
|
|
269
|
+
if (!session.fileSystem) return;
|
|
270
|
+
const previousSources = projectSourceFiles(previousProject);
|
|
271
|
+
const nextSourceKeys = new Set([...projectSourceFiles(nextProject).values()].map(sourceFileKey));
|
|
272
|
+
for (const source of previousSources.values()) {
|
|
273
|
+
const key = sourceFileKey(source);
|
|
274
|
+
if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
|
|
275
|
+
}
|
|
276
|
+
for (const key of nextSourceKeys) session.pendingStaleSourceFiles.delete(key);
|
|
277
|
+
}
|
|
278
|
+
function toBackendDiagnostics(error) {
|
|
279
|
+
return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
|
|
280
|
+
code: error.code,
|
|
281
|
+
message: error.message,
|
|
282
|
+
severity: "error",
|
|
283
|
+
operationKind: error.operationKind,
|
|
284
|
+
opIndex: error.opIndex,
|
|
285
|
+
opId: error.opId
|
|
286
|
+
}];
|
|
287
|
+
}
|
|
288
|
+
function createCapabilityUnavailableError$1(message) {
|
|
216
289
|
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
|
-
|
|
290
|
+
code: "capability_unavailable",
|
|
291
|
+
message,
|
|
292
|
+
capability: "fileSystem",
|
|
293
|
+
requiredAdapter: "BackendFileSystem"
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function createUamFidelityUnsupportedError(session) {
|
|
297
|
+
return {
|
|
298
|
+
code: "uam_fidelity_unsupported",
|
|
299
|
+
message: "The source project contains formal properties that the current UAM cannot preserve.",
|
|
300
|
+
sessionId: session.sessionId,
|
|
301
|
+
canonicalPathKey: session.canonicalPathKey
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function validationDiagnostics(sessionProject) {
|
|
305
|
+
return validateUamProject(sessionProject).map((issue) => ({
|
|
306
|
+
code: "materialize_validation_failed",
|
|
307
|
+
message: issue.message,
|
|
308
|
+
severity: "error",
|
|
309
|
+
path: issue.path,
|
|
310
|
+
operationKind: "materializeSession"
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
function toMaterializeSnapshot(session, capabilities, input) {
|
|
314
|
+
return {
|
|
315
|
+
...toSessionSnapshot(session, capabilities),
|
|
316
|
+
mode: "fullProject",
|
|
317
|
+
reason: input.reason,
|
|
318
|
+
materializeRevision: session.revision,
|
|
319
|
+
saveRevision: session.lastSavedRevision,
|
|
320
|
+
writtenPaths: [...input.writtenPaths],
|
|
321
|
+
skippedPaths: [...input.skippedPaths],
|
|
322
|
+
diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function storageCanonicalTarget(input) {
|
|
326
|
+
const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || ".");
|
|
327
|
+
return {
|
|
328
|
+
fileSystem: input.fileSystem,
|
|
329
|
+
fairyPath: input.fairyPath,
|
|
330
|
+
canonicalProjectPath,
|
|
331
|
+
canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath)
|
|
255
332
|
};
|
|
256
333
|
}
|
|
257
334
|
var AuthoringService = class {
|
|
335
|
+
sessionOperations = /* @__PURE__ */ new Map();
|
|
258
336
|
constructor(context, cacheService, eventService) {
|
|
259
337
|
this.context = context;
|
|
260
338
|
this.cacheService = cacheService;
|
|
261
339
|
this.eventService = eventService;
|
|
262
340
|
}
|
|
341
|
+
async runSessionExclusive(sessionId, operation) {
|
|
342
|
+
const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
|
|
343
|
+
let release = () => void 0;
|
|
344
|
+
const current = new Promise((resolve) => {
|
|
345
|
+
release = resolve;
|
|
346
|
+
});
|
|
347
|
+
const tail = previous.then(() => current);
|
|
348
|
+
this.sessionOperations.set(sessionId, tail);
|
|
349
|
+
await previous;
|
|
350
|
+
try {
|
|
351
|
+
return await operation();
|
|
352
|
+
} finally {
|
|
353
|
+
release();
|
|
354
|
+
if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
263
357
|
async applyTransaction(input) {
|
|
358
|
+
return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
|
|
359
|
+
}
|
|
360
|
+
async applyTransactionExclusive(input) {
|
|
264
361
|
const startedAt = Date.now();
|
|
265
362
|
const session = this.context.sessions.get(input.sessionId);
|
|
266
363
|
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
@@ -281,22 +378,21 @@ var AuthoringService = class {
|
|
|
281
378
|
operations: input.operations
|
|
282
379
|
});
|
|
283
380
|
if (result.ok === false) {
|
|
381
|
+
const diagnostics = toBackendDiagnostics(result.error);
|
|
284
382
|
this.eventService.emit({
|
|
285
383
|
kind: "transaction.rejected",
|
|
286
384
|
sessionId: session.sessionId,
|
|
287
385
|
canonicalPathKey: session.canonicalPathKey,
|
|
288
386
|
revision: session.revision,
|
|
289
|
-
diagnostics
|
|
290
|
-
code: result.error.code,
|
|
291
|
-
message: issue.message,
|
|
292
|
-
severity: "error"
|
|
293
|
-
})) ?? []
|
|
387
|
+
diagnostics
|
|
294
388
|
});
|
|
295
389
|
return failure("authoring", startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
|
|
296
390
|
sessionId: session.sessionId,
|
|
297
|
-
revision: session.revision
|
|
391
|
+
revision: session.revision,
|
|
392
|
+
diagnostics
|
|
298
393
|
});
|
|
299
394
|
}
|
|
395
|
+
recordStaleProjectFiles(session, session.project, result.project);
|
|
300
396
|
session.project = result.project;
|
|
301
397
|
session.revision += 1;
|
|
302
398
|
session.dirty = true;
|
|
@@ -320,15 +416,22 @@ var AuthoringService = class {
|
|
|
320
416
|
});
|
|
321
417
|
}
|
|
322
418
|
async saveSession(input) {
|
|
419
|
+
if (input.force === true || input.mode === "materializeCleanSession") return this.materializeSession({
|
|
420
|
+
sessionId: input.sessionId,
|
|
421
|
+
expectedRevision: input.expectedRevision,
|
|
422
|
+
targetPath: input.targetPath,
|
|
423
|
+
fileSystem: input.fileSystem,
|
|
424
|
+
mode: "fullProject",
|
|
425
|
+
reason: "force_save"
|
|
426
|
+
});
|
|
427
|
+
return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
|
|
428
|
+
}
|
|
429
|
+
async saveSessionExclusive(input) {
|
|
323
430
|
const startedAt = Date.now();
|
|
324
431
|
const session = this.context.sessions.get(input.sessionId);
|
|
325
432
|
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), {
|
|
433
|
+
const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
|
|
434
|
+
if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
|
|
332
435
|
sessionId: session.sessionId,
|
|
333
436
|
revision: session.revision
|
|
334
437
|
});
|
|
@@ -336,7 +439,6 @@ var AuthoringService = class {
|
|
|
336
439
|
sessionId: session.sessionId,
|
|
337
440
|
revision: session.revision
|
|
338
441
|
});
|
|
339
|
-
const fileSystem = this.context.fileSystem;
|
|
340
442
|
const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
|
|
341
443
|
if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
|
|
342
444
|
sessionId: session.sessionId,
|
|
@@ -346,6 +448,10 @@ var AuthoringService = class {
|
|
|
346
448
|
sessionId: session.sessionId,
|
|
347
449
|
revision: session.revision
|
|
348
450
|
});
|
|
451
|
+
if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
|
|
452
|
+
sessionId: session.sessionId,
|
|
453
|
+
revision: session.revision
|
|
454
|
+
});
|
|
349
455
|
const committedPaths = [];
|
|
350
456
|
const failedPaths = [];
|
|
351
457
|
this.eventService.emit({
|
|
@@ -355,7 +461,17 @@ var AuthoringService = class {
|
|
|
355
461
|
revision: session.revision
|
|
356
462
|
});
|
|
357
463
|
try {
|
|
358
|
-
await
|
|
464
|
+
await writeSessionProject({
|
|
465
|
+
fileSystem,
|
|
466
|
+
document: materializeUamProject(session.project),
|
|
467
|
+
fairyPath: session.fairyPath,
|
|
468
|
+
staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
|
|
469
|
+
writtenPaths: committedPaths,
|
|
470
|
+
failedPaths
|
|
471
|
+
});
|
|
472
|
+
session.fileSystem ??= fileSystem;
|
|
473
|
+
session.pendingStaleSourceFiles.clear();
|
|
474
|
+
commitUamProjectSourcePaths(session.project);
|
|
359
475
|
session.lastSavedRevision = session.revision;
|
|
360
476
|
session.dirty = false;
|
|
361
477
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
@@ -400,6 +516,172 @@ var AuthoringService = class {
|
|
|
400
516
|
});
|
|
401
517
|
}
|
|
402
518
|
}
|
|
519
|
+
async materializeSession(input) {
|
|
520
|
+
return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
|
|
521
|
+
}
|
|
522
|
+
async materializeSessionExclusive(input) {
|
|
523
|
+
const startedAt = Date.now();
|
|
524
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
525
|
+
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
526
|
+
if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
|
|
527
|
+
sessionId: session.sessionId,
|
|
528
|
+
revision: session.revision
|
|
529
|
+
});
|
|
530
|
+
const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
|
|
531
|
+
const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
|
|
532
|
+
if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("materializeSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
|
|
533
|
+
sessionId: session.sessionId,
|
|
534
|
+
revision: session.revision
|
|
535
|
+
});
|
|
536
|
+
const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
|
|
537
|
+
const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
|
|
538
|
+
if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
|
|
539
|
+
sessionId: session.sessionId,
|
|
540
|
+
revision: session.revision
|
|
541
|
+
});
|
|
542
|
+
if (storageTarget) {
|
|
543
|
+
const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
|
|
544
|
+
if (holderSessionId && holderSessionId !== session.sessionId) return failure("authoring", startedAt, {
|
|
545
|
+
code: "lock_conflict",
|
|
546
|
+
kind: "in_process_session_exists",
|
|
547
|
+
message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
|
|
548
|
+
canonicalPathKey: storageTarget.canonicalPathKey,
|
|
549
|
+
holderSessionId
|
|
550
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
551
|
+
sessionId: session.sessionId,
|
|
552
|
+
revision: session.revision
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
|
|
556
|
+
sessionId: session.sessionId,
|
|
557
|
+
revision: session.revision
|
|
558
|
+
});
|
|
559
|
+
const diagnostics = validationDiagnostics(session.project);
|
|
560
|
+
if (diagnostics.length > 0) return failure("authoring", startedAt, {
|
|
561
|
+
code: "materialize_validation_failed",
|
|
562
|
+
message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
|
|
563
|
+
sessionId: session.sessionId,
|
|
564
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
565
|
+
issueCount: diagnostics.length,
|
|
566
|
+
diagnostics
|
|
567
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
568
|
+
sessionId: session.sessionId,
|
|
569
|
+
revision: session.revision,
|
|
570
|
+
diagnostics
|
|
571
|
+
});
|
|
572
|
+
let document;
|
|
573
|
+
try {
|
|
574
|
+
document = materializeUamProject(session.project);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
const diagnosticsFromError = [{
|
|
577
|
+
code: "materialize_validation_failed",
|
|
578
|
+
message: error instanceof Error ? error.message : String(error),
|
|
579
|
+
severity: "error",
|
|
580
|
+
operationKind: "materializeSession"
|
|
581
|
+
}];
|
|
582
|
+
return failure("authoring", startedAt, {
|
|
583
|
+
code: "materialize_validation_failed",
|
|
584
|
+
message: error instanceof Error ? error.message : String(error),
|
|
585
|
+
sessionId: session.sessionId,
|
|
586
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
587
|
+
issueCount: diagnosticsFromError.length,
|
|
588
|
+
diagnostics: diagnosticsFromError
|
|
589
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
590
|
+
sessionId: session.sessionId,
|
|
591
|
+
revision: session.revision,
|
|
592
|
+
diagnostics: diagnosticsFromError
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
const writtenPaths = [];
|
|
596
|
+
const failedPaths = [];
|
|
597
|
+
const skippedPaths = [];
|
|
598
|
+
this.eventService.emit({
|
|
599
|
+
kind: "save.started",
|
|
600
|
+
sessionId: session.sessionId,
|
|
601
|
+
canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
|
|
602
|
+
revision: session.revision
|
|
603
|
+
});
|
|
604
|
+
try {
|
|
605
|
+
const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
|
|
606
|
+
await writeSessionProject({
|
|
607
|
+
fileSystem,
|
|
608
|
+
document,
|
|
609
|
+
fairyPath,
|
|
610
|
+
staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
|
|
611
|
+
writtenPaths,
|
|
612
|
+
failedPaths
|
|
613
|
+
});
|
|
614
|
+
if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
615
|
+
if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
616
|
+
if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
|
|
617
|
+
if (storageTarget) {
|
|
618
|
+
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
619
|
+
session.fileSystem = storageTarget.fileSystem;
|
|
620
|
+
session.fairyPath = storageTarget.fairyPath;
|
|
621
|
+
session.canonicalProjectPath = storageTarget.canonicalProjectPath;
|
|
622
|
+
session.canonicalPathKey = storageTarget.canonicalPathKey;
|
|
623
|
+
this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
|
|
624
|
+
}
|
|
625
|
+
session.lastSavedRevision = session.revision;
|
|
626
|
+
session.dirty = false;
|
|
627
|
+
const cacheEntry = this.cacheService.refreshSession(session);
|
|
628
|
+
this.eventService.emit({
|
|
629
|
+
kind: "save.completed",
|
|
630
|
+
sessionId: session.sessionId,
|
|
631
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
632
|
+
revision: session.revision
|
|
633
|
+
});
|
|
634
|
+
this.eventService.emit({
|
|
635
|
+
kind: "cache.updated",
|
|
636
|
+
sessionId: session.sessionId,
|
|
637
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
638
|
+
revision: session.revision,
|
|
639
|
+
cacheRevision: cacheEntry.revision
|
|
640
|
+
});
|
|
641
|
+
return success("authoring", startedAt, toMaterializeSnapshot(session, this.context.capabilities, {
|
|
642
|
+
reason: input.reason,
|
|
643
|
+
writtenPaths,
|
|
644
|
+
skippedPaths,
|
|
645
|
+
diagnostics: []
|
|
646
|
+
}), {
|
|
647
|
+
sessionId: session.sessionId,
|
|
648
|
+
revision: session.revision
|
|
649
|
+
});
|
|
650
|
+
} catch (error) {
|
|
651
|
+
const diagnosticsFromError = [{
|
|
652
|
+
code: "write_failed",
|
|
653
|
+
message: error instanceof Error ? error.message : String(error),
|
|
654
|
+
severity: "error",
|
|
655
|
+
path: failedPaths[0],
|
|
656
|
+
operationKind: "materializeSession"
|
|
657
|
+
}];
|
|
658
|
+
this.cacheService.invalidateSession(session);
|
|
659
|
+
this.eventService.emit({
|
|
660
|
+
kind: "save.failed",
|
|
661
|
+
sessionId: session.sessionId,
|
|
662
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
663
|
+
revision: session.revision,
|
|
664
|
+
diagnostics: diagnosticsFromError
|
|
665
|
+
});
|
|
666
|
+
return failure("authoring", startedAt, {
|
|
667
|
+
code: "write_failed",
|
|
668
|
+
message: error instanceof Error ? error.message : String(error),
|
|
669
|
+
sessionId: session.sessionId,
|
|
670
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
671
|
+
attemptedRevision: session.revision,
|
|
672
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
673
|
+
writtenPaths,
|
|
674
|
+
failedPaths,
|
|
675
|
+
skippedPaths,
|
|
676
|
+
diagnostics: diagnosticsFromError,
|
|
677
|
+
diskMayBePartiallyUpdated: true
|
|
678
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
679
|
+
sessionId: session.sessionId,
|
|
680
|
+
revision: session.revision,
|
|
681
|
+
diagnostics: diagnosticsFromError
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
403
685
|
};
|
|
404
686
|
//#endregion
|
|
405
687
|
//#region src/services/cache-service.ts
|
|
@@ -858,6 +1140,68 @@ function createProjectReaderFileSystem(fileSystem) {
|
|
|
858
1140
|
}
|
|
859
1141
|
};
|
|
860
1142
|
}
|
|
1143
|
+
function createCaptureFileSystem(files) {
|
|
1144
|
+
const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
1145
|
+
return {
|
|
1146
|
+
async readFile(filePath) {
|
|
1147
|
+
const value = files.get(normalize(filePath));
|
|
1148
|
+
if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
|
|
1149
|
+
return value;
|
|
1150
|
+
},
|
|
1151
|
+
async readFileRaw(filePath) {
|
|
1152
|
+
const value = files.get(normalize(filePath));
|
|
1153
|
+
if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
|
|
1154
|
+
return value.slice();
|
|
1155
|
+
},
|
|
1156
|
+
async writeFile(filePath, content) {
|
|
1157
|
+
files.set(normalize(filePath), content);
|
|
1158
|
+
},
|
|
1159
|
+
async writeFileRaw(filePath, data) {
|
|
1160
|
+
files.set(normalize(filePath), data.slice());
|
|
1161
|
+
},
|
|
1162
|
+
async mkdir() {},
|
|
1163
|
+
async readdir() {
|
|
1164
|
+
return [];
|
|
1165
|
+
},
|
|
1166
|
+
async exists(filePath) {
|
|
1167
|
+
return files.has(normalize(filePath));
|
|
1168
|
+
},
|
|
1169
|
+
join(...paths) {
|
|
1170
|
+
return normalize(paths.filter(Boolean).join("/"));
|
|
1171
|
+
},
|
|
1172
|
+
dirname(filePath) {
|
|
1173
|
+
const normalized = normalize(filePath);
|
|
1174
|
+
const separator = normalized.lastIndexOf("/");
|
|
1175
|
+
return separator < 0 ? "" : normalized.slice(0, separator);
|
|
1176
|
+
},
|
|
1177
|
+
async unlink(filePath) {
|
|
1178
|
+
files.delete(normalize(filePath));
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
function capturedFilesEqual(left, right) {
|
|
1183
|
+
if (left.size !== right.size) return false;
|
|
1184
|
+
for (const [filePath, leftValue] of left) {
|
|
1185
|
+
const rightValue = right.get(filePath);
|
|
1186
|
+
if (typeof leftValue === "string") {
|
|
1187
|
+
if (leftValue !== rightValue) return false;
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
|
|
1191
|
+
for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
|
|
1192
|
+
}
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
async function hasFullUamFidelity(document, project) {
|
|
1196
|
+
const sourceFiles = /* @__PURE__ */ new Map();
|
|
1197
|
+
const materializedFiles = /* @__PURE__ */ new Map();
|
|
1198
|
+
try {
|
|
1199
|
+
await Promise.all([new ProjectWriter(createCaptureFileSystem(sourceFiles)).write(document, "Project.fairy"), new ProjectWriter(createCaptureFileSystem(materializedFiles)).write(materializeUamProject(project), "Project.fairy")]);
|
|
1200
|
+
} catch {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
return capturedFilesEqual(sourceFiles, materializedFiles);
|
|
1204
|
+
}
|
|
861
1205
|
var RuntimeService = class {
|
|
862
1206
|
constructor(context, cacheService, eventService, jobService) {
|
|
863
1207
|
this.context = context;
|
|
@@ -892,7 +1236,8 @@ var RuntimeService = class {
|
|
|
892
1236
|
canonicalPathKey
|
|
893
1237
|
}));
|
|
894
1238
|
await advisoryLock.close();
|
|
895
|
-
const
|
|
1239
|
+
const document = await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
|
|
1240
|
+
const project = liftDocumentToUamProject(document);
|
|
896
1241
|
const sessionId = randomId();
|
|
897
1242
|
const session = {
|
|
898
1243
|
sessionId,
|
|
@@ -900,9 +1245,12 @@ var RuntimeService = class {
|
|
|
900
1245
|
canonicalProjectPath,
|
|
901
1246
|
canonicalPathKey,
|
|
902
1247
|
lockFilePath,
|
|
1248
|
+
fileSystem,
|
|
903
1249
|
project,
|
|
1250
|
+
uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
|
|
904
1251
|
revision: 0,
|
|
905
1252
|
lastSavedRevision: 0,
|
|
1253
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
906
1254
|
dirty: false,
|
|
907
1255
|
lockHeld: true,
|
|
908
1256
|
closed: false
|
|
@@ -938,8 +1286,10 @@ var RuntimeService = class {
|
|
|
938
1286
|
openProjectSession(input) {
|
|
939
1287
|
const startedAt = Date.now();
|
|
940
1288
|
const sessionId = input.sessionId ?? randomId();
|
|
941
|
-
const
|
|
942
|
-
const
|
|
1289
|
+
const storage = input.storage;
|
|
1290
|
+
const memoryProjectPath = `memory://${sessionId}`;
|
|
1291
|
+
const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
|
|
1292
|
+
const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
|
|
943
1293
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
944
1294
|
if (existingSessionId) return failure("runtime", startedAt, {
|
|
945
1295
|
code: "lock_conflict",
|
|
@@ -950,13 +1300,16 @@ var RuntimeService = class {
|
|
|
950
1300
|
});
|
|
951
1301
|
const session = {
|
|
952
1302
|
sessionId,
|
|
953
|
-
fairyPath: canonicalProjectPath,
|
|
1303
|
+
fairyPath: storage?.fairyPath ?? canonicalProjectPath,
|
|
954
1304
|
canonicalProjectPath,
|
|
955
1305
|
canonicalPathKey,
|
|
956
1306
|
lockFilePath: "",
|
|
1307
|
+
fileSystem: storage?.fileSystem,
|
|
957
1308
|
project: normalizeUamProject(input.project),
|
|
1309
|
+
uamFidelity: "full",
|
|
958
1310
|
revision: 0,
|
|
959
1311
|
lastSavedRevision: 0,
|
|
1312
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
960
1313
|
dirty: false,
|
|
961
1314
|
lockHeld: false,
|
|
962
1315
|
closed: false
|
|
@@ -1009,7 +1362,25 @@ var RuntimeService = class {
|
|
|
1009
1362
|
}
|
|
1010
1363
|
};
|
|
1011
1364
|
//#endregion
|
|
1012
|
-
//#region src/
|
|
1365
|
+
//#region src/services/artifact-service.ts
|
|
1366
|
+
function createArtifactCapabilities() {
|
|
1367
|
+
const bridge = {
|
|
1368
|
+
available: false,
|
|
1369
|
+
requiredHost: "node",
|
|
1370
|
+
executionBoundary: "external-bridge",
|
|
1371
|
+
bridgeEntrypoint: "@openfairygui/backend/node",
|
|
1372
|
+
reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
|
|
1373
|
+
};
|
|
1374
|
+
return {
|
|
1375
|
+
publish: false,
|
|
1376
|
+
restore: false,
|
|
1377
|
+
status: "bridge-required",
|
|
1378
|
+
publishBridge: bridge,
|
|
1379
|
+
restoreBridge: bridge
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
//#endregion
|
|
1383
|
+
//#region src/runtime/capabilities.ts
|
|
1013
1384
|
const BACKEND_METHODS = [
|
|
1014
1385
|
"getCapabilities",
|
|
1015
1386
|
"openSession",
|
|
@@ -1017,6 +1388,7 @@ const BACKEND_METHODS = [
|
|
|
1017
1388
|
"getSession",
|
|
1018
1389
|
"applyTransaction",
|
|
1019
1390
|
"saveSession",
|
|
1391
|
+
"materializeSession",
|
|
1020
1392
|
"closeSession",
|
|
1021
1393
|
"getEvents",
|
|
1022
1394
|
"getJob",
|
|
@@ -1050,6 +1422,11 @@ function createCapabilities() {
|
|
|
1050
1422
|
resourceKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.resourceKinds],
|
|
1051
1423
|
nodeKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.nodeKinds],
|
|
1052
1424
|
gearKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.gearKinds],
|
|
1425
|
+
transactionScope: {
|
|
1426
|
+
resourceKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.resourceKinds],
|
|
1427
|
+
nodeKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.nodeKinds],
|
|
1428
|
+
gearKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.gearKinds]
|
|
1429
|
+
},
|
|
1053
1430
|
unsupported: ["artifact.publish", "artifact.restore"]
|
|
1054
1431
|
},
|
|
1055
1432
|
artifact: createArtifactCapabilities(),
|
|
@@ -1060,7 +1437,21 @@ function createCapabilities() {
|
|
|
1060
1437
|
adapters: {
|
|
1061
1438
|
fileSystem: {
|
|
1062
1439
|
injected: true,
|
|
1063
|
-
requiredFor: [
|
|
1440
|
+
requiredFor: [
|
|
1441
|
+
"openSession",
|
|
1442
|
+
"saveSession",
|
|
1443
|
+
"materializeSession"
|
|
1444
|
+
]
|
|
1445
|
+
},
|
|
1446
|
+
projectStorage: {
|
|
1447
|
+
injected: true,
|
|
1448
|
+
browserSafe: true,
|
|
1449
|
+
requiredFor: [
|
|
1450
|
+
"openProjectSession.writeback",
|
|
1451
|
+
"saveSession",
|
|
1452
|
+
"materializeSession"
|
|
1453
|
+
],
|
|
1454
|
+
adapterFactory: "createBackendStorageFileSystem"
|
|
1064
1455
|
},
|
|
1065
1456
|
host: {
|
|
1066
1457
|
injected: true,
|
|
@@ -1109,6 +1500,8 @@ function createCapabilities() {
|
|
|
1109
1500
|
}
|
|
1110
1501
|
};
|
|
1111
1502
|
}
|
|
1503
|
+
//#endregion
|
|
1504
|
+
//#region src/runtime.ts
|
|
1112
1505
|
var BackendRuntime = class {
|
|
1113
1506
|
fileSystem;
|
|
1114
1507
|
capabilities;
|
|
@@ -1167,6 +1560,9 @@ var BackendRuntime = class {
|
|
|
1167
1560
|
async saveSession(input) {
|
|
1168
1561
|
return this.authoringService.saveSession(input);
|
|
1169
1562
|
}
|
|
1563
|
+
async materializeSession(input) {
|
|
1564
|
+
return this.authoringService.materializeSession(input);
|
|
1565
|
+
}
|
|
1170
1566
|
async closeSession(input) {
|
|
1171
1567
|
return this.runtimeService.closeSession(input);
|
|
1172
1568
|
}
|