@openfairygui/backend 0.2.0-alpha.3 → 0.2.0-alpha.31

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,38 +120,50 @@ function failure(stage, startedAt, error, session, options) {
138
120
  };
139
121
  }
140
122
  //#endregion
141
- //#region src/services/snapshot-utils.ts
142
- function cloneJsonValue(value) {
143
- if (value === void 0 || value === null) return value;
144
- return JSON.parse(JSON.stringify(value));
145
- }
146
- function cloneEventSnapshot(event) {
147
- return {
148
- ...event,
149
- diagnostics: event.diagnostics.map((diagnostic) => ({ ...diagnostic })),
150
- payload: cloneJsonValue(event.payload)
151
- };
152
- }
153
- function cloneJobSnapshot(job) {
154
- return {
155
- ...job,
156
- diagnostics: job.diagnostics.map((diagnostic) => ({ ...diagnostic })),
157
- progress: job.progress ? { ...job.progress } : void 0,
158
- result: cloneJsonValue(job.result),
159
- error: cloneJsonValue(job.error)
160
- };
161
- }
162
- function cloneCacheEntrySnapshot(entry) {
163
- return {
164
- ...entry,
165
- summary: {
166
- ...entry.summary,
167
- diagnostics: entry.summary.diagnostics.map((diagnostic) => ({ ...diagnostic }))
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;
168
133
  }
134
+ }
135
+ return {
136
+ readFile: (path) => fileSystem.readFile(path),
137
+ readFileRaw: (path) => fileSystem.readFileRaw(path),
138
+ writeFile: (path, content) => trackWrite(path, async () => {
139
+ await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
140
+ await fileSystem.writeFile(path, content);
141
+ }),
142
+ writeFileRaw: (path, data) => trackWrite(path, async () => {
143
+ await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
144
+ await fileSystem.writeFileRaw(path, data);
145
+ }),
146
+ mkdir: (path) => fileSystem.mkdir(path, { recursive: true }),
147
+ readdir: (path) => fileSystem.readdir(path),
148
+ async exists(path) {
149
+ try {
150
+ await fileSystem.stat(path);
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ },
156
+ join: (...paths) => fileSystem.join(...paths),
157
+ dirname: (path) => fileSystem.dirname(path),
158
+ unlink: (path) => trackWrite(path, () => fileSystem.unlink(path)),
159
+ rmdir: (path) => trackWrite(path, () => fileSystem.rmdir(path))
169
160
  };
170
161
  }
171
- function cloneCapabilitiesSnapshot(capabilities) {
172
- return cloneJsonValue(capabilities);
162
+ async function writeSessionProject(input) {
163
+ await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(input.fileSystem, input.writtenPaths, input.failedPaths)).write(input.document, input.fairyPath, {
164
+ staleSourceFiles: input.staleSourceFiles,
165
+ staleResourceFolders: input.staleResourceFolders
166
+ });
173
167
  }
174
168
  //#endregion
175
169
  //#region src/services/session-utils.ts
@@ -180,8 +174,9 @@ function toSessionSnapshot(session, capabilities) {
180
174
  revision: session.revision,
181
175
  lastSavedRevision: session.lastSavedRevision,
182
176
  dirty: session.dirty,
177
+ uamFidelity: session.uamFidelity,
183
178
  lockHeld: session.lockHeld,
184
- capabilities: cloneCapabilitiesSnapshot(capabilities)
179
+ capabilities: structuredClone(capabilities)
185
180
  };
186
181
  }
187
182
  function createSessionNotFoundError(sessionId) {
@@ -203,65 +198,111 @@ function createStaleWriteError(session, expectedRevision) {
203
198
  }
204
199
  //#endregion
205
200
  //#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;
215
- }
216
- }
201
+ function sourceFileKey(source) {
202
+ return [
203
+ source.branch,
204
+ source.packageName,
205
+ source.path,
206
+ source.fileName
207
+ ].join("\0");
208
+ }
209
+ function resourceFolderKey(folder) {
210
+ return [
211
+ folder.branch,
212
+ folder.packageName,
213
+ folder.path
214
+ ].join("\0");
215
+ }
216
+ function recordStaleProjectFiles(session, previousProject, nextProject) {
217
+ if (!session.fileSystem) return;
218
+ for (const source of (0, _openfairygui_core_uam.staleSourceFiles)(previousProject, nextProject)) session.pendingStaleSourceFiles.set(sourceFileKey(source), source);
219
+ for (const source of (0, _openfairygui_core_uam.staleSourceFiles)(nextProject, previousProject)) session.pendingStaleSourceFiles.delete(sourceFileKey(source));
220
+ for (const folder of (0, _openfairygui_core_uam.staleResourceFolders)(previousProject, nextProject)) session.pendingStaleResourceFolders.set(resourceFolderKey(folder), folder);
221
+ for (const folder of (0, _openfairygui_core_uam.staleResourceFolders)(nextProject, previousProject)) session.pendingStaleResourceFolders.delete(resourceFolderKey(folder));
222
+ }
223
+ function toBackendDiagnostics(error) {
224
+ return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
225
+ code: error.code,
226
+ message: error.message,
227
+ severity: "error",
228
+ operationKind: error.operationKind,
229
+ opIndex: error.opIndex,
230
+ opId: error.opId
231
+ }];
232
+ }
233
+ function createCapabilityUnavailableError$1(message) {
217
234
  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
- }
235
+ code: "capability_unavailable",
236
+ message,
237
+ capability: "fileSystem",
238
+ requiredAdapter: "BackendFileSystem"
239
+ };
240
+ }
241
+ function createUamFidelityUnsupportedError(session) {
242
+ return {
243
+ code: "uam_fidelity_unsupported",
244
+ message: "The source project contains formal properties that the current UAM cannot preserve.",
245
+ sessionId: session.sessionId,
246
+ canonicalPathKey: session.canonicalPathKey
247
+ };
248
+ }
249
+ function validationDiagnostics(sessionProject) {
250
+ return (0, _openfairygui_core_uam.validateUamProject)(sessionProject).map((issue) => ({
251
+ code: "materialize_validation_failed",
252
+ message: issue.message,
253
+ severity: "error",
254
+ path: issue.path,
255
+ operationKind: "materializeSession"
256
+ }));
257
+ }
258
+ function toMaterializeSnapshot(session, capabilities, input) {
259
+ return {
260
+ ...toSessionSnapshot(session, capabilities),
261
+ mode: "fullProject",
262
+ reason: input.reason,
263
+ materializeRevision: session.revision,
264
+ saveRevision: session.lastSavedRevision,
265
+ writtenPaths: [...input.writtenPaths],
266
+ skippedPaths: [...input.skippedPaths],
267
+ diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic }))
268
+ };
269
+ }
270
+ function storageCanonicalTarget(input) {
271
+ const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || ".");
272
+ return {
273
+ fileSystem: input.fileSystem,
274
+ fairyPath: input.fairyPath,
275
+ canonicalProjectPath,
276
+ canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath)
256
277
  };
257
278
  }
258
279
  var AuthoringService = class {
280
+ sessionOperations = /* @__PURE__ */ new Map();
259
281
  constructor(context, cacheService, eventService) {
260
282
  this.context = context;
261
283
  this.cacheService = cacheService;
262
284
  this.eventService = eventService;
263
285
  }
286
+ async runSessionExclusive(sessionId, operation) {
287
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
288
+ let release = () => void 0;
289
+ const current = new Promise((resolve) => {
290
+ release = resolve;
291
+ });
292
+ const tail = previous.then(() => current);
293
+ this.sessionOperations.set(sessionId, tail);
294
+ await previous;
295
+ try {
296
+ return await operation();
297
+ } finally {
298
+ release();
299
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
300
+ }
301
+ }
264
302
  async applyTransaction(input) {
303
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
304
+ }
305
+ async applyTransactionExclusive(input) {
265
306
  const startedAt = Date.now();
266
307
  const session = this.context.sessions.get(input.sessionId);
267
308
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -282,22 +323,21 @@ var AuthoringService = class {
282
323
  operations: input.operations
283
324
  });
284
325
  if (result.ok === false) {
326
+ const diagnostics = toBackendDiagnostics(result.error);
285
327
  this.eventService.emit({
286
328
  kind: "transaction.rejected",
287
329
  sessionId: session.sessionId,
288
330
  canonicalPathKey: session.canonicalPathKey,
289
331
  revision: session.revision,
290
- diagnostics: result.error.issues?.map((issue) => ({
291
- code: result.error.code,
292
- message: issue.message,
293
- severity: "error"
294
- })) ?? []
332
+ diagnostics
295
333
  });
296
334
  return failure("authoring", startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
297
335
  sessionId: session.sessionId,
298
- revision: session.revision
336
+ revision: session.revision,
337
+ diagnostics
299
338
  });
300
339
  }
340
+ recordStaleProjectFiles(session, session.project, result.project);
301
341
  session.project = result.project;
302
342
  session.revision += 1;
303
343
  session.dirty = true;
@@ -321,15 +361,22 @@ var AuthoringService = class {
321
361
  });
322
362
  }
323
363
  async saveSession(input) {
364
+ if (input.force === true || input.mode === "materializeCleanSession") return this.materializeSession({
365
+ sessionId: input.sessionId,
366
+ expectedRevision: input.expectedRevision,
367
+ targetPath: input.targetPath,
368
+ fileSystem: input.fileSystem,
369
+ mode: "fullProject",
370
+ reason: "force_save"
371
+ });
372
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
373
+ }
374
+ async saveSessionExclusive(input) {
324
375
  const startedAt = Date.now();
325
376
  const session = this.context.sessions.get(input.sessionId);
326
377
  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), {
378
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
379
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
333
380
  sessionId: session.sessionId,
334
381
  revision: session.revision
335
382
  });
@@ -337,7 +384,6 @@ var AuthoringService = class {
337
384
  sessionId: session.sessionId,
338
385
  revision: session.revision
339
386
  });
340
- const fileSystem = this.context.fileSystem;
341
387
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
342
388
  if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
343
389
  sessionId: session.sessionId,
@@ -347,6 +393,10 @@ var AuthoringService = class {
347
393
  sessionId: session.sessionId,
348
394
  revision: session.revision
349
395
  });
396
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
397
+ sessionId: session.sessionId,
398
+ revision: session.revision
399
+ });
350
400
  const committedPaths = [];
351
401
  const failedPaths = [];
352
402
  this.eventService.emit({
@@ -356,7 +406,19 @@ var AuthoringService = class {
356
406
  revision: session.revision
357
407
  });
358
408
  try {
359
- await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write((0, _openfairygui_core_uam.materializeUamProject)(session.project), session.fairyPath);
409
+ await writeSessionProject({
410
+ fileSystem,
411
+ document: (0, _openfairygui_core_uam.materializeUamProject)(session.project),
412
+ fairyPath: session.fairyPath,
413
+ staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
414
+ staleResourceFolders: [...session.pendingStaleResourceFolders.values()],
415
+ writtenPaths: committedPaths,
416
+ failedPaths
417
+ });
418
+ session.fileSystem ??= fileSystem;
419
+ session.pendingStaleSourceFiles.clear();
420
+ session.pendingStaleResourceFolders.clear();
421
+ (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
360
422
  session.lastSavedRevision = session.revision;
361
423
  session.dirty = false;
362
424
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -401,6 +463,179 @@ var AuthoringService = class {
401
463
  });
402
464
  }
403
465
  }
466
+ async materializeSession(input) {
467
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
468
+ }
469
+ async materializeSessionExclusive(input) {
470
+ const startedAt = Date.now();
471
+ const session = this.context.sessions.get(input.sessionId);
472
+ if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
473
+ if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
474
+ sessionId: session.sessionId,
475
+ revision: session.revision
476
+ });
477
+ const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
478
+ const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
479
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("materializeSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
480
+ sessionId: session.sessionId,
481
+ revision: session.revision
482
+ });
483
+ const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
484
+ const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
485
+ if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
486
+ sessionId: session.sessionId,
487
+ revision: session.revision
488
+ });
489
+ if (storageTarget) {
490
+ const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
491
+ if (holderSessionId && holderSessionId !== session.sessionId) return failure("authoring", startedAt, {
492
+ code: "lock_conflict",
493
+ kind: "in_process_session_exists",
494
+ message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
495
+ canonicalPathKey: storageTarget.canonicalPathKey,
496
+ holderSessionId
497
+ }, toSessionSnapshot(session, this.context.capabilities), {
498
+ sessionId: session.sessionId,
499
+ revision: session.revision
500
+ });
501
+ }
502
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
503
+ sessionId: session.sessionId,
504
+ revision: session.revision
505
+ });
506
+ const diagnostics = validationDiagnostics(session.project);
507
+ if (diagnostics.length > 0) return failure("authoring", startedAt, {
508
+ code: "materialize_validation_failed",
509
+ message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
510
+ sessionId: session.sessionId,
511
+ canonicalPathKey: session.canonicalPathKey,
512
+ issueCount: diagnostics.length,
513
+ diagnostics
514
+ }, toSessionSnapshot(session, this.context.capabilities), {
515
+ sessionId: session.sessionId,
516
+ revision: session.revision,
517
+ diagnostics
518
+ });
519
+ let document;
520
+ try {
521
+ document = (0, _openfairygui_core_uam.materializeUamProject)(session.project);
522
+ } catch (error) {
523
+ const diagnosticsFromError = [{
524
+ code: "materialize_validation_failed",
525
+ message: error instanceof Error ? error.message : String(error),
526
+ severity: "error",
527
+ operationKind: "materializeSession"
528
+ }];
529
+ return failure("authoring", startedAt, {
530
+ code: "materialize_validation_failed",
531
+ message: error instanceof Error ? error.message : String(error),
532
+ sessionId: session.sessionId,
533
+ canonicalPathKey: session.canonicalPathKey,
534
+ issueCount: diagnosticsFromError.length,
535
+ diagnostics: diagnosticsFromError
536
+ }, toSessionSnapshot(session, this.context.capabilities), {
537
+ sessionId: session.sessionId,
538
+ revision: session.revision,
539
+ diagnostics: diagnosticsFromError
540
+ });
541
+ }
542
+ const writtenPaths = [];
543
+ const failedPaths = [];
544
+ const skippedPaths = [];
545
+ this.eventService.emit({
546
+ kind: "save.started",
547
+ sessionId: session.sessionId,
548
+ canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
549
+ revision: session.revision
550
+ });
551
+ try {
552
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
553
+ await writeSessionProject({
554
+ fileSystem,
555
+ document,
556
+ fairyPath,
557
+ staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
558
+ staleResourceFolders: isSessionStorageTarget ? [...session.pendingStaleResourceFolders.values()] : [],
559
+ writtenPaths,
560
+ failedPaths
561
+ });
562
+ if (isSessionStorageTarget) {
563
+ session.pendingStaleSourceFiles.clear();
564
+ session.pendingStaleResourceFolders.clear();
565
+ }
566
+ if (storageTarget && !isSessionStorageTarget) {
567
+ session.pendingStaleSourceFiles.clear();
568
+ session.pendingStaleResourceFolders.clear();
569
+ }
570
+ if (isSessionStorageTarget || storageTarget) (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
571
+ if (storageTarget) {
572
+ this.context.sessionsByPath.delete(session.canonicalPathKey);
573
+ session.fileSystem = storageTarget.fileSystem;
574
+ session.fairyPath = storageTarget.fairyPath;
575
+ session.canonicalProjectPath = storageTarget.canonicalProjectPath;
576
+ session.canonicalPathKey = storageTarget.canonicalPathKey;
577
+ this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
578
+ }
579
+ session.lastSavedRevision = session.revision;
580
+ session.dirty = false;
581
+ const cacheEntry = this.cacheService.refreshSession(session);
582
+ this.eventService.emit({
583
+ kind: "save.completed",
584
+ sessionId: session.sessionId,
585
+ canonicalPathKey: session.canonicalPathKey,
586
+ revision: session.revision
587
+ });
588
+ this.eventService.emit({
589
+ kind: "cache.updated",
590
+ sessionId: session.sessionId,
591
+ canonicalPathKey: session.canonicalPathKey,
592
+ revision: session.revision,
593
+ cacheRevision: cacheEntry.revision
594
+ });
595
+ return success("authoring", startedAt, toMaterializeSnapshot(session, this.context.capabilities, {
596
+ reason: input.reason,
597
+ writtenPaths,
598
+ skippedPaths,
599
+ diagnostics: []
600
+ }), {
601
+ sessionId: session.sessionId,
602
+ revision: session.revision
603
+ });
604
+ } catch (error) {
605
+ const diagnosticsFromError = [{
606
+ code: "write_failed",
607
+ message: error instanceof Error ? error.message : String(error),
608
+ severity: "error",
609
+ path: failedPaths[0],
610
+ operationKind: "materializeSession"
611
+ }];
612
+ this.cacheService.invalidateSession(session);
613
+ this.eventService.emit({
614
+ kind: "save.failed",
615
+ sessionId: session.sessionId,
616
+ canonicalPathKey: session.canonicalPathKey,
617
+ revision: session.revision,
618
+ diagnostics: diagnosticsFromError
619
+ });
620
+ return failure("authoring", startedAt, {
621
+ code: "write_failed",
622
+ message: error instanceof Error ? error.message : String(error),
623
+ sessionId: session.sessionId,
624
+ canonicalPathKey: session.canonicalPathKey,
625
+ attemptedRevision: session.revision,
626
+ lastSavedRevision: session.lastSavedRevision,
627
+ writtenPaths,
628
+ failedPaths,
629
+ skippedPaths,
630
+ diagnostics: diagnosticsFromError,
631
+ diskMayBePartiallyUpdated: true
632
+ }, toSessionSnapshot(session, this.context.capabilities), {
633
+ sessionId: session.sessionId,
634
+ revision: session.revision,
635
+ diagnostics: diagnosticsFromError
636
+ });
637
+ }
638
+ }
404
639
  };
405
640
  //#endregion
406
641
  //#region src/services/cache-service.ts
@@ -431,7 +666,7 @@ var CacheService = class {
431
666
  const entry = this.context.cacheBySession.get(input.sessionId);
432
667
  return success("read", startedAt, {
433
668
  cacheRevision: entry?.revision ?? session.revision,
434
- entries: entry ? [cloneCacheEntrySnapshot(entry)] : []
669
+ entries: entry ? [structuredClone(entry)] : []
435
670
  }, {
436
671
  sessionId: session.sessionId,
437
672
  revision: session.revision
@@ -502,7 +737,7 @@ var EventService = class {
502
737
  const filtered = events.filter((event) => event.sequence > after);
503
738
  const limit = input.limit === void 0 ? filtered.length : Math.max(0, input.limit);
504
739
  return success("runtime", startedAt, {
505
- events: filtered.slice(0, limit).map(cloneEventSnapshot),
740
+ events: filtered.slice(0, limit).map((event) => structuredClone(event)),
506
741
  oldestSequence,
507
742
  currentSequence,
508
743
  cursorExpired: false
@@ -577,7 +812,7 @@ var JobService = class {
577
812
  jobId
578
813
  });
579
814
  this.scheduleRefreshJob(session.sessionId, jobId);
580
- return success("runtime", startedAt, cloneJobSnapshot(job), {
815
+ return success("runtime", startedAt, structuredClone(job), {
581
816
  sessionId: session.sessionId,
582
817
  revision: session.revision
583
818
  });
@@ -596,7 +831,7 @@ var JobService = class {
596
831
  sessionId: session.sessionId,
597
832
  revision: session.revision
598
833
  });
599
- return success("runtime", startedAt, cloneJobSnapshot(job), {
834
+ return success("runtime", startedAt, structuredClone(job), {
600
835
  sessionId: session.sessionId,
601
836
  revision: session.revision
602
837
  });
@@ -611,7 +846,7 @@ var JobService = class {
611
846
  else if (input.status === "terminal") jobs = jobs.filter((job) => isTerminal(job.status));
612
847
  else jobs = jobs.filter((job) => job.status === input.status);
613
848
  if (input.limit !== void 0) jobs = jobs.slice(0, Math.max(0, input.limit));
614
- return success("runtime", startedAt, { jobs: jobs.map(cloneJobSnapshot) }, {
849
+ return success("runtime", startedAt, { jobs: jobs.map((job) => structuredClone(job)) }, {
615
850
  sessionId: session.sessionId,
616
851
  revision: session.revision
617
852
  });
@@ -648,7 +883,8 @@ var JobService = class {
648
883
  revision: session.revision,
649
884
  jobId: job.jobId
650
885
  });
651
- return success("runtime", startedAt, cloneJobSnapshot(this.cancelRefreshJob(session.sessionId, job)), {
886
+ const cancelled = this.cancelRefreshJob(session.sessionId, job);
887
+ return success("runtime", startedAt, structuredClone(cancelled), {
652
888
  sessionId: session.sessionId,
653
889
  revision: session.revision
654
890
  });
@@ -795,7 +1031,7 @@ var ReadService = class {
795
1031
  this.context = context;
796
1032
  }
797
1033
  getCapabilities() {
798
- return success("read", Date.now(), cloneCapabilitiesSnapshot(this.context.capabilities));
1034
+ return success("read", Date.now(), structuredClone(this.context.capabilities));
799
1035
  }
800
1036
  getSession(input) {
801
1037
  const startedAt = Date.now();
@@ -859,6 +1095,75 @@ function createProjectReaderFileSystem(fileSystem) {
859
1095
  }
860
1096
  };
861
1097
  }
1098
+ function createCaptureFileSystem(files, directories) {
1099
+ const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
1100
+ return {
1101
+ async readFile(filePath) {
1102
+ const value = files.get(normalize(filePath));
1103
+ if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
1104
+ return value;
1105
+ },
1106
+ async readFileRaw(filePath) {
1107
+ const value = files.get(normalize(filePath));
1108
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
1109
+ return value.slice();
1110
+ },
1111
+ async writeFile(filePath, content) {
1112
+ files.set(normalize(filePath), content);
1113
+ },
1114
+ async writeFileRaw(filePath, data) {
1115
+ files.set(normalize(filePath), data.slice());
1116
+ },
1117
+ async mkdir(dirPath) {
1118
+ directories.add(normalize(dirPath));
1119
+ },
1120
+ async readdir() {
1121
+ return [];
1122
+ },
1123
+ async exists(filePath) {
1124
+ return files.has(normalize(filePath));
1125
+ },
1126
+ join(...paths) {
1127
+ return normalize(paths.filter(Boolean).join("/"));
1128
+ },
1129
+ dirname(filePath) {
1130
+ const normalized = normalize(filePath);
1131
+ const separator = normalized.lastIndexOf("/");
1132
+ return separator < 0 ? "" : normalized.slice(0, separator);
1133
+ },
1134
+ async unlink(filePath) {
1135
+ files.delete(normalize(filePath));
1136
+ }
1137
+ };
1138
+ }
1139
+ function capturedFilesEqual(left, right) {
1140
+ if (left.size !== right.size) return false;
1141
+ for (const [filePath, leftValue] of left) {
1142
+ const rightValue = right.get(filePath);
1143
+ if (typeof leftValue === "string") {
1144
+ if (leftValue !== rightValue) return false;
1145
+ continue;
1146
+ }
1147
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
1148
+ for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
1149
+ }
1150
+ return true;
1151
+ }
1152
+ function capturedDirectoriesEqual(left, right) {
1153
+ return left.size === right.size && [...left].every((directory) => right.has(directory));
1154
+ }
1155
+ async function hasFullUamFidelity(document, project) {
1156
+ const sourceFiles = /* @__PURE__ */ new Map();
1157
+ const materializedFiles = /* @__PURE__ */ new Map();
1158
+ const sourceDirectories = /* @__PURE__ */ new Set();
1159
+ const materializedDirectories = /* @__PURE__ */ new Set();
1160
+ try {
1161
+ await Promise.all([new _openfairygui_core_project_io.ProjectWriter(createCaptureFileSystem(sourceFiles, sourceDirectories)).write(document, "Project.fairy"), new _openfairygui_core_project_io.ProjectWriter(createCaptureFileSystem(materializedFiles, materializedDirectories)).write((0, _openfairygui_core_uam.materializeUamProject)(project), "Project.fairy")]);
1162
+ } catch {
1163
+ return false;
1164
+ }
1165
+ return capturedFilesEqual(sourceFiles, materializedFiles) && capturedDirectoriesEqual(sourceDirectories, materializedDirectories);
1166
+ }
862
1167
  var RuntimeService = class {
863
1168
  constructor(context, cacheService, eventService, jobService) {
864
1169
  this.context = context;
@@ -893,7 +1198,8 @@ var RuntimeService = class {
893
1198
  canonicalPathKey
894
1199
  }));
895
1200
  await advisoryLock.close();
896
- const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath));
1201
+ const document = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
1202
+ const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
897
1203
  const sessionId = randomId();
898
1204
  const session = {
899
1205
  sessionId,
@@ -901,9 +1207,13 @@ var RuntimeService = class {
901
1207
  canonicalProjectPath,
902
1208
  canonicalPathKey,
903
1209
  lockFilePath,
1210
+ fileSystem,
904
1211
  project,
1212
+ uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
905
1213
  revision: 0,
906
1214
  lastSavedRevision: 0,
1215
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1216
+ pendingStaleResourceFolders: /* @__PURE__ */ new Map(),
907
1217
  dirty: false,
908
1218
  lockHeld: true,
909
1219
  closed: false
@@ -939,8 +1249,10 @@ var RuntimeService = class {
939
1249
  openProjectSession(input) {
940
1250
  const startedAt = Date.now();
941
1251
  const sessionId = input.sessionId ?? randomId();
942
- const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
943
- const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
1252
+ const storage = input.storage;
1253
+ const memoryProjectPath = `memory://${sessionId}`;
1254
+ const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
1255
+ const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
944
1256
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
945
1257
  if (existingSessionId) return failure("runtime", startedAt, {
946
1258
  code: "lock_conflict",
@@ -951,13 +1263,17 @@ var RuntimeService = class {
951
1263
  });
952
1264
  const session = {
953
1265
  sessionId,
954
- fairyPath: canonicalProjectPath,
1266
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
955
1267
  canonicalProjectPath,
956
1268
  canonicalPathKey,
957
1269
  lockFilePath: "",
1270
+ fileSystem: storage?.fileSystem,
958
1271
  project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
1272
+ uamFidelity: "full",
959
1273
  revision: 0,
960
1274
  lastSavedRevision: 0,
1275
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1276
+ pendingStaleResourceFolders: /* @__PURE__ */ new Map(),
961
1277
  dirty: false,
962
1278
  lockHeld: false,
963
1279
  closed: false
@@ -1010,7 +1326,25 @@ var RuntimeService = class {
1010
1326
  }
1011
1327
  };
1012
1328
  //#endregion
1013
- //#region src/runtime.ts
1329
+ //#region src/services/artifact-service.ts
1330
+ function createArtifactCapabilities() {
1331
+ const bridge = {
1332
+ available: false,
1333
+ requiredHost: "node",
1334
+ executionBoundary: "external-bridge",
1335
+ bridgeEntrypoint: "@openfairygui/backend/node",
1336
+ reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
1337
+ };
1338
+ return {
1339
+ publish: false,
1340
+ restore: false,
1341
+ status: "bridge-required",
1342
+ publishBridge: bridge,
1343
+ restoreBridge: bridge
1344
+ };
1345
+ }
1346
+ //#endregion
1347
+ //#region src/runtime/capabilities.ts
1014
1348
  const BACKEND_METHODS = [
1015
1349
  "getCapabilities",
1016
1350
  "openSession",
@@ -1018,6 +1352,7 @@ const BACKEND_METHODS = [
1018
1352
  "getSession",
1019
1353
  "applyTransaction",
1020
1354
  "saveSession",
1355
+ "materializeSession",
1021
1356
  "closeSession",
1022
1357
  "getEvents",
1023
1358
  "getJob",
@@ -1066,7 +1401,21 @@ function createCapabilities() {
1066
1401
  adapters: {
1067
1402
  fileSystem: {
1068
1403
  injected: true,
1069
- requiredFor: ["openSession", "saveSession"]
1404
+ requiredFor: [
1405
+ "openSession",
1406
+ "saveSession",
1407
+ "materializeSession"
1408
+ ]
1409
+ },
1410
+ projectStorage: {
1411
+ injected: true,
1412
+ browserSafe: true,
1413
+ requiredFor: [
1414
+ "openProjectSession.writeback",
1415
+ "saveSession",
1416
+ "materializeSession"
1417
+ ],
1418
+ adapterFactory: "createBackendStorageFileSystem"
1070
1419
  },
1071
1420
  host: {
1072
1421
  injected: true,
@@ -1115,6 +1464,8 @@ function createCapabilities() {
1115
1464
  }
1116
1465
  };
1117
1466
  }
1467
+ //#endregion
1468
+ //#region src/runtime.ts
1118
1469
  var BackendRuntime = class {
1119
1470
  fileSystem;
1120
1471
  capabilities;
@@ -1173,6 +1524,9 @@ var BackendRuntime = class {
1173
1524
  async saveSession(input) {
1174
1525
  return this.authoringService.saveSession(input);
1175
1526
  }
1527
+ async materializeSession(input) {
1528
+ return this.authoringService.materializeSession(input);
1529
+ }
1176
1530
  async closeSession(input) {
1177
1531
  return this.runtimeService.closeSession(input);
1178
1532
  }