@openfairygui/backend 0.2.0-alpha.0 → 0.2.0-alpha.1
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 +24 -1
- package/dist/index.cjs +5 -1119
- package/dist/index.d.cts +2 -364
- package/dist/index.d.mts +2 -364
- package/dist/index.mjs +2 -1091
- 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-BXbt135W.cjs +1216 -0
- package/dist/runtime-C5JY_kRI.d.cts +424 -0
- package/dist/runtime-C5ZsWMnH.d.mts +424 -0
- package/dist/runtime-DLWLghmH.mjs +1192 -0
- package/package.json +21 -9
- package/src/index.ts +6 -1
- package/src/node.ts +96 -0
- package/src/runtime.ts +120 -71
- package/src/services/artifact-service.ts +11 -1
- package/src/services/authoring-service.ts +16 -3
- package/src/services/context.ts +13 -2
- package/src/services/event-service.ts +1 -1
- package/src/services/runtime-service.ts +114 -12
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProjectReader, liftDocumentToUamProject, normalizeUamProject } from '@openfairygui/core';
|
|
2
2
|
import { failure, success, type BackendContext, type BackendSessionState } from './context.js';
|
|
3
3
|
import type { CacheService } from './cache-service.js';
|
|
4
4
|
import type { EventService } from './event-service.js';
|
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
BackendFileHandle,
|
|
9
9
|
BackendResult,
|
|
10
10
|
BackendSessionSnapshot,
|
|
11
|
+
BackendCapabilityUnavailableError,
|
|
11
12
|
InProcessLockConflictError,
|
|
13
|
+
OpenProjectSessionInput,
|
|
12
14
|
SessionNotFoundError,
|
|
13
15
|
} from '../runtime.js';
|
|
14
16
|
import { resolveCanonicalProjectRoot } from '../path-policy.js';
|
|
@@ -18,6 +20,57 @@ function randomId(): string {
|
|
|
18
20
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function createCapabilityUnavailableError(capability: BackendCapabilityUnavailableError['capability']): BackendCapabilityUnavailableError {
|
|
24
|
+
const artifactCapability = capability === 'artifact.publish' || capability === 'artifact.restore';
|
|
25
|
+
return {
|
|
26
|
+
code: 'capability_unavailable',
|
|
27
|
+
message: artifactCapability
|
|
28
|
+
? `${capability} requires the Node bridge boundary exposed by @openfairygui/backend/node.`
|
|
29
|
+
: `${capability} requires an injected BackendFileSystem adapter.`,
|
|
30
|
+
capability,
|
|
31
|
+
requiredAdapter: capability === 'fileSystem' ? 'BackendFileSystem' : undefined,
|
|
32
|
+
requiredHost: artifactCapability ? 'node' : undefined,
|
|
33
|
+
bridgeBoundary: artifactCapability ? 'external-bridge' : undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createProjectReaderFileSystem(fileSystem: NonNullable<BackendContext['fileSystem']>): import('@openfairygui/core').FileSystem {
|
|
38
|
+
return {
|
|
39
|
+
readFile(filePath: string): Promise<string> {
|
|
40
|
+
return fileSystem.readFile(filePath);
|
|
41
|
+
},
|
|
42
|
+
readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
43
|
+
return fileSystem.readFileRaw(filePath);
|
|
44
|
+
},
|
|
45
|
+
writeFile(filePath: string, content: string): Promise<void> {
|
|
46
|
+
return fileSystem.writeFile(filePath, content);
|
|
47
|
+
},
|
|
48
|
+
writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
49
|
+
return fileSystem.writeFileRaw(filePath, data);
|
|
50
|
+
},
|
|
51
|
+
async mkdir(dirPath: string): Promise<void> {
|
|
52
|
+
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
53
|
+
},
|
|
54
|
+
readdir(dirPath: string): Promise<string[]> {
|
|
55
|
+
return fileSystem.readdir(dirPath);
|
|
56
|
+
},
|
|
57
|
+
async exists(filePath: string): Promise<boolean> {
|
|
58
|
+
try {
|
|
59
|
+
await fileSystem.stat(filePath);
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
join(...paths: string[]): string {
|
|
66
|
+
return fileSystem.join(...paths);
|
|
67
|
+
},
|
|
68
|
+
dirname(filePath: string): string {
|
|
69
|
+
return fileSystem.dirname(filePath);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
21
74
|
export class RuntimeService {
|
|
22
75
|
public constructor(
|
|
23
76
|
private readonly context: BackendContext,
|
|
@@ -26,12 +79,16 @@ export class RuntimeService {
|
|
|
26
79
|
private readonly jobService: JobService,
|
|
27
80
|
) {}
|
|
28
81
|
|
|
29
|
-
public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError>> {
|
|
82
|
+
public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError>> {
|
|
30
83
|
const startedAt = Date.now();
|
|
31
|
-
|
|
84
|
+
if (!this.context.fileSystem) {
|
|
85
|
+
return failure('runtime', startedAt, createCapabilityUnavailableError('fileSystem'));
|
|
86
|
+
}
|
|
87
|
+
const fileSystem = this.context.fileSystem;
|
|
88
|
+
const resolved = await resolveCanonicalProjectRoot(fileSystem, input.projectPath);
|
|
32
89
|
const { fairyPath, canonicalProjectPath, canonicalPathKey } = resolved;
|
|
33
90
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
34
|
-
const lockFilePath =
|
|
91
|
+
const lockFilePath = fileSystem.join(canonicalProjectPath, '.openfairygui.backend.lock');
|
|
35
92
|
|
|
36
93
|
if (existingSessionId) {
|
|
37
94
|
return failure('runtime', startedAt, {
|
|
@@ -46,16 +103,19 @@ export class RuntimeService {
|
|
|
46
103
|
|
|
47
104
|
let advisoryLock: BackendFileHandle | null = null;
|
|
48
105
|
try {
|
|
49
|
-
advisoryLock = await
|
|
50
|
-
await advisoryLock.writeFile(JSON.stringify({
|
|
51
|
-
|
|
106
|
+
advisoryLock = await fileSystem.openExclusive(lockFilePath);
|
|
107
|
+
await advisoryLock.writeFile(JSON.stringify(this.context.host?.lockMetadata?.({
|
|
108
|
+
canonicalPathKey,
|
|
109
|
+
canonicalProjectPath,
|
|
110
|
+
lockFilePath,
|
|
111
|
+
}) ?? {
|
|
52
112
|
createdAt: new Date().toISOString(),
|
|
53
113
|
canonicalPathKey,
|
|
54
114
|
}));
|
|
55
115
|
await advisoryLock.close();
|
|
56
116
|
|
|
57
|
-
const
|
|
58
|
-
const project = await
|
|
117
|
+
const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
|
|
118
|
+
const project = liftDocumentToUamProject(await reader.read(fairyPath));
|
|
59
119
|
const sessionId = randomId();
|
|
60
120
|
const session: BackendSessionState = {
|
|
61
121
|
sessionId,
|
|
@@ -80,7 +140,7 @@ export class RuntimeService {
|
|
|
80
140
|
revision: session.revision,
|
|
81
141
|
});
|
|
82
142
|
} catch (error) {
|
|
83
|
-
if (
|
|
143
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST') {
|
|
84
144
|
return failure('runtime', startedAt, {
|
|
85
145
|
code: 'lock_conflict',
|
|
86
146
|
kind: 'advisory_lock_conflict',
|
|
@@ -91,12 +151,52 @@ export class RuntimeService {
|
|
|
91
151
|
}
|
|
92
152
|
if (advisoryLock) {
|
|
93
153
|
await advisoryLock.close().catch(() => undefined);
|
|
94
|
-
await
|
|
154
|
+
await fileSystem.unlink(lockFilePath).catch(() => undefined);
|
|
95
155
|
}
|
|
96
156
|
throw error;
|
|
97
157
|
}
|
|
98
158
|
}
|
|
99
159
|
|
|
160
|
+
public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
|
|
161
|
+
const startedAt = Date.now();
|
|
162
|
+
const sessionId = input.sessionId ?? randomId();
|
|
163
|
+
const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
|
|
164
|
+
const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
|
|
165
|
+
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
166
|
+
if (existingSessionId) {
|
|
167
|
+
return failure('runtime', startedAt, {
|
|
168
|
+
code: 'lock_conflict',
|
|
169
|
+
kind: 'in_process_session_exists',
|
|
170
|
+
message: `Project is already open in this backend runtime: ${canonicalProjectPath}`,
|
|
171
|
+
canonicalPathKey,
|
|
172
|
+
holderSessionId: existingSessionId,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const session: BackendSessionState = {
|
|
177
|
+
sessionId,
|
|
178
|
+
fairyPath: canonicalProjectPath,
|
|
179
|
+
canonicalProjectPath,
|
|
180
|
+
canonicalPathKey,
|
|
181
|
+
lockFilePath: '',
|
|
182
|
+
project: normalizeUamProject(input.project),
|
|
183
|
+
revision: 0,
|
|
184
|
+
lastSavedRevision: 0,
|
|
185
|
+
dirty: false,
|
|
186
|
+
lockHeld: false,
|
|
187
|
+
closed: false,
|
|
188
|
+
};
|
|
189
|
+
this.context.sessions.set(sessionId, session);
|
|
190
|
+
this.context.sessionsByPath.set(canonicalPathKey, sessionId);
|
|
191
|
+
this.cacheService.refreshSession(session);
|
|
192
|
+
this.eventService.emit({ kind: 'session.opened', sessionId, canonicalPathKey, revision: session.revision });
|
|
193
|
+
|
|
194
|
+
return success('runtime', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
195
|
+
sessionId: session.sessionId,
|
|
196
|
+
revision: session.revision,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
100
200
|
public async closeSession(
|
|
101
201
|
input: { sessionId: string },
|
|
102
202
|
): Promise<BackendResult<{ sessionId: string; closed: true }, SessionNotFoundError>> {
|
|
@@ -107,7 +207,9 @@ export class RuntimeService {
|
|
|
107
207
|
}
|
|
108
208
|
|
|
109
209
|
this.eventService.emit({ kind: 'session.closeRequested', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
|
|
110
|
-
|
|
210
|
+
if (this.context.fileSystem && session.lockFilePath) {
|
|
211
|
+
await this.context.fileSystem.unlink(session.lockFilePath).catch(() => undefined);
|
|
212
|
+
}
|
|
111
213
|
session.lockHeld = false;
|
|
112
214
|
session.closed = true;
|
|
113
215
|
this.context.sessions.delete(session.sessionId);
|