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

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.
package/README.md CHANGED
@@ -34,7 +34,11 @@ It does **not** redefine transaction grammar or expose `Document`.
34
34
  It also does **not** implement MCP or any transport-specific wire protocol.
35
35
  The root `@openfairygui/backend` entrypoint is browser-safe: pure authoring sessions can run in memory,
36
36
  and browser editors can inject an async storage adapter for OPFS, IndexedDB, ZIP-backed virtual filesystems,
37
- or File System Access API bridges. The default Node filesystem/runtime lives under `@openfairygui/backend/node`.
37
+ or File System Access API bridges. Storage adapters must implement `unlink()` so resource rename/move/remove
38
+ can clean up stale source files. The default Node filesystem/runtime lives under `@openfairygui/backend/node`.
39
+ File-backed `openSession` hydrates primary resource bytes so browser-safe transactions can rename/move
40
+ assets or add/replace/remove binary resources. `saveSession` writes replacement bytes before it removes
41
+ stale source files, preserving the prior file when a write fails.
38
42
 
39
43
  ## Relationship to other packages
40
44
 
@@ -73,6 +77,7 @@ const fileSystem = createBackendStorageFileSystem({
73
77
  async mkdir(dirPath) { await storage.mkdir(dirPath); },
74
78
  async readdir(dirPath) { return storage.readdir(dirPath); },
75
79
  async exists(filePath) { return storage.exists(filePath); },
80
+ async unlink(filePath) { await storage.remove(filePath); },
76
81
  });
77
82
 
78
83
  const runtime = new BackendRuntime();
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_runtime = require("./runtime-GKzsXJdO.cjs");
2
+ const require_runtime = require("./runtime-DqApG30C.cjs");
3
3
  //#region src/storage.ts
4
4
  var StorageFileStat = class {
5
5
  constructor(kind) {
@@ -71,6 +71,7 @@ async function inferStat(storage, filePath) {
71
71
  }
72
72
  }
73
73
  function createBackendStorageFileSystem(storage) {
74
+ if (typeof storage.unlink !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide unlink() for project resource lifecycle writes.");
74
75
  const lockedPaths = /* @__PURE__ */ new Set();
75
76
  const fileSystem = {
76
77
  stat(filePath) {
@@ -128,8 +129,7 @@ function createBackendStorageFileSystem(storage) {
128
129
  unlink(filePath) {
129
130
  const resolved = fileSystem.resolve(filePath);
130
131
  lockedPaths.delete(resolved);
131
- if (storage.unlink) return storage.unlink(resolved);
132
- throw createPathError("ENOTSUP", "Storage adapter does not provide unlink().");
132
+ return storage.unlink(resolved);
133
133
  },
134
134
  join(...paths) {
135
135
  return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
package/dist/index.d.cts CHANGED
@@ -22,7 +22,7 @@ interface BackendAsyncStorageAdapter {
22
22
  stat?(filePath: string): Promise<BackendStorageStatLike>;
23
23
  resolvePath?(filePath: string): Promise<string>;
24
24
  openExclusive?(filePath: string): Promise<BackendFileHandle>;
25
- unlink?(filePath: string): Promise<void>;
25
+ unlink(filePath: string): Promise<void>;
26
26
  join?(...paths: string[]): string;
27
27
  dirname?(filePath: string): string;
28
28
  resolve?(...paths: string[]): string;
package/dist/index.d.mts CHANGED
@@ -22,7 +22,7 @@ interface BackendAsyncStorageAdapter {
22
22
  stat?(filePath: string): Promise<BackendStorageStatLike>;
23
23
  resolvePath?(filePath: string): Promise<string>;
24
24
  openExclusive?(filePath: string): Promise<BackendFileHandle>;
25
- unlink?(filePath: string): Promise<void>;
25
+ unlink(filePath: string): Promise<void>;
26
26
  join?(...paths: string[]): string;
27
27
  dirname?(filePath: string): string;
28
28
  resolve?(...paths: string[]): string;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as BACKEND_CONTRACT_VERSION, n as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BACKEND_COMPATIBILITY_POLICY, t as BackendRuntime } from "./runtime-DFatY9W0.mjs";
1
+ import { i as BACKEND_CONTRACT_VERSION, n as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BACKEND_COMPATIBILITY_POLICY, t as BackendRuntime } from "./runtime-B9z0n2yF.mjs";
2
2
  //#region src/storage.ts
3
3
  var StorageFileStat = class {
4
4
  constructor(kind) {
@@ -70,6 +70,7 @@ async function inferStat(storage, filePath) {
70
70
  }
71
71
  }
72
72
  function createBackendStorageFileSystem(storage) {
73
+ if (typeof storage.unlink !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide unlink() for project resource lifecycle writes.");
73
74
  const lockedPaths = /* @__PURE__ */ new Set();
74
75
  const fileSystem = {
75
76
  stat(filePath) {
@@ -127,8 +128,7 @@ function createBackendStorageFileSystem(storage) {
127
128
  unlink(filePath) {
128
129
  const resolved = fileSystem.resolve(filePath);
129
130
  lockedPaths.delete(resolved);
130
- if (storage.unlink) return storage.unlink(resolved);
131
- throw createPathError("ENOTSUP", "Storage adapter does not provide unlink().");
131
+ return storage.unlink(resolved);
132
132
  },
133
133
  join(...paths) {
134
134
  return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
package/dist/node.cjs CHANGED
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_runtime = require("./runtime-GKzsXJdO.cjs");
24
+ const require_runtime = require("./runtime-DqApG30C.cjs");
25
25
  let node_fs_promises = require("node:fs/promises");
26
26
  node_fs_promises = __toESM(node_fs_promises);
27
27
  let node_path = require("node:path");
package/dist/node.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as BackendRuntime } from "./runtime-DFatY9W0.mjs";
1
+ import { t as BackendRuntime } from "./runtime-B9z0n2yF.mjs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  //#region src/node.ts
@@ -1,4 +1,4 @@
1
- import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, liftDocumentToUamProject, materializeUamProject, normalizeUamProject, validateUamProject } from "@openfairygui/core/uam";
1
+ import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, commitUamProjectSourcePaths, 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
@@ -251,9 +251,45 @@ function createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
251
251
  },
252
252
  dirname(filePath) {
253
253
  return fileSystem.dirname(filePath);
254
+ },
255
+ async unlink(filePath) {
256
+ await trackWrite(filePath, () => fileSystem.unlink(filePath));
254
257
  }
255
258
  };
256
259
  }
260
+ function projectAssetSourceFiles(project) {
261
+ const result = /* @__PURE__ */ new Map();
262
+ for (const pkg of project.packages) for (const resource of pkg.resources) {
263
+ if (resource.kind === "component") continue;
264
+ const fileName = resource.fileName ?? resource.file ?? "";
265
+ if (!fileName) continue;
266
+ result.set(`${pkg.id}/${resource.id}`, {
267
+ packageName: pkg.name,
268
+ branch: resource.branch,
269
+ path: resource.path,
270
+ fileName
271
+ });
272
+ }
273
+ return result;
274
+ }
275
+ function sourceFileKey(source) {
276
+ return [
277
+ source.branch,
278
+ source.packageName,
279
+ source.path,
280
+ source.fileName
281
+ ].join("\0");
282
+ }
283
+ function recordStaleResourceSources(session, previousProject, nextProject) {
284
+ if (!session.fileSystem) return;
285
+ const previousSources = projectAssetSourceFiles(previousProject);
286
+ const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
287
+ for (const source of previousSources.values()) {
288
+ const key = sourceFileKey(source);
289
+ if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
290
+ }
291
+ for (const key of nextSourceKeys) session.pendingStaleSourceFiles.delete(key);
292
+ }
257
293
  function toBackendDiagnostics(error) {
258
294
  return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
259
295
  code: error.code,
@@ -343,6 +379,7 @@ var AuthoringService = class {
343
379
  diagnostics
344
380
  });
345
381
  }
382
+ recordStaleResourceSources(session, session.project, result.project);
346
383
  session.project = result.project;
347
384
  session.revision += 1;
348
385
  session.dirty = true;
@@ -377,7 +414,7 @@ var AuthoringService = class {
377
414
  const startedAt = Date.now();
378
415
  const session = this.context.sessions.get(input.sessionId);
379
416
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
380
- const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
417
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
381
418
  if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
382
419
  sessionId: session.sessionId,
383
420
  revision: session.revision
@@ -404,7 +441,10 @@ var AuthoringService = class {
404
441
  revision: session.revision
405
442
  });
406
443
  try {
407
- await new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write(materializeUamProject(session.project), session.fairyPath);
444
+ await new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write(materializeUamProject(session.project), session.fairyPath, { staleSourceFiles: [...session.pendingStaleSourceFiles.values()] });
445
+ session.fileSystem ??= fileSystem;
446
+ session.pendingStaleSourceFiles.clear();
447
+ commitUamProjectSourcePaths(session.project);
408
448
  session.lastSavedRevision = session.revision;
409
449
  session.dirty = false;
410
450
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -528,7 +568,12 @@ var AuthoringService = class {
528
568
  revision: session.revision
529
569
  });
530
570
  try {
531
- await new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths)).write(document, fairyPath);
571
+ const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
572
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
573
+ await writer.write(document, fairyPath, { staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [] });
574
+ if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
575
+ if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
576
+ if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
532
577
  if (storageTarget) {
533
578
  this.context.sessionsByPath.delete(session.canonicalPathKey);
534
579
  session.fileSystem = storageTarget.fileSystem;
@@ -1089,7 +1134,7 @@ var RuntimeService = class {
1089
1134
  canonicalPathKey
1090
1135
  }));
1091
1136
  await advisoryLock.close();
1092
- const project = liftDocumentToUamProject(await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath));
1137
+ const project = liftDocumentToUamProject(await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true }));
1093
1138
  const sessionId = randomId();
1094
1139
  const session = {
1095
1140
  sessionId,
@@ -1101,6 +1146,7 @@ var RuntimeService = class {
1101
1146
  project,
1102
1147
  revision: 0,
1103
1148
  lastSavedRevision: 0,
1149
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1104
1150
  dirty: false,
1105
1151
  lockHeld: true,
1106
1152
  closed: false
@@ -1158,6 +1204,7 @@ var RuntimeService = class {
1158
1204
  project: normalizeUamProject(input.project),
1159
1205
  revision: 0,
1160
1206
  lastSavedRevision: 0,
1207
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1161
1208
  dirty: false,
1162
1209
  lockHeld: false,
1163
1210
  closed: false
@@ -252,9 +252,45 @@ function createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
252
252
  },
253
253
  dirname(filePath) {
254
254
  return fileSystem.dirname(filePath);
255
+ },
256
+ async unlink(filePath) {
257
+ await trackWrite(filePath, () => fileSystem.unlink(filePath));
255
258
  }
256
259
  };
257
260
  }
261
+ function projectAssetSourceFiles(project) {
262
+ const result = /* @__PURE__ */ new Map();
263
+ for (const pkg of project.packages) for (const resource of pkg.resources) {
264
+ if (resource.kind === "component") continue;
265
+ const fileName = resource.fileName ?? resource.file ?? "";
266
+ if (!fileName) continue;
267
+ result.set(`${pkg.id}/${resource.id}`, {
268
+ packageName: pkg.name,
269
+ branch: resource.branch,
270
+ path: resource.path,
271
+ fileName
272
+ });
273
+ }
274
+ return result;
275
+ }
276
+ function sourceFileKey(source) {
277
+ return [
278
+ source.branch,
279
+ source.packageName,
280
+ source.path,
281
+ source.fileName
282
+ ].join("\0");
283
+ }
284
+ function recordStaleResourceSources(session, previousProject, nextProject) {
285
+ if (!session.fileSystem) return;
286
+ const previousSources = projectAssetSourceFiles(previousProject);
287
+ const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
288
+ for (const source of previousSources.values()) {
289
+ const key = sourceFileKey(source);
290
+ if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
291
+ }
292
+ for (const key of nextSourceKeys) session.pendingStaleSourceFiles.delete(key);
293
+ }
258
294
  function toBackendDiagnostics(error) {
259
295
  return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
260
296
  code: error.code,
@@ -344,6 +380,7 @@ var AuthoringService = class {
344
380
  diagnostics
345
381
  });
346
382
  }
383
+ recordStaleResourceSources(session, session.project, result.project);
347
384
  session.project = result.project;
348
385
  session.revision += 1;
349
386
  session.dirty = true;
@@ -378,7 +415,7 @@ var AuthoringService = class {
378
415
  const startedAt = Date.now();
379
416
  const session = this.context.sessions.get(input.sessionId);
380
417
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
381
- const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
418
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
382
419
  if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
383
420
  sessionId: session.sessionId,
384
421
  revision: session.revision
@@ -405,7 +442,10 @@ var AuthoringService = class {
405
442
  revision: session.revision
406
443
  });
407
444
  try {
408
- await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write((0, _openfairygui_core_uam.materializeUamProject)(session.project), session.fairyPath);
445
+ await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write((0, _openfairygui_core_uam.materializeUamProject)(session.project), session.fairyPath, { staleSourceFiles: [...session.pendingStaleSourceFiles.values()] });
446
+ session.fileSystem ??= fileSystem;
447
+ session.pendingStaleSourceFiles.clear();
448
+ (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
409
449
  session.lastSavedRevision = session.revision;
410
450
  session.dirty = false;
411
451
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -529,7 +569,12 @@ var AuthoringService = class {
529
569
  revision: session.revision
530
570
  });
531
571
  try {
532
- await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths)).write(document, fairyPath);
572
+ const writer = new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
573
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
574
+ await writer.write(document, fairyPath, { staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [] });
575
+ if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
576
+ if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
577
+ if (isSessionStorageTarget || storageTarget) (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
533
578
  if (storageTarget) {
534
579
  this.context.sessionsByPath.delete(session.canonicalPathKey);
535
580
  session.fileSystem = storageTarget.fileSystem;
@@ -1090,7 +1135,7 @@ var RuntimeService = class {
1090
1135
  canonicalPathKey
1091
1136
  }));
1092
1137
  await advisoryLock.close();
1093
- const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath));
1138
+ const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true }));
1094
1139
  const sessionId = randomId();
1095
1140
  const session = {
1096
1141
  sessionId,
@@ -1102,6 +1147,7 @@ var RuntimeService = class {
1102
1147
  project,
1103
1148
  revision: 0,
1104
1149
  lastSavedRevision: 0,
1150
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1105
1151
  dirty: false,
1106
1152
  lockHeld: true,
1107
1153
  closed: false
@@ -1159,6 +1205,7 @@ var RuntimeService = class {
1159
1205
  project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
1160
1206
  revision: 0,
1161
1207
  lastSavedRevision: 0,
1208
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1162
1209
  dirty: false,
1163
1210
  lockHeld: false,
1164
1211
  closed: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/backend",
3
- "version": "0.2.0-alpha.13",
3
+ "version": "0.2.0-alpha.14",
4
4
  "description": "FairyGUI Headless Authoring SDK — stateful backend runtime and session services.",
5
5
  "author": "OpenFairyGUI Contributors",
6
6
  "license": "MIT",
@@ -52,8 +52,8 @@
52
52
  "runtime"
53
53
  ],
54
54
  "dependencies": {
55
- "@openfairygui/core": "0.2.0-alpha.13",
56
- "@openfairygui/functions": "0.2.0-alpha.13"
55
+ "@openfairygui/functions": "0.2.0-alpha.14",
56
+ "@openfairygui/core": "0.2.0-alpha.14"
57
57
  },
58
58
  "devDependencies": {
59
59
  "ava": "^7.0.0",
@@ -1,5 +1,5 @@
1
- import { ProjectWriter, type FileSystem } from '@openfairygui/core/project-io';
2
- import { materializeUamProject, validateUamProject } from '@openfairygui/core/uam';
1
+ import { ProjectWriter, type FileSystem, type ProjectSourceFile } from '@openfairygui/core/project-io';
2
+ import { commitUamProjectSourcePaths, materializeUamProject, validateUamProject, type UamProject } from '@openfairygui/core/uam';
3
3
  import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
4
4
  import type { CacheService } from './cache-service.js';
5
5
  import { failure, success, type BackendContext } from './context.js';
@@ -79,9 +79,49 @@ function createWriterFileSystem(
79
79
  dirname(filePath: string): string {
80
80
  return fileSystem.dirname(filePath);
81
81
  },
82
+ async unlink(filePath: string): Promise<void> {
83
+ await trackWrite(filePath, () => fileSystem.unlink(filePath));
84
+ },
82
85
  };
83
86
  }
84
87
 
88
+ function projectAssetSourceFiles(project: UamProject): Map<string, ProjectSourceFile> {
89
+ const result = new Map<string, ProjectSourceFile>();
90
+ for (const pkg of project.packages) {
91
+ for (const resource of pkg.resources) {
92
+ if (resource.kind === 'component') continue;
93
+ const fileName = resource.fileName ?? resource.file ?? '';
94
+ if (!fileName) continue;
95
+ result.set(
96
+ `${pkg.id}/${resource.id}`,
97
+ { packageName: pkg.name, branch: resource.branch, path: resource.path, fileName },
98
+ );
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+
104
+ function sourceFileKey(source: ProjectSourceFile): string {
105
+ return [source.branch, source.packageName, source.path, source.fileName].join('\0');
106
+ }
107
+
108
+ function recordStaleResourceSources(
109
+ session: Parameters<typeof toSessionSnapshot>[0],
110
+ previousProject: UamProject,
111
+ nextProject: UamProject,
112
+ ): void {
113
+ if (!session.fileSystem) return;
114
+ const previousSources = projectAssetSourceFiles(previousProject);
115
+ const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
116
+ for (const source of previousSources.values()) {
117
+ const key = sourceFileKey(source);
118
+ if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
119
+ }
120
+ for (const key of nextSourceKeys) {
121
+ session.pendingStaleSourceFiles.delete(key);
122
+ }
123
+ }
124
+
85
125
  function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagnostic[] {
86
126
  return error.diagnostics.length > 0
87
127
  ? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
@@ -219,6 +259,7 @@ export class AuthoringService {
219
259
  );
220
260
  }
221
261
 
262
+ recordStaleResourceSources(session, session.project, result.project);
222
263
  session.project = result.project;
223
264
  session.revision += 1;
224
265
  session.dirty = true;
@@ -271,7 +312,7 @@ export class AuthoringService {
271
312
  if (!session || session.closed) {
272
313
  return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
273
314
  }
274
- const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
315
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
275
316
  if (!fileSystem) {
276
317
  return failure(
277
318
  'authoring',
@@ -328,7 +369,12 @@ export class AuthoringService {
328
369
  });
329
370
  try {
330
371
  const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
331
- await writer.write(materializeUamProject(session.project), session.fairyPath);
372
+ await writer.write(materializeUamProject(session.project), session.fairyPath, {
373
+ staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
374
+ });
375
+ session.fileSystem ??= fileSystem;
376
+ session.pendingStaleSourceFiles.clear();
377
+ commitUamProjectSourcePaths(session.project);
332
378
  session.lastSavedRevision = session.revision;
333
379
  session.dirty = false;
334
380
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -530,7 +576,13 @@ export class AuthoringService {
530
576
  });
531
577
  try {
532
578
  const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
533
- await writer.write(document, fairyPath);
579
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
580
+ await writer.write(document, fairyPath, {
581
+ staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
582
+ });
583
+ if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
584
+ if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
585
+ if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
534
586
  if (storageTarget) {
535
587
  this.context.sessionsByPath.delete(session.canonicalPathKey);
536
588
  session.fileSystem = storageTarget.fileSystem;
@@ -29,6 +29,8 @@ export interface BackendSessionState {
29
29
  project: import('@openfairygui/core/uam').UamProject;
30
30
  revision: number;
31
31
  lastSavedRevision: number;
32
+ /** Source files deferred until a successful replacement project write. */
33
+ pendingStaleSourceFiles: Map<string, import('@openfairygui/core/project-io').ProjectSourceFile>;
32
34
  dirty: boolean;
33
35
  lockHeld: boolean;
34
36
  closed: boolean;
@@ -129,7 +129,7 @@ export class RuntimeService {
129
129
  await advisoryLock.close();
130
130
 
131
131
  const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
132
- const project = liftDocumentToUamProject(await reader.read(fairyPath));
132
+ const project = liftDocumentToUamProject(await reader.read(fairyPath, { hydrateResourceBytes: true }));
133
133
  const sessionId = randomId();
134
134
  const session: BackendSessionState = {
135
135
  sessionId,
@@ -141,6 +141,7 @@ export class RuntimeService {
141
141
  project,
142
142
  revision: 0,
143
143
  lastSavedRevision: 0,
144
+ pendingStaleSourceFiles: new Map(),
144
145
  dirty: false,
145
146
  lockHeld: true,
146
147
  closed: false,
@@ -204,6 +205,7 @@ export class RuntimeService {
204
205
  project: normalizeUamProject(input.project),
205
206
  revision: 0,
206
207
  lastSavedRevision: 0,
208
+ pendingStaleSourceFiles: new Map(),
207
209
  dirty: false,
208
210
  lockHeld: false,
209
211
  closed: false,
package/src/storage.ts CHANGED
@@ -21,7 +21,7 @@ export interface BackendAsyncStorageAdapter {
21
21
  stat?(filePath: string): Promise<BackendStorageStatLike>;
22
22
  resolvePath?(filePath: string): Promise<string>;
23
23
  openExclusive?(filePath: string): Promise<BackendFileHandle>;
24
- unlink?(filePath: string): Promise<void>;
24
+ unlink(filePath: string): Promise<void>;
25
25
  join?(...paths: string[]): string;
26
26
  dirname?(filePath: string): string;
27
27
  resolve?(...paths: string[]): string;
@@ -114,6 +114,9 @@ async function inferStat(storage: BackendAsyncStorageAdapter, filePath: string):
114
114
  export type BackendStorageFileSystem = BackendFileSystem & CoreProjectFileSystem;
115
115
 
116
116
  export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem {
117
+ if (typeof storage.unlink !== 'function') {
118
+ throw createPathError('ENOTSUP', 'Storage adapter must provide unlink() for project resource lifecycle writes.');
119
+ }
117
120
  const lockedPaths = new Set<string>();
118
121
 
119
122
  const fileSystem: BackendStorageFileSystem = {
@@ -174,8 +177,7 @@ export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapt
174
177
  unlink(filePath: string): Promise<void> {
175
178
  const resolved = fileSystem.resolve(filePath);
176
179
  lockedPaths.delete(resolved);
177
- if (storage.unlink) return storage.unlink(resolved);
178
- throw createPathError('ENOTSUP', 'Storage adapter does not provide unlink().');
180
+ return storage.unlink(resolved);
179
181
  },
180
182
  join(...paths: string[]): string {
181
183
  return storage.join ? storage.join(...paths) : joinStoragePath(...paths);