@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 +33 -2
- package/dist/index.cjs +146 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.mts +33 -2
- package/dist/index.mjs +146 -2
- package/dist/node.cjs +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.mjs +1 -1
- package/dist/{runtime-BPuZITUC.d.mts → runtime-Bydxr_SN.d.mts} +21 -6
- package/dist/{runtime-CqARWlcn.mjs → runtime-Cn0O-wZt.mjs} +15 -5
- package/dist/{runtime-DlM27Izm.d.cts → runtime-DziTGugy.d.cts} +21 -6
- package/dist/{runtime--ytGfQCF.cjs → runtime-YK3ACpuQ.cjs} +15 -5
- package/package.json +3 -3
- package/src/index.ts +8 -0
- package/src/runtime.ts +28 -5
- package/src/services/authoring-service.ts +4 -7
- package/src/services/context.ts +1 -0
- package/src/services/runtime-service.ts +12 -4
- package/src/storage.ts +192 -0
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:
|
|
35
|
-
|
|
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
|
|
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
|
|
2
|
-
|
|
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
|
|
2
|
-
|
|
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-
|
|
2
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
950
|
-
const
|
|
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 {
|
|
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
|
-
|
|
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
|
|
951
|
-
const
|
|
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.
|
|
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.
|
|
56
|
-
"@openfairygui/functions": "0.2.0-alpha.
|
|
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
|
-
|
|
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(
|
package/src/services/context.ts
CHANGED
|
@@ -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
|
|
178
|
-
const
|
|
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
|
+
}
|