@openfairygui/backend 0.2.0-alpha.1 → 0.2.0-alpha.13

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,5 +1,6 @@
1
- import { ProjectReader, ProjectWriter, UAM_SUPPORTED_MATERIALIZATION_SCOPE, liftDocumentToUamProject, materializeUamProject, normalizeUamProject } from "@openfairygui/core";
2
- import { applyUamTransactionApp } from "@openfairygui/functions";
1
+ import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, liftDocumentToUamProject, materializeUamProject, normalizeUamProject, validateUamProject } from "@openfairygui/core/uam";
2
+ import { ProjectReader, ProjectWriter } from "@openfairygui/core/project-io";
3
+ import { applyUamTransactionApp } from "@openfairygui/functions/uam";
3
4
  //#region src/contracts.ts
4
5
  const BACKEND_CONTRACT_VERSION = "1.1.0-p2";
5
6
  const BACKEND_CAPABILITY_SCHEMA_VERSION = 2;
@@ -253,6 +254,54 @@ function createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
253
254
  }
254
255
  };
255
256
  }
257
+ function toBackendDiagnostics(error) {
258
+ return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
259
+ code: error.code,
260
+ message: error.message,
261
+ severity: "error",
262
+ operationKind: error.operationKind,
263
+ opIndex: error.opIndex,
264
+ opId: error.opId
265
+ }];
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
+ }
256
305
  var AuthoringService = class {
257
306
  constructor(context, cacheService, eventService) {
258
307
  this.context = context;
@@ -280,20 +329,18 @@ var AuthoringService = class {
280
329
  operations: input.operations
281
330
  });
282
331
  if (result.ok === false) {
332
+ const diagnostics = toBackendDiagnostics(result.error);
283
333
  this.eventService.emit({
284
334
  kind: "transaction.rejected",
285
335
  sessionId: session.sessionId,
286
336
  canonicalPathKey: session.canonicalPathKey,
287
337
  revision: session.revision,
288
- diagnostics: result.error.issues?.map((issue) => ({
289
- code: result.error.code,
290
- message: issue.message,
291
- severity: "error"
292
- })) ?? []
338
+ diagnostics
293
339
  });
294
340
  return failure("authoring", startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
295
341
  sessionId: session.sessionId,
296
- revision: session.revision
342
+ revision: session.revision,
343
+ diagnostics
297
344
  });
298
345
  }
299
346
  session.project = result.project;
@@ -319,15 +366,19 @@ var AuthoringService = class {
319
366
  });
320
367
  }
321
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
+ });
322
377
  const startedAt = Date.now();
323
378
  const session = this.context.sessions.get(input.sessionId);
324
379
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
325
- if (!this.context.fileSystem) return failure("authoring", startedAt, {
326
- code: "capability_unavailable",
327
- message: "saveSession requires an injected BackendFileSystem adapter.",
328
- capability: "fileSystem",
329
- requiredAdapter: "BackendFileSystem"
330
- }, 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), {
331
382
  sessionId: session.sessionId,
332
383
  revision: session.revision
333
384
  });
@@ -335,7 +386,6 @@ var AuthoringService = class {
335
386
  sessionId: session.sessionId,
336
387
  revision: session.revision
337
388
  });
338
- const fileSystem = this.context.fileSystem;
339
389
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
340
390
  if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
341
391
  sessionId: session.sessionId,
@@ -399,6 +449,154 @@ var AuthoringService = class {
399
449
  });
400
450
  }
401
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
+ }
402
600
  };
403
601
  //#endregion
404
602
  //#region src/services/cache-service.ts
@@ -899,6 +1097,7 @@ var RuntimeService = class {
899
1097
  canonicalProjectPath,
900
1098
  canonicalPathKey,
901
1099
  lockFilePath,
1100
+ fileSystem,
902
1101
  project,
903
1102
  revision: 0,
904
1103
  lastSavedRevision: 0,
@@ -937,8 +1136,10 @@ var RuntimeService = class {
937
1136
  openProjectSession(input) {
938
1137
  const startedAt = Date.now();
939
1138
  const sessionId = input.sessionId ?? randomId();
940
- const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
941
- 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());
942
1143
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
943
1144
  if (existingSessionId) return failure("runtime", startedAt, {
944
1145
  code: "lock_conflict",
@@ -949,10 +1150,11 @@ var RuntimeService = class {
949
1150
  });
950
1151
  const session = {
951
1152
  sessionId,
952
- fairyPath: canonicalProjectPath,
1153
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
953
1154
  canonicalProjectPath,
954
1155
  canonicalPathKey,
955
1156
  lockFilePath: "",
1157
+ fileSystem: storage?.fileSystem,
956
1158
  project: normalizeUamProject(input.project),
957
1159
  revision: 0,
958
1160
  lastSavedRevision: 0,
@@ -1016,6 +1218,7 @@ const BACKEND_METHODS = [
1016
1218
  "getSession",
1017
1219
  "applyTransaction",
1018
1220
  "saveSession",
1221
+ "materializeSession",
1019
1222
  "closeSession",
1020
1223
  "getEvents",
1021
1224
  "getJob",
@@ -1049,6 +1252,11 @@ function createCapabilities() {
1049
1252
  resourceKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.resourceKinds],
1050
1253
  nodeKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.nodeKinds],
1051
1254
  gearKinds: [...UAM_SUPPORTED_MATERIALIZATION_SCOPE.gearKinds],
1255
+ transactionScope: {
1256
+ resourceKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.resourceKinds],
1257
+ nodeKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.nodeKinds],
1258
+ gearKinds: [...UAM_SUPPORTED_TRANSACTION_SCOPE.gearKinds]
1259
+ },
1052
1260
  unsupported: ["artifact.publish", "artifact.restore"]
1053
1261
  },
1054
1262
  artifact: createArtifactCapabilities(),
@@ -1059,7 +1267,21 @@ function createCapabilities() {
1059
1267
  adapters: {
1060
1268
  fileSystem: {
1061
1269
  injected: true,
1062
- 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"
1063
1285
  },
1064
1286
  host: {
1065
1287
  injected: true,
@@ -1166,6 +1388,9 @@ var BackendRuntime = class {
1166
1388
  async saveSession(input) {
1167
1389
  return this.authoringService.saveSession(input);
1168
1390
  }
1391
+ async materializeSession(input) {
1392
+ return this.authoringService.materializeSession(input);
1393
+ }
1169
1394
  async closeSession(input) {
1170
1395
  return this.runtimeService.closeSession(input);
1171
1396
  }