@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.
@@ -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 createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
206
- async function trackWrite(targetPath, fn) {
207
- try {
208
- const result = await fn();
209
- committedPaths.push(targetPath);
210
- return result;
211
- } catch (error) {
212
- failedPaths.push(targetPath);
213
- throw error;
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
- async readFile(filePath) {
218
- return fileSystem.readFile(filePath);
219
- },
220
- async readFileRaw(filePath) {
221
- return fileSystem.readFileRaw(filePath);
222
- },
223
- async writeFile(filePath, content) {
224
- await trackWrite(filePath, async () => {
225
- await fileSystem.mkdir(fileSystem.dirname(filePath), { recursive: true });
226
- await fileSystem.writeFile(filePath, content);
227
- });
228
- },
229
- async writeFileRaw(filePath, data) {
230
- await trackWrite(filePath, async () => {
231
- await fileSystem.mkdir(fileSystem.dirname(filePath), { recursive: true });
232
- await fileSystem.writeFileRaw(filePath, data);
233
- });
234
- },
235
- async mkdir(dirPath) {
236
- await fileSystem.mkdir(dirPath, { recursive: true });
237
- },
238
- async readdir(dirPath) {
239
- return fileSystem.readdir(dirPath);
240
- },
241
- async exists(filePath) {
242
- try {
243
- await fileSystem.stat(filePath);
244
- return true;
245
- } catch {
246
- return false;
247
- }
248
- },
249
- join(...paths) {
250
- return fileSystem.join(...paths);
251
- },
252
- dirname(filePath) {
253
- return fileSystem.dirname(filePath);
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: result.error.issues?.map((issue) => ({
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
- if (!this.context.fileSystem) return failure("authoring", startedAt, {
327
- code: "capability_unavailable",
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 new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write(materializeUamProject(session.project), session.fairyPath);
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 project = liftDocumentToUamProject(await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath));
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 canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
942
- const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
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/runtime.ts
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: ["openSession", "saveSession"]
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
  }