@openfairygui/backend 0.2.0-alpha.8 → 0.2.0
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 +29 -8
- package/dist/index.cjs +1633 -27
- package/dist/index.d.cts +516 -4
- package/dist/index.d.mts +516 -4
- package/dist/index.mjs +1629 -23
- package/dist/node.cjs +1603 -9
- package/dist/node.d.cts +507 -2
- package/dist/node.d.mts +507 -2
- package/dist/node.mjs +1601 -6
- package/package.json +7 -6
- package/src/index.ts +18 -17
- package/src/node.ts +24 -8
- package/src/runtime/capabilities.ts +126 -0
- package/src/runtime/contracts.ts +518 -0
- package/src/runtime.ts +49 -623
- package/src/services/authoring-service.ts +242 -99
- package/src/services/cache-service.ts +1 -2
- package/src/services/context.ts +17 -8
- package/src/services/event-service.ts +1 -2
- package/src/services/job-service.ts +4 -5
- package/src/services/read-service.ts +1 -2
- package/src/services/runtime-service.ts +133 -27
- package/src/services/session-project-writer.ts +69 -0
- package/src/services/session-utils.ts +6 -3
- package/src/storage.ts +67 -27
- package/dist/runtime-DFatY9W0.mjs +0 -1417
- package/dist/runtime-GKzsXJdO.cjs +0 -1441
- package/dist/runtime-GyNVxAQ0.d.mts +0 -494
- package/dist/runtime-Jec6FcF5.d.cts +0 -494
- package/src/services/snapshot-utils.ts +0 -43
|
@@ -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,
|
|
9
|
-
|
|
11
|
+
BackendCapabilityUnavailableError,
|
|
10
12
|
BackendResult,
|
|
13
|
+
BackendSessionLock,
|
|
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,94 @@ function createProjectReaderFileSystem(fileSystem: NonNullable<BackendContext['f
|
|
|
74
79
|
};
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
function createCaptureFileSystem(
|
|
83
|
+
files: Map<string, string | Uint8Array>,
|
|
84
|
+
directories: Set<string>,
|
|
85
|
+
): FileSystem {
|
|
86
|
+
const normalize = (filePath: string): string => filePath.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
87
|
+
return {
|
|
88
|
+
async readFile(filePath: string): Promise<string> {
|
|
89
|
+
const value = files.get(normalize(filePath));
|
|
90
|
+
if (typeof value !== 'string') throw new Error(`Captured text file was not found: ${filePath}`);
|
|
91
|
+
return value;
|
|
92
|
+
},
|
|
93
|
+
async readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
94
|
+
const value = files.get(normalize(filePath));
|
|
95
|
+
if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
|
|
96
|
+
return value.slice();
|
|
97
|
+
},
|
|
98
|
+
async writeFile(filePath: string, content: string): Promise<void> {
|
|
99
|
+
files.set(normalize(filePath), content);
|
|
100
|
+
},
|
|
101
|
+
async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
102
|
+
files.set(normalize(filePath), data.slice());
|
|
103
|
+
},
|
|
104
|
+
async mkdir(dirPath: string): Promise<void> {
|
|
105
|
+
directories.add(normalize(dirPath));
|
|
106
|
+
},
|
|
107
|
+
async readdir(): Promise<string[]> {
|
|
108
|
+
return [];
|
|
109
|
+
},
|
|
110
|
+
async exists(filePath: string): Promise<boolean> {
|
|
111
|
+
return files.has(normalize(filePath));
|
|
112
|
+
},
|
|
113
|
+
join(...paths: string[]): string {
|
|
114
|
+
return normalize(paths.filter(Boolean).join('/'));
|
|
115
|
+
},
|
|
116
|
+
dirname(filePath: string): string {
|
|
117
|
+
const normalized = normalize(filePath);
|
|
118
|
+
const separator = normalized.lastIndexOf('/');
|
|
119
|
+
return separator < 0 ? '' : normalized.slice(0, separator);
|
|
120
|
+
},
|
|
121
|
+
async unlink(filePath: string): Promise<void> {
|
|
122
|
+
files.delete(normalize(filePath));
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function capturedFilesEqual(left: Map<string, string | Uint8Array>, right: Map<string, string | Uint8Array>): boolean {
|
|
128
|
+
if (left.size !== right.size) return false;
|
|
129
|
+
for (const [filePath, leftValue] of left) {
|
|
130
|
+
const rightValue = right.get(filePath);
|
|
131
|
+
if (typeof leftValue === 'string') {
|
|
132
|
+
if (leftValue !== rightValue) return false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
|
|
136
|
+
for (let index = 0; index < leftValue.length; index += 1) {
|
|
137
|
+
if (leftValue[index] !== rightValue[index]) return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function capturedDirectoriesEqual(left: Set<string>, right: Set<string>): boolean {
|
|
144
|
+
return left.size === right.size && [...left].every((directory) => right.has(directory));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function hasFullUamFidelity(
|
|
148
|
+
document: Awaited<ReturnType<ProjectReader['read']>>,
|
|
149
|
+
project: UamProject,
|
|
150
|
+
): Promise<boolean> {
|
|
151
|
+
const sourceFiles = new Map<string, string | Uint8Array>();
|
|
152
|
+
const materializedFiles = new Map<string, string | Uint8Array>();
|
|
153
|
+
const sourceDirectories = new Set<string>();
|
|
154
|
+
const materializedDirectories = new Set<string>();
|
|
155
|
+
try {
|
|
156
|
+
await Promise.all([
|
|
157
|
+
new ProjectWriter(createCaptureFileSystem(sourceFiles, sourceDirectories)).write(document, 'Project.fairy'),
|
|
158
|
+
new ProjectWriter(createCaptureFileSystem(materializedFiles, materializedDirectories)).write(
|
|
159
|
+
materializeUamProject(project),
|
|
160
|
+
'Project.fairy',
|
|
161
|
+
),
|
|
162
|
+
]);
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return capturedFilesEqual(sourceFiles, materializedFiles)
|
|
167
|
+
&& capturedDirectoriesEqual(sourceDirectories, materializedDirectories);
|
|
168
|
+
}
|
|
169
|
+
|
|
77
170
|
export class RuntimeService {
|
|
78
171
|
public constructor(
|
|
79
172
|
private readonly context: BackendContext,
|
|
@@ -111,10 +204,10 @@ export class RuntimeService {
|
|
|
111
204
|
});
|
|
112
205
|
}
|
|
113
206
|
|
|
114
|
-
let
|
|
207
|
+
let sessionLock: BackendSessionLock | null = null;
|
|
115
208
|
try {
|
|
116
|
-
|
|
117
|
-
await
|
|
209
|
+
sessionLock = await fileSystem.acquireSessionLock(lockFilePath);
|
|
210
|
+
await sessionLock.writeMetadata(
|
|
118
211
|
JSON.stringify(
|
|
119
212
|
this.context.host?.lockMetadata?.({
|
|
120
213
|
canonicalPathKey,
|
|
@@ -126,10 +219,9 @@ export class RuntimeService {
|
|
|
126
219
|
},
|
|
127
220
|
),
|
|
128
221
|
);
|
|
129
|
-
await advisoryLock.close();
|
|
130
|
-
|
|
131
222
|
const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
|
|
132
|
-
const
|
|
223
|
+
const document = await reader.read(fairyPath, { hydrateResourceBytes: true });
|
|
224
|
+
const project = liftDocumentToUamProject(document);
|
|
133
225
|
const sessionId = randomId();
|
|
134
226
|
const session: BackendSessionState = {
|
|
135
227
|
sessionId,
|
|
@@ -137,10 +229,15 @@ export class RuntimeService {
|
|
|
137
229
|
canonicalProjectPath,
|
|
138
230
|
canonicalPathKey,
|
|
139
231
|
lockFilePath,
|
|
232
|
+
sessionLock,
|
|
140
233
|
fileSystem,
|
|
141
234
|
project,
|
|
235
|
+
uamFidelity: (await hasFullUamFidelity(document, project)) ? 'full' : 'unsupported',
|
|
142
236
|
revision: 0,
|
|
143
237
|
lastSavedRevision: 0,
|
|
238
|
+
pendingStaleSourceFiles: new Map(),
|
|
239
|
+
pendingStaleResourceFolders: new Map(),
|
|
240
|
+
pendingStaleBranchDirectories: new Map(),
|
|
144
241
|
dirty: false,
|
|
145
242
|
lockHeld: true,
|
|
146
243
|
closed: false,
|
|
@@ -155,6 +252,7 @@ export class RuntimeService {
|
|
|
155
252
|
revision: session.revision,
|
|
156
253
|
});
|
|
157
254
|
} catch (error) {
|
|
255
|
+
if (sessionLock) await sessionLock.release().catch(() => undefined);
|
|
158
256
|
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST') {
|
|
159
257
|
return failure('runtime', startedAt, {
|
|
160
258
|
code: 'lock_conflict',
|
|
@@ -164,9 +262,11 @@ export class RuntimeService {
|
|
|
164
262
|
lockFilePath,
|
|
165
263
|
});
|
|
166
264
|
}
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
265
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOTSUP') {
|
|
266
|
+
return failure('runtime', startedAt, {
|
|
267
|
+
...createCapabilityUnavailableError('fileSystem'),
|
|
268
|
+
message: error instanceof Error ? error.message : String(error),
|
|
269
|
+
});
|
|
170
270
|
}
|
|
171
271
|
throw error;
|
|
172
272
|
}
|
|
@@ -177,12 +277,14 @@ export class RuntimeService {
|
|
|
177
277
|
const sessionId = input.sessionId ?? randomId();
|
|
178
278
|
const storage = input.storage;
|
|
179
279
|
const memoryProjectPath = `memory://${sessionId}`;
|
|
180
|
-
const canonicalProjectPath =
|
|
181
|
-
??
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
280
|
+
const canonicalProjectPath =
|
|
281
|
+
storage?.canonicalProjectPath ??
|
|
282
|
+
input.canonicalProjectPath ??
|
|
283
|
+
(storage ? storage.fileSystem.dirname(storage.fairyPath) || '.' : memoryProjectPath);
|
|
284
|
+
const canonicalPathKey =
|
|
285
|
+
storage?.canonicalPathKey ??
|
|
286
|
+
input.canonicalPathKey ??
|
|
287
|
+
(storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
|
|
186
288
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
187
289
|
if (existingSessionId) {
|
|
188
290
|
return failure('runtime', startedAt, {
|
|
@@ -200,10 +302,15 @@ export class RuntimeService {
|
|
|
200
302
|
canonicalProjectPath,
|
|
201
303
|
canonicalPathKey,
|
|
202
304
|
lockFilePath: '',
|
|
305
|
+
sessionLock: null,
|
|
203
306
|
fileSystem: storage?.fileSystem,
|
|
204
307
|
project: normalizeUamProject(input.project),
|
|
308
|
+
uamFidelity: 'full',
|
|
205
309
|
revision: 0,
|
|
206
310
|
lastSavedRevision: 0,
|
|
311
|
+
pendingStaleSourceFiles: new Map(),
|
|
312
|
+
pendingStaleResourceFolders: new Map(),
|
|
313
|
+
pendingStaleBranchDirectories: new Map(),
|
|
207
314
|
dirty: false,
|
|
208
315
|
lockHeld: false,
|
|
209
316
|
closed: false,
|
|
@@ -234,9 +341,8 @@ export class RuntimeService {
|
|
|
234
341
|
canonicalPathKey: session.canonicalPathKey,
|
|
235
342
|
revision: session.revision,
|
|
236
343
|
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
344
|
+
await session.sessionLock?.release().catch(() => undefined);
|
|
345
|
+
session.sessionLock = null;
|
|
240
346
|
session.lockHeld = false;
|
|
241
347
|
session.closed = true;
|
|
242
348
|
this.context.sessions.delete(session.sessionId);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Document } from '@openfairygui/core';
|
|
2
|
+
import { type FileSystem, type ProjectBranchDirectory, 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
|
+
rmdir: (path) => trackWrite(path, () => fileSystem.rmdir(path)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeSessionProject(input: {
|
|
52
|
+
fileSystem: BackendFileSystem;
|
|
53
|
+
document: Document;
|
|
54
|
+
fairyPath: string;
|
|
55
|
+
staleSourceFiles: ProjectSourceFile[];
|
|
56
|
+
staleResourceFolders: import('@openfairygui/core/project-io').ProjectResourceFolder[];
|
|
57
|
+
staleBranchDirectories: ProjectBranchDirectory[];
|
|
58
|
+
writtenPaths: string[];
|
|
59
|
+
failedPaths: string[];
|
|
60
|
+
}): Promise<void> {
|
|
61
|
+
const writer = new ProjectWriter(
|
|
62
|
+
createWriterFileSystem(input.fileSystem, input.writtenPaths, input.failedPaths),
|
|
63
|
+
);
|
|
64
|
+
await writer.write(input.document, input.fairyPath, {
|
|
65
|
+
staleSourceFiles: input.staleSourceFiles,
|
|
66
|
+
staleResourceFolders: input.staleResourceFolders,
|
|
67
|
+
staleBranchDirectories: input.staleBranchDirectories,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -5,17 +5,20 @@ import type {
|
|
|
5
5
|
SessionStaleWriteError,
|
|
6
6
|
} from '../runtime.js';
|
|
7
7
|
import type { BackendSessionState } from './context.js';
|
|
8
|
-
import { cloneCapabilitiesSnapshot } from './snapshot-utils.js';
|
|
9
8
|
|
|
10
|
-
export function toSessionSnapshot(
|
|
9
|
+
export function toSessionSnapshot(
|
|
10
|
+
session: BackendSessionState,
|
|
11
|
+
capabilities: BackendCapabilities,
|
|
12
|
+
): BackendSessionSnapshot {
|
|
11
13
|
return {
|
|
12
14
|
sessionId: session.sessionId,
|
|
13
15
|
canonicalProjectPath: session.canonicalProjectPath,
|
|
14
16
|
revision: session.revision,
|
|
15
17
|
lastSavedRevision: session.lastSavedRevision,
|
|
16
18
|
dirty: session.dirty,
|
|
19
|
+
uamFidelity: session.uamFidelity,
|
|
17
20
|
lockHeld: session.lockHeld,
|
|
18
|
-
capabilities:
|
|
21
|
+
capabilities: structuredClone(capabilities),
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
|
package/src/storage.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FileSystem as CoreProjectFileSystem } from '@openfairygui/core/project-io';
|
|
2
|
-
import type {
|
|
2
|
+
import type { BackendFileStat, BackendFileSystem, BackendSessionLock } from './runtime.js';
|
|
3
3
|
|
|
4
4
|
type StorageStatKind = 'file' | 'directory';
|
|
5
5
|
|
|
@@ -19,9 +19,15 @@ export interface BackendAsyncStorageAdapter {
|
|
|
19
19
|
readdir(dirPath: string): Promise<string[]>;
|
|
20
20
|
exists?(filePath: string): Promise<boolean>;
|
|
21
21
|
stat?(filePath: string): Promise<BackendStorageStatLike>;
|
|
22
|
+
/** Stable cross-context path identity; also scopes the default Web Lock name. */
|
|
22
23
|
resolvePath?(filePath: string): Promise<string>;
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Optional replacement for Web Locks. Acquisition must be atomic across browser contexts, remain held
|
|
26
|
+
* until release(), and recover automatically when the owning document terminates.
|
|
27
|
+
*/
|
|
28
|
+
acquireSessionLock?(lockPath: string): Promise<BackendSessionLock>;
|
|
29
|
+
unlink(filePath: string): Promise<void>;
|
|
30
|
+
rmdir(dirPath: string): Promise<void>;
|
|
25
31
|
join?(...paths: string[]): string;
|
|
26
32
|
dirname?(filePath: string): string;
|
|
27
33
|
resolve?(...paths: string[]): string;
|
|
@@ -45,6 +51,44 @@ function createPathError(code: string, message: string): Error & { code: string
|
|
|
45
51
|
return error;
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
function getWebLockManager(): LockManager | null {
|
|
55
|
+
if (typeof navigator === 'undefined') return null;
|
|
56
|
+
const lockManager = (navigator as Navigator & { locks?: LockManager }).locks;
|
|
57
|
+
return lockManager && typeof lockManager.request === 'function' ? lockManager : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function acquireWebSessionLock(lockManager: LockManager, lockName: string): Promise<BackendSessionLock> {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
let releasePlatformLock = (): void => undefined;
|
|
63
|
+
const held = new Promise<void>((release) => {
|
|
64
|
+
releasePlatformLock = release;
|
|
65
|
+
});
|
|
66
|
+
void lockManager
|
|
67
|
+
.request(lockName, { mode: 'exclusive', ifAvailable: true }, async (lock) => {
|
|
68
|
+
if (!lock) {
|
|
69
|
+
reject(createPathError('EEXIST', `Browser session lock is already held: ${lockName}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let released = false;
|
|
73
|
+
resolve({
|
|
74
|
+
// Web Locks are the authority; a persisted marker would survive abrupt document termination.
|
|
75
|
+
writeMetadata(): Promise<void> {
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
},
|
|
78
|
+
release(): Promise<void> {
|
|
79
|
+
if (!released) {
|
|
80
|
+
released = true;
|
|
81
|
+
releasePlatformLock();
|
|
82
|
+
}
|
|
83
|
+
return Promise.resolve();
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
await held;
|
|
87
|
+
})
|
|
88
|
+
.catch(reject);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
48
92
|
function normalizeStoragePath(value: string): string {
|
|
49
93
|
const normalized = value.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
50
94
|
const absolute = normalized.startsWith('/');
|
|
@@ -114,8 +158,12 @@ async function inferStat(storage: BackendAsyncStorageAdapter, filePath: string):
|
|
|
114
158
|
export type BackendStorageFileSystem = BackendFileSystem & CoreProjectFileSystem;
|
|
115
159
|
|
|
116
160
|
export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem {
|
|
117
|
-
|
|
118
|
-
|
|
161
|
+
if (typeof storage.unlink !== 'function') {
|
|
162
|
+
throw createPathError('ENOTSUP', 'Storage adapter must provide unlink() for project resource lifecycle writes.');
|
|
163
|
+
}
|
|
164
|
+
if (typeof storage.rmdir !== 'function') {
|
|
165
|
+
throw createPathError('ENOTSUP', 'Storage adapter must provide rmdir() for project resource folder lifecycle writes.');
|
|
166
|
+
}
|
|
119
167
|
const fileSystem: BackendStorageFileSystem = {
|
|
120
168
|
stat(filePath: string): Promise<BackendFileStat> {
|
|
121
169
|
return inferStat(storage, fileSystem.resolve(filePath));
|
|
@@ -151,31 +199,23 @@ export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapt
|
|
|
151
199
|
const resolved = fileSystem.resolve(filePath);
|
|
152
200
|
return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
|
|
153
201
|
},
|
|
154
|
-
async
|
|
155
|
-
const resolved = fileSystem.resolve(
|
|
156
|
-
if (storage.
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
async acquireSessionLock(lockPath: string): Promise<BackendSessionLock> {
|
|
203
|
+
const resolved = fileSystem.resolve(lockPath);
|
|
204
|
+
if (storage.acquireSessionLock) return storage.acquireSessionLock(resolved);
|
|
205
|
+
const lockManager = getWebLockManager();
|
|
206
|
+
if (!lockManager) {
|
|
207
|
+
throw createPathError(
|
|
208
|
+
'ENOTSUP',
|
|
209
|
+
'Browser openSession requires Web Locks or BackendAsyncStorageAdapter.acquireSessionLock().',
|
|
210
|
+
);
|
|
159
211
|
}
|
|
160
|
-
|
|
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
|
-
};
|
|
212
|
+
return acquireWebSessionLock(lockManager, `@openfairygui/backend:${resolved}`);
|
|
173
213
|
},
|
|
174
214
|
unlink(filePath: string): Promise<void> {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
215
|
+
return storage.unlink(fileSystem.resolve(filePath));
|
|
216
|
+
},
|
|
217
|
+
rmdir(dirPath: string): Promise<void> {
|
|
218
|
+
return storage.rmdir(fileSystem.resolve(dirPath));
|
|
179
219
|
},
|
|
180
220
|
join(...paths: string[]): string {
|
|
181
221
|
return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
|