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

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.
@@ -180,6 +180,7 @@ function toSessionSnapshot(session, capabilities) {
180
180
  revision: session.revision,
181
181
  lastSavedRevision: session.lastSavedRevision,
182
182
  dirty: session.dirty,
183
+ uamFidelity: session.uamFidelity,
183
184
  lockHeld: session.lockHeld,
184
185
  capabilities: cloneCapabilitiesSnapshot(capabilities)
185
186
  };
@@ -258,17 +259,32 @@ function createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
258
259
  }
259
260
  };
260
261
  }
261
- function projectAssetSourceFiles(project) {
262
+ function projectSourceFiles(project) {
262
263
  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}`, {
264
+ for (const pkg of project.packages) {
265
+ result.set(`${pkg.id}/package.xml`, {
268
266
  packageName: pkg.name,
269
- branch: resource.branch,
270
- path: resource.path,
271
- fileName
267
+ branch: "",
268
+ path: "",
269
+ fileName: "package.xml"
270
+ });
271
+ const branches = /* @__PURE__ */ new Set();
272
+ for (const resource of pkg.resources) {
273
+ if (resource.branch) branches.add(resource.branch);
274
+ const fileName = resource.kind === "component" ? `${resource.name}.xml` : resource.fileName ?? resource.file ?? "";
275
+ if (!fileName) continue;
276
+ result.set(`${pkg.id}/${resource.id}`, {
277
+ packageName: pkg.name,
278
+ branch: resource.branch,
279
+ path: resource.path,
280
+ fileName
281
+ });
282
+ }
283
+ for (const branch of branches) result.set(`${pkg.id}/branch/${branch}`, {
284
+ packageName: pkg.name,
285
+ branch,
286
+ path: "",
287
+ fileName: "package_branch.xml"
272
288
  });
273
289
  }
274
290
  return result;
@@ -281,10 +297,10 @@ function sourceFileKey(source) {
281
297
  source.fileName
282
298
  ].join("\0");
283
299
  }
284
- function recordStaleResourceSources(session, previousProject, nextProject) {
300
+ function recordStaleProjectFiles(session, previousProject, nextProject) {
285
301
  if (!session.fileSystem) return;
286
- const previousSources = projectAssetSourceFiles(previousProject);
287
- const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
302
+ const previousSources = projectSourceFiles(previousProject);
303
+ const nextSourceKeys = new Set([...projectSourceFiles(nextProject).values()].map(sourceFileKey));
288
304
  for (const source of previousSources.values()) {
289
305
  const key = sourceFileKey(source);
290
306
  if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
@@ -309,6 +325,14 @@ function createCapabilityUnavailableError$1(message) {
309
325
  requiredAdapter: "BackendFileSystem"
310
326
  };
311
327
  }
328
+ function createUamFidelityUnsupportedError(session) {
329
+ return {
330
+ code: "uam_fidelity_unsupported",
331
+ message: "The source project contains formal properties that the current UAM cannot preserve.",
332
+ sessionId: session.sessionId,
333
+ canonicalPathKey: session.canonicalPathKey
334
+ };
335
+ }
312
336
  function validationDiagnostics(sessionProject) {
313
337
  return (0, _openfairygui_core_uam.validateUamProject)(sessionProject).map((issue) => ({
314
338
  code: "materialize_validation_failed",
@@ -340,12 +364,32 @@ function storageCanonicalTarget(input) {
340
364
  };
341
365
  }
342
366
  var AuthoringService = class {
367
+ sessionOperations = /* @__PURE__ */ new Map();
343
368
  constructor(context, cacheService, eventService) {
344
369
  this.context = context;
345
370
  this.cacheService = cacheService;
346
371
  this.eventService = eventService;
347
372
  }
373
+ async runSessionExclusive(sessionId, operation) {
374
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
375
+ let release = () => void 0;
376
+ const current = new Promise((resolve) => {
377
+ release = resolve;
378
+ });
379
+ const tail = previous.then(() => current);
380
+ this.sessionOperations.set(sessionId, tail);
381
+ await previous;
382
+ try {
383
+ return await operation();
384
+ } finally {
385
+ release();
386
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
387
+ }
388
+ }
348
389
  async applyTransaction(input) {
390
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
391
+ }
392
+ async applyTransactionExclusive(input) {
349
393
  const startedAt = Date.now();
350
394
  const session = this.context.sessions.get(input.sessionId);
351
395
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -380,7 +424,7 @@ var AuthoringService = class {
380
424
  diagnostics
381
425
  });
382
426
  }
383
- recordStaleResourceSources(session, session.project, result.project);
427
+ recordStaleProjectFiles(session, session.project, result.project);
384
428
  session.project = result.project;
385
429
  session.revision += 1;
386
430
  session.dirty = true;
@@ -412,6 +456,9 @@ var AuthoringService = class {
412
456
  mode: "fullProject",
413
457
  reason: "force_save"
414
458
  });
459
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
460
+ }
461
+ async saveSessionExclusive(input) {
415
462
  const startedAt = Date.now();
416
463
  const session = this.context.sessions.get(input.sessionId);
417
464
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -433,6 +480,10 @@ var AuthoringService = class {
433
480
  sessionId: session.sessionId,
434
481
  revision: session.revision
435
482
  });
483
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
484
+ sessionId: session.sessionId,
485
+ revision: session.revision
486
+ });
436
487
  const committedPaths = [];
437
488
  const failedPaths = [];
438
489
  this.eventService.emit({
@@ -491,6 +542,9 @@ var AuthoringService = class {
491
542
  }
492
543
  }
493
544
  async materializeSession(input) {
545
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
546
+ }
547
+ async materializeSessionExclusive(input) {
494
548
  const startedAt = Date.now();
495
549
  const session = this.context.sessions.get(input.sessionId);
496
550
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
@@ -523,6 +577,10 @@ var AuthoringService = class {
523
577
  revision: session.revision
524
578
  });
525
579
  }
580
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
581
+ sessionId: session.sessionId,
582
+ revision: session.revision
583
+ });
526
584
  const diagnostics = validationDiagnostics(session.project);
527
585
  if (diagnostics.length > 0) return failure("authoring", startedAt, {
528
586
  code: "materialize_validation_failed",
@@ -1101,6 +1159,68 @@ function createProjectReaderFileSystem(fileSystem) {
1101
1159
  }
1102
1160
  };
1103
1161
  }
1162
+ function createCaptureFileSystem(files) {
1163
+ const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
1164
+ return {
1165
+ async readFile(filePath) {
1166
+ const value = files.get(normalize(filePath));
1167
+ if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
1168
+ return value;
1169
+ },
1170
+ async readFileRaw(filePath) {
1171
+ const value = files.get(normalize(filePath));
1172
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
1173
+ return value.slice();
1174
+ },
1175
+ async writeFile(filePath, content) {
1176
+ files.set(normalize(filePath), content);
1177
+ },
1178
+ async writeFileRaw(filePath, data) {
1179
+ files.set(normalize(filePath), data.slice());
1180
+ },
1181
+ async mkdir() {},
1182
+ async readdir() {
1183
+ return [];
1184
+ },
1185
+ async exists(filePath) {
1186
+ return files.has(normalize(filePath));
1187
+ },
1188
+ join(...paths) {
1189
+ return normalize(paths.filter(Boolean).join("/"));
1190
+ },
1191
+ dirname(filePath) {
1192
+ const normalized = normalize(filePath);
1193
+ const separator = normalized.lastIndexOf("/");
1194
+ return separator < 0 ? "" : normalized.slice(0, separator);
1195
+ },
1196
+ async unlink(filePath) {
1197
+ files.delete(normalize(filePath));
1198
+ }
1199
+ };
1200
+ }
1201
+ function capturedFilesEqual(left, right) {
1202
+ if (left.size !== right.size) return false;
1203
+ for (const [filePath, leftValue] of left) {
1204
+ const rightValue = right.get(filePath);
1205
+ if (typeof leftValue === "string") {
1206
+ if (leftValue !== rightValue) return false;
1207
+ continue;
1208
+ }
1209
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
1210
+ for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
1211
+ }
1212
+ return true;
1213
+ }
1214
+ async function hasFullUamFidelity(document, project) {
1215
+ const sourceFiles = /* @__PURE__ */ new Map();
1216
+ const materializedFiles = /* @__PURE__ */ new Map();
1217
+ try {
1218
+ 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")]);
1219
+ } catch {
1220
+ return false;
1221
+ }
1222
+ return capturedFilesEqual(sourceFiles, materializedFiles);
1223
+ }
1104
1224
  var RuntimeService = class {
1105
1225
  constructor(context, cacheService, eventService, jobService) {
1106
1226
  this.context = context;
@@ -1135,7 +1255,8 @@ var RuntimeService = class {
1135
1255
  canonicalPathKey
1136
1256
  }));
1137
1257
  await advisoryLock.close();
1138
- const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true }));
1258
+ const document = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
1259
+ const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
1139
1260
  const sessionId = randomId();
1140
1261
  const session = {
1141
1262
  sessionId,
@@ -1145,6 +1266,7 @@ var RuntimeService = class {
1145
1266
  lockFilePath,
1146
1267
  fileSystem,
1147
1268
  project,
1269
+ uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
1148
1270
  revision: 0,
1149
1271
  lastSavedRevision: 0,
1150
1272
  pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
@@ -1203,6 +1325,7 @@ var RuntimeService = class {
1203
1325
  lockFilePath: "",
1204
1326
  fileSystem: storage?.fileSystem,
1205
1327
  project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
1328
+ uamFidelity: "full",
1206
1329
  revision: 0,
1207
1330
  lastSavedRevision: 0,
1208
1331
  pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/backend",
3
- "version": "0.2.0-alpha.14",
3
+ "version": "0.2.0-alpha.16",
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/functions": "0.2.0-alpha.14",
56
- "@openfairygui/core": "0.2.0-alpha.14"
55
+ "@openfairygui/functions": "0.2.0-alpha.16",
56
+ "@openfairygui/core": "0.2.0-alpha.16"
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