@openfairygui/backend 0.2.0-alpha.36 → 0.2.0-alpha.38
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 +23 -10
- package/dist/index.cjs +44 -22
- package/dist/index.d.cts +8 -3
- package/dist/index.d.mts +8 -3
- package/dist/index.mjs +44 -22
- package/dist/node.cjs +19 -6
- package/dist/node.d.cts +2 -2
- package/dist/node.d.mts +2 -2
- package/dist/node.mjs +19 -6
- package/dist/{runtime-BQOEw22M.d.mts → runtime-B7rLrkcR.d.cts} +11 -6
- package/dist/{runtime-CQQmzsxV.cjs → runtime-BtavZ2ku.cjs} +12 -9
- package/dist/{runtime-Wu6vGZ-j.d.cts → runtime-CJnoedTA.d.mts} +11 -6
- package/dist/{runtime-DtCn0Y44.mjs → runtime-CNTJCi6P.mjs} +12 -9
- package/package.json +3 -3
- package/src/index.ts +1 -1
- package/src/node.ts +21 -8
- package/src/runtime/contracts.ts +9 -4
- package/src/services/context.ts +2 -0
- package/src/services/runtime-service.ts +14 -12
- package/src/storage.ts +56 -25
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ It owns:
|
|
|
13
13
|
- coordinated but non-atomic save semantics
|
|
14
14
|
- browser-safe project sessions
|
|
15
15
|
- browser-safe async project storage adapter
|
|
16
|
-
- adapter-backed file sessions and backend-local
|
|
16
|
+
- adapter-backed file sessions and backend-local session locking
|
|
17
17
|
- capability discovery
|
|
18
18
|
- transport-neutral bootstrap
|
|
19
19
|
|
|
@@ -35,13 +35,20 @@ It also does **not** implement MCP or any transport-specific wire protocol.
|
|
|
35
35
|
The root `@openfairygui/backend` entrypoint is browser-safe: pure authoring sessions can run in memory,
|
|
36
36
|
and browser editors can inject an async storage adapter for OPFS, IndexedDB, ZIP-backed virtual filesystems,
|
|
37
37
|
or File System Access API bridges. Storage adapters must implement `unlink()` so resource rename/move/remove
|
|
38
|
-
can clean up stale source files.
|
|
39
|
-
|
|
38
|
+
can clean up stale source files. Existing browser projects use a session-lifetime Web Lock: a live peer tab
|
|
39
|
+
receives `lock_conflict`, while reload or abrupt document termination releases ownership without leaving a
|
|
40
|
+
persistent `.openfairygui.backend.lock` marker. When Web Locks are unavailable, the storage adapter must
|
|
41
|
+
provide `acquireSessionLock()` with the same atomic cross-context and owner-termination semantics. The default
|
|
42
|
+
Node filesystem/runtime lives under `@openfairygui/backend/node` and retains its advisory lock file behavior.
|
|
43
|
+
Adapter-backed `openSession` hydrates primary resource bytes so browser-safe transactions can rename/move
|
|
40
44
|
assets or add/replace/remove binary resources. `saveSession` writes replacement bytes before it removes
|
|
41
45
|
stale source files, preserving the prior file when a write fails.
|
|
42
46
|
It also compares the source project with a UAM round trip through `ProjectWriter`; sessions with
|
|
43
47
|
unrepresented persisted properties expose `uamFidelity: 'unsupported'`, and write attempts fail with
|
|
44
|
-
`uam_fidelity_unsupported`.
|
|
48
|
+
`uam_fidelity_unsupported`. Existing projects in browser storage must be opened through this path by
|
|
49
|
+
injecting `createBackendStorageFileSystem(storage)` into `BackendRuntime`; `openProjectSession` is only
|
|
50
|
+
for sessions whose supplied UAM project is authoritative. Transactions, saves, and materialization are
|
|
51
|
+
serialized per session.
|
|
45
52
|
|
|
46
53
|
## Relationship to other packages
|
|
47
54
|
|
|
@@ -51,7 +58,7 @@ unrepresented persisted properties expose `uamFidelity: 'unsupported'`, and writ
|
|
|
51
58
|
|
|
52
59
|
## Example
|
|
53
60
|
|
|
54
|
-
Browser-safe project session:
|
|
61
|
+
Browser-safe authoritative UAM project session:
|
|
55
62
|
|
|
56
63
|
```ts
|
|
57
64
|
import { BackendRuntime } from '@openfairygui/backend';
|
|
@@ -67,7 +74,7 @@ const applied = await runtime.applyTransaction({
|
|
|
67
74
|
});
|
|
68
75
|
```
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
Open an existing project from browser async storage with source-fidelity checks:
|
|
71
78
|
|
|
72
79
|
```ts
|
|
73
80
|
import { BackendRuntime, createBackendStorageFileSystem } from '@openfairygui/backend';
|
|
@@ -81,15 +88,21 @@ const fileSystem = createBackendStorageFileSystem({
|
|
|
81
88
|
async readdir(dirPath) { return storage.readdir(dirPath); },
|
|
82
89
|
async exists(filePath) { return storage.exists(filePath); },
|
|
83
90
|
async unlink(filePath) { await storage.remove(filePath); },
|
|
91
|
+
async rmdir(dirPath) { await storage.rmdir(dirPath); },
|
|
84
92
|
});
|
|
85
93
|
|
|
94
|
+
const runtime = new BackendRuntime({ fileSystem });
|
|
95
|
+
const opened = await runtime.openSession({ projectPath: 'ExistingProject' });
|
|
96
|
+
if (!opened.ok) throw new Error(opened.error.message);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Materialize an authoritative UAM project into browser async storage:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
86
102
|
const runtime = new BackendRuntime();
|
|
87
103
|
const opened = runtime.openProjectSession({
|
|
88
104
|
project: uamProject,
|
|
89
|
-
storage: {
|
|
90
|
-
fileSystem,
|
|
91
|
-
fairyPath: 'Project.fairy',
|
|
92
|
-
},
|
|
105
|
+
storage: { fileSystem, fairyPath: 'NewProject/Project.fairy' },
|
|
93
106
|
});
|
|
94
107
|
if (!opened.ok) throw new Error(opened.error.message);
|
|
95
108
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_runtime = require("./runtime-
|
|
2
|
+
const require_runtime = require("./runtime-BtavZ2ku.cjs");
|
|
3
3
|
//#region src/storage.ts
|
|
4
4
|
var StorageFileStat = class {
|
|
5
5
|
constructor(kind) {
|
|
@@ -17,6 +17,42 @@ function createPathError(code, message) {
|
|
|
17
17
|
error.code = code;
|
|
18
18
|
return error;
|
|
19
19
|
}
|
|
20
|
+
function getWebLockManager() {
|
|
21
|
+
if (typeof navigator === "undefined") return null;
|
|
22
|
+
const lockManager = navigator.locks;
|
|
23
|
+
return lockManager && typeof lockManager.request === "function" ? lockManager : null;
|
|
24
|
+
}
|
|
25
|
+
function acquireWebSessionLock(lockManager, lockName) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let releasePlatformLock = () => void 0;
|
|
28
|
+
const held = new Promise((release) => {
|
|
29
|
+
releasePlatformLock = release;
|
|
30
|
+
});
|
|
31
|
+
lockManager.request(lockName, {
|
|
32
|
+
mode: "exclusive",
|
|
33
|
+
ifAvailable: true
|
|
34
|
+
}, async (lock) => {
|
|
35
|
+
if (!lock) {
|
|
36
|
+
reject(createPathError("EEXIST", `Browser session lock is already held: ${lockName}`));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let released = false;
|
|
40
|
+
resolve({
|
|
41
|
+
writeMetadata() {
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
},
|
|
44
|
+
release() {
|
|
45
|
+
if (!released) {
|
|
46
|
+
released = true;
|
|
47
|
+
releasePlatformLock();
|
|
48
|
+
}
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
await held;
|
|
53
|
+
}).catch(reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
20
56
|
function normalizeStoragePath(value) {
|
|
21
57
|
const normalized = value.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
22
58
|
const absolute = normalized.startsWith("/");
|
|
@@ -73,7 +109,6 @@ async function inferStat(storage, filePath) {
|
|
|
73
109
|
function createBackendStorageFileSystem(storage) {
|
|
74
110
|
if (typeof storage.unlink !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide unlink() for project resource lifecycle writes.");
|
|
75
111
|
if (typeof storage.rmdir !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide rmdir() for project resource folder lifecycle writes.");
|
|
76
|
-
const lockedPaths = /* @__PURE__ */ new Set();
|
|
77
112
|
const fileSystem = {
|
|
78
113
|
stat(filePath) {
|
|
79
114
|
return inferStat(storage, fileSystem.resolve(filePath));
|
|
@@ -109,28 +144,15 @@ function createBackendStorageFileSystem(storage) {
|
|
|
109
144
|
const resolved = fileSystem.resolve(filePath);
|
|
110
145
|
return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
|
|
111
146
|
},
|
|
112
|
-
async
|
|
113
|
-
const resolved = fileSystem.resolve(
|
|
114
|
-
if (storage.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
async writeFile(content) {
|
|
120
|
-
if (closed) throw createPathError("EBADF", `Storage lock handle is closed: ${resolved}`);
|
|
121
|
-
await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
|
|
122
|
-
await storage.writeFile(resolved, content);
|
|
123
|
-
},
|
|
124
|
-
async close() {
|
|
125
|
-
closed = true;
|
|
126
|
-
lockedPaths.delete(resolved);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
147
|
+
async acquireSessionLock(lockPath) {
|
|
148
|
+
const resolved = fileSystem.resolve(lockPath);
|
|
149
|
+
if (storage.acquireSessionLock) return storage.acquireSessionLock(resolved);
|
|
150
|
+
const lockManager = getWebLockManager();
|
|
151
|
+
if (!lockManager) throw createPathError("ENOTSUP", "Browser openSession requires Web Locks or BackendAsyncStorageAdapter.acquireSessionLock().");
|
|
152
|
+
return acquireWebSessionLock(lockManager, `@openfairygui/backend:${resolved}`);
|
|
129
153
|
},
|
|
130
154
|
unlink(filePath) {
|
|
131
|
-
|
|
132
|
-
lockedPaths.delete(resolved);
|
|
133
|
-
return storage.unlink(resolved);
|
|
155
|
+
return storage.unlink(fileSystem.resolve(filePath));
|
|
134
156
|
},
|
|
135
157
|
rmdir(dirPath) {
|
|
136
158
|
return storage.rmdir(fileSystem.resolve(dirPath));
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as BACKEND_COMPATIBILITY_POLICY, A as BackendSessionSnapshot, B as ListJobsInput, C as
|
|
1
|
+
import { $ as BACKEND_COMPATIBILITY_POLICY, A as BackendSessionSnapshot, B as ListJobsInput, C as BackendJobProgress, D as BackendResult, E as BackendProjectSessionStorage, F as GetCacheSnapshotInput, G as OpenProjectSessionInput, H as MaterializeSessionSnapshot, I as GetEventsInput, J as SaveSessionInput, K as RefreshCacheInput, L as GetEventsSnapshot, M as CacheRefreshFailedError, N as CancelJobInput, O as BackendRuntimeOptions, P as EventCursorInvalidError, Q as BACKEND_CAPABILITY_SCHEMA_VERSION, R as GetJobInput, S as BackendJobNotFoundError, T as BackendJobStatus, U as MaterializeValidationFailedError, V as MaterializeSessionInput, W as MaterializeWriteFailedError, X as SessionStaleWriteError, Y as SessionNotFoundError, Z as UamFidelityUnsupportedError, _ as BackendJobErrors, a as BackendCacheEntry, b as BackendJobListStatusFilter, c as BackendCapabilityManifest, d as BackendEvent, et as BACKEND_CONTRACT_VERSION, f as BackendEventKind, g as BackendHostAdapter, h as BackendFileSystem, i as BackendArtifactBridgeCapability, it as BackendStage, j as BackendSuccess, k as BackendSessionLock, l as BackendCapabilityUnavailableError, m as BackendFileStat, n as AdvisoryLockConflictError, nt as BackendMessage, o as BackendCacheSnapshot, p as BackendFailure, q as SavePartialFailureError, r as ApplySessionTransactionInput, rt as BackendResponseMeta, s as BackendCapabilities, t as BackendRuntime, tt as BackendDiagnostic, u as BackendError, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as InProcessLockConflictError } from "./runtime-B7rLrkcR.cjs";
|
|
2
2
|
import { FileSystem } from "@openfairygui/core/project-io";
|
|
3
3
|
|
|
4
4
|
//#region src/storage.d.ts
|
|
@@ -20,8 +20,13 @@ interface BackendAsyncStorageAdapter {
|
|
|
20
20
|
readdir(dirPath: string): Promise<string[]>;
|
|
21
21
|
exists?(filePath: string): Promise<boolean>;
|
|
22
22
|
stat?(filePath: string): Promise<BackendStorageStatLike>;
|
|
23
|
+
/** Stable cross-context path identity; also scopes the default Web Lock name. */
|
|
23
24
|
resolvePath?(filePath: string): Promise<string>;
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Optional replacement for Web Locks. Acquisition must be atomic across browser contexts, remain held
|
|
27
|
+
* until release(), and recover automatically when the owning document terminates.
|
|
28
|
+
*/
|
|
29
|
+
acquireSessionLock?(lockPath: string): Promise<BackendSessionLock>;
|
|
25
30
|
unlink(filePath: string): Promise<void>;
|
|
26
31
|
rmdir(dirPath: string): Promise<void>;
|
|
27
32
|
join?(...paths: string[]): string;
|
|
@@ -31,4 +36,4 @@ interface BackendAsyncStorageAdapter {
|
|
|
31
36
|
type BackendStorageFileSystem = BackendFileSystem & FileSystem;
|
|
32
37
|
declare function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem;
|
|
33
38
|
//#endregion
|
|
34
|
-
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
|
|
39
|
+
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 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 BackendSessionLock, 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 MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, type UamFidelityUnsupportedError, createBackendStorageFileSystem };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as BACKEND_COMPATIBILITY_POLICY, A as BackendSessionSnapshot, B as ListJobsInput, C as
|
|
1
|
+
import { $ as BACKEND_COMPATIBILITY_POLICY, A as BackendSessionSnapshot, B as ListJobsInput, C as BackendJobProgress, D as BackendResult, E as BackendProjectSessionStorage, F as GetCacheSnapshotInput, G as OpenProjectSessionInput, H as MaterializeSessionSnapshot, I as GetEventsInput, J as SaveSessionInput, K as RefreshCacheInput, L as GetEventsSnapshot, M as CacheRefreshFailedError, N as CancelJobInput, O as BackendRuntimeOptions, P as EventCursorInvalidError, Q as BACKEND_CAPABILITY_SCHEMA_VERSION, R as GetJobInput, S as BackendJobNotFoundError, T as BackendJobStatus, U as MaterializeValidationFailedError, V as MaterializeSessionInput, W as MaterializeWriteFailedError, X as SessionStaleWriteError, Y as SessionNotFoundError, Z as UamFidelityUnsupportedError, _ as BackendJobErrors, a as BackendCacheEntry, b as BackendJobListStatusFilter, c as BackendCapabilityManifest, d as BackendEvent, et as BACKEND_CONTRACT_VERSION, f as BackendEventKind, g as BackendHostAdapter, h as BackendFileSystem, i as BackendArtifactBridgeCapability, it as BackendStage, j as BackendSuccess, k as BackendSessionLock, l as BackendCapabilityUnavailableError, m as BackendFileStat, n as AdvisoryLockConflictError, nt as BackendMessage, o as BackendCacheSnapshot, p as BackendFailure, q as SavePartialFailureError, r as ApplySessionTransactionInput, rt as BackendResponseMeta, s as BackendCapabilities, t as BackendRuntime, tt as BackendDiagnostic, u as BackendError, v as BackendJobKind, w as BackendJobSnapshot, x as BackendJobNotCancellableError, y as BackendJobListSnapshot, z as InProcessLockConflictError } from "./runtime-CJnoedTA.mjs";
|
|
2
2
|
import { FileSystem } from "@openfairygui/core/project-io";
|
|
3
3
|
|
|
4
4
|
//#region src/storage.d.ts
|
|
@@ -20,8 +20,13 @@ interface BackendAsyncStorageAdapter {
|
|
|
20
20
|
readdir(dirPath: string): Promise<string[]>;
|
|
21
21
|
exists?(filePath: string): Promise<boolean>;
|
|
22
22
|
stat?(filePath: string): Promise<BackendStorageStatLike>;
|
|
23
|
+
/** Stable cross-context path identity; also scopes the default Web Lock name. */
|
|
23
24
|
resolvePath?(filePath: string): Promise<string>;
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Optional replacement for Web Locks. Acquisition must be atomic across browser contexts, remain held
|
|
27
|
+
* until release(), and recover automatically when the owning document terminates.
|
|
28
|
+
*/
|
|
29
|
+
acquireSessionLock?(lockPath: string): Promise<BackendSessionLock>;
|
|
25
30
|
unlink(filePath: string): Promise<void>;
|
|
26
31
|
rmdir(dirPath: string): Promise<void>;
|
|
27
32
|
join?(...paths: string[]): string;
|
|
@@ -31,4 +36,4 @@ interface BackendAsyncStorageAdapter {
|
|
|
31
36
|
type BackendStorageFileSystem = BackendFileSystem & FileSystem;
|
|
32
37
|
declare function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem;
|
|
33
38
|
//#endregion
|
|
34
|
-
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
|
|
39
|
+
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 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 BackendSessionLock, 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 MaterializeSessionInput, type MaterializeSessionSnapshot, type MaterializeValidationFailedError, type MaterializeWriteFailedError, type OpenProjectSessionInput, type RefreshCacheInput, type SavePartialFailureError, type SaveSessionInput, type SessionNotFoundError, type SessionStaleWriteError, type UamFidelityUnsupportedError, createBackendStorageFileSystem };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as BACKEND_CONTRACT_VERSION, n as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BACKEND_COMPATIBILITY_POLICY, t as BackendRuntime } from "./runtime-
|
|
1
|
+
import { i as BACKEND_CONTRACT_VERSION, n as BACKEND_CAPABILITY_SCHEMA_VERSION, r as BACKEND_COMPATIBILITY_POLICY, t as BackendRuntime } from "./runtime-CNTJCi6P.mjs";
|
|
2
2
|
//#region src/storage.ts
|
|
3
3
|
var StorageFileStat = class {
|
|
4
4
|
constructor(kind) {
|
|
@@ -16,6 +16,42 @@ function createPathError(code, message) {
|
|
|
16
16
|
error.code = code;
|
|
17
17
|
return error;
|
|
18
18
|
}
|
|
19
|
+
function getWebLockManager() {
|
|
20
|
+
if (typeof navigator === "undefined") return null;
|
|
21
|
+
const lockManager = navigator.locks;
|
|
22
|
+
return lockManager && typeof lockManager.request === "function" ? lockManager : null;
|
|
23
|
+
}
|
|
24
|
+
function acquireWebSessionLock(lockManager, lockName) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
let releasePlatformLock = () => void 0;
|
|
27
|
+
const held = new Promise((release) => {
|
|
28
|
+
releasePlatformLock = release;
|
|
29
|
+
});
|
|
30
|
+
lockManager.request(lockName, {
|
|
31
|
+
mode: "exclusive",
|
|
32
|
+
ifAvailable: true
|
|
33
|
+
}, async (lock) => {
|
|
34
|
+
if (!lock) {
|
|
35
|
+
reject(createPathError("EEXIST", `Browser session lock is already held: ${lockName}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let released = false;
|
|
39
|
+
resolve({
|
|
40
|
+
writeMetadata() {
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
},
|
|
43
|
+
release() {
|
|
44
|
+
if (!released) {
|
|
45
|
+
released = true;
|
|
46
|
+
releasePlatformLock();
|
|
47
|
+
}
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
await held;
|
|
52
|
+
}).catch(reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
19
55
|
function normalizeStoragePath(value) {
|
|
20
56
|
const normalized = value.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
21
57
|
const absolute = normalized.startsWith("/");
|
|
@@ -72,7 +108,6 @@ async function inferStat(storage, filePath) {
|
|
|
72
108
|
function createBackendStorageFileSystem(storage) {
|
|
73
109
|
if (typeof storage.unlink !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide unlink() for project resource lifecycle writes.");
|
|
74
110
|
if (typeof storage.rmdir !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide rmdir() for project resource folder lifecycle writes.");
|
|
75
|
-
const lockedPaths = /* @__PURE__ */ new Set();
|
|
76
111
|
const fileSystem = {
|
|
77
112
|
stat(filePath) {
|
|
78
113
|
return inferStat(storage, fileSystem.resolve(filePath));
|
|
@@ -108,28 +143,15 @@ function createBackendStorageFileSystem(storage) {
|
|
|
108
143
|
const resolved = fileSystem.resolve(filePath);
|
|
109
144
|
return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
|
|
110
145
|
},
|
|
111
|
-
async
|
|
112
|
-
const resolved = fileSystem.resolve(
|
|
113
|
-
if (storage.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
async writeFile(content) {
|
|
119
|
-
if (closed) throw createPathError("EBADF", `Storage lock handle is closed: ${resolved}`);
|
|
120
|
-
await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
|
|
121
|
-
await storage.writeFile(resolved, content);
|
|
122
|
-
},
|
|
123
|
-
async close() {
|
|
124
|
-
closed = true;
|
|
125
|
-
lockedPaths.delete(resolved);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
146
|
+
async acquireSessionLock(lockPath) {
|
|
147
|
+
const resolved = fileSystem.resolve(lockPath);
|
|
148
|
+
if (storage.acquireSessionLock) return storage.acquireSessionLock(resolved);
|
|
149
|
+
const lockManager = getWebLockManager();
|
|
150
|
+
if (!lockManager) throw createPathError("ENOTSUP", "Browser openSession requires Web Locks or BackendAsyncStorageAdapter.acquireSessionLock().");
|
|
151
|
+
return acquireWebSessionLock(lockManager, `@openfairygui/backend:${resolved}`);
|
|
128
152
|
},
|
|
129
153
|
unlink(filePath) {
|
|
130
|
-
|
|
131
|
-
lockedPaths.delete(resolved);
|
|
132
|
-
return storage.unlink(resolved);
|
|
154
|
+
return storage.unlink(fileSystem.resolve(filePath));
|
|
133
155
|
},
|
|
134
156
|
rmdir(dirPath) {
|
|
135
157
|
return storage.rmdir(fileSystem.resolve(dirPath));
|
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-BtavZ2ku.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");
|
|
@@ -58,14 +58,27 @@ function createNodeBackendFileSystem() {
|
|
|
58
58
|
return node_path.default.resolve(filePath);
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
-
async
|
|
61
|
+
async acquireSessionLock(filePath) {
|
|
62
62
|
const handle = await node_fs_promises.default.open(filePath, "wx");
|
|
63
|
+
let closed = false;
|
|
64
|
+
let released = false;
|
|
65
|
+
const closeHandle = async () => {
|
|
66
|
+
if (closed) return;
|
|
67
|
+
await handle.close();
|
|
68
|
+
closed = true;
|
|
69
|
+
};
|
|
63
70
|
return {
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
async writeMetadata(content) {
|
|
72
|
+
await handle.writeFile(content, "utf-8");
|
|
73
|
+
await closeHandle();
|
|
66
74
|
},
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
async release() {
|
|
76
|
+
if (released) return;
|
|
77
|
+
await closeHandle();
|
|
78
|
+
await node_fs_promises.default.unlink(filePath).catch((error) => {
|
|
79
|
+
if (error.code !== "ENOENT") throw error;
|
|
80
|
+
});
|
|
81
|
+
released = true;
|
|
69
82
|
}
|
|
70
83
|
};
|
|
71
84
|
},
|
package/dist/node.d.cts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as BackendRuntimeOptions, g as BackendHostAdapter, h as BackendFileSystem, k as BackendSessionLock, m as BackendFileStat, t as BackendRuntime } from "./runtime-B7rLrkcR.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/node.d.ts
|
|
4
4
|
declare function createNodeBackendFileSystem(): BackendFileSystem;
|
|
5
5
|
declare function createNodeBackendHostAdapter(): BackendHostAdapter;
|
|
6
6
|
declare function createNodeBackendRuntime(options?: BackendRuntimeOptions): BackendRuntime;
|
|
7
7
|
//#endregion
|
|
8
|
-
export { type
|
|
8
|
+
export { type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, BackendRuntime, type BackendRuntimeOptions, type BackendSessionLock, createNodeBackendFileSystem, createNodeBackendHostAdapter, createNodeBackendRuntime };
|
package/dist/node.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as BackendRuntimeOptions, g as BackendHostAdapter, h as BackendFileSystem, k as BackendSessionLock, m as BackendFileStat, t as BackendRuntime } from "./runtime-CJnoedTA.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/node.d.ts
|
|
4
4
|
declare function createNodeBackendFileSystem(): BackendFileSystem;
|
|
5
5
|
declare function createNodeBackendHostAdapter(): BackendHostAdapter;
|
|
6
6
|
declare function createNodeBackendRuntime(options?: BackendRuntimeOptions): BackendRuntime;
|
|
7
7
|
//#endregion
|
|
8
|
-
export { type
|
|
8
|
+
export { type BackendFileStat, type BackendFileSystem, type BackendHostAdapter, BackendRuntime, type BackendRuntimeOptions, type BackendSessionLock, createNodeBackendFileSystem, createNodeBackendHostAdapter, createNodeBackendRuntime };
|
package/dist/node.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as BackendRuntime } from "./runtime-
|
|
1
|
+
import { t as BackendRuntime } from "./runtime-CNTJCi6P.mjs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
//#region src/node.ts
|
|
@@ -33,14 +33,27 @@ function createNodeBackendFileSystem() {
|
|
|
33
33
|
return path.resolve(filePath);
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
|
-
async
|
|
36
|
+
async acquireSessionLock(filePath) {
|
|
37
37
|
const handle = await fs.open(filePath, "wx");
|
|
38
|
+
let closed = false;
|
|
39
|
+
let released = false;
|
|
40
|
+
const closeHandle = async () => {
|
|
41
|
+
if (closed) return;
|
|
42
|
+
await handle.close();
|
|
43
|
+
closed = true;
|
|
44
|
+
};
|
|
38
45
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
async writeMetadata(content) {
|
|
47
|
+
await handle.writeFile(content, "utf-8");
|
|
48
|
+
await closeHandle();
|
|
41
49
|
},
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
async release() {
|
|
51
|
+
if (released) return;
|
|
52
|
+
await closeHandle();
|
|
53
|
+
await fs.unlink(filePath).catch((error) => {
|
|
54
|
+
if (error.code !== "ENOENT") throw error;
|
|
55
|
+
});
|
|
56
|
+
released = true;
|
|
44
57
|
}
|
|
45
58
|
};
|
|
46
59
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { UamProject, UamTransactionOperation } from "@openfairygui/core/uam";
|
|
2
1
|
import { ApplyUamTransactionAppError } from "@openfairygui/functions/uam";
|
|
2
|
+
import { UamProject, UamTransactionOperation } from "@openfairygui/core/uam";
|
|
3
3
|
|
|
4
4
|
//#region src/contracts.d.ts
|
|
5
5
|
declare const BACKEND_CONTRACT_VERSION: "1.1.0-p2";
|
|
@@ -49,9 +49,12 @@ interface PathPolicyViolationError {
|
|
|
49
49
|
}
|
|
50
50
|
//#endregion
|
|
51
51
|
//#region src/runtime/contracts.d.ts
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/** An exclusive lock owned for the lifetime of one backend session. */
|
|
53
|
+
interface BackendSessionLock {
|
|
54
|
+
/** Persist optional host metadata without changing lock ownership. */
|
|
55
|
+
writeMetadata(content: string): Promise<void>;
|
|
56
|
+
/** Release ownership. Browser implementations must also release when their document terminates. */
|
|
57
|
+
release(): Promise<void>;
|
|
55
58
|
}
|
|
56
59
|
interface BackendFileStat {
|
|
57
60
|
isFile(): boolean;
|
|
@@ -68,7 +71,7 @@ interface BackendFileSystem {
|
|
|
68
71
|
recursive?: boolean;
|
|
69
72
|
}): Promise<void>;
|
|
70
73
|
resolvePath(filePath: string): Promise<string>;
|
|
71
|
-
|
|
74
|
+
acquireSessionLock(lockPath: string): Promise<BackendSessionLock>;
|
|
72
75
|
unlink(filePath: string): Promise<void>;
|
|
73
76
|
rmdir(dirPath: string): Promise<void>;
|
|
74
77
|
join(...paths: string[]): string;
|
|
@@ -424,10 +427,12 @@ interface ApplySessionTransactionInput {
|
|
|
424
427
|
operations: UamTransactionOperation[];
|
|
425
428
|
}
|
|
426
429
|
interface OpenProjectSessionInput {
|
|
430
|
+
/** Authoritative UAM project. Use BackendRuntime.openSession() when importing an existing project from storage. */
|
|
427
431
|
project: UamProject;
|
|
428
432
|
sessionId?: string;
|
|
429
433
|
canonicalProjectPath?: string;
|
|
430
434
|
canonicalPathKey?: string;
|
|
435
|
+
/** Optional writeback target for the authoritative UAM project; this is not an import source. */
|
|
431
436
|
storage?: BackendProjectSessionStorage;
|
|
432
437
|
}
|
|
433
438
|
interface BackendProjectSessionStorage {
|
|
@@ -501,4 +506,4 @@ declare class BackendRuntime {
|
|
|
501
506
|
refreshCache(input: RefreshCacheInput): BackendResult<BackendJobSnapshot, SessionNotFoundError>;
|
|
502
507
|
}
|
|
503
508
|
//#endregion
|
|
504
|
-
export { BACKEND_COMPATIBILITY_POLICY as $, BackendSessionSnapshot as A, ListJobsInput as B,
|
|
509
|
+
export { BACKEND_COMPATIBILITY_POLICY as $, BackendSessionSnapshot as A, ListJobsInput as B, BackendJobProgress as C, BackendResult as D, BackendProjectSessionStorage as E, GetCacheSnapshotInput as F, OpenProjectSessionInput as G, MaterializeSessionSnapshot as H, GetEventsInput as I, SaveSessionInput as J, RefreshCacheInput as K, GetEventsSnapshot as L, CacheRefreshFailedError as M, CancelJobInput as N, BackendRuntimeOptions as O, EventCursorInvalidError as P, BACKEND_CAPABILITY_SCHEMA_VERSION as Q, GetJobInput as R, BackendJobNotFoundError as S, BackendJobStatus as T, MaterializeValidationFailedError as U, MaterializeSessionInput as V, MaterializeWriteFailedError as W, SessionStaleWriteError as X, SessionNotFoundError as Y, UamFidelityUnsupportedError as Z, BackendJobErrors as _, BackendCacheEntry as a, BackendJobListStatusFilter as b, BackendCapabilityManifest as c, BackendEvent as d, BACKEND_CONTRACT_VERSION as et, BackendEventKind as f, BackendHostAdapter as g, BackendFileSystem as h, BackendArtifactBridgeCapability as i, BackendStage as it, BackendSuccess as j, BackendSessionLock as k, BackendCapabilityUnavailableError as l, BackendFileStat as m, AdvisoryLockConflictError as n, BackendMessage as nt, BackendCacheSnapshot as o, BackendFailure as p, SavePartialFailureError as q, ApplySessionTransactionInput as r, BackendResponseMeta as rt, BackendCapabilities as s, BackendRuntime as t, BackendDiagnostic as tt, BackendError as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, InProcessLockConflictError as z };
|
|
@@ -1211,10 +1211,10 @@ var RuntimeService = class {
|
|
|
1211
1211
|
holderSessionId: existingSessionId,
|
|
1212
1212
|
lockFilePath
|
|
1213
1213
|
});
|
|
1214
|
-
let
|
|
1214
|
+
let sessionLock = null;
|
|
1215
1215
|
try {
|
|
1216
|
-
|
|
1217
|
-
await
|
|
1216
|
+
sessionLock = await fileSystem.acquireSessionLock(lockFilePath);
|
|
1217
|
+
await sessionLock.writeMetadata(JSON.stringify(this.context.host?.lockMetadata?.({
|
|
1218
1218
|
canonicalPathKey,
|
|
1219
1219
|
canonicalProjectPath,
|
|
1220
1220
|
lockFilePath
|
|
@@ -1222,7 +1222,6 @@ var RuntimeService = class {
|
|
|
1222
1222
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1223
1223
|
canonicalPathKey
|
|
1224
1224
|
}));
|
|
1225
|
-
await advisoryLock.close();
|
|
1226
1225
|
const document = await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
|
|
1227
1226
|
const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(document);
|
|
1228
1227
|
const sessionId = randomId();
|
|
@@ -1232,6 +1231,7 @@ var RuntimeService = class {
|
|
|
1232
1231
|
canonicalProjectPath,
|
|
1233
1232
|
canonicalPathKey,
|
|
1234
1233
|
lockFilePath,
|
|
1234
|
+
sessionLock,
|
|
1235
1235
|
fileSystem,
|
|
1236
1236
|
project,
|
|
1237
1237
|
uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
|
|
@@ -1258,6 +1258,7 @@ var RuntimeService = class {
|
|
|
1258
1258
|
revision: session.revision
|
|
1259
1259
|
});
|
|
1260
1260
|
} catch (error) {
|
|
1261
|
+
if (sessionLock) await sessionLock.release().catch(() => void 0);
|
|
1261
1262
|
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") return failure("runtime", startedAt, {
|
|
1262
1263
|
code: "lock_conflict",
|
|
1263
1264
|
kind: "advisory_lock_conflict",
|
|
@@ -1265,10 +1266,10 @@ var RuntimeService = class {
|
|
|
1265
1266
|
canonicalPathKey,
|
|
1266
1267
|
lockFilePath
|
|
1267
1268
|
});
|
|
1268
|
-
if (
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1269
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOTSUP") return failure("runtime", startedAt, {
|
|
1270
|
+
...createCapabilityUnavailableError("fileSystem"),
|
|
1271
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1272
|
+
});
|
|
1272
1273
|
throw error;
|
|
1273
1274
|
}
|
|
1274
1275
|
}
|
|
@@ -1293,6 +1294,7 @@ var RuntimeService = class {
|
|
|
1293
1294
|
canonicalProjectPath,
|
|
1294
1295
|
canonicalPathKey,
|
|
1295
1296
|
lockFilePath: "",
|
|
1297
|
+
sessionLock: null,
|
|
1296
1298
|
fileSystem: storage?.fileSystem,
|
|
1297
1299
|
project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
|
|
1298
1300
|
uamFidelity: "full",
|
|
@@ -1329,7 +1331,8 @@ var RuntimeService = class {
|
|
|
1329
1331
|
canonicalPathKey: session.canonicalPathKey,
|
|
1330
1332
|
revision: session.revision
|
|
1331
1333
|
});
|
|
1332
|
-
|
|
1334
|
+
await session.sessionLock?.release().catch(() => void 0);
|
|
1335
|
+
session.sessionLock = null;
|
|
1333
1336
|
session.lockHeld = false;
|
|
1334
1337
|
session.closed = true;
|
|
1335
1338
|
this.context.sessions.delete(session.sessionId);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ApplyUamTransactionAppError } from "@openfairygui/functions/uam";
|
|
2
1
|
import { UamProject, UamTransactionOperation } from "@openfairygui/core/uam";
|
|
2
|
+
import { ApplyUamTransactionAppError } from "@openfairygui/functions/uam";
|
|
3
3
|
|
|
4
4
|
//#region src/contracts.d.ts
|
|
5
5
|
declare const BACKEND_CONTRACT_VERSION: "1.1.0-p2";
|
|
@@ -49,9 +49,12 @@ interface PathPolicyViolationError {
|
|
|
49
49
|
}
|
|
50
50
|
//#endregion
|
|
51
51
|
//#region src/runtime/contracts.d.ts
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/** An exclusive lock owned for the lifetime of one backend session. */
|
|
53
|
+
interface BackendSessionLock {
|
|
54
|
+
/** Persist optional host metadata without changing lock ownership. */
|
|
55
|
+
writeMetadata(content: string): Promise<void>;
|
|
56
|
+
/** Release ownership. Browser implementations must also release when their document terminates. */
|
|
57
|
+
release(): Promise<void>;
|
|
55
58
|
}
|
|
56
59
|
interface BackendFileStat {
|
|
57
60
|
isFile(): boolean;
|
|
@@ -68,7 +71,7 @@ interface BackendFileSystem {
|
|
|
68
71
|
recursive?: boolean;
|
|
69
72
|
}): Promise<void>;
|
|
70
73
|
resolvePath(filePath: string): Promise<string>;
|
|
71
|
-
|
|
74
|
+
acquireSessionLock(lockPath: string): Promise<BackendSessionLock>;
|
|
72
75
|
unlink(filePath: string): Promise<void>;
|
|
73
76
|
rmdir(dirPath: string): Promise<void>;
|
|
74
77
|
join(...paths: string[]): string;
|
|
@@ -424,10 +427,12 @@ interface ApplySessionTransactionInput {
|
|
|
424
427
|
operations: UamTransactionOperation[];
|
|
425
428
|
}
|
|
426
429
|
interface OpenProjectSessionInput {
|
|
430
|
+
/** Authoritative UAM project. Use BackendRuntime.openSession() when importing an existing project from storage. */
|
|
427
431
|
project: UamProject;
|
|
428
432
|
sessionId?: string;
|
|
429
433
|
canonicalProjectPath?: string;
|
|
430
434
|
canonicalPathKey?: string;
|
|
435
|
+
/** Optional writeback target for the authoritative UAM project; this is not an import source. */
|
|
431
436
|
storage?: BackendProjectSessionStorage;
|
|
432
437
|
}
|
|
433
438
|
interface BackendProjectSessionStorage {
|
|
@@ -501,4 +506,4 @@ declare class BackendRuntime {
|
|
|
501
506
|
refreshCache(input: RefreshCacheInput): BackendResult<BackendJobSnapshot, SessionNotFoundError>;
|
|
502
507
|
}
|
|
503
508
|
//#endregion
|
|
504
|
-
export { BACKEND_COMPATIBILITY_POLICY as $, BackendSessionSnapshot as A, ListJobsInput as B,
|
|
509
|
+
export { BACKEND_COMPATIBILITY_POLICY as $, BackendSessionSnapshot as A, ListJobsInput as B, BackendJobProgress as C, BackendResult as D, BackendProjectSessionStorage as E, GetCacheSnapshotInput as F, OpenProjectSessionInput as G, MaterializeSessionSnapshot as H, GetEventsInput as I, SaveSessionInput as J, RefreshCacheInput as K, GetEventsSnapshot as L, CacheRefreshFailedError as M, CancelJobInput as N, BackendRuntimeOptions as O, EventCursorInvalidError as P, BACKEND_CAPABILITY_SCHEMA_VERSION as Q, GetJobInput as R, BackendJobNotFoundError as S, BackendJobStatus as T, MaterializeValidationFailedError as U, MaterializeSessionInput as V, MaterializeWriteFailedError as W, SessionStaleWriteError as X, SessionNotFoundError as Y, UamFidelityUnsupportedError as Z, BackendJobErrors as _, BackendCacheEntry as a, BackendJobListStatusFilter as b, BackendCapabilityManifest as c, BackendEvent as d, BACKEND_CONTRACT_VERSION as et, BackendEventKind as f, BackendHostAdapter as g, BackendFileSystem as h, BackendArtifactBridgeCapability as i, BackendStage as it, BackendSuccess as j, BackendSessionLock as k, BackendCapabilityUnavailableError as l, BackendFileStat as m, AdvisoryLockConflictError as n, BackendMessage as nt, BackendCacheSnapshot as o, BackendFailure as p, SavePartialFailureError as q, ApplySessionTransactionInput as r, BackendResponseMeta as rt, BackendCapabilities as s, BackendRuntime as t, BackendDiagnostic as tt, BackendError as u, BackendJobKind as v, BackendJobSnapshot as w, BackendJobNotCancellableError as x, BackendJobListSnapshot as y, InProcessLockConflictError as z };
|
|
@@ -1210,10 +1210,10 @@ var RuntimeService = class {
|
|
|
1210
1210
|
holderSessionId: existingSessionId,
|
|
1211
1211
|
lockFilePath
|
|
1212
1212
|
});
|
|
1213
|
-
let
|
|
1213
|
+
let sessionLock = null;
|
|
1214
1214
|
try {
|
|
1215
|
-
|
|
1216
|
-
await
|
|
1215
|
+
sessionLock = await fileSystem.acquireSessionLock(lockFilePath);
|
|
1216
|
+
await sessionLock.writeMetadata(JSON.stringify(this.context.host?.lockMetadata?.({
|
|
1217
1217
|
canonicalPathKey,
|
|
1218
1218
|
canonicalProjectPath,
|
|
1219
1219
|
lockFilePath
|
|
@@ -1221,7 +1221,6 @@ var RuntimeService = class {
|
|
|
1221
1221
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1222
1222
|
canonicalPathKey
|
|
1223
1223
|
}));
|
|
1224
|
-
await advisoryLock.close();
|
|
1225
1224
|
const document = await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true });
|
|
1226
1225
|
const project = liftDocumentToUamProject(document);
|
|
1227
1226
|
const sessionId = randomId();
|
|
@@ -1231,6 +1230,7 @@ var RuntimeService = class {
|
|
|
1231
1230
|
canonicalProjectPath,
|
|
1232
1231
|
canonicalPathKey,
|
|
1233
1232
|
lockFilePath,
|
|
1233
|
+
sessionLock,
|
|
1234
1234
|
fileSystem,
|
|
1235
1235
|
project,
|
|
1236
1236
|
uamFidelity: await hasFullUamFidelity(document, project) ? "full" : "unsupported",
|
|
@@ -1257,6 +1257,7 @@ var RuntimeService = class {
|
|
|
1257
1257
|
revision: session.revision
|
|
1258
1258
|
});
|
|
1259
1259
|
} catch (error) {
|
|
1260
|
+
if (sessionLock) await sessionLock.release().catch(() => void 0);
|
|
1260
1261
|
if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") return failure("runtime", startedAt, {
|
|
1261
1262
|
code: "lock_conflict",
|
|
1262
1263
|
kind: "advisory_lock_conflict",
|
|
@@ -1264,10 +1265,10 @@ var RuntimeService = class {
|
|
|
1264
1265
|
canonicalPathKey,
|
|
1265
1266
|
lockFilePath
|
|
1266
1267
|
});
|
|
1267
|
-
if (
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
}
|
|
1268
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOTSUP") return failure("runtime", startedAt, {
|
|
1269
|
+
...createCapabilityUnavailableError("fileSystem"),
|
|
1270
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1271
|
+
});
|
|
1271
1272
|
throw error;
|
|
1272
1273
|
}
|
|
1273
1274
|
}
|
|
@@ -1292,6 +1293,7 @@ var RuntimeService = class {
|
|
|
1292
1293
|
canonicalProjectPath,
|
|
1293
1294
|
canonicalPathKey,
|
|
1294
1295
|
lockFilePath: "",
|
|
1296
|
+
sessionLock: null,
|
|
1295
1297
|
fileSystem: storage?.fileSystem,
|
|
1296
1298
|
project: normalizeUamProject(input.project),
|
|
1297
1299
|
uamFidelity: "full",
|
|
@@ -1328,7 +1330,8 @@ var RuntimeService = class {
|
|
|
1328
1330
|
canonicalPathKey: session.canonicalPathKey,
|
|
1329
1331
|
revision: session.revision
|
|
1330
1332
|
});
|
|
1331
|
-
|
|
1333
|
+
await session.sessionLock?.release().catch(() => void 0);
|
|
1334
|
+
session.sessionLock = null;
|
|
1332
1335
|
session.lockHeld = false;
|
|
1333
1336
|
session.closed = true;
|
|
1334
1337
|
this.context.sessions.delete(session.sessionId);
|
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.38",
|
|
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.38",
|
|
56
|
+
"@openfairygui/functions": "0.2.0-alpha.38"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"ava": "^7.0.0",
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,6 @@ export {
|
|
|
20
20
|
type BackendEvent,
|
|
21
21
|
type BackendEventKind,
|
|
22
22
|
type BackendFailure,
|
|
23
|
-
type BackendFileHandle,
|
|
24
23
|
type BackendFileStat,
|
|
25
24
|
type BackendFileSystem,
|
|
26
25
|
type BackendHostAdapter,
|
|
@@ -37,6 +36,7 @@ export {
|
|
|
37
36
|
type BackendResult,
|
|
38
37
|
BackendRuntime,
|
|
39
38
|
type BackendRuntimeOptions,
|
|
39
|
+
type BackendSessionLock,
|
|
40
40
|
type BackendSessionSnapshot,
|
|
41
41
|
type BackendSuccess,
|
|
42
42
|
type CacheRefreshFailedError,
|
package/src/node.ts
CHANGED
|
@@ -2,11 +2,11 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
4
|
BackendRuntime,
|
|
5
|
-
type BackendFileHandle,
|
|
6
5
|
type BackendFileStat,
|
|
7
6
|
type BackendFileSystem,
|
|
8
7
|
type BackendHostAdapter,
|
|
9
8
|
type BackendRuntimeOptions,
|
|
9
|
+
type BackendSessionLock,
|
|
10
10
|
} from './runtime.js';
|
|
11
11
|
|
|
12
12
|
export function createNodeBackendFileSystem(): BackendFileSystem {
|
|
@@ -40,14 +40,27 @@ export function createNodeBackendFileSystem(): BackendFileSystem {
|
|
|
40
40
|
return path.resolve(filePath);
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
|
-
async
|
|
43
|
+
async acquireSessionLock(filePath: string): Promise<BackendSessionLock> {
|
|
44
44
|
const handle = await fs.open(filePath, 'wx');
|
|
45
|
+
let closed = false;
|
|
46
|
+
let released = false;
|
|
47
|
+
const closeHandle = async (): Promise<void> => {
|
|
48
|
+
if (closed) return;
|
|
49
|
+
await handle.close();
|
|
50
|
+
closed = true;
|
|
51
|
+
};
|
|
45
52
|
return {
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
async writeMetadata(content: string): Promise<void> {
|
|
54
|
+
await handle.writeFile(content, 'utf-8');
|
|
55
|
+
await closeHandle();
|
|
48
56
|
},
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
async release(): Promise<void> {
|
|
58
|
+
if (released) return;
|
|
59
|
+
await closeHandle();
|
|
60
|
+
await fs.unlink(filePath).catch((error: NodeJS.ErrnoException) => {
|
|
61
|
+
if (error.code !== 'ENOENT') throw error;
|
|
62
|
+
});
|
|
63
|
+
released = true;
|
|
51
64
|
},
|
|
52
65
|
};
|
|
53
66
|
},
|
|
@@ -89,11 +102,11 @@ export function createNodeBackendRuntime(options: BackendRuntimeOptions = {}): B
|
|
|
89
102
|
});
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
export { BackendRuntime };
|
|
93
105
|
export type {
|
|
94
|
-
BackendFileHandle,
|
|
95
106
|
BackendFileStat,
|
|
96
107
|
BackendFileSystem,
|
|
97
108
|
BackendHostAdapter,
|
|
98
109
|
BackendRuntimeOptions,
|
|
110
|
+
BackendSessionLock,
|
|
99
111
|
} from './runtime.js';
|
|
112
|
+
export { BackendRuntime };
|
package/src/runtime/contracts.ts
CHANGED
|
@@ -9,9 +9,12 @@ import type {
|
|
|
9
9
|
} from '../contracts.js';
|
|
10
10
|
import type { PathPolicyViolationError } from '../path-policy.js';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/** An exclusive lock owned for the lifetime of one backend session. */
|
|
13
|
+
export interface BackendSessionLock {
|
|
14
|
+
/** Persist optional host metadata without changing lock ownership. */
|
|
15
|
+
writeMetadata(content: string): Promise<void>;
|
|
16
|
+
/** Release ownership. Browser implementations must also release when their document terminates. */
|
|
17
|
+
release(): Promise<void>;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export interface BackendFileStat {
|
|
@@ -28,7 +31,7 @@ export interface BackendFileSystem {
|
|
|
28
31
|
writeFileRaw(filePath: string, data: Uint8Array): Promise<void>;
|
|
29
32
|
mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void>;
|
|
30
33
|
resolvePath(filePath: string): Promise<string>;
|
|
31
|
-
|
|
34
|
+
acquireSessionLock(lockPath: string): Promise<BackendSessionLock>;
|
|
32
35
|
unlink(filePath: string): Promise<void>;
|
|
33
36
|
rmdir(dirPath: string): Promise<void>;
|
|
34
37
|
join(...paths: string[]): string;
|
|
@@ -474,10 +477,12 @@ export interface ApplySessionTransactionInput {
|
|
|
474
477
|
}
|
|
475
478
|
|
|
476
479
|
export interface OpenProjectSessionInput {
|
|
480
|
+
/** Authoritative UAM project. Use BackendRuntime.openSession() when importing an existing project from storage. */
|
|
477
481
|
project: UamProject;
|
|
478
482
|
sessionId?: string;
|
|
479
483
|
canonicalProjectPath?: string;
|
|
480
484
|
canonicalPathKey?: string;
|
|
485
|
+
/** Optional writeback target for the authoritative UAM project; this is not an import source. */
|
|
481
486
|
storage?: BackendProjectSessionStorage;
|
|
482
487
|
}
|
|
483
488
|
|
package/src/services/context.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
BackendFileSystem,
|
|
16
16
|
BackendHostAdapter,
|
|
17
17
|
BackendJobSnapshot,
|
|
18
|
+
BackendSessionLock,
|
|
18
19
|
BackendSessionSnapshot,
|
|
19
20
|
BackendSuccess,
|
|
20
21
|
} from '../runtime.js';
|
|
@@ -25,6 +26,7 @@ export interface BackendSessionState {
|
|
|
25
26
|
canonicalProjectPath: string;
|
|
26
27
|
canonicalPathKey: string;
|
|
27
28
|
lockFilePath: string;
|
|
29
|
+
sessionLock: BackendSessionLock | null;
|
|
28
30
|
fileSystem?: BackendFileSystem;
|
|
29
31
|
project: import('@openfairygui/core/uam').UamProject;
|
|
30
32
|
uamFidelity: 'full' | 'unsupported';
|
|
@@ -9,8 +9,8 @@ import { normalizeComparablePath, resolveCanonicalProjectRoot } from '../path-po
|
|
|
9
9
|
import type {
|
|
10
10
|
AdvisoryLockConflictError,
|
|
11
11
|
BackendCapabilityUnavailableError,
|
|
12
|
-
BackendFileHandle,
|
|
13
12
|
BackendResult,
|
|
13
|
+
BackendSessionLock,
|
|
14
14
|
BackendSessionSnapshot,
|
|
15
15
|
InProcessLockConflictError,
|
|
16
16
|
OpenProjectSessionInput,
|
|
@@ -204,10 +204,10 @@ export class RuntimeService {
|
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
let
|
|
207
|
+
let sessionLock: BackendSessionLock | null = null;
|
|
208
208
|
try {
|
|
209
|
-
|
|
210
|
-
await
|
|
209
|
+
sessionLock = await fileSystem.acquireSessionLock(lockFilePath);
|
|
210
|
+
await sessionLock.writeMetadata(
|
|
211
211
|
JSON.stringify(
|
|
212
212
|
this.context.host?.lockMetadata?.({
|
|
213
213
|
canonicalPathKey,
|
|
@@ -219,8 +219,6 @@ export class RuntimeService {
|
|
|
219
219
|
},
|
|
220
220
|
),
|
|
221
221
|
);
|
|
222
|
-
await advisoryLock.close();
|
|
223
|
-
|
|
224
222
|
const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
|
|
225
223
|
const document = await reader.read(fairyPath, { hydrateResourceBytes: true });
|
|
226
224
|
const project = liftDocumentToUamProject(document);
|
|
@@ -231,6 +229,7 @@ export class RuntimeService {
|
|
|
231
229
|
canonicalProjectPath,
|
|
232
230
|
canonicalPathKey,
|
|
233
231
|
lockFilePath,
|
|
232
|
+
sessionLock,
|
|
234
233
|
fileSystem,
|
|
235
234
|
project,
|
|
236
235
|
uamFidelity: (await hasFullUamFidelity(document, project)) ? 'full' : 'unsupported',
|
|
@@ -253,6 +252,7 @@ export class RuntimeService {
|
|
|
253
252
|
revision: session.revision,
|
|
254
253
|
});
|
|
255
254
|
} catch (error) {
|
|
255
|
+
if (sessionLock) await sessionLock.release().catch(() => undefined);
|
|
256
256
|
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST') {
|
|
257
257
|
return failure('runtime', startedAt, {
|
|
258
258
|
code: 'lock_conflict',
|
|
@@ -262,9 +262,11 @@ export class RuntimeService {
|
|
|
262
262
|
lockFilePath,
|
|
263
263
|
});
|
|
264
264
|
}
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
});
|
|
268
270
|
}
|
|
269
271
|
throw error;
|
|
270
272
|
}
|
|
@@ -300,6 +302,7 @@ export class RuntimeService {
|
|
|
300
302
|
canonicalProjectPath,
|
|
301
303
|
canonicalPathKey,
|
|
302
304
|
lockFilePath: '',
|
|
305
|
+
sessionLock: null,
|
|
303
306
|
fileSystem: storage?.fileSystem,
|
|
304
307
|
project: normalizeUamProject(input.project),
|
|
305
308
|
uamFidelity: 'full',
|
|
@@ -338,9 +341,8 @@ export class RuntimeService {
|
|
|
338
341
|
canonicalPathKey: session.canonicalPathKey,
|
|
339
342
|
revision: session.revision,
|
|
340
343
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
+
await session.sessionLock?.release().catch(() => undefined);
|
|
345
|
+
session.sessionLock = null;
|
|
344
346
|
session.lockHeld = false;
|
|
345
347
|
session.closed = true;
|
|
346
348
|
this.context.sessions.delete(session.sessionId);
|
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,8 +19,13 @@ 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
|
+
/**
|
|
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>;
|
|
24
29
|
unlink(filePath: string): Promise<void>;
|
|
25
30
|
rmdir(dirPath: string): Promise<void>;
|
|
26
31
|
join?(...paths: string[]): string;
|
|
@@ -46,6 +51,44 @@ function createPathError(code: string, message: string): Error & { code: string
|
|
|
46
51
|
return error;
|
|
47
52
|
}
|
|
48
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
|
+
|
|
49
92
|
function normalizeStoragePath(value: string): string {
|
|
50
93
|
const normalized = value.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
51
94
|
const absolute = normalized.startsWith('/');
|
|
@@ -121,8 +164,6 @@ export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapt
|
|
|
121
164
|
if (typeof storage.rmdir !== 'function') {
|
|
122
165
|
throw createPathError('ENOTSUP', 'Storage adapter must provide rmdir() for project resource folder lifecycle writes.');
|
|
123
166
|
}
|
|
124
|
-
const lockedPaths = new Set<string>();
|
|
125
|
-
|
|
126
167
|
const fileSystem: BackendStorageFileSystem = {
|
|
127
168
|
stat(filePath: string): Promise<BackendFileStat> {
|
|
128
169
|
return inferStat(storage, fileSystem.resolve(filePath));
|
|
@@ -158,30 +199,20 @@ export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapt
|
|
|
158
199
|
const resolved = fileSystem.resolve(filePath);
|
|
159
200
|
return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
|
|
160
201
|
},
|
|
161
|
-
async
|
|
162
|
-
const resolved = fileSystem.resolve(
|
|
163
|
-
if (storage.
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
);
|
|
166
211
|
}
|
|
167
|
-
|
|
168
|
-
let closed = false;
|
|
169
|
-
return {
|
|
170
|
-
async writeFile(content: string): Promise<void> {
|
|
171
|
-
if (closed) throw createPathError('EBADF', `Storage lock handle is closed: ${resolved}`);
|
|
172
|
-
await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
|
|
173
|
-
await storage.writeFile(resolved, content);
|
|
174
|
-
},
|
|
175
|
-
async close(): Promise<void> {
|
|
176
|
-
closed = true;
|
|
177
|
-
lockedPaths.delete(resolved);
|
|
178
|
-
},
|
|
179
|
-
};
|
|
212
|
+
return acquireWebSessionLock(lockManager, `@openfairygui/backend:${resolved}`);
|
|
180
213
|
},
|
|
181
214
|
unlink(filePath: string): Promise<void> {
|
|
182
|
-
|
|
183
|
-
lockedPaths.delete(resolved);
|
|
184
|
-
return storage.unlink(resolved);
|
|
215
|
+
return storage.unlink(fileSystem.resolve(filePath));
|
|
185
216
|
},
|
|
186
217
|
rmdir(dirPath: string): Promise<void> {
|
|
187
218
|
return storage.rmdir(fileSystem.resolve(dirPath));
|