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