@openfairygui/backend 0.2.0-alpha.9 → 0.2.1

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/dist/index.cjs CHANGED
@@ -1,5 +1,1585 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_runtime = require("./runtime-GKzsXJdO.cjs");
2
+ let _openfairygui_core_uam = require("@openfairygui/core/uam");
3
+ let _openfairygui_functions_uam = require("@openfairygui/functions/uam");
4
+ let _openfairygui_core_project_io = require("@openfairygui/core/project-io");
5
+ //#region src/contracts.ts
6
+ const BACKEND_CONTRACT_VERSION = "1.1.0-p2";
7
+ const BACKEND_CAPABILITY_SCHEMA_VERSION = 2;
8
+ const BACKEND_COMPATIBILITY_POLICY = {
9
+ incompatibleChange: "requires contractVersion bump",
10
+ capabilitySchemaChange: "requires capabilitySchemaVersion bump",
11
+ additiveChange: "allowed without breaking existing consumers"
12
+ };
13
+ //#endregion
14
+ //#region src/path-policy.ts
15
+ function normalizeComparablePath(value) {
16
+ const normalized = value.replace(/[/\\]+$/, "").replace(/\\/g, "/");
17
+ const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
18
+ const drivePrefix = driveMatch?.[1].toLowerCase() ?? "";
19
+ const remainder = driveMatch ? driveMatch[2] ?? "" : normalized;
20
+ const hasRoot = driveMatch ? true : remainder.startsWith("/");
21
+ const rawSegments = remainder.split("/").filter((segment) => segment.length > 0);
22
+ const segments = [];
23
+ for (const segment of rawSegments) {
24
+ if (segment === ".") continue;
25
+ if (segment === "..") {
26
+ if (segments.length > 0 && segments[segments.length - 1] !== "..") segments.pop();
27
+ else if (!hasRoot) segments.push("..");
28
+ continue;
29
+ }
30
+ segments.push(segment);
31
+ }
32
+ const joined = segments.join("/");
33
+ return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
34
+ }
35
+ function createRuntimePathPolicy() {
36
+ return {
37
+ canonicalization: "realpath+normalized-casefold",
38
+ sessionIdentity: "project-root",
39
+ saveTarget: "opened-project-only",
40
+ outputTargets: "deferred",
41
+ workspaceBoundary: "project-root-only"
42
+ };
43
+ }
44
+ async function resolveFairyPath(fileSystem, input) {
45
+ const resolvedInput = fileSystem.resolve(input);
46
+ const stat = await fileSystem.stat(resolvedInput);
47
+ if (stat.isFile() && resolvedInput.endsWith(".fairy")) return await fileSystem.resolvePath(resolvedInput);
48
+ if (stat.isDirectory()) {
49
+ const fairyFiles = (await fileSystem.readdir(resolvedInput)).filter((entry) => entry.endsWith(".fairy"));
50
+ if (fairyFiles.length === 1) return await fileSystem.resolvePath(fileSystem.join(resolvedInput, fairyFiles[0]));
51
+ if (fairyFiles.length > 1) throw new Error(`Multiple .fairy files found in ${resolvedInput}: ${fairyFiles.join(", ")}`);
52
+ throw new Error(`No .fairy file found in ${resolvedInput}`);
53
+ }
54
+ throw new Error(`Input is not a .fairy file or directory: ${resolvedInput}`);
55
+ }
56
+ async function resolveCanonicalProjectRoot(fileSystem, input) {
57
+ const fairyPath = await resolveFairyPath(fileSystem, input);
58
+ const canonicalProjectPath = await fileSystem.resolvePath(fileSystem.dirname(fairyPath));
59
+ return {
60
+ fairyPath,
61
+ canonicalProjectPath,
62
+ canonicalPathKey: normalizeComparablePath(canonicalProjectPath)
63
+ };
64
+ }
65
+ async function validateSaveTarget(fileSystem, openedFairyPath, targetPath) {
66
+ if (!targetPath) return null;
67
+ const attemptedPath = await fileSystem.resolvePath(fileSystem.resolve(targetPath));
68
+ const allowedPath = await fileSystem.resolvePath(openedFairyPath);
69
+ if (normalizeComparablePath(attemptedPath) === normalizeComparablePath(allowedPath)) return null;
70
+ return {
71
+ code: "path_policy_violation",
72
+ message: `Save target is restricted to the originally opened project file: ${allowedPath}`,
73
+ policy: "save_target",
74
+ attemptedPath,
75
+ allowedPath
76
+ };
77
+ }
78
+ //#endregion
79
+ //#region src/services/context.ts
80
+ function diagnosticFromError(error) {
81
+ return {
82
+ code: error.code,
83
+ message: error.message,
84
+ severity: "error"
85
+ };
86
+ }
87
+ function randomId$1() {
88
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
89
+ }
90
+ function createMeta(stage, startedAt, options) {
91
+ return {
92
+ requestId: options?.requestId ?? randomId$1(),
93
+ sessionId: options?.sessionId,
94
+ revision: options?.revision,
95
+ durationMs: Math.max(0, Date.now() - startedAt),
96
+ warnings: options?.warnings ?? [],
97
+ diagnostics: options?.diagnostics ?? [],
98
+ stage,
99
+ contractVersion: BACKEND_CONTRACT_VERSION,
100
+ capabilitySchemaVersion: 2
101
+ };
102
+ }
103
+ function success(stage, startedAt, data, options) {
104
+ return {
105
+ ok: true,
106
+ meta: createMeta(stage, startedAt, options),
107
+ data
108
+ };
109
+ }
110
+ function failure(stage, startedAt, error, session, options) {
111
+ const diagnostics = options?.diagnostics ?? [diagnosticFromError(error)];
112
+ return {
113
+ ok: false,
114
+ meta: createMeta(stage, startedAt, {
115
+ ...options,
116
+ diagnostics
117
+ }),
118
+ error,
119
+ session
120
+ };
121
+ }
122
+ //#endregion
123
+ //#region src/services/session-project-writer.ts
124
+ function createWriterFileSystem(fileSystem, writtenPaths, failedPaths) {
125
+ async function trackWrite(targetPath, write) {
126
+ try {
127
+ const result = await write();
128
+ writtenPaths.push(targetPath);
129
+ return result;
130
+ } catch (error) {
131
+ failedPaths.push(targetPath);
132
+ throw error;
133
+ }
134
+ }
135
+ return {
136
+ readFile: (path) => fileSystem.readFile(path),
137
+ readFileRaw: (path) => fileSystem.readFileRaw(path),
138
+ writeFile: (path, content) => trackWrite(path, async () => {
139
+ await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
140
+ await fileSystem.writeFile(path, content);
141
+ }),
142
+ writeFileRaw: (path, data) => trackWrite(path, async () => {
143
+ await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
144
+ await fileSystem.writeFileRaw(path, data);
145
+ }),
146
+ mkdir: (path) => fileSystem.mkdir(path, { recursive: true }),
147
+ readdir: (path) => fileSystem.readdir(path),
148
+ async exists(path) {
149
+ try {
150
+ await fileSystem.stat(path);
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ },
156
+ join: (...paths) => fileSystem.join(...paths),
157
+ dirname: (path) => fileSystem.dirname(path),
158
+ unlink: (path) => trackWrite(path, () => fileSystem.unlink(path)),
159
+ rmdir: (path) => trackWrite(path, () => fileSystem.rmdir(path))
160
+ };
161
+ }
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
+ staleBranchDirectories: input.staleBranchDirectories
167
+ });
168
+ }
169
+ //#endregion
170
+ //#region src/services/session-utils.ts
171
+ function toSessionSnapshot(session, capabilities) {
172
+ return {
173
+ sessionId: session.sessionId,
174
+ canonicalProjectPath: session.canonicalProjectPath,
175
+ revision: session.revision,
176
+ lastSavedRevision: session.lastSavedRevision,
177
+ dirty: session.dirty,
178
+ uamFidelity: session.uamFidelity,
179
+ lockHeld: session.lockHeld,
180
+ capabilities: structuredClone(capabilities)
181
+ };
182
+ }
183
+ function createSessionNotFoundError(sessionId) {
184
+ return {
185
+ code: "session_not_found",
186
+ message: `Session was not found: ${sessionId}`,
187
+ sessionId
188
+ };
189
+ }
190
+ function createStaleWriteError(session, expectedRevision) {
191
+ return {
192
+ code: "stale_write",
193
+ message: `Expected revision ${expectedRevision} does not match current revision ${session.revision}.`,
194
+ sessionId: session.sessionId,
195
+ canonicalPathKey: session.canonicalPathKey,
196
+ expectedRevision,
197
+ actualRevision: session.revision
198
+ };
199
+ }
200
+ //#endregion
201
+ //#region src/services/authoring-service.ts
202
+ function sourceFileKey(source) {
203
+ return [
204
+ source.branch,
205
+ source.packageName,
206
+ source.path,
207
+ source.fileName
208
+ ].join("\0");
209
+ }
210
+ function resourceFolderKey(folder) {
211
+ return [
212
+ folder.branch,
213
+ folder.packageName,
214
+ folder.path
215
+ ].join("\0");
216
+ }
217
+ function branchDirectoryKey(directory) {
218
+ return [directory.branch, directory.packageName ?? ""].join("\0");
219
+ }
220
+ function recordStaleProjectFiles(session, previousProject, nextProject) {
221
+ if (!session.fileSystem) return;
222
+ for (const source of (0, _openfairygui_core_uam.staleSourceFiles)(previousProject, nextProject)) session.pendingStaleSourceFiles.set(sourceFileKey(source), source);
223
+ for (const source of (0, _openfairygui_core_uam.staleSourceFiles)(nextProject, previousProject)) session.pendingStaleSourceFiles.delete(sourceFileKey(source));
224
+ for (const folder of (0, _openfairygui_core_uam.staleResourceFolders)(previousProject, nextProject)) session.pendingStaleResourceFolders.set(resourceFolderKey(folder), folder);
225
+ for (const folder of (0, _openfairygui_core_uam.staleResourceFolders)(nextProject, previousProject)) session.pendingStaleResourceFolders.delete(resourceFolderKey(folder));
226
+ for (const directory of (0, _openfairygui_core_uam.staleBranchDirectories)(previousProject, nextProject)) session.pendingStaleBranchDirectories.set(branchDirectoryKey(directory), directory);
227
+ for (const directory of (0, _openfairygui_core_uam.staleBranchDirectories)(nextProject, previousProject)) session.pendingStaleBranchDirectories.delete(branchDirectoryKey(directory));
228
+ }
229
+ function toBackendDiagnostics(error) {
230
+ return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
231
+ code: error.code,
232
+ message: error.message,
233
+ severity: "error",
234
+ operationKind: error.operationKind,
235
+ opIndex: error.opIndex,
236
+ opId: error.opId
237
+ }];
238
+ }
239
+ function createCapabilityUnavailableError$1(message) {
240
+ return {
241
+ code: "capability_unavailable",
242
+ message,
243
+ capability: "fileSystem",
244
+ requiredAdapter: "BackendFileSystem"
245
+ };
246
+ }
247
+ function createUamFidelityUnsupportedError(session) {
248
+ return {
249
+ code: "uam_fidelity_unsupported",
250
+ message: "The source project contains formal properties that the current UAM cannot preserve.",
251
+ sessionId: session.sessionId,
252
+ canonicalPathKey: session.canonicalPathKey
253
+ };
254
+ }
255
+ function validationDiagnostics(sessionProject) {
256
+ return (0, _openfairygui_core_uam.validateUamProject)(sessionProject).map((issue) => ({
257
+ code: "materialize_validation_failed",
258
+ message: issue.message,
259
+ severity: "error",
260
+ path: issue.path,
261
+ operationKind: "materializeSession"
262
+ }));
263
+ }
264
+ function toMaterializeSnapshot(session, capabilities, input) {
265
+ return {
266
+ ...toSessionSnapshot(session, capabilities),
267
+ mode: "fullProject",
268
+ reason: input.reason,
269
+ materializeRevision: session.revision,
270
+ saveRevision: session.lastSavedRevision,
271
+ writtenPaths: [...input.writtenPaths],
272
+ skippedPaths: [...input.skippedPaths],
273
+ diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic }))
274
+ };
275
+ }
276
+ function storageCanonicalTarget(input) {
277
+ const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || ".");
278
+ return {
279
+ fileSystem: input.fileSystem,
280
+ fairyPath: input.fairyPath,
281
+ canonicalProjectPath,
282
+ canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath)
283
+ };
284
+ }
285
+ function detachSharedByteViews(value, seen = /* @__PURE__ */ new WeakSet()) {
286
+ if (!value || typeof value !== "object" || seen.has(value)) return;
287
+ seen.add(value);
288
+ for (const [key, child] of Object.entries(value)) {
289
+ if (child instanceof Uint8Array) {
290
+ if (typeof SharedArrayBuffer !== "undefined" && child.buffer instanceof SharedArrayBuffer) value[key] = new Uint8Array(child);
291
+ continue;
292
+ }
293
+ detachSharedByteViews(child, seen);
294
+ }
295
+ }
296
+ var AuthoringService = class {
297
+ sessionOperations = /* @__PURE__ */ new Map();
298
+ constructor(context, cacheService, eventService) {
299
+ this.context = context;
300
+ this.cacheService = cacheService;
301
+ this.eventService = eventService;
302
+ }
303
+ async runSessionExclusive(sessionId, operation) {
304
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
305
+ let release = () => void 0;
306
+ const current = new Promise((resolve) => {
307
+ release = resolve;
308
+ });
309
+ const tail = previous.then(() => current);
310
+ this.sessionOperations.set(sessionId, tail);
311
+ await previous;
312
+ try {
313
+ return await operation();
314
+ } finally {
315
+ release();
316
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
317
+ }
318
+ }
319
+ async applyTransaction(input) {
320
+ const queuedInput = structuredClone(input);
321
+ detachSharedByteViews(queuedInput);
322
+ return this.runSessionExclusive(queuedInput.sessionId, () => this.applyTransactionExclusive(queuedInput));
323
+ }
324
+ async applyTransactionExclusive(input) {
325
+ const startedAt = Date.now();
326
+ const session = this.context.sessions.get(input.sessionId);
327
+ if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
328
+ if (input.expectedRevision !== session.revision) {
329
+ this.eventService.emit({
330
+ kind: "transaction.rejected",
331
+ sessionId: session.sessionId,
332
+ canonicalPathKey: session.canonicalPathKey,
333
+ revision: session.revision
334
+ });
335
+ return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
336
+ sessionId: session.sessionId,
337
+ revision: session.revision
338
+ });
339
+ }
340
+ const result = await (0, _openfairygui_functions_uam.applyUamTransactionAppAsync)({
341
+ project: session.project,
342
+ operations: input.operations
343
+ });
344
+ if (this.context.sessions.get(input.sessionId) !== session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
345
+ if (result.ok === false) {
346
+ const diagnostics = toBackendDiagnostics(result.error);
347
+ this.eventService.emit({
348
+ kind: "transaction.rejected",
349
+ sessionId: session.sessionId,
350
+ canonicalPathKey: session.canonicalPathKey,
351
+ revision: session.revision,
352
+ diagnostics
353
+ });
354
+ return failure("authoring", startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
355
+ sessionId: session.sessionId,
356
+ revision: session.revision,
357
+ diagnostics
358
+ });
359
+ }
360
+ recordStaleProjectFiles(session, session.project, result.project);
361
+ session.project = result.project;
362
+ session.revision += 1;
363
+ session.dirty = true;
364
+ const cacheEntry = this.cacheService.invalidateSession(session);
365
+ this.eventService.emit({
366
+ kind: "transaction.applied",
367
+ sessionId: session.sessionId,
368
+ canonicalPathKey: session.canonicalPathKey,
369
+ revision: session.revision
370
+ });
371
+ this.eventService.emit({
372
+ kind: "cache.invalidated",
373
+ sessionId: session.sessionId,
374
+ canonicalPathKey: session.canonicalPathKey,
375
+ revision: session.revision,
376
+ cacheRevision: cacheEntry.revision
377
+ });
378
+ return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
379
+ sessionId: session.sessionId,
380
+ revision: session.revision
381
+ });
382
+ }
383
+ async saveSession(input) {
384
+ if (input.force === true || input.mode === "materializeCleanSession") return this.materializeSession({
385
+ sessionId: input.sessionId,
386
+ expectedRevision: input.expectedRevision,
387
+ targetPath: input.targetPath,
388
+ fileSystem: input.fileSystem,
389
+ mode: "fullProject",
390
+ reason: "force_save"
391
+ });
392
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
393
+ }
394
+ async saveSessionExclusive(input) {
395
+ const startedAt = Date.now();
396
+ const session = this.context.sessions.get(input.sessionId);
397
+ if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
398
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
399
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
400
+ sessionId: session.sessionId,
401
+ revision: session.revision
402
+ });
403
+ if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
404
+ sessionId: session.sessionId,
405
+ revision: session.revision
406
+ });
407
+ const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
408
+ if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
409
+ sessionId: session.sessionId,
410
+ revision: session.revision
411
+ });
412
+ if (!session.dirty) return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
413
+ sessionId: session.sessionId,
414
+ revision: session.revision
415
+ });
416
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
417
+ sessionId: session.sessionId,
418
+ revision: session.revision
419
+ });
420
+ const committedPaths = [];
421
+ const failedPaths = [];
422
+ this.eventService.emit({
423
+ kind: "save.started",
424
+ sessionId: session.sessionId,
425
+ canonicalPathKey: session.canonicalPathKey,
426
+ revision: session.revision
427
+ });
428
+ try {
429
+ await writeSessionProject({
430
+ fileSystem,
431
+ document: (0, _openfairygui_core_uam.materializeUamProject)(session.project),
432
+ fairyPath: session.fairyPath,
433
+ staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
434
+ staleResourceFolders: [...session.pendingStaleResourceFolders.values()],
435
+ staleBranchDirectories: [...session.pendingStaleBranchDirectories.values()],
436
+ writtenPaths: committedPaths,
437
+ failedPaths
438
+ });
439
+ session.fileSystem ??= fileSystem;
440
+ session.pendingStaleSourceFiles.clear();
441
+ session.pendingStaleResourceFolders.clear();
442
+ session.pendingStaleBranchDirectories.clear();
443
+ (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
444
+ session.lastSavedRevision = session.revision;
445
+ session.dirty = false;
446
+ const cacheEntry = this.cacheService.refreshSession(session);
447
+ this.eventService.emit({
448
+ kind: "save.completed",
449
+ sessionId: session.sessionId,
450
+ canonicalPathKey: session.canonicalPathKey,
451
+ revision: session.revision
452
+ });
453
+ this.eventService.emit({
454
+ kind: "cache.updated",
455
+ sessionId: session.sessionId,
456
+ canonicalPathKey: session.canonicalPathKey,
457
+ revision: session.revision,
458
+ cacheRevision: cacheEntry.revision
459
+ });
460
+ return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
461
+ sessionId: session.sessionId,
462
+ revision: session.revision
463
+ });
464
+ } catch (error) {
465
+ this.cacheService.invalidateSession(session);
466
+ this.eventService.emit({
467
+ kind: "save.failed",
468
+ sessionId: session.sessionId,
469
+ canonicalPathKey: session.canonicalPathKey,
470
+ revision: session.revision
471
+ });
472
+ return failure("authoring", startedAt, {
473
+ code: "save_partial_failure",
474
+ message: error instanceof Error ? error.message : String(error),
475
+ sessionId: session.sessionId,
476
+ canonicalPathKey: session.canonicalPathKey,
477
+ attemptedRevision: session.revision,
478
+ lastSavedRevision: session.lastSavedRevision,
479
+ committedPaths,
480
+ failedPaths,
481
+ diskMayBePartiallyUpdated: true
482
+ }, toSessionSnapshot(session, this.context.capabilities), {
483
+ sessionId: session.sessionId,
484
+ revision: session.revision
485
+ });
486
+ }
487
+ }
488
+ async materializeSession(input) {
489
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
490
+ }
491
+ async materializeSessionExclusive(input) {
492
+ const startedAt = Date.now();
493
+ const session = this.context.sessions.get(input.sessionId);
494
+ if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
495
+ if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
496
+ sessionId: session.sessionId,
497
+ revision: session.revision
498
+ });
499
+ const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
500
+ const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
501
+ if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("materializeSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
502
+ sessionId: session.sessionId,
503
+ revision: session.revision
504
+ });
505
+ const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
506
+ const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
507
+ if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
508
+ sessionId: session.sessionId,
509
+ revision: session.revision
510
+ });
511
+ if (storageTarget) {
512
+ const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
513
+ if (holderSessionId && holderSessionId !== session.sessionId) return failure("authoring", startedAt, {
514
+ code: "lock_conflict",
515
+ kind: "in_process_session_exists",
516
+ message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
517
+ canonicalPathKey: storageTarget.canonicalPathKey,
518
+ holderSessionId
519
+ }, toSessionSnapshot(session, this.context.capabilities), {
520
+ sessionId: session.sessionId,
521
+ revision: session.revision
522
+ });
523
+ }
524
+ if (session.uamFidelity === "unsupported") return failure("authoring", startedAt, createUamFidelityUnsupportedError(session), toSessionSnapshot(session, this.context.capabilities), {
525
+ sessionId: session.sessionId,
526
+ revision: session.revision
527
+ });
528
+ const diagnostics = validationDiagnostics(session.project);
529
+ if (diagnostics.length > 0) return failure("authoring", startedAt, {
530
+ code: "materialize_validation_failed",
531
+ message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
532
+ sessionId: session.sessionId,
533
+ canonicalPathKey: session.canonicalPathKey,
534
+ issueCount: diagnostics.length,
535
+ diagnostics
536
+ }, toSessionSnapshot(session, this.context.capabilities), {
537
+ sessionId: session.sessionId,
538
+ revision: session.revision,
539
+ diagnostics
540
+ });
541
+ let document;
542
+ try {
543
+ document = (0, _openfairygui_core_uam.materializeUamProject)(session.project);
544
+ } catch (error) {
545
+ const diagnosticsFromError = [{
546
+ code: "materialize_validation_failed",
547
+ message: error instanceof Error ? error.message : String(error),
548
+ severity: "error",
549
+ operationKind: "materializeSession"
550
+ }];
551
+ return failure("authoring", startedAt, {
552
+ code: "materialize_validation_failed",
553
+ message: error instanceof Error ? error.message : String(error),
554
+ sessionId: session.sessionId,
555
+ canonicalPathKey: session.canonicalPathKey,
556
+ issueCount: diagnosticsFromError.length,
557
+ diagnostics: diagnosticsFromError
558
+ }, toSessionSnapshot(session, this.context.capabilities), {
559
+ sessionId: session.sessionId,
560
+ revision: session.revision,
561
+ diagnostics: diagnosticsFromError
562
+ });
563
+ }
564
+ const writtenPaths = [];
565
+ const failedPaths = [];
566
+ const skippedPaths = [];
567
+ this.eventService.emit({
568
+ kind: "save.started",
569
+ sessionId: session.sessionId,
570
+ canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
571
+ revision: session.revision
572
+ });
573
+ try {
574
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
575
+ await writeSessionProject({
576
+ fileSystem,
577
+ document,
578
+ fairyPath,
579
+ staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
580
+ staleResourceFolders: isSessionStorageTarget ? [...session.pendingStaleResourceFolders.values()] : [],
581
+ staleBranchDirectories: isSessionStorageTarget ? [...session.pendingStaleBranchDirectories.values()] : [],
582
+ writtenPaths,
583
+ failedPaths
584
+ });
585
+ if (isSessionStorageTarget) {
586
+ session.pendingStaleSourceFiles.clear();
587
+ session.pendingStaleResourceFolders.clear();
588
+ session.pendingStaleBranchDirectories.clear();
589
+ }
590
+ if (storageTarget && !isSessionStorageTarget) {
591
+ session.pendingStaleSourceFiles.clear();
592
+ session.pendingStaleResourceFolders.clear();
593
+ session.pendingStaleBranchDirectories.clear();
594
+ }
595
+ if (isSessionStorageTarget || storageTarget) (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
596
+ if (storageTarget) {
597
+ this.context.sessionsByPath.delete(session.canonicalPathKey);
598
+ session.fileSystem = storageTarget.fileSystem;
599
+ session.fairyPath = storageTarget.fairyPath;
600
+ session.canonicalProjectPath = storageTarget.canonicalProjectPath;
601
+ session.canonicalPathKey = storageTarget.canonicalPathKey;
602
+ this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
603
+ }
604
+ session.lastSavedRevision = session.revision;
605
+ session.dirty = false;
606
+ const cacheEntry = this.cacheService.refreshSession(session);
607
+ this.eventService.emit({
608
+ kind: "save.completed",
609
+ sessionId: session.sessionId,
610
+ canonicalPathKey: session.canonicalPathKey,
611
+ revision: session.revision
612
+ });
613
+ this.eventService.emit({
614
+ kind: "cache.updated",
615
+ sessionId: session.sessionId,
616
+ canonicalPathKey: session.canonicalPathKey,
617
+ revision: session.revision,
618
+ cacheRevision: cacheEntry.revision
619
+ });
620
+ return success("authoring", startedAt, toMaterializeSnapshot(session, this.context.capabilities, {
621
+ reason: input.reason,
622
+ writtenPaths,
623
+ skippedPaths,
624
+ diagnostics: []
625
+ }), {
626
+ sessionId: session.sessionId,
627
+ revision: session.revision
628
+ });
629
+ } catch (error) {
630
+ const diagnosticsFromError = [{
631
+ code: "write_failed",
632
+ message: error instanceof Error ? error.message : String(error),
633
+ severity: "error",
634
+ path: failedPaths[0],
635
+ operationKind: "materializeSession"
636
+ }];
637
+ this.cacheService.invalidateSession(session);
638
+ this.eventService.emit({
639
+ kind: "save.failed",
640
+ sessionId: session.sessionId,
641
+ canonicalPathKey: session.canonicalPathKey,
642
+ revision: session.revision,
643
+ diagnostics: diagnosticsFromError
644
+ });
645
+ return failure("authoring", startedAt, {
646
+ code: "write_failed",
647
+ message: error instanceof Error ? error.message : String(error),
648
+ sessionId: session.sessionId,
649
+ canonicalPathKey: session.canonicalPathKey,
650
+ attemptedRevision: session.revision,
651
+ lastSavedRevision: session.lastSavedRevision,
652
+ writtenPaths,
653
+ failedPaths,
654
+ skippedPaths,
655
+ diagnostics: diagnosticsFromError,
656
+ diskMayBePartiallyUpdated: true
657
+ }, toSessionSnapshot(session, this.context.capabilities), {
658
+ sessionId: session.sessionId,
659
+ revision: session.revision,
660
+ diagnostics: diagnosticsFromError
661
+ });
662
+ }
663
+ }
664
+ };
665
+ //#endregion
666
+ //#region src/services/cache-service.ts
667
+ function createCacheEntry(session, valid) {
668
+ return {
669
+ canonicalPathKey: session.canonicalPathKey,
670
+ sessionId: session.sessionId,
671
+ revision: session.revision,
672
+ lastSavedRevision: session.lastSavedRevision,
673
+ dirty: session.dirty,
674
+ valid,
675
+ indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
676
+ summary: {
677
+ packageCount: session.project.packages.length,
678
+ resourceCount: session.project.packages.reduce((total, pkg) => total + pkg.resources.length, 0),
679
+ diagnostics: []
680
+ }
681
+ };
682
+ }
683
+ var CacheService = class {
684
+ constructor(context) {
685
+ this.context = context;
686
+ }
687
+ getCacheSnapshot(input) {
688
+ const startedAt = Date.now();
689
+ const session = this.context.sessions.get(input.sessionId);
690
+ if (!session || session.closed) return failure("read", startedAt, createSessionNotFoundError(input.sessionId));
691
+ const entry = this.context.cacheBySession.get(input.sessionId);
692
+ return success("read", startedAt, {
693
+ cacheRevision: entry?.revision ?? session.revision,
694
+ entries: entry ? [structuredClone(entry)] : []
695
+ }, {
696
+ sessionId: session.sessionId,
697
+ revision: session.revision
698
+ });
699
+ }
700
+ refreshSession(session) {
701
+ const entry = createCacheEntry(session, true);
702
+ this.context.cacheBySession.set(session.sessionId, entry);
703
+ return entry;
704
+ }
705
+ invalidateSession(session) {
706
+ const entry = createCacheEntry(session, false);
707
+ this.context.cacheBySession.set(session.sessionId, entry);
708
+ return entry;
709
+ }
710
+ removeSession(sessionId) {
711
+ this.context.cacheBySession.delete(sessionId);
712
+ }
713
+ };
714
+ //#endregion
715
+ //#region src/services/event-service.ts
716
+ const DEFAULT_EVENT_RETENTION_LIMIT = 1e3;
717
+ var EventService = class {
718
+ constructor(context) {
719
+ this.context = context;
720
+ }
721
+ emit(event) {
722
+ const emitted = {
723
+ ...event,
724
+ sequence: this.context.nextEventSequence(),
725
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
726
+ diagnostics: event.diagnostics ?? []
727
+ };
728
+ const sessionId = event.sessionId;
729
+ if (!sessionId) return emitted;
730
+ const events = this.context.eventsBySession.get(sessionId) ?? [];
731
+ events.push(emitted);
732
+ while (events.length > DEFAULT_EVENT_RETENTION_LIMIT) events.shift();
733
+ this.context.eventsBySession.set(sessionId, events);
734
+ return emitted;
735
+ }
736
+ getEvents(input) {
737
+ const startedAt = Date.now();
738
+ const session = this.context.sessions.get(input.sessionId);
739
+ if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
740
+ const events = this.context.eventsBySession.get(input.sessionId) ?? [];
741
+ const oldestSequence = events[0]?.sequence ?? (events.length === 0 ? 1 : 0);
742
+ const currentSequence = events.at(-1)?.sequence ?? 0;
743
+ const after = input.after === void 0 ? 0 : Number(input.after);
744
+ if (!Number.isInteger(after) || after < 0) return failure("runtime", startedAt, {
745
+ code: "event_cursor_invalid",
746
+ message: `Invalid event cursor: ${input.after}`,
747
+ sessionId: input.sessionId,
748
+ after: String(input.after)
749
+ });
750
+ if (events.length > 0 && after !== 0 && after < oldestSequence - 1) return failure("runtime", startedAt, {
751
+ code: "event_cursor_invalid",
752
+ message: `Event cursor has expired: ${after}`,
753
+ sessionId: input.sessionId,
754
+ after: String(input.after)
755
+ });
756
+ if (after > currentSequence) return failure("runtime", startedAt, {
757
+ code: "event_cursor_invalid",
758
+ message: `Unknown event cursor: ${after}`,
759
+ sessionId: input.sessionId,
760
+ after: String(input.after)
761
+ });
762
+ const filtered = events.filter((event) => event.sequence > after);
763
+ const limit = input.limit === void 0 ? filtered.length : Math.max(0, input.limit);
764
+ return success("runtime", startedAt, {
765
+ events: filtered.slice(0, limit).map((event) => structuredClone(event)),
766
+ oldestSequence,
767
+ currentSequence,
768
+ cursorExpired: false
769
+ }, {
770
+ sessionId: session.sessionId,
771
+ revision: session.revision
772
+ });
773
+ }
774
+ removeSession(sessionId) {
775
+ this.context.eventsBySession.delete(sessionId);
776
+ }
777
+ };
778
+ //#endregion
779
+ //#region src/services/job-service.ts
780
+ const COMPLETED_JOB_RETENTION_LIMIT = 100;
781
+ const REFRESH_START_DELAY_MS = 0;
782
+ const REFRESH_COMPLETE_DELAY_MS = 50;
783
+ function isTerminal(status) {
784
+ return status === "completed" || status === "failed" || status === "cancelled";
785
+ }
786
+ var JobService = class {
787
+ constructor(context, cacheService, eventService) {
788
+ this.context = context;
789
+ this.cacheService = cacheService;
790
+ this.eventService = eventService;
791
+ }
792
+ cancellationRequests = /* @__PURE__ */ new Set();
793
+ getJobs(sessionId) {
794
+ return this.context.jobsBySession.get(sessionId) ?? [];
795
+ }
796
+ setJobs(sessionId, jobs) {
797
+ const retained = [];
798
+ let terminalCount = 0;
799
+ for (let index = jobs.length - 1; index >= 0; index -= 1) {
800
+ const job = jobs[index];
801
+ if (isTerminal(job.status)) {
802
+ if (terminalCount >= COMPLETED_JOB_RETENTION_LIMIT) continue;
803
+ terminalCount += 1;
804
+ }
805
+ retained.push(job);
806
+ }
807
+ this.context.jobsBySession.set(sessionId, retained.reverse());
808
+ }
809
+ refreshCache(input) {
810
+ const startedAt = Date.now();
811
+ const session = this.context.sessions.get(input.sessionId);
812
+ if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
813
+ const jobId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
814
+ const job = {
815
+ jobId,
816
+ kind: "cache.refresh",
817
+ status: "queued",
818
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
819
+ sessionId: session.sessionId,
820
+ canonicalPathKey: session.canonicalPathKey,
821
+ revision: session.revision,
822
+ diagnostics: [],
823
+ progress: {
824
+ completed: 0,
825
+ total: 1,
826
+ message: input.reason ?? "manual"
827
+ }
828
+ };
829
+ const jobs = this.getJobs(session.sessionId);
830
+ jobs.push(job);
831
+ this.setJobs(session.sessionId, jobs);
832
+ this.eventService.emit({
833
+ kind: "job.created",
834
+ sessionId: session.sessionId,
835
+ canonicalPathKey: session.canonicalPathKey,
836
+ revision: session.revision,
837
+ jobId
838
+ });
839
+ this.scheduleRefreshJob(session.sessionId, jobId);
840
+ return success("runtime", startedAt, structuredClone(job), {
841
+ sessionId: session.sessionId,
842
+ revision: session.revision
843
+ });
844
+ }
845
+ getJob(input) {
846
+ const startedAt = Date.now();
847
+ const session = this.context.sessions.get(input.sessionId);
848
+ if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
849
+ const job = this.getJobs(input.sessionId).find((item) => item.jobId === input.jobId);
850
+ if (!job) return failure("runtime", startedAt, {
851
+ code: "job_not_found",
852
+ message: `Job was not found: ${input.jobId}`,
853
+ sessionId: input.sessionId,
854
+ jobId: input.jobId
855
+ }, void 0, {
856
+ sessionId: session.sessionId,
857
+ revision: session.revision
858
+ });
859
+ return success("runtime", startedAt, structuredClone(job), {
860
+ sessionId: session.sessionId,
861
+ revision: session.revision
862
+ });
863
+ }
864
+ listJobs(input) {
865
+ const startedAt = Date.now();
866
+ const session = this.context.sessions.get(input.sessionId);
867
+ if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
868
+ let jobs = [...this.getJobs(input.sessionId)];
869
+ if (input.kind) jobs = jobs.filter((job) => job.kind === input.kind);
870
+ if (input.status) if (input.status === "active") jobs = jobs.filter((job) => !isTerminal(job.status));
871
+ else if (input.status === "terminal") jobs = jobs.filter((job) => isTerminal(job.status));
872
+ else jobs = jobs.filter((job) => job.status === input.status);
873
+ if (input.limit !== void 0) jobs = jobs.slice(0, Math.max(0, input.limit));
874
+ return success("runtime", startedAt, { jobs: jobs.map((job) => structuredClone(job)) }, {
875
+ sessionId: session.sessionId,
876
+ revision: session.revision
877
+ });
878
+ }
879
+ cancelJob(input) {
880
+ const startedAt = Date.now();
881
+ const session = this.context.sessions.get(input.sessionId);
882
+ if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
883
+ const job = this.getJobs(input.sessionId).find((item) => item.jobId === input.jobId);
884
+ if (!job) return failure("runtime", startedAt, {
885
+ code: "job_not_found",
886
+ message: `Job was not found: ${input.jobId}`,
887
+ sessionId: input.sessionId,
888
+ jobId: input.jobId
889
+ }, void 0, {
890
+ sessionId: session.sessionId,
891
+ revision: session.revision
892
+ });
893
+ if (isTerminal(job.status)) return failure("runtime", startedAt, {
894
+ code: "job_not_cancellable",
895
+ message: `Job is already terminal: ${input.jobId}`,
896
+ sessionId: input.sessionId,
897
+ jobId: input.jobId,
898
+ status: job.status
899
+ }, void 0, {
900
+ sessionId: session.sessionId,
901
+ revision: session.revision
902
+ });
903
+ this.cancellationRequests.add(job.jobId);
904
+ this.eventService.emit({
905
+ kind: "job.cancelRequested",
906
+ sessionId: input.sessionId,
907
+ canonicalPathKey: session.canonicalPathKey,
908
+ revision: session.revision,
909
+ jobId: job.jobId
910
+ });
911
+ const cancelled = this.cancelRefreshJob(session.sessionId, job);
912
+ return success("runtime", startedAt, structuredClone(cancelled), {
913
+ sessionId: session.sessionId,
914
+ revision: session.revision
915
+ });
916
+ }
917
+ removeSession(sessionId) {
918
+ for (const job of this.getJobs(sessionId)) this.cancellationRequests.delete(job.jobId);
919
+ this.context.jobsBySession.delete(sessionId);
920
+ }
921
+ replaceJob(sessionId, nextJob) {
922
+ const jobs = this.getJobs(sessionId).map((job) => job.jobId === nextJob.jobId ? nextJob : job);
923
+ this.setJobs(sessionId, jobs);
924
+ }
925
+ scheduleRefreshJob(sessionId, jobId) {
926
+ setTimeout(() => this.startRefreshJob(sessionId, jobId), REFRESH_START_DELAY_MS);
927
+ }
928
+ startRefreshJob(sessionId, jobId) {
929
+ const session = this.context.sessions.get(sessionId);
930
+ if (!session || session.closed) return;
931
+ const job = this.getJobs(sessionId).find((item) => item.jobId === jobId);
932
+ if (!job || isTerminal(job.status)) return;
933
+ if (this.cancellationRequests.has(jobId)) {
934
+ this.cancelRefreshJob(sessionId, job);
935
+ return;
936
+ }
937
+ const running = {
938
+ ...job,
939
+ status: "running",
940
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
941
+ progress: {
942
+ completed: 0,
943
+ total: 1,
944
+ message: "refreshing cache"
945
+ }
946
+ };
947
+ this.replaceJob(sessionId, running);
948
+ this.eventService.emit({
949
+ kind: "job.started",
950
+ sessionId,
951
+ canonicalPathKey: session.canonicalPathKey,
952
+ revision: session.revision,
953
+ jobId
954
+ });
955
+ this.eventService.emit({
956
+ kind: "job.progress",
957
+ sessionId,
958
+ canonicalPathKey: session.canonicalPathKey,
959
+ revision: session.revision,
960
+ jobId,
961
+ payload: running.progress
962
+ });
963
+ setTimeout(() => this.completeRefreshJob(sessionId, jobId), REFRESH_COMPLETE_DELAY_MS);
964
+ }
965
+ completeRefreshJob(sessionId, jobId) {
966
+ const session = this.context.sessions.get(sessionId);
967
+ if (!session || session.closed) return;
968
+ const job = this.getJobs(sessionId).find((item) => item.jobId === jobId);
969
+ if (!job || isTerminal(job.status)) return;
970
+ if (this.cancellationRequests.has(jobId)) {
971
+ this.cancelRefreshJob(sessionId, job);
972
+ return;
973
+ }
974
+ try {
975
+ const entry = this.cacheService.refreshSession(session);
976
+ const completed = {
977
+ ...job,
978
+ status: "completed",
979
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
980
+ cacheRevision: entry.revision,
981
+ progress: {
982
+ completed: 1,
983
+ total: 1,
984
+ message: "cache refreshed"
985
+ },
986
+ result: { cacheRevision: entry.revision }
987
+ };
988
+ this.replaceJob(sessionId, completed);
989
+ this.eventService.emit({
990
+ kind: "job.completed",
991
+ sessionId,
992
+ canonicalPathKey: session.canonicalPathKey,
993
+ revision: session.revision,
994
+ cacheRevision: entry.revision,
995
+ jobId
996
+ });
997
+ this.eventService.emit({
998
+ kind: "cache.updated",
999
+ sessionId,
1000
+ canonicalPathKey: session.canonicalPathKey,
1001
+ revision: session.revision,
1002
+ cacheRevision: entry.revision
1003
+ });
1004
+ } catch (error) {
1005
+ const failed = {
1006
+ ...job,
1007
+ status: "failed",
1008
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1009
+ error: {
1010
+ code: "cache_refresh_failed",
1011
+ message: error instanceof Error ? error.message : "Cache refresh failed",
1012
+ sessionId,
1013
+ jobId
1014
+ }
1015
+ };
1016
+ this.replaceJob(sessionId, failed);
1017
+ this.eventService.emit({
1018
+ kind: "job.failed",
1019
+ sessionId,
1020
+ canonicalPathKey: session.canonicalPathKey,
1021
+ revision: session.revision,
1022
+ jobId,
1023
+ diagnostics: failed.diagnostics
1024
+ });
1025
+ }
1026
+ }
1027
+ cancelRefreshJob(sessionId, job) {
1028
+ const session = this.context.sessions.get(sessionId);
1029
+ const cancelled = {
1030
+ ...job,
1031
+ status: "cancelled",
1032
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1033
+ error: {
1034
+ code: "job_cancelled",
1035
+ message: `Job was cancelled: ${job.jobId}`,
1036
+ sessionId,
1037
+ jobId: job.jobId
1038
+ }
1039
+ };
1040
+ this.replaceJob(sessionId, cancelled);
1041
+ this.cancellationRequests.delete(job.jobId);
1042
+ this.eventService.emit({
1043
+ kind: "job.cancelled",
1044
+ sessionId,
1045
+ canonicalPathKey: session?.canonicalPathKey ?? job.canonicalPathKey,
1046
+ revision: session?.revision ?? job.revision,
1047
+ jobId: job.jobId
1048
+ });
1049
+ return cancelled;
1050
+ }
1051
+ };
1052
+ //#endregion
1053
+ //#region src/services/read-service.ts
1054
+ var ReadService = class {
1055
+ constructor(context) {
1056
+ this.context = context;
1057
+ }
1058
+ getCapabilities() {
1059
+ return success("read", Date.now(), structuredClone(this.context.capabilities));
1060
+ }
1061
+ getSession(input) {
1062
+ const startedAt = Date.now();
1063
+ const session = this.context.sessions.get(input.sessionId);
1064
+ if (!session || session.closed) return failure("read", startedAt, createSessionNotFoundError(input.sessionId));
1065
+ return success("read", startedAt, toSessionSnapshot(session, this.context.capabilities), {
1066
+ sessionId: session.sessionId,
1067
+ revision: session.revision
1068
+ });
1069
+ }
1070
+ };
1071
+ //#endregion
1072
+ //#region src/services/runtime-service.ts
1073
+ function randomId() {
1074
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1075
+ }
1076
+ function createCapabilityUnavailableError(capability) {
1077
+ const artifactCapability = capability === "artifact.publish" || capability === "artifact.restore";
1078
+ return {
1079
+ code: "capability_unavailable",
1080
+ message: artifactCapability ? `${capability} requires the Node bridge boundary exposed by @openfairygui/backend/node.` : `${capability} requires an injected BackendFileSystem adapter.`,
1081
+ capability,
1082
+ requiredAdapter: capability === "fileSystem" ? "BackendFileSystem" : void 0,
1083
+ requiredHost: artifactCapability ? "node" : void 0,
1084
+ bridgeBoundary: artifactCapability ? "external-bridge" : void 0
1085
+ };
1086
+ }
1087
+ function createProjectReaderFileSystem(fileSystem) {
1088
+ return {
1089
+ readFile(filePath) {
1090
+ return fileSystem.readFile(filePath);
1091
+ },
1092
+ readFileRaw(filePath) {
1093
+ return fileSystem.readFileRaw(filePath);
1094
+ },
1095
+ writeFile(filePath, content) {
1096
+ return fileSystem.writeFile(filePath, content);
1097
+ },
1098
+ writeFileRaw(filePath, data) {
1099
+ return fileSystem.writeFileRaw(filePath, data);
1100
+ },
1101
+ async mkdir(dirPath) {
1102
+ await fileSystem.mkdir(dirPath, { recursive: true });
1103
+ },
1104
+ readdir(dirPath) {
1105
+ return fileSystem.readdir(dirPath);
1106
+ },
1107
+ async exists(filePath) {
1108
+ try {
1109
+ await fileSystem.stat(filePath);
1110
+ return true;
1111
+ } catch {
1112
+ return false;
1113
+ }
1114
+ },
1115
+ join(...paths) {
1116
+ return fileSystem.join(...paths);
1117
+ },
1118
+ dirname(filePath) {
1119
+ return fileSystem.dirname(filePath);
1120
+ }
1121
+ };
1122
+ }
1123
+ function createCaptureFileSystem(files, directories) {
1124
+ const normalize = (filePath) => filePath.replace(/\\/g, "/").replace(/\/+/g, "/");
1125
+ return {
1126
+ async readFile(filePath) {
1127
+ const value = files.get(normalize(filePath));
1128
+ if (typeof value !== "string") throw new Error(`Captured text file was not found: ${filePath}`);
1129
+ return value;
1130
+ },
1131
+ async readFileRaw(filePath) {
1132
+ const value = files.get(normalize(filePath));
1133
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
1134
+ return value.slice();
1135
+ },
1136
+ async writeFile(filePath, content) {
1137
+ files.set(normalize(filePath), content);
1138
+ },
1139
+ async writeFileRaw(filePath, data) {
1140
+ files.set(normalize(filePath), data.slice());
1141
+ },
1142
+ async mkdir(dirPath) {
1143
+ directories.add(normalize(dirPath));
1144
+ },
1145
+ async readdir() {
1146
+ return [];
1147
+ },
1148
+ async exists(filePath) {
1149
+ return files.has(normalize(filePath));
1150
+ },
1151
+ join(...paths) {
1152
+ return normalize(paths.filter(Boolean).join("/"));
1153
+ },
1154
+ dirname(filePath) {
1155
+ const normalized = normalize(filePath);
1156
+ const separator = normalized.lastIndexOf("/");
1157
+ return separator < 0 ? "" : normalized.slice(0, separator);
1158
+ },
1159
+ async unlink(filePath) {
1160
+ files.delete(normalize(filePath));
1161
+ }
1162
+ };
1163
+ }
1164
+ function capturedFilesEqual(left, right) {
1165
+ if (left.size !== right.size) return false;
1166
+ for (const [filePath, leftValue] of left) {
1167
+ const rightValue = right.get(filePath);
1168
+ if (typeof leftValue === "string") {
1169
+ if (leftValue !== rightValue) return false;
1170
+ continue;
1171
+ }
1172
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
1173
+ for (let index = 0; index < leftValue.length; index += 1) if (leftValue[index] !== rightValue[index]) return false;
1174
+ }
1175
+ return true;
1176
+ }
1177
+ function capturedDirectoriesEqual(left, right) {
1178
+ return left.size === right.size && [...left].every((directory) => right.has(directory));
1179
+ }
1180
+ async function hasFullUamFidelity(document, project) {
1181
+ const sourceFiles = /* @__PURE__ */ new Map();
1182
+ const materializedFiles = /* @__PURE__ */ new Map();
1183
+ const sourceDirectories = /* @__PURE__ */ new Set();
1184
+ const materializedDirectories = /* @__PURE__ */ new Set();
1185
+ try {
1186
+ 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")]);
1187
+ } catch {
1188
+ return false;
1189
+ }
1190
+ return capturedFilesEqual(sourceFiles, materializedFiles) && capturedDirectoriesEqual(sourceDirectories, materializedDirectories);
1191
+ }
1192
+ var RuntimeService = class {
1193
+ constructor(context, cacheService, eventService, jobService) {
1194
+ this.context = context;
1195
+ this.cacheService = cacheService;
1196
+ this.eventService = eventService;
1197
+ this.jobService = jobService;
1198
+ }
1199
+ async openSession(input) {
1200
+ const startedAt = Date.now();
1201
+ if (!this.context.fileSystem) return failure("runtime", startedAt, createCapabilityUnavailableError("fileSystem"));
1202
+ const fileSystem = this.context.fileSystem;
1203
+ const { fairyPath, canonicalProjectPath, canonicalPathKey } = await resolveCanonicalProjectRoot(fileSystem, input.projectPath);
1204
+ const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
1205
+ const lockFilePath = fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
1206
+ if (existingSessionId) return failure("runtime", startedAt, {
1207
+ code: "lock_conflict",
1208
+ kind: "in_process_session_exists",
1209
+ message: `Project is already open in this backend runtime: ${canonicalProjectPath}`,
1210
+ canonicalPathKey,
1211
+ holderSessionId: existingSessionId,
1212
+ lockFilePath
1213
+ });
1214
+ let sessionLock = null;
1215
+ try {
1216
+ sessionLock = await fileSystem.acquireSessionLock(lockFilePath);
1217
+ await sessionLock.writeMetadata(JSON.stringify(this.context.host?.lockMetadata?.({
1218
+ canonicalPathKey,
1219
+ canonicalProjectPath,
1220
+ lockFilePath
1221
+ }) ?? {
1222
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1223
+ canonicalPathKey
1224
+ }));
1225
+ const document = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
1226
+ const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
1227
+ const sessionId = randomId();
1228
+ const session = {
1229
+ sessionId,
1230
+ fairyPath,
1231
+ canonicalProjectPath,
1232
+ canonicalPathKey,
1233
+ lockFilePath,
1234
+ sessionLock,
1235
+ fileSystem,
1236
+ project,
1237
+ uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
1238
+ revision: 0,
1239
+ lastSavedRevision: 0,
1240
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1241
+ pendingStaleResourceFolders: /* @__PURE__ */ new Map(),
1242
+ pendingStaleBranchDirectories: /* @__PURE__ */ new Map(),
1243
+ dirty: false,
1244
+ lockHeld: true,
1245
+ closed: false
1246
+ };
1247
+ this.context.sessions.set(sessionId, session);
1248
+ this.context.sessionsByPath.set(canonicalPathKey, sessionId);
1249
+ this.cacheService.refreshSession(session);
1250
+ this.eventService.emit({
1251
+ kind: "session.opened",
1252
+ sessionId,
1253
+ canonicalPathKey,
1254
+ revision: session.revision
1255
+ });
1256
+ return success("runtime", startedAt, toSessionSnapshot(session, this.context.capabilities), {
1257
+ sessionId: session.sessionId,
1258
+ revision: session.revision
1259
+ });
1260
+ } catch (error) {
1261
+ if (sessionLock) await sessionLock.release().catch(() => void 0);
1262
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") return failure("runtime", startedAt, {
1263
+ code: "lock_conflict",
1264
+ kind: "advisory_lock_conflict",
1265
+ message: `Advisory lock already exists for project: ${canonicalProjectPath}`,
1266
+ canonicalPathKey,
1267
+ lockFilePath
1268
+ });
1269
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOTSUP") return failure("runtime", startedAt, {
1270
+ ...createCapabilityUnavailableError("fileSystem"),
1271
+ message: error instanceof Error ? error.message : String(error)
1272
+ });
1273
+ throw error;
1274
+ }
1275
+ }
1276
+ openProjectSession(input) {
1277
+ const startedAt = Date.now();
1278
+ const sessionId = input.sessionId ?? randomId();
1279
+ const storage = input.storage;
1280
+ const memoryProjectPath = `memory://${sessionId}`;
1281
+ const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
1282
+ const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
1283
+ const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
1284
+ if (existingSessionId) return failure("runtime", startedAt, {
1285
+ code: "lock_conflict",
1286
+ kind: "in_process_session_exists",
1287
+ message: `Project is already open in this backend runtime: ${canonicalProjectPath}`,
1288
+ canonicalPathKey,
1289
+ holderSessionId: existingSessionId
1290
+ });
1291
+ const session = {
1292
+ sessionId,
1293
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
1294
+ canonicalProjectPath,
1295
+ canonicalPathKey,
1296
+ lockFilePath: "",
1297
+ sessionLock: null,
1298
+ fileSystem: storage?.fileSystem,
1299
+ project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
1300
+ uamFidelity: "full",
1301
+ revision: 0,
1302
+ lastSavedRevision: 0,
1303
+ pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
1304
+ pendingStaleResourceFolders: /* @__PURE__ */ new Map(),
1305
+ pendingStaleBranchDirectories: /* @__PURE__ */ new Map(),
1306
+ dirty: false,
1307
+ lockHeld: false,
1308
+ closed: false
1309
+ };
1310
+ this.context.sessions.set(sessionId, session);
1311
+ this.context.sessionsByPath.set(canonicalPathKey, sessionId);
1312
+ this.cacheService.refreshSession(session);
1313
+ this.eventService.emit({
1314
+ kind: "session.opened",
1315
+ sessionId,
1316
+ canonicalPathKey,
1317
+ revision: session.revision
1318
+ });
1319
+ return success("runtime", startedAt, toSessionSnapshot(session, this.context.capabilities), {
1320
+ sessionId: session.sessionId,
1321
+ revision: session.revision
1322
+ });
1323
+ }
1324
+ async closeSession(input) {
1325
+ const startedAt = Date.now();
1326
+ const session = this.context.sessions.get(input.sessionId);
1327
+ if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
1328
+ this.eventService.emit({
1329
+ kind: "session.closeRequested",
1330
+ sessionId: session.sessionId,
1331
+ canonicalPathKey: session.canonicalPathKey,
1332
+ revision: session.revision
1333
+ });
1334
+ await session.sessionLock?.release().catch(() => void 0);
1335
+ session.sessionLock = null;
1336
+ session.lockHeld = false;
1337
+ session.closed = true;
1338
+ this.context.sessions.delete(session.sessionId);
1339
+ this.context.sessionsByPath.delete(session.canonicalPathKey);
1340
+ this.cacheService.removeSession(session.sessionId);
1341
+ this.jobService.removeSession(session.sessionId);
1342
+ this.eventService.emit({
1343
+ kind: "session.closed",
1344
+ sessionId: session.sessionId,
1345
+ canonicalPathKey: session.canonicalPathKey,
1346
+ revision: session.revision
1347
+ });
1348
+ this.eventService.removeSession(session.sessionId);
1349
+ return success("runtime", startedAt, {
1350
+ sessionId: session.sessionId,
1351
+ closed: true
1352
+ }, {
1353
+ sessionId: session.sessionId,
1354
+ revision: session.revision
1355
+ });
1356
+ }
1357
+ };
1358
+ //#endregion
1359
+ //#region src/services/artifact-service.ts
1360
+ function createArtifactCapabilities() {
1361
+ const bridge = {
1362
+ available: false,
1363
+ requiredHost: "node",
1364
+ executionBoundary: "external-bridge",
1365
+ bridgeEntrypoint: "@openfairygui/backend/node",
1366
+ reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
1367
+ };
1368
+ return {
1369
+ publish: false,
1370
+ restore: false,
1371
+ status: "bridge-required",
1372
+ publishBridge: bridge,
1373
+ restoreBridge: bridge
1374
+ };
1375
+ }
1376
+ //#endregion
1377
+ //#region src/runtime/capabilities.ts
1378
+ const BACKEND_METHODS = [
1379
+ "getCapabilities",
1380
+ "openSession",
1381
+ "openProjectSession",
1382
+ "getSession",
1383
+ "applyTransaction",
1384
+ "saveSession",
1385
+ "materializeSession",
1386
+ "closeSession",
1387
+ "getEvents",
1388
+ "getJob",
1389
+ "listJobs",
1390
+ "cancelJob",
1391
+ "getCacheSnapshot",
1392
+ "refreshCache"
1393
+ ];
1394
+ const ARTIFACT_BRIDGE_CAPABILITY = {
1395
+ available: false,
1396
+ requiredHost: "node",
1397
+ executionBoundary: "external-bridge",
1398
+ bridgeEntrypoint: "@openfairygui/backend/node",
1399
+ reason: "publish/restore require explicit Node-hosted filesystem and artifact execution."
1400
+ };
1401
+ function createCapabilities() {
1402
+ return {
1403
+ contractVersion: BACKEND_CONTRACT_VERSION,
1404
+ capabilitySchemaVersion: 2,
1405
+ transactionKernelOwner: "@openfairygui/core",
1406
+ appSeamOwner: "@openfairygui/functions",
1407
+ runtimeOwner: "@openfairygui/backend",
1408
+ methods: BACKEND_METHODS,
1409
+ read: {
1410
+ capabilitySnapshot: true,
1411
+ sessionSnapshot: true
1412
+ },
1413
+ authoring: {
1414
+ applyTransaction: true,
1415
+ saveSession: true,
1416
+ resourceKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_MATERIALIZATION_SCOPE.resourceKinds],
1417
+ nodeKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_MATERIALIZATION_SCOPE.nodeKinds],
1418
+ gearKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_MATERIALIZATION_SCOPE.gearKinds],
1419
+ transactionScope: {
1420
+ resourceKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_TRANSACTION_SCOPE.resourceKinds],
1421
+ nodeKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_TRANSACTION_SCOPE.nodeKinds],
1422
+ gearKinds: [..._openfairygui_core_uam.UAM_SUPPORTED_TRANSACTION_SCOPE.gearKinds]
1423
+ },
1424
+ unsupported: ["artifact.publish", "artifact.restore"]
1425
+ },
1426
+ artifact: createArtifactCapabilities(),
1427
+ manifest: {
1428
+ browserSafe: true,
1429
+ rootEntrypoint: "@openfairygui/backend",
1430
+ nodeEntrypoint: "@openfairygui/backend/node",
1431
+ adapters: {
1432
+ fileSystem: {
1433
+ injected: true,
1434
+ requiredFor: [
1435
+ "openSession",
1436
+ "saveSession",
1437
+ "materializeSession"
1438
+ ]
1439
+ },
1440
+ projectStorage: {
1441
+ injected: true,
1442
+ browserSafe: true,
1443
+ requiredFor: [
1444
+ "openProjectSession.writeback",
1445
+ "saveSession",
1446
+ "materializeSession"
1447
+ ],
1448
+ adapterFactory: "createBackendStorageFileSystem"
1449
+ },
1450
+ host: {
1451
+ injected: true,
1452
+ requiredFor: ["advisoryLockMetadata"]
1453
+ }
1454
+ },
1455
+ executionBoundaries: {
1456
+ projectSession: "in-process-browser-safe",
1457
+ fileBackedSession: "adapter-backed",
1458
+ artifactPublish: ARTIFACT_BRIDGE_CAPABILITY,
1459
+ artifactRestore: ARTIFACT_BRIDGE_CAPABILITY
1460
+ },
1461
+ diagnostics: {
1462
+ stableCodes: true,
1463
+ errorDiagnosticMirror: true
1464
+ }
1465
+ },
1466
+ compatibilityPolicy: BACKEND_COMPATIBILITY_POLICY,
1467
+ runtime: {
1468
+ sessionRuntime: true,
1469
+ advisoryLocking: true,
1470
+ coordinatedSave: true,
1471
+ atomicSave: false,
1472
+ staleRevisionProtection: true,
1473
+ pathPolicy: createRuntimePathPolicy(),
1474
+ events: {
1475
+ polling: true,
1476
+ subscriptions: false,
1477
+ retentionLimit: 1e3,
1478
+ sequenceScope: "runtime"
1479
+ },
1480
+ jobs: {
1481
+ inMemory: true,
1482
+ cooperativeCancel: true,
1483
+ persistent: false,
1484
+ supportedKinds: ["cache.refresh"],
1485
+ artifactJobs: false,
1486
+ completedRetentionLimit: 100
1487
+ },
1488
+ cache: {
1489
+ derivedReadOnly: true,
1490
+ keyedBy: "canonicalPathKey",
1491
+ sourceOfTruth: false,
1492
+ refreshMethod: "refreshCache"
1493
+ }
1494
+ }
1495
+ };
1496
+ }
1497
+ //#endregion
1498
+ //#region src/runtime.ts
1499
+ var BackendRuntime = class {
1500
+ fileSystem;
1501
+ capabilities;
1502
+ sessions = /* @__PURE__ */ new Map();
1503
+ sessionsByPath = /* @__PURE__ */ new Map();
1504
+ eventsBySession = /* @__PURE__ */ new Map();
1505
+ jobsBySession = /* @__PURE__ */ new Map();
1506
+ cacheBySession = /* @__PURE__ */ new Map();
1507
+ eventSequence = 0;
1508
+ context;
1509
+ readService;
1510
+ runtimeService;
1511
+ authoringService;
1512
+ cacheService;
1513
+ eventService;
1514
+ jobService;
1515
+ constructor(options = {}) {
1516
+ this.fileSystem = options.fileSystem;
1517
+ this.capabilities = createCapabilities();
1518
+ this.context = {
1519
+ fileSystem: this.fileSystem,
1520
+ host: options.host,
1521
+ capabilities: this.capabilities,
1522
+ sessions: this.sessions,
1523
+ sessionsByPath: this.sessionsByPath,
1524
+ eventsBySession: this.eventsBySession,
1525
+ jobsBySession: this.jobsBySession,
1526
+ cacheBySession: this.cacheBySession,
1527
+ nextEventSequence: () => {
1528
+ this.eventSequence += 1;
1529
+ return this.eventSequence;
1530
+ }
1531
+ };
1532
+ this.readService = new ReadService(this.context);
1533
+ this.eventService = new EventService(this.context);
1534
+ this.cacheService = new CacheService(this.context);
1535
+ this.jobService = new JobService(this.context, this.cacheService, this.eventService);
1536
+ this.runtimeService = new RuntimeService(this.context, this.cacheService, this.eventService, this.jobService);
1537
+ this.authoringService = new AuthoringService(this.context, this.cacheService, this.eventService);
1538
+ }
1539
+ getCapabilities() {
1540
+ return this.readService.getCapabilities();
1541
+ }
1542
+ async openSession(input) {
1543
+ return this.runtimeService.openSession(input);
1544
+ }
1545
+ openProjectSession(input) {
1546
+ return this.runtimeService.openProjectSession(input);
1547
+ }
1548
+ getSession(input) {
1549
+ return this.readService.getSession(input);
1550
+ }
1551
+ async applyTransaction(input) {
1552
+ return this.authoringService.applyTransaction(input);
1553
+ }
1554
+ async saveSession(input) {
1555
+ return this.authoringService.saveSession(input);
1556
+ }
1557
+ async materializeSession(input) {
1558
+ return this.authoringService.materializeSession(input);
1559
+ }
1560
+ async closeSession(input) {
1561
+ return this.runtimeService.closeSession(input);
1562
+ }
1563
+ getEvents(input) {
1564
+ return this.eventService.getEvents(input);
1565
+ }
1566
+ getJob(input) {
1567
+ return this.jobService.getJob(input);
1568
+ }
1569
+ listJobs(input) {
1570
+ return this.jobService.listJobs(input);
1571
+ }
1572
+ cancelJob(input) {
1573
+ return this.jobService.cancelJob(input);
1574
+ }
1575
+ getCacheSnapshot(input) {
1576
+ return this.cacheService.getCacheSnapshot(input);
1577
+ }
1578
+ refreshCache(input) {
1579
+ return this.jobService.refreshCache(input);
1580
+ }
1581
+ };
1582
+ //#endregion
3
1583
  //#region src/storage.ts
4
1584
  var StorageFileStat = class {
5
1585
  constructor(kind) {
@@ -17,6 +1597,42 @@ function createPathError(code, message) {
17
1597
  error.code = code;
18
1598
  return error;
19
1599
  }
1600
+ function getWebLockManager() {
1601
+ if (typeof navigator === "undefined") return null;
1602
+ const lockManager = navigator.locks;
1603
+ return lockManager && typeof lockManager.request === "function" ? lockManager : null;
1604
+ }
1605
+ function acquireWebSessionLock(lockManager, lockName) {
1606
+ return new Promise((resolve, reject) => {
1607
+ let releasePlatformLock = () => void 0;
1608
+ const held = new Promise((release) => {
1609
+ releasePlatformLock = release;
1610
+ });
1611
+ lockManager.request(lockName, {
1612
+ mode: "exclusive",
1613
+ ifAvailable: true
1614
+ }, async (lock) => {
1615
+ if (!lock) {
1616
+ reject(createPathError("EEXIST", `Browser session lock is already held: ${lockName}`));
1617
+ return;
1618
+ }
1619
+ let released = false;
1620
+ resolve({
1621
+ writeMetadata() {
1622
+ return Promise.resolve();
1623
+ },
1624
+ release() {
1625
+ if (!released) {
1626
+ released = true;
1627
+ releasePlatformLock();
1628
+ }
1629
+ return Promise.resolve();
1630
+ }
1631
+ });
1632
+ await held;
1633
+ }).catch(reject);
1634
+ });
1635
+ }
20
1636
  function normalizeStoragePath(value) {
21
1637
  const normalized = value.replace(/\\/g, "/").replace(/\/+/g, "/");
22
1638
  const absolute = normalized.startsWith("/");
@@ -71,7 +1687,8 @@ async function inferStat(storage, filePath) {
71
1687
  }
72
1688
  }
73
1689
  function createBackendStorageFileSystem(storage) {
74
- const lockedPaths = /* @__PURE__ */ new Set();
1690
+ if (typeof storage.unlink !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide unlink() for project resource lifecycle writes.");
1691
+ if (typeof storage.rmdir !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide rmdir() for project resource folder lifecycle writes.");
75
1692
  const fileSystem = {
76
1693
  stat(filePath) {
77
1694
  return inferStat(storage, fileSystem.resolve(filePath));
@@ -107,29 +1724,18 @@ function createBackendStorageFileSystem(storage) {
107
1724
  const resolved = fileSystem.resolve(filePath);
108
1725
  return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
109
1726
  },
110
- async openExclusive(filePath) {
111
- const resolved = fileSystem.resolve(filePath);
112
- if (storage.openExclusive) return storage.openExclusive(resolved);
113
- if (lockedPaths.has(resolved) || await fileSystem.exists(resolved)) throw createPathError("EEXIST", `Storage path already exists: ${resolved}`);
114
- lockedPaths.add(resolved);
115
- let closed = false;
116
- return {
117
- async writeFile(content) {
118
- if (closed) throw createPathError("EBADF", `Storage lock handle is closed: ${resolved}`);
119
- await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
120
- await storage.writeFile(resolved, content);
121
- },
122
- async close() {
123
- closed = true;
124
- lockedPaths.delete(resolved);
125
- }
126
- };
1727
+ async acquireSessionLock(lockPath) {
1728
+ const resolved = fileSystem.resolve(lockPath);
1729
+ if (storage.acquireSessionLock) return storage.acquireSessionLock(resolved);
1730
+ const lockManager = getWebLockManager();
1731
+ if (!lockManager) throw createPathError("ENOTSUP", "Browser openSession requires Web Locks or BackendAsyncStorageAdapter.acquireSessionLock().");
1732
+ return acquireWebSessionLock(lockManager, `@openfairygui/backend:${resolved}`);
127
1733
  },
128
1734
  unlink(filePath) {
129
- const resolved = fileSystem.resolve(filePath);
130
- lockedPaths.delete(resolved);
131
- if (storage.unlink) return storage.unlink(resolved);
132
- throw createPathError("ENOTSUP", "Storage adapter does not provide unlink().");
1735
+ return storage.unlink(fileSystem.resolve(filePath));
1736
+ },
1737
+ rmdir(dirPath) {
1738
+ return storage.rmdir(fileSystem.resolve(dirPath));
133
1739
  },
134
1740
  join(...paths) {
135
1741
  return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
@@ -144,8 +1750,8 @@ function createBackendStorageFileSystem(storage) {
144
1750
  return fileSystem;
145
1751
  }
146
1752
  //#endregion
147
- exports.BACKEND_CAPABILITY_SCHEMA_VERSION = require_runtime.BACKEND_CAPABILITY_SCHEMA_VERSION;
148
- exports.BACKEND_COMPATIBILITY_POLICY = require_runtime.BACKEND_COMPATIBILITY_POLICY;
149
- exports.BACKEND_CONTRACT_VERSION = require_runtime.BACKEND_CONTRACT_VERSION;
150
- exports.BackendRuntime = require_runtime.BackendRuntime;
1753
+ exports.BACKEND_CAPABILITY_SCHEMA_VERSION = BACKEND_CAPABILITY_SCHEMA_VERSION;
1754
+ exports.BACKEND_COMPATIBILITY_POLICY = BACKEND_COMPATIBILITY_POLICY;
1755
+ exports.BACKEND_CONTRACT_VERSION = BACKEND_CONTRACT_VERSION;
1756
+ exports.BackendRuntime = BackendRuntime;
151
1757
  exports.createBackendStorageFileSystem = createBackendStorageFileSystem;