@openfairygui/backend 0.2.0-alpha.2 → 0.2.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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,48 @@ 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
+ };
160
+ }
161
+ async function writeSessionProject(input) {
162
+ await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(input.fileSystem, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, { staleSourceFiles: input.staleSourceFiles });
163
+ }
164
+ //#endregion
141
165
  //#region src/services/snapshot-utils.ts
142
166
  function cloneJsonValue(value) {
143
167
  if (value === void 0 || value === null) return value;
@@ -180,6 +204,7 @@ function toSessionSnapshot(session, capabilities) {
180
204
  revision: session.revision,
181
205
  lastSavedRevision: session.lastSavedRevision,
182
206
  dirty: session.dirty,
207
+ uamFidelity: session.uamFidelity,
183
208
  lockHeld: session.lockHeld,
184
209
  capabilities: cloneCapabilitiesSnapshot(capabilities)
185
210
  };
@@ -203,65 +228,137 @@ function createStaleWriteError(session, expectedRevision) {
203
228
  }
204
229
  //#endregion
205
230
  //#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;
231
+ function projectSourceFiles(project) {
232
+ const result = /* @__PURE__ */ new Map();
233
+ for (const pkg of project.packages) {
234
+ result.set(`${pkg.id}/package.xml`, {
235
+ packageName: pkg.name,
236
+ branch: "",
237
+ path: "",
238
+ fileName: "package.xml"
239
+ });
240
+ const branches = /* @__PURE__ */ new Set();
241
+ for (const resource of pkg.resources) {
242
+ if (resource.branch) branches.add(resource.branch);
243
+ const fileName = resource.kind === "component" ? `${resource.name}.xml` : resource.fileName ?? resource.file ?? "";
244
+ if (!fileName) continue;
245
+ result.set(`${pkg.id}/${resource.id}`, {
246
+ packageName: pkg.name,
247
+ branch: resource.branch,
248
+ path: resource.path,
249
+ fileName
250
+ });
215
251
  }
252
+ for (const branch of branches) result.set(`${pkg.id}/branch/${branch}`, {
253
+ packageName: pkg.name,
254
+ branch,
255
+ path: "",
256
+ fileName: "package_branch.xml"
257
+ });
216
258
  }
259
+ return result;
260
+ }
261
+ function sourceFileKey(source) {
262
+ return [
263
+ source.branch,
264
+ source.packageName,
265
+ source.path,
266
+ source.fileName
267
+ ].join("\0");
268
+ }
269
+ function recordStaleProjectFiles(session, previousProject, nextProject) {
270
+ if (!session.fileSystem) return;
271
+ const previousSources = projectSourceFiles(previousProject);
272
+ const nextSourceKeys = new Set([...projectSourceFiles(nextProject).values()].map(sourceFileKey));
273
+ for (const source of previousSources.values()) {
274
+ const key = sourceFileKey(source);
275
+ if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
276
+ }
277
+ for (const key of nextSourceKeys) session.pendingStaleSourceFiles.delete(key);
278
+ }
279
+ function toBackendDiagnostics(error) {
280
+ return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
281
+ code: error.code,
282
+ message: error.message,
283
+ severity: "error",
284
+ operationKind: error.operationKind,
285
+ opIndex: error.opIndex,
286
+ opId: error.opId
287
+ }];
288
+ }
289
+ function createCapabilityUnavailableError$1(message) {
217
290
  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
- }
291
+ code: "capability_unavailable",
292
+ message,
293
+ capability: "fileSystem",
294
+ requiredAdapter: "BackendFileSystem"
295
+ };
296
+ }
297
+ function createUamFidelityUnsupportedError(session) {
298
+ return {
299
+ code: "uam_fidelity_unsupported",
300
+ message: "The source project contains formal properties that the current UAM cannot preserve.",
301
+ sessionId: session.sessionId,
302
+ canonicalPathKey: session.canonicalPathKey
303
+ };
304
+ }
305
+ function validationDiagnostics(sessionProject) {
306
+ return (0, _openfairygui_core_uam.validateUamProject)(sessionProject).map((issue) => ({
307
+ code: "materialize_validation_failed",
308
+ message: issue.message,
309
+ severity: "error",
310
+ path: issue.path,
311
+ operationKind: "materializeSession"
312
+ }));
313
+ }
314
+ function toMaterializeSnapshot(session, capabilities, input) {
315
+ return {
316
+ ...toSessionSnapshot(session, capabilities),
317
+ mode: "fullProject",
318
+ reason: input.reason,
319
+ materializeRevision: session.revision,
320
+ saveRevision: session.lastSavedRevision,
321
+ writtenPaths: [...input.writtenPaths],
322
+ skippedPaths: [...input.skippedPaths],
323
+ diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic }))
324
+ };
325
+ }
326
+ function storageCanonicalTarget(input) {
327
+ const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || ".");
328
+ return {
329
+ fileSystem: input.fileSystem,
330
+ fairyPath: input.fairyPath,
331
+ canonicalProjectPath,
332
+ canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath)
256
333
  };
257
334
  }
258
335
  var AuthoringService = class {
336
+ sessionOperations = /* @__PURE__ */ new Map();
259
337
  constructor(context, cacheService, eventService) {
260
338
  this.context = context;
261
339
  this.cacheService = cacheService;
262
340
  this.eventService = eventService;
263
341
  }
342
+ async runSessionExclusive(sessionId, operation) {
343
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
344
+ let release = () => void 0;
345
+ const current = new Promise((resolve) => {
346
+ release = resolve;
347
+ });
348
+ const tail = previous.then(() => current);
349
+ this.sessionOperations.set(sessionId, tail);
350
+ await previous;
351
+ try {
352
+ return await operation();
353
+ } finally {
354
+ release();
355
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
356
+ }
357
+ }
264
358
  async applyTransaction(input) {
359
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
360
+ }
361
+ async applyTransactionExclusive(input) {
265
362
  const startedAt = Date.now();
266
363
  const session = this.context.sessions.get(input.sessionId);
267
364
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -282,22 +379,21 @@ var AuthoringService = class {
282
379
  operations: input.operations
283
380
  });
284
381
  if (result.ok === false) {
382
+ const diagnostics = toBackendDiagnostics(result.error);
285
383
  this.eventService.emit({
286
384
  kind: "transaction.rejected",
287
385
  sessionId: session.sessionId,
288
386
  canonicalPathKey: session.canonicalPathKey,
289
387
  revision: session.revision,
290
- diagnostics: result.error.issues?.map((issue) => ({
291
- code: result.error.code,
292
- message: issue.message,
293
- severity: "error"
294
- })) ?? []
388
+ diagnostics
295
389
  });
296
390
  return failure("authoring", startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
297
391
  sessionId: session.sessionId,
298
- revision: session.revision
392
+ revision: session.revision,
393
+ diagnostics
299
394
  });
300
395
  }
396
+ recordStaleProjectFiles(session, session.project, result.project);
301
397
  session.project = result.project;
302
398
  session.revision += 1;
303
399
  session.dirty = true;
@@ -321,15 +417,22 @@ var AuthoringService = class {
321
417
  });
322
418
  }
323
419
  async saveSession(input) {
420
+ if (input.force === true || input.mode === "materializeCleanSession") return this.materializeSession({
421
+ sessionId: input.sessionId,
422
+ expectedRevision: input.expectedRevision,
423
+ targetPath: input.targetPath,
424
+ fileSystem: input.fileSystem,
425
+ mode: "fullProject",
426
+ reason: "force_save"
427
+ });
428
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
429
+ }
430
+ async saveSessionExclusive(input) {
324
431
  const startedAt = Date.now();
325
432
  const session = this.context.sessions.get(input.sessionId);
326
433
  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), {
434
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
435
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
333
436
  sessionId: session.sessionId,
334
437
  revision: session.revision
335
438
  });
@@ -337,7 +440,6 @@ var AuthoringService = class {
337
440
  sessionId: session.sessionId,
338
441
  revision: session.revision
339
442
  });
340
- const fileSystem = this.context.fileSystem;
341
443
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
342
444
  if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
343
445
  sessionId: session.sessionId,
@@ -347,6 +449,10 @@ var AuthoringService = class {
347
449
  sessionId: session.sessionId,
348
450
  revision: session.revision
349
451
  });
452
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
453
+ sessionId: session.sessionId,
454
+ revision: session.revision
455
+ });
350
456
  const committedPaths = [];
351
457
  const failedPaths = [];
352
458
  this.eventService.emit({
@@ -356,7 +462,17 @@ var AuthoringService = class {
356
462
  revision: session.revision
357
463
  });
358
464
  try {
359
- await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write((0, _openfairygui_core_uam.materializeUamProject)(session.project), session.fairyPath);
465
+ await writeSessionProject({
466
+ fileSystem,
467
+ document: (0, _openfairygui_core_uam.materializeUamProject)(session.project),
468
+ fairyPath: session.fairyPath,
469
+ staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
470
+ writtenPaths: committedPaths,
471
+ failedPaths
472
+ });
473
+ session.fileSystem ??= fileSystem;
474
+ session.pendingStaleSourceFiles.clear();
475
+ (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
360
476
  session.lastSavedRevision = session.revision;
361
477
  session.dirty = false;
362
478
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -401,6 +517,172 @@ var AuthoringService = class {
401
517
  });
402
518
  }
403
519
  }
520
+ async materializeSession(input) {
521
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
522
+ }
523
+ async materializeSessionExclusive(input) {
524
+ const startedAt = Date.now();
525
+ const session = this.context.sessions.get(input.sessionId);
526
+ if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
527
+ if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
528
+ sessionId: session.sessionId,
529
+ revision: session.revision
530
+ });
531
+ const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
532
+ const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
533
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("materializeSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
534
+ sessionId: session.sessionId,
535
+ revision: session.revision
536
+ });
537
+ const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
538
+ const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
539
+ if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
540
+ sessionId: session.sessionId,
541
+ revision: session.revision
542
+ });
543
+ if (storageTarget) {
544
+ const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
545
+ if (holderSessionId && holderSessionId !== session.sessionId) return failure("authoring", startedAt, {
546
+ code: "lock_conflict",
547
+ kind: "in_process_session_exists",
548
+ message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
549
+ canonicalPathKey: storageTarget.canonicalPathKey,
550
+ holderSessionId
551
+ }, toSessionSnapshot(session, this.context.capabilities), {
552
+ sessionId: session.sessionId,
553
+ revision: session.revision
554
+ });
555
+ }
556
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
557
+ sessionId: session.sessionId,
558
+ revision: session.revision
559
+ });
560
+ const diagnostics = validationDiagnostics(session.project);
561
+ if (diagnostics.length > 0) return failure("authoring", startedAt, {
562
+ code: "materialize_validation_failed",
563
+ message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
564
+ sessionId: session.sessionId,
565
+ canonicalPathKey: session.canonicalPathKey,
566
+ issueCount: diagnostics.length,
567
+ diagnostics
568
+ }, toSessionSnapshot(session, this.context.capabilities), {
569
+ sessionId: session.sessionId,
570
+ revision: session.revision,
571
+ diagnostics
572
+ });
573
+ let document;
574
+ try {
575
+ document = (0, _openfairygui_core_uam.materializeUamProject)(session.project);
576
+ } catch (error) {
577
+ const diagnosticsFromError = [{
578
+ code: "materialize_validation_failed",
579
+ message: error instanceof Error ? error.message : String(error),
580
+ severity: "error",
581
+ operationKind: "materializeSession"
582
+ }];
583
+ return failure("authoring", startedAt, {
584
+ code: "materialize_validation_failed",
585
+ message: error instanceof Error ? error.message : String(error),
586
+ sessionId: session.sessionId,
587
+ canonicalPathKey: session.canonicalPathKey,
588
+ issueCount: diagnosticsFromError.length,
589
+ diagnostics: diagnosticsFromError
590
+ }, toSessionSnapshot(session, this.context.capabilities), {
591
+ sessionId: session.sessionId,
592
+ revision: session.revision,
593
+ diagnostics: diagnosticsFromError
594
+ });
595
+ }
596
+ const writtenPaths = [];
597
+ const failedPaths = [];
598
+ const skippedPaths = [];
599
+ this.eventService.emit({
600
+ kind: "save.started",
601
+ sessionId: session.sessionId,
602
+ canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
603
+ revision: session.revision
604
+ });
605
+ try {
606
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
607
+ await writeSessionProject({
608
+ fileSystem,
609
+ document,
610
+ fairyPath,
611
+ staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
612
+ writtenPaths,
613
+ failedPaths
614
+ });
615
+ if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
616
+ if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
617
+ if (isSessionStorageTarget || storageTarget) (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
618
+ if (storageTarget) {
619
+ this.context.sessionsByPath.delete(session.canonicalPathKey);
620
+ session.fileSystem = storageTarget.fileSystem;
621
+ session.fairyPath = storageTarget.fairyPath;
622
+ session.canonicalProjectPath = storageTarget.canonicalProjectPath;
623
+ session.canonicalPathKey = storageTarget.canonicalPathKey;
624
+ this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
625
+ }
626
+ session.lastSavedRevision = session.revision;
627
+ session.dirty = false;
628
+ const cacheEntry = this.cacheService.refreshSession(session);
629
+ this.eventService.emit({
630
+ kind: "save.completed",
631
+ sessionId: session.sessionId,
632
+ canonicalPathKey: session.canonicalPathKey,
633
+ revision: session.revision
634
+ });
635
+ this.eventService.emit({
636
+ kind: "cache.updated",
637
+ sessionId: session.sessionId,
638
+ canonicalPathKey: session.canonicalPathKey,
639
+ revision: session.revision,
640
+ cacheRevision: cacheEntry.revision
641
+ });
642
+ return success("authoring", startedAt, toMaterializeSnapshot(session, this.context.capabilities, {
643
+ reason: input.reason,
644
+ writtenPaths,
645
+ skippedPaths,
646
+ diagnostics: []
647
+ }), {
648
+ sessionId: session.sessionId,
649
+ revision: session.revision
650
+ });
651
+ } catch (error) {
652
+ const diagnosticsFromError = [{
653
+ code: "write_failed",
654
+ message: error instanceof Error ? error.message : String(error),
655
+ severity: "error",
656
+ path: failedPaths[0],
657
+ operationKind: "materializeSession"
658
+ }];
659
+ this.cacheService.invalidateSession(session);
660
+ this.eventService.emit({
661
+ kind: "save.failed",
662
+ sessionId: session.sessionId,
663
+ canonicalPathKey: session.canonicalPathKey,
664
+ revision: session.revision,
665
+ diagnostics: diagnosticsFromError
666
+ });
667
+ return failure("authoring", startedAt, {
668
+ code: "write_failed",
669
+ message: error instanceof Error ? error.message : String(error),
670
+ sessionId: session.sessionId,
671
+ canonicalPathKey: session.canonicalPathKey,
672
+ attemptedRevision: session.revision,
673
+ lastSavedRevision: session.lastSavedRevision,
674
+ writtenPaths,
675
+ failedPaths,
676
+ skippedPaths,
677
+ diagnostics: diagnosticsFromError,
678
+ diskMayBePartiallyUpdated: true
679
+ }, toSessionSnapshot(session, this.context.capabilities), {
680
+ sessionId: session.sessionId,
681
+ revision: session.revision,
682
+ diagnostics: diagnosticsFromError
683
+ });
684
+ }
685
+ }
404
686
  };
405
687
  //#endregion
406
688
  //#region src/services/cache-service.ts
@@ -859,6 +1141,68 @@ function createProjectReaderFileSystem(fileSystem) {
859
1141
  }
860
1142
  };
861
1143
  }
1144
+ function createCaptureFileSystem(files) {
1145
+ const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
1146
+ return {
1147
+ async readFile(filePath) {
1148
+ const value = files.get(normalize(filePath));
1149
+ if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
1150
+ return value;
1151
+ },
1152
+ async readFileRaw(filePath) {
1153
+ const value = files.get(normalize(filePath));
1154
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
1155
+ return value.slice();
1156
+ },
1157
+ async writeFile(filePath, content) {
1158
+ files.set(normalize(filePath), content);
1159
+ },
1160
+ async writeFileRaw(filePath, data) {
1161
+ files.set(normalize(filePath), data.slice());
1162
+ },
1163
+ async mkdir() {},
1164
+ async readdir() {
1165
+ return [];
1166
+ },
1167
+ async exists(filePath) {
1168
+ return files.has(normalize(filePath));
1169
+ },
1170
+ join(...paths) {
1171
+ return normalize(paths.filter(Boolean).join("/"));
1172
+ },
1173
+ dirname(filePath) {
1174
+ const normalized = normalize(filePath);
1175
+ const separator = normalized.lastIndexOf("/");
1176
+ return separator < 0 ? "" : normalized.slice(0, separator);
1177
+ },
1178
+ async unlink(filePath) {
1179
+ files.delete(normalize(filePath));
1180
+ }
1181
+ };
1182
+ }
1183
+ function capturedFilesEqual(left, right) {
1184
+ if (left.size !== right.size) return false;
1185
+ for (const [filePath, leftValue] of left) {
1186
+ const rightValue = right.get(filePath);
1187
+ if (typeof leftValue === "string") {
1188
+ if (leftValue !== rightValue) return false;
1189
+ continue;
1190
+ }
1191
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
1192
+ for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
1193
+ }
1194
+ return true;
1195
+ }
1196
+ async function hasFullUamFidelity(document, project) {
1197
+ const sourceFiles = /* @__PURE__ */ new Map();
1198
+ const materializedFiles = /* @__PURE__ */ new Map();
1199
+ try {
1200
+ await Promise.all([new _openfairygui_core_project_io.ProjectWriter(createCaptureFileSystem(sourceFiles)).write(document, "Project.fairy"), new _openfairygui_core_project_io.ProjectWriter(createCaptureFileSystem(materializedFiles)).write((0, _openfairygui_core_uam.materializeUamProject)(project), "Project.fairy")]);
1201
+ } catch {
1202
+ return false;
1203
+ }
1204
+ return capturedFilesEqual(sourceFiles, materializedFiles);
1205
+ }
862
1206
  var RuntimeService = class {
863
1207
  constructor(context, cacheService, eventService, jobService) {
864
1208
  this.context = context;
@@ -893,7 +1237,8 @@ var RuntimeService = class {
893
1237
  canonicalPathKey
894
1238
  }));
895
1239
  await advisoryLock.close();
896
- const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath));
1240
+ const document = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
1241
+ const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
897
1242
  const sessionId = randomId();
898
1243
  const session = {
899
1244
  sessionId,
@@ -901,9 +1246,12 @@ var RuntimeService = class {
901
1246
  canonicalProjectPath,
902
1247
  canonicalPathKey,
903
1248
  lockFilePath,
1249
+ fileSystem,
904
1250
  project,
1251
+ uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
905
1252
  revision: 0,
906
1253
  lastSavedRevision: 0,
1254
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
907
1255
  dirty: false,
908
1256
  lockHeld: true,
909
1257
  closed: false
@@ -939,8 +1287,10 @@ var RuntimeService = class {
939
1287
  openProjectSession(input) {
940
1288
  const startedAt = Date.now();
941
1289
  const sessionId = input.sessionId ?? randomId();
942
- const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
943
- const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
1290
+ const storage = input.storage;
1291
+ const memoryProjectPath = `memory://${sessionId}`;
1292
+ const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
1293
+ const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
944
1294
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
945
1295
  if (existingSessionId) return failure("runtime", startedAt, {
946
1296
  code: "lock_conflict",
@@ -951,13 +1301,16 @@ var RuntimeService = class {
951
1301
  });
952
1302
  const session = {
953
1303
  sessionId,
954
- fairyPath: canonicalProjectPath,
1304
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
955
1305
  canonicalProjectPath,
956
1306
  canonicalPathKey,
957
1307
  lockFilePath: "",
1308
+ fileSystem: storage?.fileSystem,
958
1309
  project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
1310
+ uamFidelity: "full",
959
1311
  revision: 0,
960
1312
  lastSavedRevision: 0,
1313
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
961
1314
  dirty: false,
962
1315
  lockHeld: false,
963
1316
  closed: false
@@ -1010,7 +1363,25 @@ var RuntimeService = class {
1010
1363
  }
1011
1364
  };
1012
1365
  //#endregion
1013
- //#region src/runtime.ts
1366
+ //#region src/services/artifact-service.ts
1367
+ function createArtifactCapabilities() {
1368
+ const bridge = {
1369
+ available: false,
1370
+ requiredHost: "node",
1371
+ executionBoundary: "external-bridge",
1372
+ bridgeEntrypoint: "@openfairygui/backend/node",
1373
+ reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
1374
+ };
1375
+ return {
1376
+ publish: false,
1377
+ restore: false,
1378
+ status: "bridge-required",
1379
+ publishBridge: bridge,
1380
+ restoreBridge: bridge
1381
+ };
1382
+ }
1383
+ //#endregion
1384
+ //#region src/runtime/capabilities.ts
1014
1385
  const BACKEND_METHODS = [
1015
1386
  "getCapabilities",
1016
1387
  "openSession",
@@ -1018,6 +1389,7 @@ const BACKEND_METHODS = [
1018
1389
  "getSession",
1019
1390
  "applyTransaction",
1020
1391
  "saveSession",
1392
+ "materializeSession",
1021
1393
  "closeSession",
1022
1394
  "getEvents",
1023
1395
  "getJob",
@@ -1051,6 +1423,11 @@ function createCapabilities() {
1051
1423
  resourceKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_MATERIALIZATION_SCOPE.resourceKinds],
1052
1424
  nodeKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_MATERIALIZATION_SCOPE.nodeKinds],
1053
1425
  gearKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_MATERIALIZATION_SCOPE.gearKinds],
1426
+ transactionScope: {
1427
+ resourceKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_TRANSACTION_SCOPE.resourceKinds],
1428
+ nodeKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_TRANSACTION_SCOPE.nodeKinds],
1429
+ gearKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_TRANSACTION_SCOPE.gearKinds]
1430
+ },
1054
1431
  unsupported: ["artifact.publish", "artifact.restore"]
1055
1432
  },
1056
1433
  artifact: createArtifactCapabilities(),
@@ -1061,7 +1438,21 @@ function createCapabilities() {
1061
1438
  adapters: {
1062
1439
  fileSystem: {
1063
1440
  injected: true,
1064
- requiredFor: ["openSession", "saveSession"]
1441
+ requiredFor: [
1442
+ "openSession",
1443
+ "saveSession",
1444
+ "materializeSession"
1445
+ ]
1446
+ },
1447
+ projectStorage: {
1448
+ injected: true,
1449
+ browserSafe: true,
1450
+ requiredFor: [
1451
+ "openProjectSession.writeback",
1452
+ "saveSession",
1453
+ "materializeSession"
1454
+ ],
1455
+ adapterFactory: "createBackendStorageFileSystem"
1065
1456
  },
1066
1457
  host: {
1067
1458
  injected: true,
@@ -1110,6 +1501,8 @@ function createCapabilities() {
1110
1501
  }
1111
1502
  };
1112
1503
  }
1504
+ //#endregion
1505
+ //#region src/runtime.ts
1113
1506
  var BackendRuntime = class {
1114
1507
  fileSystem;
1115
1508
  capabilities;
@@ -1168,6 +1561,9 @@ var BackendRuntime = class {
1168
1561
  async saveSession(input) {
1169
1562
  return this.authoringService.saveSession(input);
1170
1563
  }
1564
+ async materializeSession(input) {
1565
+ return this.authoringService.materializeSession(input);
1566
+ }
1171
1567
  async closeSession(input) {
1172
1568
  return this.runtimeService.closeSession(input);
1173
1569
  }