@openfairygui/backend 0.2.0-alpha.2 → 0.2.0-alpha.21
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 +48 -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-OO1sHuCl.d.mts → runtime-B4SwHTRC.d.mts} +89 -10
- package/dist/{runtime-qcSlD_9-.d.cts → runtime-BB_3Ws5y.d.cts} +90 -11
- package/dist/{runtime-BPWzx-Tv.mjs → runtime-CqGK_D9A.mjs} +483 -87
- package/dist/{runtime-BL9Pkfc7.cjs → runtime-D3bShb1X.cjs} +482 -86
- package/package.json +5 -4
- package/src/contracts.ts +8 -0
- package/src/index.ts +25 -12
- package/src/runtime/capabilities.ts +126 -0
- package/src/runtime/contracts.ts +512 -0
- package/src/runtime.ts +67 -540
- package/src/services/authoring-service.ts +520 -80
- package/src/services/context.ts +12 -8
- package/src/services/runtime-service.ts +108 -12
- package/src/services/session-project-writer.ts +64 -0
- package/src/services/session-utils.ts +5 -1
- package/src/storage.ts +194 -0
package/src/services/context.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKEND_CAPABILITY_SCHEMA_VERSION,
|
|
3
|
+
BACKEND_CONTRACT_VERSION,
|
|
4
|
+
type BackendDiagnostic,
|
|
5
|
+
type BackendMessage,
|
|
6
|
+
type BackendResponseMeta,
|
|
7
|
+
type BackendStage,
|
|
8
|
+
} from '../contracts.js';
|
|
1
9
|
import type {
|
|
2
10
|
BackendCacheEntry,
|
|
3
11
|
BackendCapabilities,
|
|
@@ -10,14 +18,6 @@ import type {
|
|
|
10
18
|
BackendSessionSnapshot,
|
|
11
19
|
BackendSuccess,
|
|
12
20
|
} from '../runtime.js';
|
|
13
|
-
import {
|
|
14
|
-
BACKEND_CAPABILITY_SCHEMA_VERSION,
|
|
15
|
-
BACKEND_CONTRACT_VERSION,
|
|
16
|
-
type BackendDiagnostic,
|
|
17
|
-
type BackendMessage,
|
|
18
|
-
type BackendResponseMeta,
|
|
19
|
-
type BackendStage,
|
|
20
|
-
} from '../contracts.js';
|
|
21
21
|
|
|
22
22
|
export interface BackendSessionState {
|
|
23
23
|
sessionId: string;
|
|
@@ -25,9 +25,13 @@ 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;
|
|
30
|
+
uamFidelity: 'full' | 'unsupported';
|
|
29
31
|
revision: number;
|
|
30
32
|
lastSavedRevision: number;
|
|
33
|
+
/** Package-controlled files deferred until a successful replacement project write. */
|
|
34
|
+
pendingStaleSourceFiles: Map<string, import('@openfairygui/core/project-io').ProjectSourceFile>;
|
|
31
35
|
dirty: boolean;
|
|
32
36
|
lockHeld: boolean;
|
|
33
37
|
closed: boolean;
|
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
import { ProjectReader,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { type FileSystem, ProjectReader, ProjectWriter } from '@openfairygui/core/project-io';
|
|
2
|
+
import {
|
|
3
|
+
liftDocumentToUamProject,
|
|
4
|
+
materializeUamProject,
|
|
5
|
+
normalizeUamProject,
|
|
6
|
+
type UamProject,
|
|
7
|
+
} from '@openfairygui/core/uam';
|
|
8
|
+
import { normalizeComparablePath, resolveCanonicalProjectRoot } from '../path-policy.js';
|
|
7
9
|
import type {
|
|
8
10
|
AdvisoryLockConflictError,
|
|
11
|
+
BackendCapabilityUnavailableError,
|
|
9
12
|
BackendFileHandle,
|
|
10
13
|
BackendResult,
|
|
11
14
|
BackendSessionSnapshot,
|
|
12
|
-
BackendCapabilityUnavailableError,
|
|
13
15
|
InProcessLockConflictError,
|
|
14
16
|
OpenProjectSessionInput,
|
|
15
17
|
SessionNotFoundError,
|
|
16
18
|
} from '../runtime.js';
|
|
17
|
-
import {
|
|
19
|
+
import type { CacheService } from './cache-service.js';
|
|
20
|
+
import { type BackendContext, type BackendSessionState, failure, success } from './context.js';
|
|
21
|
+
import type { EventService } from './event-service.js';
|
|
22
|
+
import type { JobService } from './job-service.js';
|
|
18
23
|
import { createSessionNotFoundError, toSessionSnapshot } from './session-utils.js';
|
|
19
24
|
|
|
20
25
|
function randomId(): string {
|
|
@@ -74,6 +79,82 @@ function createProjectReaderFileSystem(fileSystem: NonNullable<BackendContext['f
|
|
|
74
79
|
};
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
function createCaptureFileSystem(files: Map<string, string | Uint8Array>): FileSystem {
|
|
83
|
+
const normalize = (filePath: string): string => filePath.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
84
|
+
return {
|
|
85
|
+
async readFile(filePath: string): Promise<string> {
|
|
86
|
+
const value = files.get(normalize(filePath));
|
|
87
|
+
if (typeof value !== 'string') throw new Error(`Captured text file was not found: ${filePath}`);
|
|
88
|
+
return value;
|
|
89
|
+
},
|
|
90
|
+
async readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
91
|
+
const value = files.get(normalize(filePath));
|
|
92
|
+
if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
|
|
93
|
+
return value.slice();
|
|
94
|
+
},
|
|
95
|
+
async writeFile(filePath: string, content: string): Promise<void> {
|
|
96
|
+
files.set(normalize(filePath), content);
|
|
97
|
+
},
|
|
98
|
+
async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
99
|
+
files.set(normalize(filePath), data.slice());
|
|
100
|
+
},
|
|
101
|
+
async mkdir(): Promise<void> {},
|
|
102
|
+
async readdir(): Promise<string[]> {
|
|
103
|
+
return [];
|
|
104
|
+
},
|
|
105
|
+
async exists(filePath: string): Promise<boolean> {
|
|
106
|
+
return files.has(normalize(filePath));
|
|
107
|
+
},
|
|
108
|
+
join(...paths: string[]): string {
|
|
109
|
+
return normalize(paths.filter(Boolean).join('/'));
|
|
110
|
+
},
|
|
111
|
+
dirname(filePath: string): string {
|
|
112
|
+
const normalized = normalize(filePath);
|
|
113
|
+
const separator = normalized.lastIndexOf('/');
|
|
114
|
+
return separator < 0 ? '' : normalized.slice(0, separator);
|
|
115
|
+
},
|
|
116
|
+
async unlink(filePath: string): Promise<void> {
|
|
117
|
+
files.delete(normalize(filePath));
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function capturedFilesEqual(left: Map<string, string | Uint8Array>, right: Map<string, string | Uint8Array>): boolean {
|
|
123
|
+
if (left.size !== right.size) return false;
|
|
124
|
+
for (const [filePath, leftValue] of left) {
|
|
125
|
+
const rightValue = right.get(filePath);
|
|
126
|
+
if (typeof leftValue === 'string') {
|
|
127
|
+
if (leftValue !== rightValue) return false;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
|
|
131
|
+
for (let index = 0; index < leftValue.length; index += 1) {
|
|
132
|
+
if (leftValue[index] !== rightValue[index]) return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function hasFullUamFidelity(
|
|
139
|
+
document: Awaited<ReturnType<ProjectReader['read']>>,
|
|
140
|
+
project: UamProject,
|
|
141
|
+
): Promise<boolean> {
|
|
142
|
+
const sourceFiles = new Map<string, string | Uint8Array>();
|
|
143
|
+
const materializedFiles = new Map<string, string | Uint8Array>();
|
|
144
|
+
try {
|
|
145
|
+
await Promise.all([
|
|
146
|
+
new ProjectWriter(createCaptureFileSystem(sourceFiles)).write(document, 'Project.fairy'),
|
|
147
|
+
new ProjectWriter(createCaptureFileSystem(materializedFiles)).write(
|
|
148
|
+
materializeUamProject(project),
|
|
149
|
+
'Project.fairy',
|
|
150
|
+
),
|
|
151
|
+
]);
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
return capturedFilesEqual(sourceFiles, materializedFiles);
|
|
156
|
+
}
|
|
157
|
+
|
|
77
158
|
export class RuntimeService {
|
|
78
159
|
public constructor(
|
|
79
160
|
private readonly context: BackendContext,
|
|
@@ -129,7 +210,8 @@ export class RuntimeService {
|
|
|
129
210
|
await advisoryLock.close();
|
|
130
211
|
|
|
131
212
|
const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
|
|
132
|
-
const
|
|
213
|
+
const document = await reader.read(fairyPath, { hydrateResourceBytes: true });
|
|
214
|
+
const project = liftDocumentToUamProject(document);
|
|
133
215
|
const sessionId = randomId();
|
|
134
216
|
const session: BackendSessionState = {
|
|
135
217
|
sessionId,
|
|
@@ -137,9 +219,12 @@ export class RuntimeService {
|
|
|
137
219
|
canonicalProjectPath,
|
|
138
220
|
canonicalPathKey,
|
|
139
221
|
lockFilePath,
|
|
222
|
+
fileSystem,
|
|
140
223
|
project,
|
|
224
|
+
uamFidelity: (await hasFullUamFidelity(document, project)) ? 'full' : 'unsupported',
|
|
141
225
|
revision: 0,
|
|
142
226
|
lastSavedRevision: 0,
|
|
227
|
+
pendingStaleSourceFiles: new Map(),
|
|
143
228
|
dirty: false,
|
|
144
229
|
lockHeld: true,
|
|
145
230
|
closed: false,
|
|
@@ -174,8 +259,16 @@ export class RuntimeService {
|
|
|
174
259
|
public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
|
|
175
260
|
const startedAt = Date.now();
|
|
176
261
|
const sessionId = input.sessionId ?? randomId();
|
|
177
|
-
const
|
|
178
|
-
const
|
|
262
|
+
const storage = input.storage;
|
|
263
|
+
const memoryProjectPath = `memory://${sessionId}`;
|
|
264
|
+
const canonicalProjectPath =
|
|
265
|
+
storage?.canonicalProjectPath ??
|
|
266
|
+
input.canonicalProjectPath ??
|
|
267
|
+
(storage ? storage.fileSystem.dirname(storage.fairyPath) || '.' : memoryProjectPath);
|
|
268
|
+
const canonicalPathKey =
|
|
269
|
+
storage?.canonicalPathKey ??
|
|
270
|
+
input.canonicalPathKey ??
|
|
271
|
+
(storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
|
|
179
272
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
180
273
|
if (existingSessionId) {
|
|
181
274
|
return failure('runtime', startedAt, {
|
|
@@ -189,13 +282,16 @@ export class RuntimeService {
|
|
|
189
282
|
|
|
190
283
|
const session: BackendSessionState = {
|
|
191
284
|
sessionId,
|
|
192
|
-
fairyPath: canonicalProjectPath,
|
|
285
|
+
fairyPath: storage?.fairyPath ?? canonicalProjectPath,
|
|
193
286
|
canonicalProjectPath,
|
|
194
287
|
canonicalPathKey,
|
|
195
288
|
lockFilePath: '',
|
|
289
|
+
fileSystem: storage?.fileSystem,
|
|
196
290
|
project: normalizeUamProject(input.project),
|
|
291
|
+
uamFidelity: 'full',
|
|
197
292
|
revision: 0,
|
|
198
293
|
lastSavedRevision: 0,
|
|
294
|
+
pendingStaleSourceFiles: new Map(),
|
|
199
295
|
dirty: false,
|
|
200
296
|
lockHeld: false,
|
|
201
297
|
closed: false,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Document } from '@openfairygui/core';
|
|
2
|
+
import { type FileSystem, type ProjectSourceFile, ProjectWriter } from '@openfairygui/core/project-io';
|
|
3
|
+
import type { BackendFileSystem } from '../runtime.js';
|
|
4
|
+
|
|
5
|
+
function createWriterFileSystem(
|
|
6
|
+
fileSystem: BackendFileSystem,
|
|
7
|
+
writtenPaths: string[],
|
|
8
|
+
failedPaths: string[],
|
|
9
|
+
): FileSystem {
|
|
10
|
+
async function trackWrite<T>(targetPath: string, write: () => Promise<T>): Promise<T> {
|
|
11
|
+
try {
|
|
12
|
+
const result = await write();
|
|
13
|
+
writtenPaths.push(targetPath);
|
|
14
|
+
return result;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
failedPaths.push(targetPath);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
readFile: (path) => fileSystem.readFile(path),
|
|
23
|
+
readFileRaw: (path) => fileSystem.readFileRaw(path),
|
|
24
|
+
writeFile: (path, content) =>
|
|
25
|
+
trackWrite(path, async () => {
|
|
26
|
+
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
27
|
+
await fileSystem.writeFile(path, content);
|
|
28
|
+
}),
|
|
29
|
+
writeFileRaw: (path, data) =>
|
|
30
|
+
trackWrite(path, async () => {
|
|
31
|
+
await fileSystem.mkdir(fileSystem.dirname(path), { recursive: true });
|
|
32
|
+
await fileSystem.writeFileRaw(path, data);
|
|
33
|
+
}),
|
|
34
|
+
mkdir: (path) => fileSystem.mkdir(path, { recursive: true }),
|
|
35
|
+
readdir: (path) => fileSystem.readdir(path),
|
|
36
|
+
async exists(path): Promise<boolean> {
|
|
37
|
+
try {
|
|
38
|
+
await fileSystem.stat(path);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
join: (...paths) => fileSystem.join(...paths),
|
|
45
|
+
dirname: (path) => fileSystem.dirname(path),
|
|
46
|
+
unlink: (path) => trackWrite(path, () => fileSystem.unlink(path)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function writeSessionProject(input: {
|
|
51
|
+
fileSystem: BackendFileSystem;
|
|
52
|
+
document: Document;
|
|
53
|
+
fairyPath: string;
|
|
54
|
+
staleSourceFiles: ProjectSourceFile[];
|
|
55
|
+
writtenPaths: string[];
|
|
56
|
+
failedPaths: string[];
|
|
57
|
+
}): Promise<void> {
|
|
58
|
+
const writer = new ProjectWriter(
|
|
59
|
+
createWriterFileSystem(input.fileSystem, input.writtenPaths, input.failedPaths),
|
|
60
|
+
);
|
|
61
|
+
await writer.write(input.document, input.fairyPath, {
|
|
62
|
+
staleSourceFiles: input.staleSourceFiles,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -7,13 +7,17 @@ import type {
|
|
|
7
7
|
import type { BackendSessionState } from './context.js';
|
|
8
8
|
import { cloneCapabilitiesSnapshot } from './snapshot-utils.js';
|
|
9
9
|
|
|
10
|
-
export function toSessionSnapshot(
|
|
10
|
+
export function toSessionSnapshot(
|
|
11
|
+
session: BackendSessionState,
|
|
12
|
+
capabilities: BackendCapabilities,
|
|
13
|
+
): BackendSessionSnapshot {
|
|
11
14
|
return {
|
|
12
15
|
sessionId: session.sessionId,
|
|
13
16
|
canonicalProjectPath: session.canonicalProjectPath,
|
|
14
17
|
revision: session.revision,
|
|
15
18
|
lastSavedRevision: session.lastSavedRevision,
|
|
16
19
|
dirty: session.dirty,
|
|
20
|
+
uamFidelity: session.uamFidelity,
|
|
17
21
|
lockHeld: session.lockHeld,
|
|
18
22
|
capabilities: cloneCapabilitiesSnapshot(capabilities),
|
|
19
23
|
};
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
if (typeof storage.unlink !== 'function') {
|
|
118
|
+
throw createPathError('ENOTSUP', 'Storage adapter must provide unlink() for project resource lifecycle writes.');
|
|
119
|
+
}
|
|
120
|
+
const lockedPaths = new Set<string>();
|
|
121
|
+
|
|
122
|
+
const fileSystem: BackendStorageFileSystem = {
|
|
123
|
+
stat(filePath: string): Promise<BackendFileStat> {
|
|
124
|
+
return inferStat(storage, fileSystem.resolve(filePath));
|
|
125
|
+
},
|
|
126
|
+
readdir(dirPath: string): Promise<string[]> {
|
|
127
|
+
return storage.readdir(fileSystem.resolve(dirPath));
|
|
128
|
+
},
|
|
129
|
+
readFile(filePath: string): Promise<string> {
|
|
130
|
+
return storage.readFile(fileSystem.resolve(filePath));
|
|
131
|
+
},
|
|
132
|
+
readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
133
|
+
return storage.readFileRaw(fileSystem.resolve(filePath));
|
|
134
|
+
},
|
|
135
|
+
writeFile(filePath: string, content: string): Promise<void> {
|
|
136
|
+
return storage.writeFile(fileSystem.resolve(filePath), content);
|
|
137
|
+
},
|
|
138
|
+
writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
139
|
+
return storage.writeFileRaw(fileSystem.resolve(filePath), data);
|
|
140
|
+
},
|
|
141
|
+
mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
|
|
142
|
+
return storage.mkdir(fileSystem.resolve(dirPath), options);
|
|
143
|
+
},
|
|
144
|
+
async exists(filePath: string): Promise<boolean> {
|
|
145
|
+
if (storage.exists) return storage.exists(fileSystem.resolve(filePath));
|
|
146
|
+
try {
|
|
147
|
+
await fileSystem.stat(filePath);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
resolvePath(filePath: string): Promise<string> {
|
|
154
|
+
const resolved = fileSystem.resolve(filePath);
|
|
155
|
+
return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
|
|
156
|
+
},
|
|
157
|
+
async openExclusive(filePath: string): Promise<BackendFileHandle> {
|
|
158
|
+
const resolved = fileSystem.resolve(filePath);
|
|
159
|
+
if (storage.openExclusive) return storage.openExclusive(resolved);
|
|
160
|
+
if (lockedPaths.has(resolved) || await fileSystem.exists(resolved)) {
|
|
161
|
+
throw createPathError('EEXIST', `Storage path already exists: ${resolved}`);
|
|
162
|
+
}
|
|
163
|
+
lockedPaths.add(resolved);
|
|
164
|
+
let closed = false;
|
|
165
|
+
return {
|
|
166
|
+
async writeFile(content: string): Promise<void> {
|
|
167
|
+
if (closed) throw createPathError('EBADF', `Storage lock handle is closed: ${resolved}`);
|
|
168
|
+
await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
|
|
169
|
+
await storage.writeFile(resolved, content);
|
|
170
|
+
},
|
|
171
|
+
async close(): Promise<void> {
|
|
172
|
+
closed = true;
|
|
173
|
+
lockedPaths.delete(resolved);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
unlink(filePath: string): Promise<void> {
|
|
178
|
+
const resolved = fileSystem.resolve(filePath);
|
|
179
|
+
lockedPaths.delete(resolved);
|
|
180
|
+
return storage.unlink(resolved);
|
|
181
|
+
},
|
|
182
|
+
join(...paths: string[]): string {
|
|
183
|
+
return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
|
|
184
|
+
},
|
|
185
|
+
dirname(filePath: string): string {
|
|
186
|
+
return storage.dirname ? storage.dirname(filePath) : dirnameStoragePath(filePath);
|
|
187
|
+
},
|
|
188
|
+
resolve(...paths: string[]): string {
|
|
189
|
+
return storage.resolve ? storage.resolve(...paths) : joinStoragePath(...paths);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return fileSystem;
|
|
194
|
+
}
|