@openfairygui/backend 0.2.0-alpha.6 → 0.2.0-alpha.8

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,4 +1,4 @@
1
- import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, liftDocumentToUamProject, materializeUamProject, normalizeUamProject } from "@openfairygui/core/uam";
1
+ import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, liftDocumentToUamProject, materializeUamProject, normalizeUamProject, validateUamProject } from "@openfairygui/core/uam";
2
2
  import { ProjectReader, ProjectWriter } from "@openfairygui/core/project-io";
3
3
  import { applyUamTransactionApp } from "@openfairygui/functions/uam";
4
4
  //#region src/contracts.ts
@@ -264,6 +264,44 @@ function toBackendDiagnostics(error) {
264
264
  opId: error.opId
265
265
  }];
266
266
  }
267
+ function createCapabilityUnavailableError$1(message) {
268
+ return {
269
+ code: "capability_unavailable",
270
+ message,
271
+ capability: "fileSystem",
272
+ requiredAdapter: "BackendFileSystem"
273
+ };
274
+ }
275
+ function validationDiagnostics(sessionProject) {
276
+ return validateUamProject(sessionProject).map((issue) => ({
277
+ code: "materialize_validation_failed",
278
+ message: issue.message,
279
+ severity: "error",
280
+ path: issue.path,
281
+ operationKind: "materializeSession"
282
+ }));
283
+ }
284
+ function toMaterializeSnapshot(session, capabilities, input) {
285
+ return {
286
+ ...toSessionSnapshot(session, capabilities),
287
+ mode: "fullProject",
288
+ reason: input.reason,
289
+ materializeRevision: session.revision,
290
+ saveRevision: session.lastSavedRevision,
291
+ writtenPaths: [...input.writtenPaths],
292
+ skippedPaths: [...input.skippedPaths],
293
+ diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic }))
294
+ };
295
+ }
296
+ function storageCanonicalTarget(input) {
297
+ const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || ".");
298
+ return {
299
+ fileSystem: input.fileSystem,
300
+ fairyPath: input.fairyPath,
301
+ canonicalProjectPath,
302
+ canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath)
303
+ };
304
+ }
267
305
  var AuthoringService = class {
268
306
  constructor(context, cacheService, eventService) {
269
307
  this.context = context;
@@ -328,15 +366,19 @@ var AuthoringService = class {
328
366
  });
329
367
  }
330
368
  async saveSession(input) {
369
+ if (input.force === true || input.mode === "materializeCleanSession") return this.materializeSession({
370
+ sessionId: input.sessionId,
371
+ expectedRevision: input.expectedRevision,
372
+ targetPath: input.targetPath,
373
+ fileSystem: input.fileSystem,
374
+ mode: "fullProject",
375
+ reason: "force_save"
376
+ });
331
377
  const startedAt = Date.now();
332
378
  const session = this.context.sessions.get(input.sessionId);
333
379
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
334
- if (!this.context.fileSystem) return failure("authoring", startedAt, {
335
- code: "capability_unavailable",
336
- message: "saveSession requires an injected BackendFileSystem adapter.",
337
- capability: "fileSystem",
338
- requiredAdapter: "BackendFileSystem"
339
- }, toSessionSnapshot(session, this.context.capabilities), {
380
+ const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
381
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
340
382
  sessionId: session.sessionId,
341
383
  revision: session.revision
342
384
  });
@@ -344,7 +386,6 @@ var AuthoringService = class {
344
386
  sessionId: session.sessionId,
345
387
  revision: session.revision
346
388
  });
347
- const fileSystem = this.context.fileSystem;
348
389
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
349
390
  if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
350
391
  sessionId: session.sessionId,
@@ -408,6 +449,154 @@ var AuthoringService = class {
408
449
  });
409
450
  }
410
451
  }
452
+ async materializeSession(input) {
453
+ const startedAt = Date.now();
454
+ const session = this.context.sessions.get(input.sessionId);
455
+ if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
456
+ if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
457
+ sessionId: session.sessionId,
458
+ revision: session.revision
459
+ });
460
+ const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
461
+ const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
462
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("materializeSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
463
+ sessionId: session.sessionId,
464
+ revision: session.revision
465
+ });
466
+ const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
467
+ const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
468
+ if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
469
+ sessionId: session.sessionId,
470
+ revision: session.revision
471
+ });
472
+ if (storageTarget) {
473
+ const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
474
+ if (holderSessionId && holderSessionId !== session.sessionId) return failure("authoring", startedAt, {
475
+ code: "lock_conflict",
476
+ kind: "in_process_session_exists",
477
+ message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
478
+ canonicalPathKey: storageTarget.canonicalPathKey,
479
+ holderSessionId
480
+ }, toSessionSnapshot(session, this.context.capabilities), {
481
+ sessionId: session.sessionId,
482
+ revision: session.revision
483
+ });
484
+ }
485
+ const diagnostics = validationDiagnostics(session.project);
486
+ if (diagnostics.length > 0) return failure("authoring", startedAt, {
487
+ code: "materialize_validation_failed",
488
+ message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
489
+ sessionId: session.sessionId,
490
+ canonicalPathKey: session.canonicalPathKey,
491
+ issueCount: diagnostics.length,
492
+ diagnostics
493
+ }, toSessionSnapshot(session, this.context.capabilities), {
494
+ sessionId: session.sessionId,
495
+ revision: session.revision,
496
+ diagnostics
497
+ });
498
+ let document;
499
+ try {
500
+ document = materializeUamProject(session.project);
501
+ } catch (error) {
502
+ const diagnosticsFromError = [{
503
+ code: "materialize_validation_failed",
504
+ message: error instanceof Error ? error.message : String(error),
505
+ severity: "error",
506
+ operationKind: "materializeSession"
507
+ }];
508
+ return failure("authoring", startedAt, {
509
+ code: "materialize_validation_failed",
510
+ message: error instanceof Error ? error.message : String(error),
511
+ sessionId: session.sessionId,
512
+ canonicalPathKey: session.canonicalPathKey,
513
+ issueCount: diagnosticsFromError.length,
514
+ diagnostics: diagnosticsFromError
515
+ }, toSessionSnapshot(session, this.context.capabilities), {
516
+ sessionId: session.sessionId,
517
+ revision: session.revision,
518
+ diagnostics: diagnosticsFromError
519
+ });
520
+ }
521
+ const writtenPaths = [];
522
+ const failedPaths = [];
523
+ const skippedPaths = [];
524
+ this.eventService.emit({
525
+ kind: "save.started",
526
+ sessionId: session.sessionId,
527
+ canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
528
+ revision: session.revision
529
+ });
530
+ try {
531
+ await new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths)).write(document, fairyPath);
532
+ if (storageTarget) {
533
+ this.context.sessionsByPath.delete(session.canonicalPathKey);
534
+ session.fileSystem = storageTarget.fileSystem;
535
+ session.fairyPath = storageTarget.fairyPath;
536
+ session.canonicalProjectPath = storageTarget.canonicalProjectPath;
537
+ session.canonicalPathKey = storageTarget.canonicalPathKey;
538
+ this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
539
+ }
540
+ session.lastSavedRevision = session.revision;
541
+ session.dirty = false;
542
+ const cacheEntry = this.cacheService.refreshSession(session);
543
+ this.eventService.emit({
544
+ kind: "save.completed",
545
+ sessionId: session.sessionId,
546
+ canonicalPathKey: session.canonicalPathKey,
547
+ revision: session.revision
548
+ });
549
+ this.eventService.emit({
550
+ kind: "cache.updated",
551
+ sessionId: session.sessionId,
552
+ canonicalPathKey: session.canonicalPathKey,
553
+ revision: session.revision,
554
+ cacheRevision: cacheEntry.revision
555
+ });
556
+ return success("authoring", startedAt, toMaterializeSnapshot(session, this.context.capabilities, {
557
+ reason: input.reason,
558
+ writtenPaths,
559
+ skippedPaths,
560
+ diagnostics: []
561
+ }), {
562
+ sessionId: session.sessionId,
563
+ revision: session.revision
564
+ });
565
+ } catch (error) {
566
+ const diagnosticsFromError = [{
567
+ code: "write_failed",
568
+ message: error instanceof Error ? error.message : String(error),
569
+ severity: "error",
570
+ path: failedPaths[0],
571
+ operationKind: "materializeSession"
572
+ }];
573
+ this.cacheService.invalidateSession(session);
574
+ this.eventService.emit({
575
+ kind: "save.failed",
576
+ sessionId: session.sessionId,
577
+ canonicalPathKey: session.canonicalPathKey,
578
+ revision: session.revision,
579
+ diagnostics: diagnosticsFromError
580
+ });
581
+ return failure("authoring", startedAt, {
582
+ code: "write_failed",
583
+ message: error instanceof Error ? error.message : String(error),
584
+ sessionId: session.sessionId,
585
+ canonicalPathKey: session.canonicalPathKey,
586
+ attemptedRevision: session.revision,
587
+ lastSavedRevision: session.lastSavedRevision,
588
+ writtenPaths,
589
+ failedPaths,
590
+ skippedPaths,
591
+ diagnostics: diagnosticsFromError,
592
+ diskMayBePartiallyUpdated: true
593
+ }, toSessionSnapshot(session, this.context.capabilities), {
594
+ sessionId: session.sessionId,
595
+ revision: session.revision,
596
+ diagnostics: diagnosticsFromError
597
+ });
598
+ }
599
+ }
411
600
  };
412
601
  //#endregion
413
602
  //#region src/services/cache-service.ts
@@ -908,6 +1097,7 @@ var RuntimeService = class {
908
1097
  canonicalProjectPath,
909
1098
  canonicalPathKey,
910
1099
  lockFilePath,
1100
+ fileSystem,
911
1101
  project,
912
1102
  revision: 0,
913
1103
  lastSavedRevision: 0,
@@ -946,8 +1136,10 @@ var RuntimeService = class {
946
1136
  openProjectSession(input) {
947
1137
  const startedAt = Date.now();
948
1138
  const sessionId = input.sessionId ?? randomId();
949
- const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
950
- const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
1139
+ const storage = input.storage;
1140
+ const memoryProjectPath = `memory://${sessionId}`;
1141
+ const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
1142
+ const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
951
1143
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
952
1144
  if (existingSessionId) return failure("runtime", startedAt, {
953
1145
  code: "lock_conflict",
@@ -958,10 +1150,11 @@ var RuntimeService = class {
958
1150
  });
959
1151
  const session = {
960
1152
  sessionId,
961
- fairyPath: canonicalProjectPath,
1153
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
962
1154
  canonicalProjectPath,
963
1155
  canonicalPathKey,
964
1156
  lockFilePath: "",
1157
+ fileSystem: storage?.fileSystem,
965
1158
  project: normalizeUamProject(input.project),
966
1159
  revision: 0,
967
1160
  lastSavedRevision: 0,
@@ -1025,6 +1218,7 @@ const BACKEND_METHODS = [
1025
1218
  "getSession",
1026
1219
  "applyTransaction",
1027
1220
  "saveSession",
1221
+ "materializeSession",
1028
1222
  "closeSession",
1029
1223
  "getEvents",
1030
1224
  "getJob",
@@ -1073,7 +1267,21 @@ function createCapabilities() {
1073
1267
  adapters: {
1074
1268
  fileSystem: {
1075
1269
  injected: true,
1076
- requiredFor: ["openSession", "saveSession"]
1270
+ requiredFor: [
1271
+ "openSession",
1272
+ "saveSession",
1273
+ "materializeSession"
1274
+ ]
1275
+ },
1276
+ projectStorage: {
1277
+ injected: true,
1278
+ browserSafe: true,
1279
+ requiredFor: [
1280
+ "openProjectSession.writeback",
1281
+ "saveSession",
1282
+ "materializeSession"
1283
+ ],
1284
+ adapterFactory: "createBackendStorageFileSystem"
1077
1285
  },
1078
1286
  host: {
1079
1287
  injected: true,
@@ -1180,6 +1388,9 @@ var BackendRuntime = class {
1180
1388
  async saveSession(input) {
1181
1389
  return this.authoringService.saveSession(input);
1182
1390
  }
1391
+ async materializeSession(input) {
1392
+ return this.authoringService.materializeSession(input);
1393
+ }
1183
1394
  async closeSession(input) {
1184
1395
  return this.runtimeService.closeSession(input);
1185
1396
  }
@@ -265,6 +265,44 @@ function toBackendDiagnostics(error) {
265
265
  opId: error.opId
266
266
  }];
267
267
  }
268
+ function createCapabilityUnavailableError$1(message) {
269
+ return {
270
+ code: "capability_unavailable",
271
+ message,
272
+ capability: "fileSystem",
273
+ requiredAdapter: "BackendFileSystem"
274
+ };
275
+ }
276
+ function validationDiagnostics(sessionProject) {
277
+ return (0, _openfairygui_core_uam.validateUamProject)(sessionProject).map((issue) => ({
278
+ code: "materialize_validation_failed",
279
+ message: issue.message,
280
+ severity: "error",
281
+ path: issue.path,
282
+ operationKind: "materializeSession"
283
+ }));
284
+ }
285
+ function toMaterializeSnapshot(session, capabilities, input) {
286
+ return {
287
+ ...toSessionSnapshot(session, capabilities),
288
+ mode: "fullProject",
289
+ reason: input.reason,
290
+ materializeRevision: session.revision,
291
+ saveRevision: session.lastSavedRevision,
292
+ writtenPaths: [...input.writtenPaths],
293
+ skippedPaths: [...input.skippedPaths],
294
+ diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic }))
295
+ };
296
+ }
297
+ function storageCanonicalTarget(input) {
298
+ const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || ".");
299
+ return {
300
+ fileSystem: input.fileSystem,
301
+ fairyPath: input.fairyPath,
302
+ canonicalProjectPath,
303
+ canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath)
304
+ };
305
+ }
268
306
  var AuthoringService = class {
269
307
  constructor(context, cacheService, eventService) {
270
308
  this.context = context;
@@ -329,15 +367,19 @@ var AuthoringService = class {
329
367
  });
330
368
  }
331
369
  async saveSession(input) {
370
+ if (input.force === true || input.mode === "materializeCleanSession") return this.materializeSession({
371
+ sessionId: input.sessionId,
372
+ expectedRevision: input.expectedRevision,
373
+ targetPath: input.targetPath,
374
+ fileSystem: input.fileSystem,
375
+ mode: "fullProject",
376
+ reason: "force_save"
377
+ });
332
378
  const startedAt = Date.now();
333
379
  const session = this.context.sessions.get(input.sessionId);
334
380
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
335
- if (!this.context.fileSystem) return failure("authoring", startedAt, {
336
- code: "capability_unavailable",
337
- message: "saveSession requires an injected BackendFileSystem adapter.",
338
- capability: "fileSystem",
339
- requiredAdapter: "BackendFileSystem"
340
- }, toSessionSnapshot(session, this.context.capabilities), {
381
+ const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
382
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
341
383
  sessionId: session.sessionId,
342
384
  revision: session.revision
343
385
  });
@@ -345,7 +387,6 @@ var AuthoringService = class {
345
387
  sessionId: session.sessionId,
346
388
  revision: session.revision
347
389
  });
348
- const fileSystem = this.context.fileSystem;
349
390
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
350
391
  if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
351
392
  sessionId: session.sessionId,
@@ -409,6 +450,154 @@ var AuthoringService = class {
409
450
  });
410
451
  }
411
452
  }
453
+ async materializeSession(input) {
454
+ const startedAt = Date.now();
455
+ const session = this.context.sessions.get(input.sessionId);
456
+ if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
457
+ if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
458
+ sessionId: session.sessionId,
459
+ revision: session.revision
460
+ });
461
+ const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
462
+ const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
463
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("materializeSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
464
+ sessionId: session.sessionId,
465
+ revision: session.revision
466
+ });
467
+ const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
468
+ const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
469
+ if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
470
+ sessionId: session.sessionId,
471
+ revision: session.revision
472
+ });
473
+ if (storageTarget) {
474
+ const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
475
+ if (holderSessionId && holderSessionId !== session.sessionId) return failure("authoring", startedAt, {
476
+ code: "lock_conflict",
477
+ kind: "in_process_session_exists",
478
+ message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
479
+ canonicalPathKey: storageTarget.canonicalPathKey,
480
+ holderSessionId
481
+ }, toSessionSnapshot(session, this.context.capabilities), {
482
+ sessionId: session.sessionId,
483
+ revision: session.revision
484
+ });
485
+ }
486
+ const diagnostics = validationDiagnostics(session.project);
487
+ if (diagnostics.length > 0) return failure("authoring", startedAt, {
488
+ code: "materialize_validation_failed",
489
+ message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
490
+ sessionId: session.sessionId,
491
+ canonicalPathKey: session.canonicalPathKey,
492
+ issueCount: diagnostics.length,
493
+ diagnostics
494
+ }, toSessionSnapshot(session, this.context.capabilities), {
495
+ sessionId: session.sessionId,
496
+ revision: session.revision,
497
+ diagnostics
498
+ });
499
+ let document;
500
+ try {
501
+ document = (0, _openfairygui_core_uam.materializeUamProject)(session.project);
502
+ } catch (error) {
503
+ const diagnosticsFromError = [{
504
+ code: "materialize_validation_failed",
505
+ message: error instanceof Error ? error.message : String(error),
506
+ severity: "error",
507
+ operationKind: "materializeSession"
508
+ }];
509
+ return failure("authoring", startedAt, {
510
+ code: "materialize_validation_failed",
511
+ message: error instanceof Error ? error.message : String(error),
512
+ sessionId: session.sessionId,
513
+ canonicalPathKey: session.canonicalPathKey,
514
+ issueCount: diagnosticsFromError.length,
515
+ diagnostics: diagnosticsFromError
516
+ }, toSessionSnapshot(session, this.context.capabilities), {
517
+ sessionId: session.sessionId,
518
+ revision: session.revision,
519
+ diagnostics: diagnosticsFromError
520
+ });
521
+ }
522
+ const writtenPaths = [];
523
+ const failedPaths = [];
524
+ const skippedPaths = [];
525
+ this.eventService.emit({
526
+ kind: "save.started",
527
+ sessionId: session.sessionId,
528
+ canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
529
+ revision: session.revision
530
+ });
531
+ try {
532
+ await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths)).write(document, fairyPath);
533
+ if (storageTarget) {
534
+ this.context.sessionsByPath.delete(session.canonicalPathKey);
535
+ session.fileSystem = storageTarget.fileSystem;
536
+ session.fairyPath = storageTarget.fairyPath;
537
+ session.canonicalProjectPath = storageTarget.canonicalProjectPath;
538
+ session.canonicalPathKey = storageTarget.canonicalPathKey;
539
+ this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
540
+ }
541
+ session.lastSavedRevision = session.revision;
542
+ session.dirty = false;
543
+ const cacheEntry = this.cacheService.refreshSession(session);
544
+ this.eventService.emit({
545
+ kind: "save.completed",
546
+ sessionId: session.sessionId,
547
+ canonicalPathKey: session.canonicalPathKey,
548
+ revision: session.revision
549
+ });
550
+ this.eventService.emit({
551
+ kind: "cache.updated",
552
+ sessionId: session.sessionId,
553
+ canonicalPathKey: session.canonicalPathKey,
554
+ revision: session.revision,
555
+ cacheRevision: cacheEntry.revision
556
+ });
557
+ return success("authoring", startedAt, toMaterializeSnapshot(session, this.context.capabilities, {
558
+ reason: input.reason,
559
+ writtenPaths,
560
+ skippedPaths,
561
+ diagnostics: []
562
+ }), {
563
+ sessionId: session.sessionId,
564
+ revision: session.revision
565
+ });
566
+ } catch (error) {
567
+ const diagnosticsFromError = [{
568
+ code: "write_failed",
569
+ message: error instanceof Error ? error.message : String(error),
570
+ severity: "error",
571
+ path: failedPaths[0],
572
+ operationKind: "materializeSession"
573
+ }];
574
+ this.cacheService.invalidateSession(session);
575
+ this.eventService.emit({
576
+ kind: "save.failed",
577
+ sessionId: session.sessionId,
578
+ canonicalPathKey: session.canonicalPathKey,
579
+ revision: session.revision,
580
+ diagnostics: diagnosticsFromError
581
+ });
582
+ return failure("authoring", startedAt, {
583
+ code: "write_failed",
584
+ message: error instanceof Error ? error.message : String(error),
585
+ sessionId: session.sessionId,
586
+ canonicalPathKey: session.canonicalPathKey,
587
+ attemptedRevision: session.revision,
588
+ lastSavedRevision: session.lastSavedRevision,
589
+ writtenPaths,
590
+ failedPaths,
591
+ skippedPaths,
592
+ diagnostics: diagnosticsFromError,
593
+ diskMayBePartiallyUpdated: true
594
+ }, toSessionSnapshot(session, this.context.capabilities), {
595
+ sessionId: session.sessionId,
596
+ revision: session.revision,
597
+ diagnostics: diagnosticsFromError
598
+ });
599
+ }
600
+ }
412
601
  };
413
602
  //#endregion
414
603
  //#region src/services/cache-service.ts
@@ -909,6 +1098,7 @@ var RuntimeService = class {
909
1098
  canonicalProjectPath,
910
1099
  canonicalPathKey,
911
1100
  lockFilePath,
1101
+ fileSystem,
912
1102
  project,
913
1103
  revision: 0,
914
1104
  lastSavedRevision: 0,
@@ -947,8 +1137,10 @@ var RuntimeService = class {
947
1137
  openProjectSession(input) {
948
1138
  const startedAt = Date.now();
949
1139
  const sessionId = input.sessionId ?? randomId();
950
- const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
951
- const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
1140
+ const storage = input.storage;
1141
+ const memoryProjectPath = `memory://${sessionId}`;
1142
+ const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
1143
+ const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
952
1144
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
953
1145
  if (existingSessionId) return failure("runtime", startedAt, {
954
1146
  code: "lock_conflict",
@@ -959,10 +1151,11 @@ var RuntimeService = class {
959
1151
  });
960
1152
  const session = {
961
1153
  sessionId,
962
- fairyPath: canonicalProjectPath,
1154
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
963
1155
  canonicalProjectPath,
964
1156
  canonicalPathKey,
965
1157
  lockFilePath: "",
1158
+ fileSystem: storage?.fileSystem,
966
1159
  project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
967
1160
  revision: 0,
968
1161
  lastSavedRevision: 0,
@@ -1026,6 +1219,7 @@ const BACKEND_METHODS = [
1026
1219
  "getSession",
1027
1220
  "applyTransaction",
1028
1221
  "saveSession",
1222
+ "materializeSession",
1029
1223
  "closeSession",
1030
1224
  "getEvents",
1031
1225
  "getJob",
@@ -1074,7 +1268,21 @@ function createCapabilities() {
1074
1268
  adapters: {
1075
1269
  fileSystem: {
1076
1270
  injected: true,
1077
- requiredFor: ["openSession", "saveSession"]
1271
+ requiredFor: [
1272
+ "openSession",
1273
+ "saveSession",
1274
+ "materializeSession"
1275
+ ]
1276
+ },
1277
+ projectStorage: {
1278
+ injected: true,
1279
+ browserSafe: true,
1280
+ requiredFor: [
1281
+ "openProjectSession.writeback",
1282
+ "saveSession",
1283
+ "materializeSession"
1284
+ ],
1285
+ adapterFactory: "createBackendStorageFileSystem"
1078
1286
  },
1079
1287
  host: {
1080
1288
  injected: true,
@@ -1181,6 +1389,9 @@ var BackendRuntime = class {
1181
1389
  async saveSession(input) {
1182
1390
  return this.authoringService.saveSession(input);
1183
1391
  }
1392
+ async materializeSession(input) {
1393
+ return this.authoringService.materializeSession(input);
1394
+ }
1184
1395
  async closeSession(input) {
1185
1396
  return this.runtimeService.closeSession(input);
1186
1397
  }