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