@openfairygui/backend 0.2.0-alpha.5 → 0.2.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,7 @@ It owns:
12
12
  - revisioned request handling
13
13
  - coordinated but non-atomic save semantics
14
14
  - browser-safe project sessions
15
+ - browser-safe async project storage adapter
15
16
  - adapter-backed file sessions and backend-local advisory locking
16
17
  - capability discovery
17
18
  - transport-neutral bootstrap
@@ -31,8 +32,9 @@ It also provides:
31
32
 
32
33
  It does **not** redefine transaction grammar or expose `Document`.
33
34
  It also does **not** implement MCP or any transport-specific wire protocol.
34
- The root `@openfairygui/backend` entrypoint is browser-safe: file-backed sessions require an injected
35
- `BackendFileSystem`, while the default Node filesystem/runtime lives under `@openfairygui/backend/node`.
35
+ The root `@openfairygui/backend` entrypoint is browser-safe: pure authoring sessions can run in memory,
36
+ and browser editors can inject an async storage adapter for OPFS, IndexedDB, ZIP-backed virtual filesystems,
37
+ or File System Access API bridges. The default Node filesystem/runtime lives under `@openfairygui/backend/node`.
36
38
 
37
39
  ## Relationship to other packages
38
40
 
@@ -58,6 +60,35 @@ const applied = await runtime.applyTransaction({
58
60
  });
59
61
  ```
60
62
 
63
+ Browser-safe project session with injected storage:
64
+
65
+ ```ts
66
+ import { BackendRuntime, createBackendStorageFileSystem } from '@openfairygui/backend';
67
+
68
+ const fileSystem = createBackendStorageFileSystem({
69
+ async readFile(filePath) { return storage.readText(filePath); },
70
+ async readFileRaw(filePath) { return storage.readBytes(filePath); },
71
+ async writeFile(filePath, content) { await storage.writeText(filePath, content); },
72
+ async writeFileRaw(filePath, data) { await storage.writeBytes(filePath, data); },
73
+ async mkdir(dirPath) { await storage.mkdir(dirPath); },
74
+ async readdir(dirPath) { return storage.readdir(dirPath); },
75
+ async exists(filePath) { return storage.exists(filePath); },
76
+ });
77
+
78
+ const runtime = new BackendRuntime();
79
+ const opened = runtime.openProjectSession({
80
+ project: uamProject,
81
+ storage: {
82
+ fileSystem,
83
+ fairyPath: 'Project.fairy',
84
+ },
85
+ });
86
+ if (!opened.ok) throw new Error(opened.error.message);
87
+
88
+ const saved = await runtime.saveSession({ sessionId: opened.data.sessionId });
89
+ if (!saved.ok) throw new Error(saved.error.message);
90
+ ```
91
+
61
92
  Node file-backed session:
62
93
 
63
94
  ```ts
package/dist/index.cjs CHANGED
@@ -1,6 +1,151 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_runtime = require("./runtime--ytGfQCF.cjs");
2
+ const require_runtime = require("./runtime-YK3ACpuQ.cjs");
3
+ //#region src/storage.ts
4
+ var StorageFileStat = class {
5
+ constructor(kind) {
6
+ this.kind = kind;
7
+ }
8
+ isFile() {
9
+ return this.kind === "file";
10
+ }
11
+ isDirectory() {
12
+ return this.kind === "directory";
13
+ }
14
+ };
15
+ function createPathError(code, message) {
16
+ const error = new Error(message);
17
+ error.code = code;
18
+ return error;
19
+ }
20
+ function normalizeStoragePath(value) {
21
+ const normalized = value.replace(/\\/g, "/").replace(/\/+/g, "/");
22
+ const absolute = normalized.startsWith("/");
23
+ const rawSegments = normalized.split("/").filter((segment) => segment.length > 0);
24
+ const segments = [];
25
+ for (const segment of rawSegments) {
26
+ if (segment === ".") continue;
27
+ if (segment === "..") {
28
+ if (segments.length > 0) segments.pop();
29
+ continue;
30
+ }
31
+ segments.push(segment);
32
+ }
33
+ const joined = segments.join("/");
34
+ if (absolute) return joined ? `/${joined}` : "/";
35
+ return joined || ".";
36
+ }
37
+ function joinStoragePath(...paths) {
38
+ return normalizeStoragePath(paths.filter((part) => part.length > 0).join("/"));
39
+ }
40
+ function dirnameStoragePath(filePath) {
41
+ const normalized = normalizeStoragePath(filePath);
42
+ if (normalized === "/" || normalized === ".") return ".";
43
+ const absolute = normalized.startsWith("/");
44
+ const parts = normalized.split("/").filter((part) => part.length > 0);
45
+ parts.pop();
46
+ if (parts.length === 0) return absolute ? "/" : ".";
47
+ return `${absolute ? "/" : ""}${parts.join("/")}`;
48
+ }
49
+ function statFromLike(stat) {
50
+ if (typeof stat.isFile === "function" && typeof stat.isDirectory === "function") return stat;
51
+ const kind = stat.kind ?? stat.type;
52
+ if (kind === "file" || kind === "directory") return new StorageFileStat(kind);
53
+ throw createPathError("EINVAL", "Storage stat must provide kind/type or isFile()/isDirectory().");
54
+ }
55
+ async function inferStat(storage, filePath) {
56
+ if (storage.stat) return statFromLike(await storage.stat(filePath));
57
+ try {
58
+ await storage.readdir(filePath);
59
+ return new StorageFileStat("directory");
60
+ } catch {}
61
+ try {
62
+ await storage.readFileRaw(filePath);
63
+ return new StorageFileStat("file");
64
+ } catch {
65
+ try {
66
+ await storage.readFile(filePath);
67
+ return new StorageFileStat("file");
68
+ } catch {
69
+ throw createPathError("ENOENT", `Storage path not found: ${filePath}`);
70
+ }
71
+ }
72
+ }
73
+ function createBackendStorageFileSystem(storage) {
74
+ const lockedPaths = /* @__PURE__ */ new Set();
75
+ const fileSystem = {
76
+ stat(filePath) {
77
+ return inferStat(storage, fileSystem.resolve(filePath));
78
+ },
79
+ readdir(dirPath) {
80
+ return storage.readdir(fileSystem.resolve(dirPath));
81
+ },
82
+ readFile(filePath) {
83
+ return storage.readFile(fileSystem.resolve(filePath));
84
+ },
85
+ readFileRaw(filePath) {
86
+ return storage.readFileRaw(fileSystem.resolve(filePath));
87
+ },
88
+ writeFile(filePath, content) {
89
+ return storage.writeFile(fileSystem.resolve(filePath), content);
90
+ },
91
+ writeFileRaw(filePath, data) {
92
+ return storage.writeFileRaw(fileSystem.resolve(filePath), data);
93
+ },
94
+ mkdir(dirPath, options) {
95
+ return storage.mkdir(fileSystem.resolve(dirPath), options);
96
+ },
97
+ async exists(filePath) {
98
+ if (storage.exists) return storage.exists(fileSystem.resolve(filePath));
99
+ try {
100
+ await fileSystem.stat(filePath);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ },
106
+ resolvePath(filePath) {
107
+ const resolved = fileSystem.resolve(filePath);
108
+ return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
109
+ },
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
+ };
127
+ },
128
+ 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().");
133
+ },
134
+ join(...paths) {
135
+ return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
136
+ },
137
+ dirname(filePath) {
138
+ return storage.dirname ? storage.dirname(filePath) : dirnameStoragePath(filePath);
139
+ },
140
+ resolve(...paths) {
141
+ return storage.resolve ? storage.resolve(...paths) : joinStoragePath(...paths);
142
+ }
143
+ };
144
+ return fileSystem;
145
+ }
146
+ //#endregion
3
147
  exports.BACKEND_CAPABILITY_SCHEMA_VERSION = require_runtime.BACKEND_CAPABILITY_SCHEMA_VERSION;
4
148
  exports.BACKEND_COMPATIBILITY_POLICY = require_runtime.BACKEND_COMPATIBILITY_POLICY;
5
149
  exports.BACKEND_CONTRACT_VERSION = require_runtime.BACKEND_CONTRACT_VERSION;
6
150
  exports.BackendRuntime = require_runtime.BackendRuntime;
151
+ exports.createBackendStorageFileSystem = createBackendStorageFileSystem;
package/dist/index.d.cts CHANGED
@@ -1,2 +1,33 @@
1
- import { A as BackendSuccess, B as OpenProjectSessionInput, C as BackendJobProgress, D as BackendRuntime, E as BackendResult, F as GetEventsInput, G as BACKEND_CAPABILITY_SCHEMA_VERSION, H as SavePartialFailureError, I as GetEventsSnapshot, J as BackendDiagnostic, K as BACKEND_COMPATIBILITY_POLICY, L as GetJobInput, M as CancelJobInput, N as EventCursorInvalidError, O as BackendRuntimeOptions, P as GetCacheSnapshotInput, R as InProcessLockConflictError, S as BackendJobNotFoundError, T as BackendJobStatus, U as SessionNotFoundError, V as RefreshCacheInput, W as SessionStaleWriteError, X as BackendResponseMeta, Y as BackendMessage, Z as BackendStage, _ as BackendJobErrors, a as BackendCacheSnapshot, b as BackendJobListStatusFilter, c as BackendCapabilityUnavailableError, d as BackendEventKind, f as BackendFailure, g as BackendHostAdapter, h as BackendFileSystem, i as BackendCacheEntry, j as CacheRefreshFailedError, k as BackendSessionSnapshot, l as BackendError, m as BackendFileStat, n as ApplySessionTransactionInput, o as BackendCapabilities, p as BackendFileHandle, q as BACKEND_CONTRACT_VERSION, r as BackendArtifactBridgeCapability, s as BackendCapabilityManifest, t as AdvisoryLockConflictError, u as BackendEvent, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as ListJobsInput } from "./runtime-DlM27Izm.cjs";
2
- export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileHandle, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionSnapshot, type BackendStage, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type InProcessLockConflictError, type ListJobsInput, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SessionNotFoundError, type SessionStaleWriteError };
1
+ import { $ as BackendStage, A as BackendSessionSnapshot, B as ListJobsInput, C as BackendJobProgress, D as BackendResult, E as BackendProjectSessionStorage, F as GetCacheSnapshotInput, G as SessionNotFoundError, H as RefreshCacheInput, I as GetEventsInput, J as BACKEND_COMPATIBILITY_POLICY, K as SessionStaleWriteError, L as GetEventsSnapshot, M as CacheRefreshFailedError, N as CancelJobInput, O as BackendRuntime, P as EventCursorInvalidError, Q as BackendResponseMeta, R as GetJobInput, S as BackendJobNotFoundError, T as BackendJobStatus, U as SavePartialFailureError, V as OpenProjectSessionInput, W as SaveSessionInput, X as BackendDiagnostic, Y as BACKEND_CONTRACT_VERSION, Z as BackendMessage, _ as BackendJobErrors, a as BackendCacheSnapshot, b as BackendJobListStatusFilter, c as BackendCapabilityUnavailableError, d as BackendEventKind, f as BackendFailure, g as BackendHostAdapter, h as BackendFileSystem, i as BackendCacheEntry, j as BackendSuccess, k as BackendRuntimeOptions, l as BackendError, m as BackendFileStat, n as ApplySessionTransactionInput, o as BackendCapabilities, p as BackendFileHandle, q as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BackendArtifactBridgeCapability, s as BackendCapabilityManifest, t as AdvisoryLockConflictError, u as BackendEvent, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as InProcessLockConflictError } from "./runtime-DziTGugy.cjs";
2
+ import { FileSystem } from "@openfairygui/core/project-io";
3
+
4
+ //#region src/storage.d.ts
5
+ type StorageStatKind = 'file' | 'directory';
6
+ interface BackendStorageStatLike {
7
+ kind?: StorageStatKind;
8
+ type?: StorageStatKind;
9
+ isFile?(): boolean;
10
+ isDirectory?(): boolean;
11
+ }
12
+ interface BackendAsyncStorageAdapter {
13
+ readFile(filePath: string): Promise<string>;
14
+ readFileRaw(filePath: string): Promise<Uint8Array>;
15
+ writeFile(filePath: string, content: string): Promise<void>;
16
+ writeFileRaw(filePath: string, data: Uint8Array): Promise<void>;
17
+ mkdir(dirPath: string, options?: {
18
+ recursive?: boolean;
19
+ }): Promise<void>;
20
+ readdir(dirPath: string): Promise<string[]>;
21
+ exists?(filePath: string): Promise<boolean>;
22
+ stat?(filePath: string): Promise<BackendStorageStatLike>;
23
+ resolvePath?(filePath: string): Promise<string>;
24
+ openExclusive?(filePath: string): Promise<BackendFileHandle>;
25
+ unlink?(filePath: string): Promise<void>;
26
+ join?(...paths: string[]): string;
27
+ dirname?(filePath: string): string;
28
+ resolve?(...paths: string[]): string;
29
+ }
30
+ type BackendStorageFileSystem = BackendFileSystem & FileSystem;
31
+ declare function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem;
32
+ //#endregion
33
+ export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileHandle, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type InProcessLockConflictError, type ListJobsInput, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, createBackendStorageFileSystem };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,33 @@
1
- import { A as BackendSuccess, B as OpenProjectSessionInput, C as BackendJobProgress, D as BackendRuntime, E as BackendResult, F as GetEventsInput, G as BACKEND_CAPABILITY_SCHEMA_VERSION, H as SavePartialFailureError, I as GetEventsSnapshot, J as BackendDiagnostic, K as BACKEND_COMPATIBILITY_POLICY, L as GetJobInput, M as CancelJobInput, N as EventCursorInvalidError, O as BackendRuntimeOptions, P as GetCacheSnapshotInput, R as InProcessLockConflictError, S as BackendJobNotFoundError, T as BackendJobStatus, U as SessionNotFoundError, V as RefreshCacheInput, W as SessionStaleWriteError, X as BackendResponseMeta, Y as BackendMessage, Z as BackendStage, _ as BackendJobErrors, a as BackendCacheSnapshot, b as BackendJobListStatusFilter, c as BackendCapabilityUnavailableError, d as BackendEventKind, f as BackendFailure, g as BackendHostAdapter, h as BackendFileSystem, i as BackendCacheEntry, j as CacheRefreshFailedError, k as BackendSessionSnapshot, l as BackendError, m as BackendFileStat, n as ApplySessionTransactionInput, o as BackendCapabilities, p as BackendFileHandle, q as BACKEND_CONTRACT_VERSION, r as BackendArtifactBridgeCapability, s as BackendCapabilityManifest, t as AdvisoryLockConflictError, u as BackendEvent, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as ListJobsInput } from "./runtime-BPuZITUC.mjs";
2
- export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileHandle, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionSnapshot, type BackendStage, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type InProcessLockConflictError, type ListJobsInput, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SessionNotFoundError, type SessionStaleWriteError };
1
+ import { $ as BackendStage, A as BackendSessionSnapshot, B as ListJobsInput, C as BackendJobProgress, D as BackendResult, E as BackendProjectSessionStorage, F as GetCacheSnapshotInput, G as SessionNotFoundError, H as RefreshCacheInput, I as GetEventsInput, J as BACKEND_COMPATIBILITY_POLICY, K as SessionStaleWriteError, L as GetEventsSnapshot, M as CacheRefreshFailedError, N as CancelJobInput, O as BackendRuntime, P as EventCursorInvalidError, Q as BackendResponseMeta, R as GetJobInput, S as BackendJobNotFoundError, T as BackendJobStatus, U as SavePartialFailureError, V as OpenProjectSessionInput, W as SaveSessionInput, X as BackendDiagnostic, Y as BACKEND_CONTRACT_VERSION, Z as BackendMessage, _ as BackendJobErrors, a as BackendCacheSnapshot, b as BackendJobListStatusFilter, c as BackendCapabilityUnavailableError, d as BackendEventKind, f as BackendFailure, g as BackendHostAdapter, h as BackendFileSystem, i as BackendCacheEntry, j as BackendSuccess, k as BackendRuntimeOptions, l as BackendError, m as BackendFileStat, n as ApplySessionTransactionInput, o as BackendCapabilities, p as BackendFileHandle, q as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BackendArtifactBridgeCapability, s as BackendCapabilityManifest, t as AdvisoryLockConflictError, u as BackendEvent, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as InProcessLockConflictError } from "./runtime-Bydxr_SN.mjs";
2
+ import { FileSystem } from "@openfairygui/core/project-io";
3
+
4
+ //#region src/storage.d.ts
5
+ type StorageStatKind = 'file' | 'directory';
6
+ interface BackendStorageStatLike {
7
+ kind?: StorageStatKind;
8
+ type?: StorageStatKind;
9
+ isFile?(): boolean;
10
+ isDirectory?(): boolean;
11
+ }
12
+ interface BackendAsyncStorageAdapter {
13
+ readFile(filePath: string): Promise<string>;
14
+ readFileRaw(filePath: string): Promise<Uint8Array>;
15
+ writeFile(filePath: string, content: string): Promise<void>;
16
+ writeFileRaw(filePath: string, data: Uint8Array): Promise<void>;
17
+ mkdir(dirPath: string, options?: {
18
+ recursive?: boolean;
19
+ }): Promise<void>;
20
+ readdir(dirPath: string): Promise<string[]>;
21
+ exists?(filePath: string): Promise<boolean>;
22
+ stat?(filePath: string): Promise<BackendStorageStatLike>;
23
+ resolvePath?(filePath: string): Promise<string>;
24
+ openExclusive?(filePath: string): Promise<BackendFileHandle>;
25
+ unlink?(filePath: string): Promise<void>;
26
+ join?(...paths: string[]): string;
27
+ dirname?(filePath: string): string;
28
+ resolve?(...paths: string[]): string;
29
+ }
30
+ type BackendStorageFileSystem = BackendFileSystem & FileSystem;
31
+ declare function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem;
32
+ //#endregion
33
+ export { type AdvisoryLockConflictError, type ApplySessionTransactionInput, BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, type BackendArtifactBridgeCapability, type BackendAsyncStorageAdapter, type BackendCacheEntry, type BackendCacheSnapshot, type BackendCapabilities, type BackendCapabilityManifest, type BackendCapabilityUnavailableError, type BackendDiagnostic, type BackendError, type BackendEvent, type BackendEventKind, type BackendFailure, type BackendFileHandle, type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, type BackendJobErrors, type BackendJobKind, type BackendJobListSnapshot, type BackendJobListStatusFilter, type BackendJobNotCancellableError, type BackendJobNotFoundError, type BackendJobProgress, type BackendJobSnapshot, type BackendJobStatus, type BackendMessage, type BackendProjectSessionStorage, type BackendResponseMeta, type BackendResult, BackendRuntime, type BackendRuntimeOptions, type BackendSessionSnapshot, type BackendStage, type BackendStorageFileSystem, type BackendStorageStatLike, type BackendSuccess, type CacheRefreshFailedError, type CancelJobInput, type EventCursorInvalidError, type GetCacheSnapshotInput, type GetEventsInput, type GetEventsSnapshot, type GetJobInput, type InProcessLockConflictError, type ListJobsInput, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, createBackendStorageFileSystem };
package/dist/index.mjs CHANGED
@@ -1,2 +1,146 @@
1
- import { i as BACKEND_CONTRACT_VERSION, n as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BACKEND_COMPATIBILITY_POLICY, t as BackendRuntime } from "./runtime-CqARWlcn.mjs";
2
- export { BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, BackendRuntime };
1
+ import { i as BACKEND_CONTRACT_VERSION, n as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BACKEND_COMPATIBILITY_POLICY, t as BackendRuntime } from "./runtime-Cn0O-wZt.mjs";
2
+ //#region src/storage.ts
3
+ var StorageFileStat = class {
4
+ constructor(kind) {
5
+ this.kind = kind;
6
+ }
7
+ isFile() {
8
+ return this.kind === "file";
9
+ }
10
+ isDirectory() {
11
+ return this.kind === "directory";
12
+ }
13
+ };
14
+ function createPathError(code, message) {
15
+ const error = new Error(message);
16
+ error.code = code;
17
+ return error;
18
+ }
19
+ function normalizeStoragePath(value) {
20
+ const normalized = value.replace(/\\/g, "/").replace(/\/+/g, "/");
21
+ const absolute = normalized.startsWith("/");
22
+ const rawSegments = normalized.split("/").filter((segment) => segment.length > 0);
23
+ const segments = [];
24
+ for (const segment of rawSegments) {
25
+ if (segment === ".") continue;
26
+ if (segment === "..") {
27
+ if (segments.length > 0) segments.pop();
28
+ continue;
29
+ }
30
+ segments.push(segment);
31
+ }
32
+ const joined = segments.join("/");
33
+ if (absolute) return joined ? `/${joined}` : "/";
34
+ return joined || ".";
35
+ }
36
+ function joinStoragePath(...paths) {
37
+ return normalizeStoragePath(paths.filter((part) => part.length > 0).join("/"));
38
+ }
39
+ function dirnameStoragePath(filePath) {
40
+ const normalized = normalizeStoragePath(filePath);
41
+ if (normalized === "/" || normalized === ".") return ".";
42
+ const absolute = normalized.startsWith("/");
43
+ const parts = normalized.split("/").filter((part) => part.length > 0);
44
+ parts.pop();
45
+ if (parts.length === 0) return absolute ? "/" : ".";
46
+ return `${absolute ? "/" : ""}${parts.join("/")}`;
47
+ }
48
+ function statFromLike(stat) {
49
+ if (typeof stat.isFile === "function" && typeof stat.isDirectory === "function") return stat;
50
+ const kind = stat.kind ?? stat.type;
51
+ if (kind === "file" || kind === "directory") return new StorageFileStat(kind);
52
+ throw createPathError("EINVAL", "Storage stat must provide kind/type or isFile()/isDirectory().");
53
+ }
54
+ async function inferStat(storage, filePath) {
55
+ if (storage.stat) return statFromLike(await storage.stat(filePath));
56
+ try {
57
+ await storage.readdir(filePath);
58
+ return new StorageFileStat("directory");
59
+ } catch {}
60
+ try {
61
+ await storage.readFileRaw(filePath);
62
+ return new StorageFileStat("file");
63
+ } catch {
64
+ try {
65
+ await storage.readFile(filePath);
66
+ return new StorageFileStat("file");
67
+ } catch {
68
+ throw createPathError("ENOENT", `Storage path not found: ${filePath}`);
69
+ }
70
+ }
71
+ }
72
+ function createBackendStorageFileSystem(storage) {
73
+ const lockedPaths = /* @__PURE__ */ new Set();
74
+ const fileSystem = {
75
+ stat(filePath) {
76
+ return inferStat(storage, fileSystem.resolve(filePath));
77
+ },
78
+ readdir(dirPath) {
79
+ return storage.readdir(fileSystem.resolve(dirPath));
80
+ },
81
+ readFile(filePath) {
82
+ return storage.readFile(fileSystem.resolve(filePath));
83
+ },
84
+ readFileRaw(filePath) {
85
+ return storage.readFileRaw(fileSystem.resolve(filePath));
86
+ },
87
+ writeFile(filePath, content) {
88
+ return storage.writeFile(fileSystem.resolve(filePath), content);
89
+ },
90
+ writeFileRaw(filePath, data) {
91
+ return storage.writeFileRaw(fileSystem.resolve(filePath), data);
92
+ },
93
+ mkdir(dirPath, options) {
94
+ return storage.mkdir(fileSystem.resolve(dirPath), options);
95
+ },
96
+ async exists(filePath) {
97
+ if (storage.exists) return storage.exists(fileSystem.resolve(filePath));
98
+ try {
99
+ await fileSystem.stat(filePath);
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ },
105
+ resolvePath(filePath) {
106
+ const resolved = fileSystem.resolve(filePath);
107
+ return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
108
+ },
109
+ async openExclusive(filePath) {
110
+ const resolved = fileSystem.resolve(filePath);
111
+ if (storage.openExclusive) return storage.openExclusive(resolved);
112
+ if (lockedPaths.has(resolved) || await fileSystem.exists(resolved)) throw createPathError("EEXIST", `Storage path already exists: ${resolved}`);
113
+ lockedPaths.add(resolved);
114
+ let closed = false;
115
+ return {
116
+ async writeFile(content) {
117
+ if (closed) throw createPathError("EBADF", `Storage lock handle is closed: ${resolved}`);
118
+ await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
119
+ await storage.writeFile(resolved, content);
120
+ },
121
+ async close() {
122
+ closed = true;
123
+ lockedPaths.delete(resolved);
124
+ }
125
+ };
126
+ },
127
+ unlink(filePath) {
128
+ const resolved = fileSystem.resolve(filePath);
129
+ lockedPaths.delete(resolved);
130
+ if (storage.unlink) return storage.unlink(resolved);
131
+ throw createPathError("ENOTSUP", "Storage adapter does not provide unlink().");
132
+ },
133
+ join(...paths) {
134
+ return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
135
+ },
136
+ dirname(filePath) {
137
+ return storage.dirname ? storage.dirname(filePath) : dirnameStoragePath(filePath);
138
+ },
139
+ resolve(...paths) {
140
+ return storage.resolve ? storage.resolve(...paths) : joinStoragePath(...paths);
141
+ }
142
+ };
143
+ return fileSystem;
144
+ }
145
+ //#endregion
146
+ export { BACKEND_CAPABILITY_SCHEMA_VERSION, BACKEND_COMPATIBILITY_POLICY, BACKEND_CONTRACT_VERSION, BackendRuntime, createBackendStorageFileSystem };
package/dist/node.cjs CHANGED
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_runtime = require("./runtime--ytGfQCF.cjs");
24
+ const require_runtime = require("./runtime-YK3ACpuQ.cjs");
25
25
  let node_fs_promises = require("node:fs/promises");
26
26
  node_fs_promises = __toESM(node_fs_promises);
27
27
  let node_path = require("node:path");
package/dist/node.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as BackendRuntime, O as BackendRuntimeOptions, g as BackendHostAdapter, h as BackendFileSystem, m as BackendFileStat, p as BackendFileHandle } from "./runtime-DlM27Izm.cjs";
1
+ import { O as BackendRuntime, g as BackendHostAdapter, h as BackendFileSystem, k as BackendRuntimeOptions, m as BackendFileStat, p as BackendFileHandle } from "./runtime-DziTGugy.cjs";
2
2
 
3
3
  //#region src/node.d.ts
4
4
  declare function createNodeBackendFileSystem(): BackendFileSystem;
package/dist/node.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as BackendRuntime, O as BackendRuntimeOptions, g as BackendHostAdapter, h as BackendFileSystem, m as BackendFileStat, p as BackendFileHandle } from "./runtime-BPuZITUC.mjs";
1
+ import { O as BackendRuntime, g as BackendHostAdapter, h as BackendFileSystem, k as BackendRuntimeOptions, m as BackendFileStat, p as BackendFileHandle } from "./runtime-Bydxr_SN.mjs";
2
2
 
3
3
  //#region src/node.d.ts
4
4
  declare function createNodeBackendFileSystem(): BackendFileSystem;
package/dist/node.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as BackendRuntime } from "./runtime-CqARWlcn.mjs";
1
+ import { t as BackendRuntime } from "./runtime-Cn0O-wZt.mjs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  //#region src/node.ts
@@ -97,6 +97,12 @@ interface BackendCapabilityManifest {
97
97
  injected: true;
98
98
  requiredFor: readonly ['openSession', 'saveSession'];
99
99
  };
100
+ projectStorage: {
101
+ injected: true;
102
+ browserSafe: true;
103
+ requiredFor: readonly ['openProjectSession.writeback', 'saveSession'];
104
+ adapterFactory: 'createBackendStorageFileSystem';
105
+ };
100
106
  host: {
101
107
  injected: true;
102
108
  requiredFor: readonly ['advisoryLockMetadata'];
@@ -384,6 +390,19 @@ interface OpenProjectSessionInput {
384
390
  sessionId?: string;
385
391
  canonicalProjectPath?: string;
386
392
  canonicalPathKey?: string;
393
+ storage?: BackendProjectSessionStorage;
394
+ }
395
+ interface BackendProjectSessionStorage {
396
+ fileSystem: BackendFileSystem;
397
+ fairyPath: string;
398
+ canonicalProjectPath?: string;
399
+ canonicalPathKey?: string;
400
+ }
401
+ interface SaveSessionInput {
402
+ sessionId: string;
403
+ expectedRevision?: number;
404
+ targetPath?: string;
405
+ fileSystem?: BackendFileSystem;
387
406
  }
388
407
  interface BackendRuntimeOptions {
389
408
  fileSystem?: BackendFileSystem;
@@ -415,11 +434,7 @@ declare class BackendRuntime {
415
434
  sessionId: string;
416
435
  }): BackendResult<BackendSessionSnapshot, SessionNotFoundError>;
417
436
  applyTransaction(input: ApplySessionTransactionInput): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError>>;
418
- saveSession(input: {
419
- sessionId: string;
420
- expectedRevision?: number;
421
- targetPath?: string;
422
- }): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>>;
437
+ saveSession(input: SaveSessionInput): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>>;
423
438
  closeSession(input: {
424
439
  sessionId: string;
425
440
  }): Promise<BackendResult<{
@@ -434,4 +449,4 @@ declare class BackendRuntime {
434
449
  refreshCache(input: RefreshCacheInput): BackendResult<BackendJobSnapshot, SessionNotFoundError>;
435
450
  }
436
451
  //#endregion
437
- export { BackendSuccess as A, OpenProjectSessionInput as B, BackendJobProgress as C, BackendRuntime as D, BackendResult as E, GetEventsInput as F, BACKEND_CAPABILITY_SCHEMA_VERSION as G, SavePartialFailureError as H, GetEventsSnapshot as I, BackendDiagnostic as J, BACKEND_COMPATIBILITY_POLICY as K, GetJobInput as L, CancelJobInput as M, EventCursorInvalidError as N, BackendRuntimeOptions as O, GetCacheSnapshotInput as P, InProcessLockConflictError as R, BackendJobNotFoundError as S, BackendJobStatus as T, SessionNotFoundError as U, RefreshCacheInput as V, SessionStaleWriteError as W, BackendResponseMeta as X, BackendMessage as Y, BackendStage as Z, BackendJobErrors as _, BackendCacheSnapshot as a, BackendJobListStatusFilter as b, BackendCapabilityUnavailableError as c, BackendEventKind as d, BackendFailure as f, BackendHostAdapter as g, BackendFileSystem as h, BackendCacheEntry as i, CacheRefreshFailedError as j, BackendSessionSnapshot as k, BackendError as l, BackendFileStat as m, ApplySessionTransactionInput as n, BackendCapabilities as o, BackendFileHandle as p, BACKEND_CONTRACT_VERSION as q, BackendArtifactBridgeCapability as r, BackendCapabilityManifest as s, AdvisoryLockConflictError as t, BackendEvent as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, ListJobsInput as z };
452
+ export { BackendStage as $, BackendSessionSnapshot as A, ListJobsInput as B, BackendJobProgress as C, BackendResult as D, BackendProjectSessionStorage as E, GetCacheSnapshotInput as F, SessionNotFoundError as G, RefreshCacheInput as H, GetEventsInput as I, BACKEND_COMPATIBILITY_POLICY as J, SessionStaleWriteError as K, GetEventsSnapshot as L, CacheRefreshFailedError as M, CancelJobInput as N, BackendRuntime as O, EventCursorInvalidError as P, BackendResponseMeta as Q, GetJobInput as R, BackendJobNotFoundError as S, BackendJobStatus as T, SavePartialFailureError as U, OpenProjectSessionInput as V, SaveSessionInput as W, BackendDiagnostic as X, BACKEND_CONTRACT_VERSION as Y, BackendMessage as Z, BackendJobErrors as _, BackendCacheSnapshot as a, BackendJobListStatusFilter as b, BackendCapabilityUnavailableError as c, BackendEventKind as d, BackendFailure as f, BackendHostAdapter as g, BackendFileSystem as h, BackendCacheEntry as i, BackendSuccess as j, BackendRuntimeOptions as k, BackendError as l, BackendFileStat as m, ApplySessionTransactionInput as n, BackendCapabilities as o, BackendFileHandle as p, BACKEND_CAPABILITY_SCHEMA_VERSION as q, BackendArtifactBridgeCapability as r, BackendCapabilityManifest as s, AdvisoryLockConflictError as t, BackendEvent as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, InProcessLockConflictError as z };
@@ -331,7 +331,8 @@ var AuthoringService = class {
331
331
  const startedAt = Date.now();
332
332
  const session = this.context.sessions.get(input.sessionId);
333
333
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
334
- if (!this.context.fileSystem) return failure("authoring", startedAt, {
334
+ const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
335
+ if (!fileSystem) return failure("authoring", startedAt, {
335
336
  code: "capability_unavailable",
336
337
  message: "saveSession requires an injected BackendFileSystem adapter.",
337
338
  capability: "fileSystem",
@@ -344,7 +345,6 @@ var AuthoringService = class {
344
345
  sessionId: session.sessionId,
345
346
  revision: session.revision
346
347
  });
347
- const fileSystem = this.context.fileSystem;
348
348
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
349
349
  if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
350
350
  sessionId: session.sessionId,
@@ -908,6 +908,7 @@ var RuntimeService = class {
908
908
  canonicalProjectPath,
909
909
  canonicalPathKey,
910
910
  lockFilePath,
911
+ fileSystem,
911
912
  project,
912
913
  revision: 0,
913
914
  lastSavedRevision: 0,
@@ -946,8 +947,10 @@ var RuntimeService = class {
946
947
  openProjectSession(input) {
947
948
  const startedAt = Date.now();
948
949
  const sessionId = input.sessionId ?? randomId();
949
- const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
950
- const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
950
+ const storage = input.storage;
951
+ const memoryProjectPath = `memory://${sessionId}`;
952
+ const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
953
+ const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
951
954
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
952
955
  if (existingSessionId) return failure("runtime", startedAt, {
953
956
  code: "lock_conflict",
@@ -958,10 +961,11 @@ var RuntimeService = class {
958
961
  });
959
962
  const session = {
960
963
  sessionId,
961
- fairyPath: canonicalProjectPath,
964
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
962
965
  canonicalProjectPath,
963
966
  canonicalPathKey,
964
967
  lockFilePath: "",
968
+ fileSystem: storage?.fileSystem,
965
969
  project: normalizeUamProject(input.project),
966
970
  revision: 0,
967
971
  lastSavedRevision: 0,
@@ -1075,6 +1079,12 @@ function createCapabilities() {
1075
1079
  injected: true,
1076
1080
  requiredFor: ["openSession", "saveSession"]
1077
1081
  },
1082
+ projectStorage: {
1083
+ injected: true,
1084
+ browserSafe: true,
1085
+ requiredFor: ["openProjectSession.writeback", "saveSession"],
1086
+ adapterFactory: "createBackendStorageFileSystem"
1087
+ },
1078
1088
  host: {
1079
1089
  injected: true,
1080
1090
  requiredFor: ["advisoryLockMetadata"]
@@ -97,6 +97,12 @@ interface BackendCapabilityManifest {
97
97
  injected: true;
98
98
  requiredFor: readonly ['openSession', 'saveSession'];
99
99
  };
100
+ projectStorage: {
101
+ injected: true;
102
+ browserSafe: true;
103
+ requiredFor: readonly ['openProjectSession.writeback', 'saveSession'];
104
+ adapterFactory: 'createBackendStorageFileSystem';
105
+ };
100
106
  host: {
101
107
  injected: true;
102
108
  requiredFor: readonly ['advisoryLockMetadata'];
@@ -384,6 +390,19 @@ interface OpenProjectSessionInput {
384
390
  sessionId?: string;
385
391
  canonicalProjectPath?: string;
386
392
  canonicalPathKey?: string;
393
+ storage?: BackendProjectSessionStorage;
394
+ }
395
+ interface BackendProjectSessionStorage {
396
+ fileSystem: BackendFileSystem;
397
+ fairyPath: string;
398
+ canonicalProjectPath?: string;
399
+ canonicalPathKey?: string;
400
+ }
401
+ interface SaveSessionInput {
402
+ sessionId: string;
403
+ expectedRevision?: number;
404
+ targetPath?: string;
405
+ fileSystem?: BackendFileSystem;
387
406
  }
388
407
  interface BackendRuntimeOptions {
389
408
  fileSystem?: BackendFileSystem;
@@ -415,11 +434,7 @@ declare class BackendRuntime {
415
434
  sessionId: string;
416
435
  }): BackendResult<BackendSessionSnapshot, SessionNotFoundError>;
417
436
  applyTransaction(input: ApplySessionTransactionInput): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError>>;
418
- saveSession(input: {
419
- sessionId: string;
420
- expectedRevision?: number;
421
- targetPath?: string;
422
- }): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>>;
437
+ saveSession(input: SaveSessionInput): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>>;
423
438
  closeSession(input: {
424
439
  sessionId: string;
425
440
  }): Promise<BackendResult<{
@@ -434,4 +449,4 @@ declare class BackendRuntime {
434
449
  refreshCache(input: RefreshCacheInput): BackendResult<BackendJobSnapshot, SessionNotFoundError>;
435
450
  }
436
451
  //#endregion
437
- export { BackendSuccess as A, OpenProjectSessionInput as B, BackendJobProgress as C, BackendRuntime as D, BackendResult as E, GetEventsInput as F, BACKEND_CAPABILITY_SCHEMA_VERSION as G, SavePartialFailureError as H, GetEventsSnapshot as I, BackendDiagnostic as J, BACKEND_COMPATIBILITY_POLICY as K, GetJobInput as L, CancelJobInput as M, EventCursorInvalidError as N, BackendRuntimeOptions as O, GetCacheSnapshotInput as P, InProcessLockConflictError as R, BackendJobNotFoundError as S, BackendJobStatus as T, SessionNotFoundError as U, RefreshCacheInput as V, SessionStaleWriteError as W, BackendResponseMeta as X, BackendMessage as Y, BackendStage as Z, BackendJobErrors as _, BackendCacheSnapshot as a, BackendJobListStatusFilter as b, BackendCapabilityUnavailableError as c, BackendEventKind as d, BackendFailure as f, BackendHostAdapter as g, BackendFileSystem as h, BackendCacheEntry as i, CacheRefreshFailedError as j, BackendSessionSnapshot as k, BackendError as l, BackendFileStat as m, ApplySessionTransactionInput as n, BackendCapabilities as o, BackendFileHandle as p, BACKEND_CONTRACT_VERSION as q, BackendArtifactBridgeCapability as r, BackendCapabilityManifest as s, AdvisoryLockConflictError as t, BackendEvent as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, ListJobsInput as z };
452
+ export { BackendStage as $, BackendSessionSnapshot as A, ListJobsInput as B, BackendJobProgress as C, BackendResult as D, BackendProjectSessionStorage as E, GetCacheSnapshotInput as F, SessionNotFoundError as G, RefreshCacheInput as H, GetEventsInput as I, BACKEND_COMPATIBILITY_POLICY as J, SessionStaleWriteError as K, GetEventsSnapshot as L, CacheRefreshFailedError as M, CancelJobInput as N, BackendRuntime as O, EventCursorInvalidError as P, BackendResponseMeta as Q, GetJobInput as R, BackendJobNotFoundError as S, BackendJobStatus as T, SavePartialFailureError as U, OpenProjectSessionInput as V, SaveSessionInput as W, BackendDiagnostic as X, BACKEND_CONTRACT_VERSION as Y, BackendMessage as Z, BackendJobErrors as _, BackendCacheSnapshot as a, BackendJobListStatusFilter as b, BackendCapabilityUnavailableError as c, BackendEventKind as d, BackendFailure as f, BackendHostAdapter as g, BackendFileSystem as h, BackendCacheEntry as i, BackendSuccess as j, BackendRuntimeOptions as k, BackendError as l, BackendFileStat as m, ApplySessionTransactionInput as n, BackendCapabilities as o, BackendFileHandle as p, BACKEND_CAPABILITY_SCHEMA_VERSION as q, BackendArtifactBridgeCapability as r, BackendCapabilityManifest as s, AdvisoryLockConflictError as t, BackendEvent as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, InProcessLockConflictError as z };
@@ -332,7 +332,8 @@ var AuthoringService = class {
332
332
  const startedAt = Date.now();
333
333
  const session = this.context.sessions.get(input.sessionId);
334
334
  if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
335
- if (!this.context.fileSystem) return failure("authoring", startedAt, {
335
+ const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
336
+ if (!fileSystem) return failure("authoring", startedAt, {
336
337
  code: "capability_unavailable",
337
338
  message: "saveSession requires an injected BackendFileSystem adapter.",
338
339
  capability: "fileSystem",
@@ -345,7 +346,6 @@ var AuthoringService = class {
345
346
  sessionId: session.sessionId,
346
347
  revision: session.revision
347
348
  });
348
- const fileSystem = this.context.fileSystem;
349
349
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
350
350
  if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
351
351
  sessionId: session.sessionId,
@@ -909,6 +909,7 @@ var RuntimeService = class {
909
909
  canonicalProjectPath,
910
910
  canonicalPathKey,
911
911
  lockFilePath,
912
+ fileSystem,
912
913
  project,
913
914
  revision: 0,
914
915
  lastSavedRevision: 0,
@@ -947,8 +948,10 @@ var RuntimeService = class {
947
948
  openProjectSession(input) {
948
949
  const startedAt = Date.now();
949
950
  const sessionId = input.sessionId ?? randomId();
950
- const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
951
- const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
951
+ const storage = input.storage;
952
+ const memoryProjectPath = `memory://${sessionId}`;
953
+ const canonicalProjectPath = storage?.canonicalProjectPath ?? input.canonicalProjectPath ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || "." : memoryProjectPath);
954
+ const canonicalPathKey = storage?.canonicalPathKey ?? input.canonicalPathKey ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
952
955
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
953
956
  if (existingSessionId) return failure("runtime", startedAt, {
954
957
  code: "lock_conflict",
@@ -959,10 +962,11 @@ var RuntimeService = class {
959
962
  });
960
963
  const session = {
961
964
  sessionId,
962
- fairyPath: canonicalProjectPath,
965
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
963
966
  canonicalProjectPath,
964
967
  canonicalPathKey,
965
968
  lockFilePath: "",
969
+ fileSystem: storage?.fileSystem,
966
970
  project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
967
971
  revision: 0,
968
972
  lastSavedRevision: 0,
@@ -1076,6 +1080,12 @@ function createCapabilities() {
1076
1080
  injected: true,
1077
1081
  requiredFor: ["openSession", "saveSession"]
1078
1082
  },
1083
+ projectStorage: {
1084
+ injected: true,
1085
+ browserSafe: true,
1086
+ requiredFor: ["openProjectSession.writeback", "saveSession"],
1087
+ adapterFactory: "createBackendStorageFileSystem"
1088
+ },
1079
1089
  host: {
1080
1090
  injected: true,
1081
1091
  requiredFor: ["advisoryLockMetadata"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/backend",
3
- "version": "0.2.0-alpha.5",
3
+ "version": "0.2.0-alpha.7",
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.5",
56
- "@openfairygui/functions": "0.2.0-alpha.5"
55
+ "@openfairygui/core": "0.2.0-alpha.7",
56
+ "@openfairygui/functions": "0.2.0-alpha.7"
57
57
  },
58
58
  "devDependencies": {
59
59
  "ava": "^7.0.0",
package/src/index.ts CHANGED
@@ -39,11 +39,19 @@ export {
39
39
  type InProcessLockConflictError,
40
40
  type ListJobsInput,
41
41
  type OpenProjectSessionInput,
42
+ type BackendProjectSessionStorage,
42
43
  type RefreshCacheInput,
44
+ type SaveSessionInput,
43
45
  type SavePartialFailureError,
44
46
  type SessionNotFoundError,
45
47
  type SessionStaleWriteError,
46
48
  } from './runtime.js';
49
+ export {
50
+ createBackendStorageFileSystem,
51
+ type BackendAsyncStorageAdapter,
52
+ type BackendStorageFileSystem,
53
+ type BackendStorageStatLike,
54
+ } from './storage.js';
47
55
  export {
48
56
  type BackendDiagnostic,
49
57
  type BackendMessage,
package/src/runtime.ts CHANGED
@@ -68,6 +68,12 @@ export interface BackendCapabilityManifest {
68
68
  injected: true;
69
69
  requiredFor: readonly ['openSession', 'saveSession'];
70
70
  };
71
+ projectStorage: {
72
+ injected: true;
73
+ browserSafe: true;
74
+ requiredFor: readonly ['openProjectSession.writeback', 'saveSession'];
75
+ adapterFactory: 'createBackendStorageFileSystem';
76
+ };
71
77
  host: {
72
78
  injected: true;
73
79
  requiredFor: readonly ['advisoryLockMetadata'];
@@ -438,6 +444,21 @@ export interface OpenProjectSessionInput {
438
444
  sessionId?: string;
439
445
  canonicalProjectPath?: string;
440
446
  canonicalPathKey?: string;
447
+ storage?: BackendProjectSessionStorage;
448
+ }
449
+
450
+ export interface BackendProjectSessionStorage {
451
+ fileSystem: BackendFileSystem;
452
+ fairyPath: string;
453
+ canonicalProjectPath?: string;
454
+ canonicalPathKey?: string;
455
+ }
456
+
457
+ export interface SaveSessionInput {
458
+ sessionId: string;
459
+ expectedRevision?: number;
460
+ targetPath?: string;
461
+ fileSystem?: BackendFileSystem;
441
462
  }
442
463
 
443
464
  export interface BackendRuntimeOptions {
@@ -504,6 +525,12 @@ function createCapabilities(): BackendCapabilities {
504
525
  injected: true,
505
526
  requiredFor: ['openSession', 'saveSession'],
506
527
  },
528
+ projectStorage: {
529
+ injected: true,
530
+ browserSafe: true,
531
+ requiredFor: ['openProjectSession.writeback', 'saveSession'],
532
+ adapterFactory: 'createBackendStorageFileSystem',
533
+ },
507
534
  host: {
508
535
  injected: true,
509
536
  requiredFor: ['advisoryLockMetadata'],
@@ -628,11 +655,7 @@ export class BackendRuntime {
628
655
  return this.authoringService.applyTransaction(input);
629
656
  }
630
657
 
631
- public async saveSession(input: {
632
- sessionId: string;
633
- expectedRevision?: number;
634
- targetPath?: string;
635
- }): Promise<
658
+ public async saveSession(input: SaveSessionInput): Promise<
636
659
  BackendResult<
637
660
  BackendSessionSnapshot,
638
661
  | SessionNotFoundError
@@ -10,6 +10,7 @@ import type {
10
10
  BackendFileSystem,
11
11
  BackendResult,
12
12
  BackendSessionSnapshot,
13
+ SaveSessionInput,
13
14
  SavePartialFailureError,
14
15
  SessionNotFoundError,
15
16
  SessionStaleWriteError,
@@ -180,11 +181,7 @@ export class AuthoringService {
180
181
  });
181
182
  }
182
183
 
183
- public async saveSession(input: {
184
- sessionId: string;
185
- expectedRevision?: number;
186
- targetPath?: string;
187
- }): Promise<
184
+ public async saveSession(input: SaveSessionInput): Promise<
188
185
  BackendResult<
189
186
  BackendSessionSnapshot,
190
187
  | SessionNotFoundError
@@ -199,7 +196,8 @@ export class AuthoringService {
199
196
  if (!session || session.closed) {
200
197
  return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
201
198
  }
202
- if (!this.context.fileSystem) {
199
+ const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
200
+ if (!fileSystem) {
203
201
  return failure(
204
202
  'authoring',
205
203
  startedAt,
@@ -228,7 +226,6 @@ export class AuthoringService {
228
226
  },
229
227
  );
230
228
  }
231
- const fileSystem = this.context.fileSystem;
232
229
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
233
230
  if (targetViolation) {
234
231
  return failure(
@@ -25,6 +25,7 @@ export interface BackendSessionState {
25
25
  canonicalProjectPath: string;
26
26
  canonicalPathKey: string;
27
27
  lockFilePath: string;
28
+ fileSystem?: BackendFileSystem;
28
29
  project: import('@openfairygui/core/uam').UamProject;
29
30
  revision: number;
30
31
  lastSavedRevision: number;
@@ -14,7 +14,7 @@ import type {
14
14
  OpenProjectSessionInput,
15
15
  SessionNotFoundError,
16
16
  } from '../runtime.js';
17
- import { resolveCanonicalProjectRoot } from '../path-policy.js';
17
+ import { normalizeComparablePath, resolveCanonicalProjectRoot } from '../path-policy.js';
18
18
  import { createSessionNotFoundError, toSessionSnapshot } from './session-utils.js';
19
19
 
20
20
  function randomId(): string {
@@ -137,6 +137,7 @@ export class RuntimeService {
137
137
  canonicalProjectPath,
138
138
  canonicalPathKey,
139
139
  lockFilePath,
140
+ fileSystem,
140
141
  project,
141
142
  revision: 0,
142
143
  lastSavedRevision: 0,
@@ -174,8 +175,14 @@ export class RuntimeService {
174
175
  public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
175
176
  const startedAt = Date.now();
176
177
  const sessionId = input.sessionId ?? randomId();
177
- const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
178
- const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
178
+ const storage = input.storage;
179
+ const memoryProjectPath = `memory://${sessionId}`;
180
+ const canonicalProjectPath = storage?.canonicalProjectPath
181
+ ?? input.canonicalProjectPath
182
+ ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || '.' : memoryProjectPath);
183
+ const canonicalPathKey = storage?.canonicalPathKey
184
+ ?? input.canonicalPathKey
185
+ ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
179
186
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
180
187
  if (existingSessionId) {
181
188
  return failure('runtime', startedAt, {
@@ -189,10 +196,11 @@ export class RuntimeService {
189
196
 
190
197
  const session: BackendSessionState = {
191
198
  sessionId,
192
- fairyPath: canonicalProjectPath,
199
+ fairyPath: storage?.fairyPath ?? canonicalProjectPath,
193
200
  canonicalProjectPath,
194
201
  canonicalPathKey,
195
202
  lockFilePath: '',
203
+ fileSystem: storage?.fileSystem,
196
204
  project: normalizeUamProject(input.project),
197
205
  revision: 0,
198
206
  lastSavedRevision: 0,
package/src/storage.ts ADDED
@@ -0,0 +1,192 @@
1
+ import type { FileSystem as CoreProjectFileSystem } from '@openfairygui/core/project-io';
2
+ import type { BackendFileHandle, BackendFileStat, BackendFileSystem } from './runtime.js';
3
+
4
+ type StorageStatKind = 'file' | 'directory';
5
+
6
+ export interface BackendStorageStatLike {
7
+ kind?: StorageStatKind;
8
+ type?: StorageStatKind;
9
+ isFile?(): boolean;
10
+ isDirectory?(): boolean;
11
+ }
12
+
13
+ export interface BackendAsyncStorageAdapter {
14
+ readFile(filePath: string): Promise<string>;
15
+ readFileRaw(filePath: string): Promise<Uint8Array>;
16
+ writeFile(filePath: string, content: string): Promise<void>;
17
+ writeFileRaw(filePath: string, data: Uint8Array): Promise<void>;
18
+ mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void>;
19
+ readdir(dirPath: string): Promise<string[]>;
20
+ exists?(filePath: string): Promise<boolean>;
21
+ stat?(filePath: string): Promise<BackendStorageStatLike>;
22
+ resolvePath?(filePath: string): Promise<string>;
23
+ openExclusive?(filePath: string): Promise<BackendFileHandle>;
24
+ unlink?(filePath: string): Promise<void>;
25
+ join?(...paths: string[]): string;
26
+ dirname?(filePath: string): string;
27
+ resolve?(...paths: string[]): string;
28
+ }
29
+
30
+ class StorageFileStat implements BackendFileStat {
31
+ public constructor(private readonly kind: StorageStatKind) {}
32
+
33
+ public isFile(): boolean {
34
+ return this.kind === 'file';
35
+ }
36
+
37
+ public isDirectory(): boolean {
38
+ return this.kind === 'directory';
39
+ }
40
+ }
41
+
42
+ function createPathError(code: string, message: string): Error & { code: string } {
43
+ const error = new Error(message) as Error & { code: string };
44
+ error.code = code;
45
+ return error;
46
+ }
47
+
48
+ function normalizeStoragePath(value: string): string {
49
+ const normalized = value.replace(/\\/g, '/').replace(/\/+/g, '/');
50
+ const absolute = normalized.startsWith('/');
51
+ const rawSegments = normalized.split('/').filter((segment) => segment.length > 0);
52
+ const segments: string[] = [];
53
+
54
+ for (const segment of rawSegments) {
55
+ if (segment === '.') continue;
56
+ if (segment === '..') {
57
+ if (segments.length > 0) segments.pop();
58
+ continue;
59
+ }
60
+ segments.push(segment);
61
+ }
62
+
63
+ const joined = segments.join('/');
64
+ if (absolute) return joined ? `/${joined}` : '/';
65
+ return joined || '.';
66
+ }
67
+
68
+ function joinStoragePath(...paths: string[]): string {
69
+ return normalizeStoragePath(paths.filter((part) => part.length > 0).join('/'));
70
+ }
71
+
72
+ function dirnameStoragePath(filePath: string): string {
73
+ const normalized = normalizeStoragePath(filePath);
74
+ if (normalized === '/' || normalized === '.') return '.';
75
+ const absolute = normalized.startsWith('/');
76
+ const parts = normalized.split('/').filter((part) => part.length > 0);
77
+ parts.pop();
78
+ if (parts.length === 0) return absolute ? '/' : '.';
79
+ return `${absolute ? '/' : ''}${parts.join('/')}`;
80
+ }
81
+
82
+ function statFromLike(stat: BackendStorageStatLike): BackendFileStat {
83
+ if (typeof stat.isFile === 'function' && typeof stat.isDirectory === 'function') {
84
+ return stat as BackendFileStat;
85
+ }
86
+ const kind = stat.kind ?? stat.type;
87
+ if (kind === 'file' || kind === 'directory') return new StorageFileStat(kind);
88
+ throw createPathError('EINVAL', 'Storage stat must provide kind/type or isFile()/isDirectory().');
89
+ }
90
+
91
+ async function inferStat(storage: BackendAsyncStorageAdapter, filePath: string): Promise<BackendFileStat> {
92
+ if (storage.stat) return statFromLike(await storage.stat(filePath));
93
+
94
+ try {
95
+ await storage.readdir(filePath);
96
+ return new StorageFileStat('directory');
97
+ } catch {
98
+ // Try file probes below.
99
+ }
100
+
101
+ try {
102
+ await storage.readFileRaw(filePath);
103
+ return new StorageFileStat('file');
104
+ } catch {
105
+ try {
106
+ await storage.readFile(filePath);
107
+ return new StorageFileStat('file');
108
+ } catch {
109
+ throw createPathError('ENOENT', `Storage path not found: ${filePath}`);
110
+ }
111
+ }
112
+ }
113
+
114
+ export type BackendStorageFileSystem = BackendFileSystem & CoreProjectFileSystem;
115
+
116
+ export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem {
117
+ const lockedPaths = new Set<string>();
118
+
119
+ const fileSystem: BackendStorageFileSystem = {
120
+ stat(filePath: string): Promise<BackendFileStat> {
121
+ return inferStat(storage, fileSystem.resolve(filePath));
122
+ },
123
+ readdir(dirPath: string): Promise<string[]> {
124
+ return storage.readdir(fileSystem.resolve(dirPath));
125
+ },
126
+ readFile(filePath: string): Promise<string> {
127
+ return storage.readFile(fileSystem.resolve(filePath));
128
+ },
129
+ readFileRaw(filePath: string): Promise<Uint8Array> {
130
+ return storage.readFileRaw(fileSystem.resolve(filePath));
131
+ },
132
+ writeFile(filePath: string, content: string): Promise<void> {
133
+ return storage.writeFile(fileSystem.resolve(filePath), content);
134
+ },
135
+ writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
136
+ return storage.writeFileRaw(fileSystem.resolve(filePath), data);
137
+ },
138
+ mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
139
+ return storage.mkdir(fileSystem.resolve(dirPath), options);
140
+ },
141
+ async exists(filePath: string): Promise<boolean> {
142
+ if (storage.exists) return storage.exists(fileSystem.resolve(filePath));
143
+ try {
144
+ await fileSystem.stat(filePath);
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ },
150
+ resolvePath(filePath: string): Promise<string> {
151
+ const resolved = fileSystem.resolve(filePath);
152
+ return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
153
+ },
154
+ async openExclusive(filePath: string): Promise<BackendFileHandle> {
155
+ const resolved = fileSystem.resolve(filePath);
156
+ if (storage.openExclusive) return storage.openExclusive(resolved);
157
+ if (lockedPaths.has(resolved) || await fileSystem.exists(resolved)) {
158
+ throw createPathError('EEXIST', `Storage path already exists: ${resolved}`);
159
+ }
160
+ lockedPaths.add(resolved);
161
+ let closed = false;
162
+ return {
163
+ async writeFile(content: string): Promise<void> {
164
+ if (closed) throw createPathError('EBADF', `Storage lock handle is closed: ${resolved}`);
165
+ await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
166
+ await storage.writeFile(resolved, content);
167
+ },
168
+ async close(): Promise<void> {
169
+ closed = true;
170
+ lockedPaths.delete(resolved);
171
+ },
172
+ };
173
+ },
174
+ unlink(filePath: string): Promise<void> {
175
+ const resolved = fileSystem.resolve(filePath);
176
+ lockedPaths.delete(resolved);
177
+ if (storage.unlink) return storage.unlink(resolved);
178
+ throw createPathError('ENOTSUP', 'Storage adapter does not provide unlink().');
179
+ },
180
+ join(...paths: string[]): string {
181
+ return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
182
+ },
183
+ dirname(filePath: string): string {
184
+ return storage.dirname ? storage.dirname(filePath) : dirnameStoragePath(filePath);
185
+ },
186
+ resolve(...paths: string[]): string {
187
+ return storage.resolve ? storage.resolve(...paths) : joinStoragePath(...paths);
188
+ },
189
+ };
190
+
191
+ return fileSystem;
192
+ }