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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, liftDocumentToUamProject, materializeUamProject, normalizeUamProject, 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
@@ -179,6 +179,7 @@ function toSessionSnapshot(session, capabilities) {
179
179
  revision: session.revision,
180
180
  lastSavedRevision: session.lastSavedRevision,
181
181
  dirty: session.dirty,
182
+ uamFidelity: session.uamFidelity,
182
183
  lockHeld: session.lockHeld,
183
184
  capabilities: cloneCapabilitiesSnapshot(capabilities)
184
185
  };
@@ -251,9 +252,45 @@ function createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
251
252
  },
252
253
  dirname(filePath) {
253
254
  return fileSystem.dirname(filePath);
255
+ },
256
+ async unlink(filePath) {
257
+ await trackWrite(filePath, () => fileSystem.unlink(filePath));
254
258
  }
255
259
  };
256
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
+ }
257
294
  function toBackendDiagnostics(error) {
258
295
  return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
259
296
  code: error.code,
@@ -272,6 +309,14 @@ function createCapabilityUnavailableError$1(message) {
272
309
  requiredAdapter: "BackendFileSystem"
273
310
  };
274
311
  }
312
+ function createUamFidelityUnsupportedError(session) {
313
+ return {
314
+ code: "uam_fidelity_unsupported",
315
+ message: "The source project contains formal properties that the current UAM cannot preserve.",
316
+ sessionId: session.sessionId,
317
+ canonicalPathKey: session.canonicalPathKey
318
+ };
319
+ }
275
320
  function validationDiagnostics(sessionProject) {
276
321
  return validateUamProject(sessionProject).map((issue) => ({
277
322
  code: "materialize_validation_failed",
@@ -303,12 +348,32 @@ function storageCanonicalTarget(input) {
303
348
  };
304
349
  }
305
350
  var AuthoringService = class {
351
+ sessionOperations = /* @__PURE__ */ new Map();
306
352
  constructor(context, cacheService, eventService) {
307
353
  this.context = context;
308
354
  this.cacheService = cacheService;
309
355
  this.eventService = eventService;
310
356
  }
357
+ async runSessionExclusive(sessionId, operation) {
358
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
359
+ let release = () => void 0;
360
+ const current = new Promise((resolve) => {
361
+ release = resolve;
362
+ });
363
+ const tail = previous.then(() => current);
364
+ this.sessionOperations.set(sessionId, tail);
365
+ await previous;
366
+ try {
367
+ return await operation();
368
+ } finally {
369
+ release();
370
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
371
+ }
372
+ }
311
373
  async applyTransaction(input) {
374
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
375
+ }
376
+ async applyTransactionExclusive(input) {
312
377
  const startedAt = Date.now();
313
378
  const session = this.context.sessions.get(input.sessionId);
314
379
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -343,6 +408,7 @@ var AuthoringService = class {
343
408
  diagnostics
344
409
  });
345
410
  }
411
+ recordStaleResourceSources(session, session.project, result.project);
346
412
  session.project = result.project;
347
413
  session.revision += 1;
348
414
  session.dirty = true;
@@ -374,10 +440,13 @@ var AuthoringService = class {
374
440
  mode: "fullProject",
375
441
  reason: "force_save"
376
442
  });
443
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
444
+ }
445
+ async saveSessionExclusive(input) {
377
446
  const startedAt = Date.now();
378
447
  const session = this.context.sessions.get(input.sessionId);
379
448
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
380
- const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
449
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
381
450
  if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
382
451
  sessionId: session.sessionId,
383
452
  revision: session.revision
@@ -395,6 +464,10 @@ var AuthoringService = class {
395
464
  sessionId: session.sessionId,
396
465
  revision: session.revision
397
466
  });
467
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
468
+ sessionId: session.sessionId,
469
+ revision: session.revision
470
+ });
398
471
  const committedPaths = [];
399
472
  const failedPaths = [];
400
473
  this.eventService.emit({
@@ -404,7 +477,10 @@ var AuthoringService = class {
404
477
  revision: session.revision
405
478
  });
406
479
  try {
407
- await new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write(materializeUamProject(session.project), session.fairyPath);
480
+ await new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write(materializeUamProject(session.project), session.fairyPath, { staleSourceFiles: [...session.pendingStaleSourceFiles.values()] });
481
+ session.fileSystem ??= fileSystem;
482
+ session.pendingStaleSourceFiles.clear();
483
+ commitUamProjectSourcePaths(session.project);
408
484
  session.lastSavedRevision = session.revision;
409
485
  session.dirty = false;
410
486
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -450,6 +526,9 @@ var AuthoringService = class {
450
526
  }
451
527
  }
452
528
  async materializeSession(input) {
529
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
530
+ }
531
+ async materializeSessionExclusive(input) {
453
532
  const startedAt = Date.now();
454
533
  const session = this.context.sessions.get(input.sessionId);
455
534
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -482,6 +561,10 @@ var AuthoringService = class {
482
561
  revision: session.revision
483
562
  });
484
563
  }
564
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
565
+ sessionId: session.sessionId,
566
+ revision: session.revision
567
+ });
485
568
  const diagnostics = validationDiagnostics(session.project);
486
569
  if (diagnostics.length > 0) return failure("authoring", startedAt, {
487
570
  code: "materialize_validation_failed",
@@ -528,7 +611,12 @@ var AuthoringService = class {
528
611
  revision: session.revision
529
612
  });
530
613
  try {
531
- await new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths)).write(document, fairyPath);
614
+ const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
615
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
616
+ await writer.write(document, fairyPath, { staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [] });
617
+ if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
618
+ if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
619
+ if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
532
620
  if (storageTarget) {
533
621
  this.context.sessionsByPath.delete(session.canonicalPathKey);
534
622
  session.fileSystem = storageTarget.fileSystem;
@@ -1055,6 +1143,68 @@ function createProjectReaderFileSystem(fileSystem) {
1055
1143
  }
1056
1144
  };
1057
1145
  }
1146
+ function createCaptureFileSystem(files) {
1147
+ const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
1148
+ return {
1149
+ async readFile(filePath) {
1150
+ const value = files.get(normalize(filePath));
1151
+ if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
1152
+ return value;
1153
+ },
1154
+ async readFileRaw(filePath) {
1155
+ const value = files.get(normalize(filePath));
1156
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
1157
+ return value.slice();
1158
+ },
1159
+ async writeFile(filePath, content) {
1160
+ files.set(normalize(filePath), content);
1161
+ },
1162
+ async writeFileRaw(filePath, data) {
1163
+ files.set(normalize(filePath), data.slice());
1164
+ },
1165
+ async mkdir() {},
1166
+ async readdir() {
1167
+ return [];
1168
+ },
1169
+ async exists(filePath) {
1170
+ return files.has(normalize(filePath));
1171
+ },
1172
+ join(...paths) {
1173
+ return normalize(paths.filter(Boolean).join("/"));
1174
+ },
1175
+ dirname(filePath) {
1176
+ const normalized = normalize(filePath);
1177
+ const separator = normalized.lastIndexOf("/");
1178
+ return separator < 0 ? "" : normalized.slice(0, separator);
1179
+ },
1180
+ async unlink(filePath) {
1181
+ files.delete(normalize(filePath));
1182
+ }
1183
+ };
1184
+ }
1185
+ function capturedFilesEqual(left, right) {
1186
+ if (left.size !== right.size) return false;
1187
+ for (const [filePath, leftValue] of left) {
1188
+ const rightValue = right.get(filePath);
1189
+ if (typeof leftValue === "string") {
1190
+ if (leftValue !== rightValue) return false;
1191
+ continue;
1192
+ }
1193
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
1194
+ for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
1195
+ }
1196
+ return true;
1197
+ }
1198
+ async function hasFullUamFidelity(document, project) {
1199
+ const sourceFiles = /* @__PURE__ */ new Map();
1200
+ const materializedFiles = /* @__PURE__ */ new Map();
1201
+ try {
1202
+ await Promise.all([new ProjectWriter(createCaptureFileSystem(sourceFiles)).write(document, "Project.fairy"), new ProjectWriter(createCaptureFileSystem(materializedFiles)).write(materializeUamProject(project), "Project.fairy")]);
1203
+ } catch {
1204
+ return false;
1205
+ }
1206
+ return capturedFilesEqual(sourceFiles, materializedFiles);
1207
+ }
1058
1208
  var RuntimeService = class {
1059
1209
  constructor(context, cacheService, eventService, jobService) {
1060
1210
  this.context = context;
@@ -1089,7 +1239,8 @@ var RuntimeService = class {
1089
1239
  canonicalPathKey
1090
1240
  }));
1091
1241
  await advisoryLock.close();
1092
- const project = liftDocumentToUamProject(await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath));
1242
+ const document = await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
1243
+ const project = liftDocumentToUamProject(document);
1093
1244
  const sessionId = randomId();
1094
1245
  const session = {
1095
1246
  sessionId,
@@ -1099,8 +1250,10 @@ var RuntimeService = class {
1099
1250
  lockFilePath,
1100
1251
  fileSystem,
1101
1252
  project,
1253
+ uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
1102
1254
  revision: 0,
1103
1255
  lastSavedRevision: 0,
1256
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1104
1257
  dirty: false,
1105
1258
  lockHeld: true,
1106
1259
  closed: false
@@ -1156,8 +1309,10 @@ var RuntimeService = class {
1156
1309
  lockFilePath: "",
1157
1310
  fileSystem: storage?.fileSystem,
1158
1311
  project: normalizeUamProject(input.project),
1312
+ uamFidelity: "full",
1159
1313
  revision: 0,
1160
1314
  lastSavedRevision: 0,
1315
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1161
1316
  dirty: false,
1162
1317
  lockHeld: false,
1163
1318
  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.15",
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/core": "0.2.0-alpha.15",
56
+ "@openfairygui/functions": "0.2.0-alpha.15"
57
57
  },
58
58
  "devDependencies": {
59
59
  "ava": "^7.0.0",
package/src/index.ts CHANGED
@@ -1,13 +1,21 @@
1
1
  export {
2
- BackendRuntime,
2
+ BACKEND_CAPABILITY_SCHEMA_VERSION,
3
+ BACKEND_COMPATIBILITY_POLICY,
4
+ BACKEND_CONTRACT_VERSION,
5
+ type BackendDiagnostic,
6
+ type BackendMessage,
7
+ type BackendResponseMeta,
8
+ type BackendStage,
9
+ } from './contracts.js';
10
+ export {
3
11
  type AdvisoryLockConflictError,
4
12
  type ApplySessionTransactionInput,
5
13
  type BackendArtifactBridgeCapability,
6
14
  type BackendCacheEntry,
7
15
  type BackendCacheSnapshot,
16
+ type BackendCapabilities,
8
17
  type BackendCapabilityManifest,
9
18
  type BackendCapabilityUnavailableError,
10
- type BackendCapabilities,
11
19
  type BackendError,
12
20
  type BackendEvent,
13
21
  type BackendEventKind,
@@ -16,6 +24,7 @@ export {
16
24
  type BackendFileStat,
17
25
  type BackendFileSystem,
18
26
  type BackendHostAdapter,
27
+ type BackendJobErrors,
19
28
  type BackendJobKind,
20
29
  type BackendJobListSnapshot,
21
30
  type BackendJobListStatusFilter,
@@ -24,11 +33,12 @@ export {
24
33
  type BackendJobProgress,
25
34
  type BackendJobSnapshot,
26
35
  type BackendJobStatus,
27
- type BackendJobErrors,
36
+ type BackendProjectSessionStorage,
28
37
  type BackendResult,
38
+ BackendRuntime,
39
+ type BackendRuntimeOptions,
29
40
  type BackendSessionSnapshot,
30
41
  type BackendSuccess,
31
- type BackendRuntimeOptions,
32
42
  type CacheRefreshFailedError,
33
43
  type CancelJobInput,
34
44
  type EventCursorInvalidError,
@@ -43,25 +53,16 @@ export {
43
53
  type MaterializeValidationFailedError,
44
54
  type MaterializeWriteFailedError,
45
55
  type OpenProjectSessionInput,
46
- type BackendProjectSessionStorage,
47
56
  type RefreshCacheInput,
48
- type SaveSessionInput,
49
57
  type SavePartialFailureError,
58
+ type SaveSessionInput,
50
59
  type SessionNotFoundError,
51
60
  type SessionStaleWriteError,
61
+ type UamFidelityUnsupportedError,
52
62
  } from './runtime.js';
53
63
  export {
54
- createBackendStorageFileSystem,
55
64
  type BackendAsyncStorageAdapter,
56
65
  type BackendStorageFileSystem,
57
66
  type BackendStorageStatLike,
67
+ createBackendStorageFileSystem,
58
68
  } from './storage.js';
59
- export {
60
- type BackendDiagnostic,
61
- type BackendMessage,
62
- type BackendResponseMeta,
63
- type BackendStage,
64
- BACKEND_CAPABILITY_SCHEMA_VERSION,
65
- BACKEND_COMPATIBILITY_POLICY,
66
- BACKEND_CONTRACT_VERSION,
67
- } from './contracts.js';
package/src/runtime.ts CHANGED
@@ -181,6 +181,7 @@ export interface BackendSessionSnapshot {
181
181
  revision: number;
182
182
  lastSavedRevision: number;
183
183
  dirty: boolean;
184
+ uamFidelity: 'full' | 'unsupported';
184
185
  lockHeld: boolean;
185
186
  capabilities: BackendCapabilities;
186
187
  }
@@ -255,6 +256,13 @@ export interface SavePartialFailureError {
255
256
  diskMayBePartiallyUpdated: true;
256
257
  }
257
258
 
259
+ export interface UamFidelityUnsupportedError {
260
+ code: 'uam_fidelity_unsupported';
261
+ message: string;
262
+ sessionId: string;
263
+ canonicalPathKey: string;
264
+ }
265
+
258
266
  export interface MaterializeValidationFailedError {
259
267
  code: 'materialize_validation_failed';
260
268
  message: string;
@@ -458,6 +466,7 @@ export type BackendError =
458
466
  | InProcessLockConflictError
459
467
  | AdvisoryLockConflictError
460
468
  | SavePartialFailureError
469
+ | UamFidelityUnsupportedError
461
470
  | MaterializeValidationFailedError
462
471
  | MaterializeWriteFailedError
463
472
  | PathPolicyViolationError
@@ -704,12 +713,15 @@ export class BackendRuntime {
704
713
  return this.authoringService.applyTransaction(input);
705
714
  }
706
715
 
707
- public async saveSession(input: SaveSessionInput): Promise<
716
+ public async saveSession(
717
+ input: SaveSessionInput,
718
+ ): Promise<
708
719
  BackendResult<
709
720
  BackendSessionSnapshot | MaterializeSessionSnapshot,
710
721
  | SessionNotFoundError
711
722
  | SessionStaleWriteError
712
723
  | SavePartialFailureError
724
+ | UamFidelityUnsupportedError
713
725
  | MaterializeValidationFailedError
714
726
  | MaterializeWriteFailedError
715
727
  | PathPolicyViolationError
@@ -720,11 +732,14 @@ export class BackendRuntime {
720
732
  return this.authoringService.saveSession(input);
721
733
  }
722
734
 
723
- public async materializeSession(input: MaterializeSessionInput): Promise<
735
+ public async materializeSession(
736
+ input: MaterializeSessionInput,
737
+ ): Promise<
724
738
  BackendResult<
725
739
  MaterializeSessionSnapshot,
726
740
  | SessionNotFoundError
727
741
  | SessionStaleWriteError
742
+ | UamFidelityUnsupportedError
728
743
  | MaterializeValidationFailedError
729
744
  | MaterializeWriteFailedError
730
745
  | PathPolicyViolationError