@openfairygui/backend 0.2.0-alpha.0 → 0.2.0-alpha.10
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/LICENSE +21 -0
- package/README.md +62 -1
- package/dist/index.cjs +102 -1071
- package/dist/index.d.cts +24 -355
- package/dist/index.d.mts +24 -355
- package/dist/index.mjs +99 -1044
- package/dist/node.cjs +107 -0
- package/dist/node.d.cts +8 -0
- package/dist/node.d.mts +8 -0
- package/dist/node.mjs +78 -0
- package/dist/runtime-DFatY9W0.mjs +1417 -0
- package/dist/runtime-GKzsXJdO.cjs +1441 -0
- package/dist/runtime-GyNVxAQ0.d.mts +494 -0
- package/dist/runtime-Jec6FcF5.d.cts +494 -0
- package/package.json +65 -53
- package/src/contracts.ts +8 -0
- package/src/index.ts +18 -1
- package/src/node.ts +96 -0
- package/src/runtime.ts +256 -80
- package/src/services/artifact-service.ts +11 -1
- package/src/services/authoring-service.ts +466 -37
- package/src/services/context.ts +15 -3
- package/src/services/event-service.ts +1 -1
- package/src/services/runtime-service.ts +164 -25
- package/src/storage.ts +192 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProjectReader, type FileSystem } from '@openfairygui/core/project-io';
|
|
2
|
+
import { liftDocumentToUamProject, normalizeUamProject } from '@openfairygui/core/uam';
|
|
2
3
|
import { failure, success, type BackendContext, type BackendSessionState } from './context.js';
|
|
3
4
|
import type { CacheService } from './cache-service.js';
|
|
4
5
|
import type { EventService } from './event-service.js';
|
|
@@ -8,16 +9,71 @@ import type {
|
|
|
8
9
|
BackendFileHandle,
|
|
9
10
|
BackendResult,
|
|
10
11
|
BackendSessionSnapshot,
|
|
12
|
+
BackendCapabilityUnavailableError,
|
|
11
13
|
InProcessLockConflictError,
|
|
14
|
+
OpenProjectSessionInput,
|
|
12
15
|
SessionNotFoundError,
|
|
13
16
|
} from '../runtime.js';
|
|
14
|
-
import { resolveCanonicalProjectRoot } from '../path-policy.js';
|
|
17
|
+
import { normalizeComparablePath, resolveCanonicalProjectRoot } from '../path-policy.js';
|
|
15
18
|
import { createSessionNotFoundError, toSessionSnapshot } from './session-utils.js';
|
|
16
19
|
|
|
17
20
|
function randomId(): string {
|
|
18
21
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
function createCapabilityUnavailableError(
|
|
25
|
+
capability: BackendCapabilityUnavailableError['capability'],
|
|
26
|
+
): BackendCapabilityUnavailableError {
|
|
27
|
+
const artifactCapability = capability === 'artifact.publish' || capability === 'artifact.restore';
|
|
28
|
+
return {
|
|
29
|
+
code: 'capability_unavailable',
|
|
30
|
+
message: artifactCapability
|
|
31
|
+
? `${capability} requires the Node bridge boundary exposed by @openfairygui/backend/node.`
|
|
32
|
+
: `${capability} requires an injected BackendFileSystem adapter.`,
|
|
33
|
+
capability,
|
|
34
|
+
requiredAdapter: capability === 'fileSystem' ? 'BackendFileSystem' : undefined,
|
|
35
|
+
requiredHost: artifactCapability ? 'node' : undefined,
|
|
36
|
+
bridgeBoundary: artifactCapability ? 'external-bridge' : undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createProjectReaderFileSystem(fileSystem: NonNullable<BackendContext['fileSystem']>): FileSystem {
|
|
41
|
+
return {
|
|
42
|
+
readFile(filePath: string): Promise<string> {
|
|
43
|
+
return fileSystem.readFile(filePath);
|
|
44
|
+
},
|
|
45
|
+
readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
46
|
+
return fileSystem.readFileRaw(filePath);
|
|
47
|
+
},
|
|
48
|
+
writeFile(filePath: string, content: string): Promise<void> {
|
|
49
|
+
return fileSystem.writeFile(filePath, content);
|
|
50
|
+
},
|
|
51
|
+
writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
52
|
+
return fileSystem.writeFileRaw(filePath, data);
|
|
53
|
+
},
|
|
54
|
+
async mkdir(dirPath: string): Promise<void> {
|
|
55
|
+
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
56
|
+
},
|
|
57
|
+
readdir(dirPath: string): Promise<string[]> {
|
|
58
|
+
return fileSystem.readdir(dirPath);
|
|
59
|
+
},
|
|
60
|
+
async exists(filePath: string): Promise<boolean> {
|
|
61
|
+
try {
|
|
62
|
+
await fileSystem.stat(filePath);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
join(...paths: string[]): string {
|
|
69
|
+
return fileSystem.join(...paths);
|
|
70
|
+
},
|
|
71
|
+
dirname(filePath: string): string {
|
|
72
|
+
return fileSystem.dirname(filePath);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
21
77
|
export class RuntimeService {
|
|
22
78
|
public constructor(
|
|
23
79
|
private readonly context: BackendContext,
|
|
@@ -26,12 +82,23 @@ export class RuntimeService {
|
|
|
26
82
|
private readonly jobService: JobService,
|
|
27
83
|
) {}
|
|
28
84
|
|
|
29
|
-
public async openSession(input: {
|
|
85
|
+
public async openSession(input: {
|
|
86
|
+
projectPath: string;
|
|
87
|
+
}): Promise<
|
|
88
|
+
BackendResult<
|
|
89
|
+
BackendSessionSnapshot,
|
|
90
|
+
InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError
|
|
91
|
+
>
|
|
92
|
+
> {
|
|
30
93
|
const startedAt = Date.now();
|
|
31
|
-
|
|
94
|
+
if (!this.context.fileSystem) {
|
|
95
|
+
return failure('runtime', startedAt, createCapabilityUnavailableError('fileSystem'));
|
|
96
|
+
}
|
|
97
|
+
const fileSystem = this.context.fileSystem;
|
|
98
|
+
const resolved = await resolveCanonicalProjectRoot(fileSystem, input.projectPath);
|
|
32
99
|
const { fairyPath, canonicalProjectPath, canonicalPathKey } = resolved;
|
|
33
100
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
34
|
-
const lockFilePath =
|
|
101
|
+
const lockFilePath = fileSystem.join(canonicalProjectPath, '.openfairygui.backend.lock');
|
|
35
102
|
|
|
36
103
|
if (existingSessionId) {
|
|
37
104
|
return failure('runtime', startedAt, {
|
|
@@ -46,16 +113,23 @@ export class RuntimeService {
|
|
|
46
113
|
|
|
47
114
|
let advisoryLock: BackendFileHandle | null = null;
|
|
48
115
|
try {
|
|
49
|
-
advisoryLock = await
|
|
50
|
-
await advisoryLock.writeFile(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
116
|
+
advisoryLock = await fileSystem.openExclusive(lockFilePath);
|
|
117
|
+
await advisoryLock.writeFile(
|
|
118
|
+
JSON.stringify(
|
|
119
|
+
this.context.host?.lockMetadata?.({
|
|
120
|
+
canonicalPathKey,
|
|
121
|
+
canonicalProjectPath,
|
|
122
|
+
lockFilePath,
|
|
123
|
+
}) ?? {
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
canonicalPathKey,
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
);
|
|
55
129
|
await advisoryLock.close();
|
|
56
130
|
|
|
57
|
-
const
|
|
58
|
-
const project = await
|
|
131
|
+
const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
|
|
132
|
+
const project = liftDocumentToUamProject(await reader.read(fairyPath));
|
|
59
133
|
const sessionId = randomId();
|
|
60
134
|
const session: BackendSessionState = {
|
|
61
135
|
sessionId,
|
|
@@ -63,6 +137,7 @@ export class RuntimeService {
|
|
|
63
137
|
canonicalProjectPath,
|
|
64
138
|
canonicalPathKey,
|
|
65
139
|
lockFilePath,
|
|
140
|
+
fileSystem,
|
|
66
141
|
project,
|
|
67
142
|
revision: 0,
|
|
68
143
|
lastSavedRevision: 0,
|
|
@@ -80,7 +155,7 @@ export class RuntimeService {
|
|
|
80
155
|
revision: session.revision,
|
|
81
156
|
});
|
|
82
157
|
} catch (error) {
|
|
83
|
-
if (
|
|
158
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST') {
|
|
84
159
|
return failure('runtime', startedAt, {
|
|
85
160
|
code: 'lock_conflict',
|
|
86
161
|
kind: 'advisory_lock_conflict',
|
|
@@ -91,35 +166,99 @@ export class RuntimeService {
|
|
|
91
166
|
}
|
|
92
167
|
if (advisoryLock) {
|
|
93
168
|
await advisoryLock.close().catch(() => undefined);
|
|
94
|
-
await
|
|
169
|
+
await fileSystem.unlink(lockFilePath).catch(() => undefined);
|
|
95
170
|
}
|
|
96
171
|
throw error;
|
|
97
172
|
}
|
|
98
173
|
}
|
|
99
174
|
|
|
100
|
-
public
|
|
101
|
-
|
|
102
|
-
|
|
175
|
+
public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
|
|
176
|
+
const startedAt = Date.now();
|
|
177
|
+
const sessionId = input.sessionId ?? randomId();
|
|
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());
|
|
186
|
+
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
187
|
+
if (existingSessionId) {
|
|
188
|
+
return failure('runtime', startedAt, {
|
|
189
|
+
code: 'lock_conflict',
|
|
190
|
+
kind: 'in_process_session_exists',
|
|
191
|
+
message: `Project is already open in this backend runtime: ${canonicalProjectPath}`,
|
|
192
|
+
canonicalPathKey,
|
|
193
|
+
holderSessionId: existingSessionId,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const session: BackendSessionState = {
|
|
198
|
+
sessionId,
|
|
199
|
+
fairyPath: storage?.fairyPath ?? canonicalProjectPath,
|
|
200
|
+
canonicalProjectPath,
|
|
201
|
+
canonicalPathKey,
|
|
202
|
+
lockFilePath: '',
|
|
203
|
+
fileSystem: storage?.fileSystem,
|
|
204
|
+
project: normalizeUamProject(input.project),
|
|
205
|
+
revision: 0,
|
|
206
|
+
lastSavedRevision: 0,
|
|
207
|
+
dirty: false,
|
|
208
|
+
lockHeld: false,
|
|
209
|
+
closed: false,
|
|
210
|
+
};
|
|
211
|
+
this.context.sessions.set(sessionId, session);
|
|
212
|
+
this.context.sessionsByPath.set(canonicalPathKey, sessionId);
|
|
213
|
+
this.cacheService.refreshSession(session);
|
|
214
|
+
this.eventService.emit({ kind: 'session.opened', sessionId, canonicalPathKey, revision: session.revision });
|
|
215
|
+
|
|
216
|
+
return success('runtime', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
217
|
+
sessionId: session.sessionId,
|
|
218
|
+
revision: session.revision,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public async closeSession(input: {
|
|
223
|
+
sessionId: string;
|
|
224
|
+
}): Promise<BackendResult<{ sessionId: string; closed: true }, SessionNotFoundError>> {
|
|
103
225
|
const startedAt = Date.now();
|
|
104
226
|
const session = this.context.sessions.get(input.sessionId);
|
|
105
227
|
if (!session || session.closed) {
|
|
106
228
|
return failure('runtime', startedAt, createSessionNotFoundError(input.sessionId));
|
|
107
229
|
}
|
|
108
230
|
|
|
109
|
-
this.eventService.emit({
|
|
110
|
-
|
|
231
|
+
this.eventService.emit({
|
|
232
|
+
kind: 'session.closeRequested',
|
|
233
|
+
sessionId: session.sessionId,
|
|
234
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
235
|
+
revision: session.revision,
|
|
236
|
+
});
|
|
237
|
+
if (this.context.fileSystem && session.lockFilePath) {
|
|
238
|
+
await this.context.fileSystem.unlink(session.lockFilePath).catch(() => undefined);
|
|
239
|
+
}
|
|
111
240
|
session.lockHeld = false;
|
|
112
241
|
session.closed = true;
|
|
113
242
|
this.context.sessions.delete(session.sessionId);
|
|
114
243
|
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
115
244
|
this.cacheService.removeSession(session.sessionId);
|
|
116
245
|
this.jobService.removeSession(session.sessionId);
|
|
117
|
-
this.eventService.emit({
|
|
246
|
+
this.eventService.emit({
|
|
247
|
+
kind: 'session.closed',
|
|
248
|
+
sessionId: session.sessionId,
|
|
249
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
250
|
+
revision: session.revision,
|
|
251
|
+
});
|
|
118
252
|
this.eventService.removeSession(session.sessionId);
|
|
119
253
|
|
|
120
|
-
return success(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
254
|
+
return success(
|
|
255
|
+
'runtime',
|
|
256
|
+
startedAt,
|
|
257
|
+
{
|
|
258
|
+
sessionId: session.sessionId,
|
|
259
|
+
closed: true,
|
|
260
|
+
},
|
|
261
|
+
{ sessionId: session.sessionId, revision: session.revision },
|
|
262
|
+
);
|
|
124
263
|
}
|
|
125
264
|
}
|
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
|
+
}
|