@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
package/dist/index.cjs
CHANGED
|
@@ -1,1120 +1,151 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
get: ((k) => from[k]).bind(null, key),
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
-
});
|
|
2
|
+
const require_runtime = require("./runtime-GKzsXJdO.cjs");
|
|
3
|
+
//#region src/storage.ts
|
|
4
|
+
var StorageFileStat = class {
|
|
5
|
+
constructor(kind) {
|
|
6
|
+
this.kind = kind;
|
|
7
|
+
}
|
|
8
|
+
isFile() {
|
|
9
|
+
return this.kind === "file";
|
|
10
|
+
}
|
|
11
|
+
isDirectory() {
|
|
12
|
+
return this.kind === "directory";
|
|
16
13
|
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
-
value: mod,
|
|
21
|
-
enumerable: true
|
|
22
|
-
}) : target, mod));
|
|
23
|
-
//#endregion
|
|
24
|
-
let _openfairygui_core = require("@openfairygui/core");
|
|
25
|
-
let node_fs_promises = require("node:fs/promises");
|
|
26
|
-
node_fs_promises = __toESM(node_fs_promises);
|
|
27
|
-
let node_path = require("node:path");
|
|
28
|
-
node_path = __toESM(node_path);
|
|
29
|
-
let _openfairygui_functions = require("@openfairygui/functions");
|
|
30
|
-
//#region src/contracts.ts
|
|
31
|
-
const BACKEND_CONTRACT_VERSION = "1.1.0-p2";
|
|
32
|
-
const BACKEND_CAPABILITY_SCHEMA_VERSION = 2;
|
|
33
|
-
const BACKEND_COMPATIBILITY_POLICY = {
|
|
34
|
-
incompatibleChange: "requires contractVersion bump",
|
|
35
|
-
capabilitySchemaChange: "requires capabilitySchemaVersion bump",
|
|
36
|
-
additiveChange: "allowed without breaking existing consumers"
|
|
37
14
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const rawSegments =
|
|
15
|
+
function createPathError(code, message) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.code = code;
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
function normalizeStoragePath(value) {
|
|
21
|
+
const normalized = value.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
22
|
+
const absolute = normalized.startsWith("/");
|
|
23
|
+
const rawSegments = normalized.split("/").filter((segment) => segment.length > 0);
|
|
47
24
|
const segments = [];
|
|
48
25
|
for (const segment of rawSegments) {
|
|
49
26
|
if (segment === ".") continue;
|
|
50
27
|
if (segment === "..") {
|
|
51
|
-
if (segments.length > 0
|
|
52
|
-
else if (!hasRoot) segments.push("..");
|
|
28
|
+
if (segments.length > 0) segments.pop();
|
|
53
29
|
continue;
|
|
54
30
|
}
|
|
55
31
|
segments.push(segment);
|
|
56
32
|
}
|
|
57
33
|
const joined = segments.join("/");
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async function validateSaveTarget(fileSystem, openedFairyPath, targetPath) {
|
|
91
|
-
if (!targetPath) return null;
|
|
92
|
-
const attemptedPath = await fileSystem.resolvePath(fileSystem.resolve(targetPath));
|
|
93
|
-
const allowedPath = await fileSystem.resolvePath(openedFairyPath);
|
|
94
|
-
if (normalizeComparablePath(attemptedPath) === normalizeComparablePath(allowedPath)) return null;
|
|
95
|
-
return {
|
|
96
|
-
code: "path_policy_violation",
|
|
97
|
-
message: `Save target is restricted to the originally opened project file: ${allowedPath}`,
|
|
98
|
-
policy: "save_target",
|
|
99
|
-
attemptedPath,
|
|
100
|
-
allowedPath
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
//#endregion
|
|
104
|
-
//#region src/services/artifact-service.ts
|
|
105
|
-
function createArtifactCapabilities() {
|
|
106
|
-
return {
|
|
107
|
-
publish: false,
|
|
108
|
-
restore: false,
|
|
109
|
-
status: "deferred"
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region src/services/context.ts
|
|
114
|
-
function randomId$1() {
|
|
115
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
116
|
-
}
|
|
117
|
-
function createMeta(stage, startedAt, options) {
|
|
118
|
-
return {
|
|
119
|
-
requestId: options?.requestId ?? randomId$1(),
|
|
120
|
-
sessionId: options?.sessionId,
|
|
121
|
-
revision: options?.revision,
|
|
122
|
-
durationMs: Math.max(0, Date.now() - startedAt),
|
|
123
|
-
warnings: options?.warnings ?? [],
|
|
124
|
-
diagnostics: options?.diagnostics ?? [],
|
|
125
|
-
stage,
|
|
126
|
-
contractVersion: BACKEND_CONTRACT_VERSION,
|
|
127
|
-
capabilitySchemaVersion: 2
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
function success(stage, startedAt, data, options) {
|
|
131
|
-
return {
|
|
132
|
-
ok: true,
|
|
133
|
-
meta: createMeta(stage, startedAt, options),
|
|
134
|
-
data
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
function failure(stage, startedAt, error, session, options) {
|
|
138
|
-
return {
|
|
139
|
-
ok: false,
|
|
140
|
-
meta: createMeta(stage, startedAt, options),
|
|
141
|
-
error,
|
|
142
|
-
session
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
//#endregion
|
|
146
|
-
//#region src/services/snapshot-utils.ts
|
|
147
|
-
function cloneJsonValue(value) {
|
|
148
|
-
if (value === void 0 || value === null) return value;
|
|
149
|
-
return JSON.parse(JSON.stringify(value));
|
|
150
|
-
}
|
|
151
|
-
function cloneEventSnapshot(event) {
|
|
152
|
-
return {
|
|
153
|
-
...event,
|
|
154
|
-
diagnostics: event.diagnostics.map((diagnostic) => ({ ...diagnostic })),
|
|
155
|
-
payload: cloneJsonValue(event.payload)
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
function cloneJobSnapshot(job) {
|
|
159
|
-
return {
|
|
160
|
-
...job,
|
|
161
|
-
diagnostics: job.diagnostics.map((diagnostic) => ({ ...diagnostic })),
|
|
162
|
-
progress: job.progress ? { ...job.progress } : void 0,
|
|
163
|
-
result: cloneJsonValue(job.result),
|
|
164
|
-
error: cloneJsonValue(job.error)
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
function cloneCacheEntrySnapshot(entry) {
|
|
168
|
-
return {
|
|
169
|
-
...entry,
|
|
170
|
-
summary: {
|
|
171
|
-
...entry.summary,
|
|
172
|
-
diagnostics: entry.summary.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
function cloneCapabilitiesSnapshot(capabilities) {
|
|
177
|
-
return cloneJsonValue(capabilities);
|
|
178
|
-
}
|
|
179
|
-
//#endregion
|
|
180
|
-
//#region src/services/session-utils.ts
|
|
181
|
-
function toSessionSnapshot(session, capabilities) {
|
|
182
|
-
return {
|
|
183
|
-
sessionId: session.sessionId,
|
|
184
|
-
canonicalProjectPath: session.canonicalProjectPath,
|
|
185
|
-
revision: session.revision,
|
|
186
|
-
lastSavedRevision: session.lastSavedRevision,
|
|
187
|
-
dirty: session.dirty,
|
|
188
|
-
lockHeld: session.lockHeld,
|
|
189
|
-
capabilities: cloneCapabilitiesSnapshot(capabilities)
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
function createSessionNotFoundError(sessionId) {
|
|
193
|
-
return {
|
|
194
|
-
code: "session_not_found",
|
|
195
|
-
message: `Session was not found: ${sessionId}`,
|
|
196
|
-
sessionId
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
function createStaleWriteError(session, expectedRevision) {
|
|
200
|
-
return {
|
|
201
|
-
code: "stale_write",
|
|
202
|
-
message: `Expected revision ${expectedRevision} does not match current revision ${session.revision}.`,
|
|
203
|
-
sessionId: session.sessionId,
|
|
204
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
205
|
-
expectedRevision,
|
|
206
|
-
actualRevision: session.revision
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
//#endregion
|
|
210
|
-
//#region src/services/authoring-service.ts
|
|
211
|
-
function createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
|
|
212
|
-
async function trackWrite(targetPath, fn) {
|
|
34
|
+
if (absolute) return joined ? `/${joined}` : "/";
|
|
35
|
+
return joined || ".";
|
|
36
|
+
}
|
|
37
|
+
function joinStoragePath(...paths) {
|
|
38
|
+
return normalizeStoragePath(paths.filter((part) => part.length > 0).join("/"));
|
|
39
|
+
}
|
|
40
|
+
function dirnameStoragePath(filePath) {
|
|
41
|
+
const normalized = normalizeStoragePath(filePath);
|
|
42
|
+
if (normalized === "/" || normalized === ".") return ".";
|
|
43
|
+
const absolute = normalized.startsWith("/");
|
|
44
|
+
const parts = normalized.split("/").filter((part) => part.length > 0);
|
|
45
|
+
parts.pop();
|
|
46
|
+
if (parts.length === 0) return absolute ? "/" : ".";
|
|
47
|
+
return `${absolute ? "/" : ""}${parts.join("/")}`;
|
|
48
|
+
}
|
|
49
|
+
function statFromLike(stat) {
|
|
50
|
+
if (typeof stat.isFile === "function" && typeof stat.isDirectory === "function") return stat;
|
|
51
|
+
const kind = stat.kind ?? stat.type;
|
|
52
|
+
if (kind === "file" || kind === "directory") return new StorageFileStat(kind);
|
|
53
|
+
throw createPathError("EINVAL", "Storage stat must provide kind/type or isFile()/isDirectory().");
|
|
54
|
+
}
|
|
55
|
+
async function inferStat(storage, filePath) {
|
|
56
|
+
if (storage.stat) return statFromLike(await storage.stat(filePath));
|
|
57
|
+
try {
|
|
58
|
+
await storage.readdir(filePath);
|
|
59
|
+
return new StorageFileStat("directory");
|
|
60
|
+
} catch {}
|
|
61
|
+
try {
|
|
62
|
+
await storage.readFileRaw(filePath);
|
|
63
|
+
return new StorageFileStat("file");
|
|
64
|
+
} catch {
|
|
213
65
|
try {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
failedPaths.push(targetPath);
|
|
219
|
-
throw error;
|
|
66
|
+
await storage.readFile(filePath);
|
|
67
|
+
return new StorageFileStat("file");
|
|
68
|
+
} catch {
|
|
69
|
+
throw createPathError("ENOENT", `Storage path not found: ${filePath}`);
|
|
220
70
|
}
|
|
221
71
|
}
|
|
222
|
-
return {
|
|
223
|
-
async readFile(filePath) {
|
|
224
|
-
return fileSystem.readFile(filePath);
|
|
225
|
-
},
|
|
226
|
-
async readFileRaw(filePath) {
|
|
227
|
-
return fileSystem.readFileRaw(filePath);
|
|
228
|
-
},
|
|
229
|
-
async writeFile(filePath, content) {
|
|
230
|
-
await trackWrite(filePath, async () => {
|
|
231
|
-
await fileSystem.mkdir(fileSystem.dirname(filePath), { recursive: true });
|
|
232
|
-
await fileSystem.writeFile(filePath, content);
|
|
233
|
-
});
|
|
234
|
-
},
|
|
235
|
-
async writeFileRaw(filePath, data) {
|
|
236
|
-
await trackWrite(filePath, async () => {
|
|
237
|
-
await fileSystem.mkdir(fileSystem.dirname(filePath), { recursive: true });
|
|
238
|
-
await fileSystem.writeFileRaw(filePath, data);
|
|
239
|
-
});
|
|
240
|
-
},
|
|
241
|
-
async mkdir(dirPath) {
|
|
242
|
-
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
243
|
-
},
|
|
244
|
-
async readdir(dirPath) {
|
|
245
|
-
return fileSystem.readdir(dirPath);
|
|
246
|
-
},
|
|
247
|
-
async exists(filePath) {
|
|
248
|
-
try {
|
|
249
|
-
await fileSystem.stat(filePath);
|
|
250
|
-
return true;
|
|
251
|
-
} catch {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
join(...paths) {
|
|
256
|
-
return fileSystem.join(...paths);
|
|
257
|
-
},
|
|
258
|
-
dirname(filePath) {
|
|
259
|
-
return fileSystem.dirname(filePath);
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
72
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.cacheService = cacheService;
|
|
267
|
-
this.eventService = eventService;
|
|
268
|
-
}
|
|
269
|
-
async applyTransaction(input) {
|
|
270
|
-
const startedAt = Date.now();
|
|
271
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
272
|
-
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
273
|
-
if (input.expectedRevision !== session.revision) {
|
|
274
|
-
this.eventService.emit({
|
|
275
|
-
kind: "transaction.rejected",
|
|
276
|
-
sessionId: session.sessionId,
|
|
277
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
278
|
-
revision: session.revision
|
|
279
|
-
});
|
|
280
|
-
return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
|
|
281
|
-
sessionId: session.sessionId,
|
|
282
|
-
revision: session.revision
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
const result = (0, _openfairygui_functions.applyUamTransactionApp)({
|
|
286
|
-
project: session.project,
|
|
287
|
-
operations: input.operations
|
|
288
|
-
});
|
|
289
|
-
if (result.ok === false) {
|
|
290
|
-
this.eventService.emit({
|
|
291
|
-
kind: "transaction.rejected",
|
|
292
|
-
sessionId: session.sessionId,
|
|
293
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
294
|
-
revision: session.revision,
|
|
295
|
-
diagnostics: result.error.issues?.map((issue) => ({
|
|
296
|
-
code: result.error.code,
|
|
297
|
-
message: issue.message,
|
|
298
|
-
severity: "error"
|
|
299
|
-
})) ?? []
|
|
300
|
-
});
|
|
301
|
-
return failure("authoring", startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
|
|
302
|
-
sessionId: session.sessionId,
|
|
303
|
-
revision: session.revision
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
session.project = result.project;
|
|
307
|
-
session.revision += 1;
|
|
308
|
-
session.dirty = true;
|
|
309
|
-
const cacheEntry = this.cacheService.invalidateSession(session);
|
|
310
|
-
this.eventService.emit({
|
|
311
|
-
kind: "transaction.applied",
|
|
312
|
-
sessionId: session.sessionId,
|
|
313
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
314
|
-
revision: session.revision
|
|
315
|
-
});
|
|
316
|
-
this.eventService.emit({
|
|
317
|
-
kind: "cache.invalidated",
|
|
318
|
-
sessionId: session.sessionId,
|
|
319
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
320
|
-
revision: session.revision,
|
|
321
|
-
cacheRevision: cacheEntry.revision
|
|
322
|
-
});
|
|
323
|
-
return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
324
|
-
sessionId: session.sessionId,
|
|
325
|
-
revision: session.revision
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
async saveSession(input) {
|
|
329
|
-
const startedAt = Date.now();
|
|
330
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
331
|
-
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
332
|
-
if (input.expectedRevision !== void 0 && input.expectedRevision !== session.revision) return failure("authoring", startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
|
|
333
|
-
sessionId: session.sessionId,
|
|
334
|
-
revision: session.revision
|
|
335
|
-
});
|
|
336
|
-
const targetViolation = await validateSaveTarget(this.context.fileSystem, session.fairyPath, input.targetPath);
|
|
337
|
-
if (targetViolation) return failure("authoring", startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
|
|
338
|
-
sessionId: session.sessionId,
|
|
339
|
-
revision: session.revision
|
|
340
|
-
});
|
|
341
|
-
if (!session.dirty) return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
342
|
-
sessionId: session.sessionId,
|
|
343
|
-
revision: session.revision
|
|
344
|
-
});
|
|
345
|
-
const committedPaths = [];
|
|
346
|
-
const failedPaths = [];
|
|
347
|
-
this.eventService.emit({
|
|
348
|
-
kind: "save.started",
|
|
349
|
-
sessionId: session.sessionId,
|
|
350
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
351
|
-
revision: session.revision
|
|
352
|
-
});
|
|
353
|
-
try {
|
|
354
|
-
await new _openfairygui_core.ProjectWriter(createWriterFileSystem(this.context.fileSystem, committedPaths, failedPaths)).write((0, _openfairygui_core.materializeUamProject)(session.project), session.fairyPath);
|
|
355
|
-
session.lastSavedRevision = session.revision;
|
|
356
|
-
session.dirty = false;
|
|
357
|
-
const cacheEntry = this.cacheService.refreshSession(session);
|
|
358
|
-
this.eventService.emit({
|
|
359
|
-
kind: "save.completed",
|
|
360
|
-
sessionId: session.sessionId,
|
|
361
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
362
|
-
revision: session.revision
|
|
363
|
-
});
|
|
364
|
-
this.eventService.emit({
|
|
365
|
-
kind: "cache.updated",
|
|
366
|
-
sessionId: session.sessionId,
|
|
367
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
368
|
-
revision: session.revision,
|
|
369
|
-
cacheRevision: cacheEntry.revision
|
|
370
|
-
});
|
|
371
|
-
return success("authoring", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
372
|
-
sessionId: session.sessionId,
|
|
373
|
-
revision: session.revision
|
|
374
|
-
});
|
|
375
|
-
} catch (error) {
|
|
376
|
-
this.cacheService.invalidateSession(session);
|
|
377
|
-
this.eventService.emit({
|
|
378
|
-
kind: "save.failed",
|
|
379
|
-
sessionId: session.sessionId,
|
|
380
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
381
|
-
revision: session.revision
|
|
382
|
-
});
|
|
383
|
-
return failure("authoring", startedAt, {
|
|
384
|
-
code: "save_partial_failure",
|
|
385
|
-
message: error instanceof Error ? error.message : String(error),
|
|
386
|
-
sessionId: session.sessionId,
|
|
387
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
388
|
-
attemptedRevision: session.revision,
|
|
389
|
-
lastSavedRevision: session.lastSavedRevision,
|
|
390
|
-
committedPaths,
|
|
391
|
-
failedPaths,
|
|
392
|
-
diskMayBePartiallyUpdated: true
|
|
393
|
-
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
394
|
-
sessionId: session.sessionId,
|
|
395
|
-
revision: session.revision
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
//#endregion
|
|
401
|
-
//#region src/services/cache-service.ts
|
|
402
|
-
function createCacheEntry(session, valid) {
|
|
403
|
-
return {
|
|
404
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
405
|
-
sessionId: session.sessionId,
|
|
406
|
-
revision: session.revision,
|
|
407
|
-
lastSavedRevision: session.lastSavedRevision,
|
|
408
|
-
dirty: session.dirty,
|
|
409
|
-
valid,
|
|
410
|
-
indexedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
411
|
-
summary: {
|
|
412
|
-
packageCount: session.project.packages.length,
|
|
413
|
-
resourceCount: session.project.packages.reduce((total, pkg) => total + pkg.resources.length, 0),
|
|
414
|
-
diagnostics: []
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
var CacheService = class {
|
|
419
|
-
constructor(context) {
|
|
420
|
-
this.context = context;
|
|
421
|
-
}
|
|
422
|
-
getCacheSnapshot(input) {
|
|
423
|
-
const startedAt = Date.now();
|
|
424
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
425
|
-
if (!session || session.closed) return failure("read", startedAt, createSessionNotFoundError(input.sessionId));
|
|
426
|
-
const entry = this.context.cacheBySession.get(input.sessionId);
|
|
427
|
-
return success("read", startedAt, {
|
|
428
|
-
cacheRevision: entry?.revision ?? session.revision,
|
|
429
|
-
entries: entry ? [cloneCacheEntrySnapshot(entry)] : []
|
|
430
|
-
}, {
|
|
431
|
-
sessionId: session.sessionId,
|
|
432
|
-
revision: session.revision
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
refreshSession(session) {
|
|
436
|
-
const entry = createCacheEntry(session, true);
|
|
437
|
-
this.context.cacheBySession.set(session.sessionId, entry);
|
|
438
|
-
return entry;
|
|
439
|
-
}
|
|
440
|
-
invalidateSession(session) {
|
|
441
|
-
const entry = createCacheEntry(session, false);
|
|
442
|
-
this.context.cacheBySession.set(session.sessionId, entry);
|
|
443
|
-
return entry;
|
|
444
|
-
}
|
|
445
|
-
removeSession(sessionId) {
|
|
446
|
-
this.context.cacheBySession.delete(sessionId);
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
//#endregion
|
|
450
|
-
//#region src/services/event-service.ts
|
|
451
|
-
const DEFAULT_EVENT_RETENTION_LIMIT = 1e3;
|
|
452
|
-
var EventService = class {
|
|
453
|
-
constructor(context) {
|
|
454
|
-
this.context = context;
|
|
455
|
-
}
|
|
456
|
-
emit(event) {
|
|
457
|
-
const emitted = {
|
|
458
|
-
...event,
|
|
459
|
-
sequence: this.context.nextEventSequence(),
|
|
460
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
461
|
-
diagnostics: event.diagnostics ?? []
|
|
462
|
-
};
|
|
463
|
-
const sessionId = event.sessionId;
|
|
464
|
-
if (!sessionId) return emitted;
|
|
465
|
-
const events = this.context.eventsBySession.get(sessionId) ?? [];
|
|
466
|
-
events.push(emitted);
|
|
467
|
-
while (events.length > DEFAULT_EVENT_RETENTION_LIMIT) events.shift();
|
|
468
|
-
this.context.eventsBySession.set(sessionId, events);
|
|
469
|
-
return emitted;
|
|
470
|
-
}
|
|
471
|
-
getEvents(input) {
|
|
472
|
-
const startedAt = Date.now();
|
|
473
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
474
|
-
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
475
|
-
const events = this.context.eventsBySession.get(input.sessionId) ?? [];
|
|
476
|
-
const oldestSequence = events[0]?.sequence ?? (events.length === 0 ? 1 : 0);
|
|
477
|
-
const currentSequence = events.at(-1)?.sequence ?? 0;
|
|
478
|
-
const after = input.after === void 0 ? 0 : Number(input.after);
|
|
479
|
-
if (!Number.isInteger(after) || after < 0) return failure("runtime", startedAt, {
|
|
480
|
-
code: "event_cursor_invalid",
|
|
481
|
-
message: `Invalid event cursor: ${input.after}`,
|
|
482
|
-
sessionId: input.sessionId,
|
|
483
|
-
after: String(input.after)
|
|
484
|
-
});
|
|
485
|
-
if (events.length > 0 && after < oldestSequence - 1) return failure("runtime", startedAt, {
|
|
486
|
-
code: "event_cursor_invalid",
|
|
487
|
-
message: `Event cursor has expired: ${after}`,
|
|
488
|
-
sessionId: input.sessionId,
|
|
489
|
-
after: String(input.after)
|
|
490
|
-
});
|
|
491
|
-
if (after > currentSequence) return failure("runtime", startedAt, {
|
|
492
|
-
code: "event_cursor_invalid",
|
|
493
|
-
message: `Unknown event cursor: ${after}`,
|
|
494
|
-
sessionId: input.sessionId,
|
|
495
|
-
after: String(input.after)
|
|
496
|
-
});
|
|
497
|
-
const filtered = events.filter((event) => event.sequence > after);
|
|
498
|
-
const limit = input.limit === void 0 ? filtered.length : Math.max(0, input.limit);
|
|
499
|
-
return success("runtime", startedAt, {
|
|
500
|
-
events: filtered.slice(0, limit).map(cloneEventSnapshot),
|
|
501
|
-
oldestSequence,
|
|
502
|
-
currentSequence,
|
|
503
|
-
cursorExpired: false
|
|
504
|
-
}, {
|
|
505
|
-
sessionId: session.sessionId,
|
|
506
|
-
revision: session.revision
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
removeSession(sessionId) {
|
|
510
|
-
this.context.eventsBySession.delete(sessionId);
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
//#endregion
|
|
514
|
-
//#region src/services/job-service.ts
|
|
515
|
-
const COMPLETED_JOB_RETENTION_LIMIT = 100;
|
|
516
|
-
const REFRESH_START_DELAY_MS = 0;
|
|
517
|
-
const REFRESH_COMPLETE_DELAY_MS = 50;
|
|
518
|
-
function isTerminal(status) {
|
|
519
|
-
return status === "completed" || status === "failed" || status === "cancelled";
|
|
520
|
-
}
|
|
521
|
-
var JobService = class {
|
|
522
|
-
constructor(context, cacheService, eventService) {
|
|
523
|
-
this.context = context;
|
|
524
|
-
this.cacheService = cacheService;
|
|
525
|
-
this.eventService = eventService;
|
|
526
|
-
}
|
|
527
|
-
cancellationRequests = /* @__PURE__ */ new Set();
|
|
528
|
-
getJobs(sessionId) {
|
|
529
|
-
return this.context.jobsBySession.get(sessionId) ?? [];
|
|
530
|
-
}
|
|
531
|
-
setJobs(sessionId, jobs) {
|
|
532
|
-
const retained = [];
|
|
533
|
-
let terminalCount = 0;
|
|
534
|
-
for (let index = jobs.length - 1; index >= 0; index -= 1) {
|
|
535
|
-
const job = jobs[index];
|
|
536
|
-
if (isTerminal(job.status)) {
|
|
537
|
-
if (terminalCount >= COMPLETED_JOB_RETENTION_LIMIT) continue;
|
|
538
|
-
terminalCount += 1;
|
|
539
|
-
}
|
|
540
|
-
retained.push(job);
|
|
541
|
-
}
|
|
542
|
-
this.context.jobsBySession.set(sessionId, retained.reverse());
|
|
543
|
-
}
|
|
544
|
-
refreshCache(input) {
|
|
545
|
-
const startedAt = Date.now();
|
|
546
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
547
|
-
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
548
|
-
const jobId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
549
|
-
const job = {
|
|
550
|
-
jobId,
|
|
551
|
-
kind: "cache.refresh",
|
|
552
|
-
status: "queued",
|
|
553
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
554
|
-
sessionId: session.sessionId,
|
|
555
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
556
|
-
revision: session.revision,
|
|
557
|
-
diagnostics: [],
|
|
558
|
-
progress: {
|
|
559
|
-
completed: 0,
|
|
560
|
-
total: 1,
|
|
561
|
-
message: input.reason ?? "manual"
|
|
562
|
-
}
|
|
563
|
-
};
|
|
564
|
-
const jobs = this.getJobs(session.sessionId);
|
|
565
|
-
jobs.push(job);
|
|
566
|
-
this.setJobs(session.sessionId, jobs);
|
|
567
|
-
this.eventService.emit({
|
|
568
|
-
kind: "job.created",
|
|
569
|
-
sessionId: session.sessionId,
|
|
570
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
571
|
-
revision: session.revision,
|
|
572
|
-
jobId
|
|
573
|
-
});
|
|
574
|
-
this.scheduleRefreshJob(session.sessionId, jobId);
|
|
575
|
-
return success("runtime", startedAt, cloneJobSnapshot(job), {
|
|
576
|
-
sessionId: session.sessionId,
|
|
577
|
-
revision: session.revision
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
getJob(input) {
|
|
581
|
-
const startedAt = Date.now();
|
|
582
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
583
|
-
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
584
|
-
const job = this.getJobs(input.sessionId).find((item) => item.jobId === input.jobId);
|
|
585
|
-
if (!job) return failure("runtime", startedAt, {
|
|
586
|
-
code: "job_not_found",
|
|
587
|
-
message: `Job was not found: ${input.jobId}`,
|
|
588
|
-
sessionId: input.sessionId,
|
|
589
|
-
jobId: input.jobId
|
|
590
|
-
}, void 0, {
|
|
591
|
-
sessionId: session.sessionId,
|
|
592
|
-
revision: session.revision
|
|
593
|
-
});
|
|
594
|
-
return success("runtime", startedAt, cloneJobSnapshot(job), {
|
|
595
|
-
sessionId: session.sessionId,
|
|
596
|
-
revision: session.revision
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
listJobs(input) {
|
|
600
|
-
const startedAt = Date.now();
|
|
601
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
602
|
-
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
603
|
-
let jobs = [...this.getJobs(input.sessionId)];
|
|
604
|
-
if (input.kind) jobs = jobs.filter((job) => job.kind === input.kind);
|
|
605
|
-
if (input.status) if (input.status === "active") jobs = jobs.filter((job) => !isTerminal(job.status));
|
|
606
|
-
else if (input.status === "terminal") jobs = jobs.filter((job) => isTerminal(job.status));
|
|
607
|
-
else jobs = jobs.filter((job) => job.status === input.status);
|
|
608
|
-
if (input.limit !== void 0) jobs = jobs.slice(0, Math.max(0, input.limit));
|
|
609
|
-
return success("runtime", startedAt, { jobs: jobs.map(cloneJobSnapshot) }, {
|
|
610
|
-
sessionId: session.sessionId,
|
|
611
|
-
revision: session.revision
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
cancelJob(input) {
|
|
615
|
-
const startedAt = Date.now();
|
|
616
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
617
|
-
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
618
|
-
const job = this.getJobs(input.sessionId).find((item) => item.jobId === input.jobId);
|
|
619
|
-
if (!job) return failure("runtime", startedAt, {
|
|
620
|
-
code: "job_not_found",
|
|
621
|
-
message: `Job was not found: ${input.jobId}`,
|
|
622
|
-
sessionId: input.sessionId,
|
|
623
|
-
jobId: input.jobId
|
|
624
|
-
}, void 0, {
|
|
625
|
-
sessionId: session.sessionId,
|
|
626
|
-
revision: session.revision
|
|
627
|
-
});
|
|
628
|
-
if (isTerminal(job.status)) return failure("runtime", startedAt, {
|
|
629
|
-
code: "job_not_cancellable",
|
|
630
|
-
message: `Job is already terminal: ${input.jobId}`,
|
|
631
|
-
sessionId: input.sessionId,
|
|
632
|
-
jobId: input.jobId,
|
|
633
|
-
status: job.status
|
|
634
|
-
}, void 0, {
|
|
635
|
-
sessionId: session.sessionId,
|
|
636
|
-
revision: session.revision
|
|
637
|
-
});
|
|
638
|
-
this.cancellationRequests.add(job.jobId);
|
|
639
|
-
this.eventService.emit({
|
|
640
|
-
kind: "job.cancelRequested",
|
|
641
|
-
sessionId: input.sessionId,
|
|
642
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
643
|
-
revision: session.revision,
|
|
644
|
-
jobId: job.jobId
|
|
645
|
-
});
|
|
646
|
-
return success("runtime", startedAt, cloneJobSnapshot(this.cancelRefreshJob(session.sessionId, job)), {
|
|
647
|
-
sessionId: session.sessionId,
|
|
648
|
-
revision: session.revision
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
removeSession(sessionId) {
|
|
652
|
-
for (const job of this.getJobs(sessionId)) this.cancellationRequests.delete(job.jobId);
|
|
653
|
-
this.context.jobsBySession.delete(sessionId);
|
|
654
|
-
}
|
|
655
|
-
replaceJob(sessionId, nextJob) {
|
|
656
|
-
const jobs = this.getJobs(sessionId).map((job) => job.jobId === nextJob.jobId ? nextJob : job);
|
|
657
|
-
this.setJobs(sessionId, jobs);
|
|
658
|
-
}
|
|
659
|
-
scheduleRefreshJob(sessionId, jobId) {
|
|
660
|
-
setTimeout(() => this.startRefreshJob(sessionId, jobId), REFRESH_START_DELAY_MS);
|
|
661
|
-
}
|
|
662
|
-
startRefreshJob(sessionId, jobId) {
|
|
663
|
-
const session = this.context.sessions.get(sessionId);
|
|
664
|
-
if (!session || session.closed) return;
|
|
665
|
-
const job = this.getJobs(sessionId).find((item) => item.jobId === jobId);
|
|
666
|
-
if (!job || isTerminal(job.status)) return;
|
|
667
|
-
if (this.cancellationRequests.has(jobId)) {
|
|
668
|
-
this.cancelRefreshJob(sessionId, job);
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
const running = {
|
|
672
|
-
...job,
|
|
673
|
-
status: "running",
|
|
674
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
675
|
-
progress: {
|
|
676
|
-
completed: 0,
|
|
677
|
-
total: 1,
|
|
678
|
-
message: "refreshing cache"
|
|
679
|
-
}
|
|
680
|
-
};
|
|
681
|
-
this.replaceJob(sessionId, running);
|
|
682
|
-
this.eventService.emit({
|
|
683
|
-
kind: "job.started",
|
|
684
|
-
sessionId,
|
|
685
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
686
|
-
revision: session.revision,
|
|
687
|
-
jobId
|
|
688
|
-
});
|
|
689
|
-
this.eventService.emit({
|
|
690
|
-
kind: "job.progress",
|
|
691
|
-
sessionId,
|
|
692
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
693
|
-
revision: session.revision,
|
|
694
|
-
jobId,
|
|
695
|
-
payload: running.progress
|
|
696
|
-
});
|
|
697
|
-
setTimeout(() => this.completeRefreshJob(sessionId, jobId), REFRESH_COMPLETE_DELAY_MS);
|
|
698
|
-
}
|
|
699
|
-
completeRefreshJob(sessionId, jobId) {
|
|
700
|
-
const session = this.context.sessions.get(sessionId);
|
|
701
|
-
if (!session || session.closed) return;
|
|
702
|
-
const job = this.getJobs(sessionId).find((item) => item.jobId === jobId);
|
|
703
|
-
if (!job || isTerminal(job.status)) return;
|
|
704
|
-
if (this.cancellationRequests.has(jobId)) {
|
|
705
|
-
this.cancelRefreshJob(sessionId, job);
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
try {
|
|
709
|
-
const entry = this.cacheService.refreshSession(session);
|
|
710
|
-
const completed = {
|
|
711
|
-
...job,
|
|
712
|
-
status: "completed",
|
|
713
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
714
|
-
cacheRevision: entry.revision,
|
|
715
|
-
progress: {
|
|
716
|
-
completed: 1,
|
|
717
|
-
total: 1,
|
|
718
|
-
message: "cache refreshed"
|
|
719
|
-
},
|
|
720
|
-
result: { cacheRevision: entry.revision }
|
|
721
|
-
};
|
|
722
|
-
this.replaceJob(sessionId, completed);
|
|
723
|
-
this.eventService.emit({
|
|
724
|
-
kind: "job.completed",
|
|
725
|
-
sessionId,
|
|
726
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
727
|
-
revision: session.revision,
|
|
728
|
-
cacheRevision: entry.revision,
|
|
729
|
-
jobId
|
|
730
|
-
});
|
|
731
|
-
this.eventService.emit({
|
|
732
|
-
kind: "cache.updated",
|
|
733
|
-
sessionId,
|
|
734
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
735
|
-
revision: session.revision,
|
|
736
|
-
cacheRevision: entry.revision
|
|
737
|
-
});
|
|
738
|
-
} catch (error) {
|
|
739
|
-
const failed = {
|
|
740
|
-
...job,
|
|
741
|
-
status: "failed",
|
|
742
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
743
|
-
error: {
|
|
744
|
-
code: "cache_refresh_failed",
|
|
745
|
-
message: error instanceof Error ? error.message : "Cache refresh failed",
|
|
746
|
-
sessionId,
|
|
747
|
-
jobId
|
|
748
|
-
}
|
|
749
|
-
};
|
|
750
|
-
this.replaceJob(sessionId, failed);
|
|
751
|
-
this.eventService.emit({
|
|
752
|
-
kind: "job.failed",
|
|
753
|
-
sessionId,
|
|
754
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
755
|
-
revision: session.revision,
|
|
756
|
-
jobId,
|
|
757
|
-
diagnostics: failed.diagnostics
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
cancelRefreshJob(sessionId, job) {
|
|
762
|
-
const session = this.context.sessions.get(sessionId);
|
|
763
|
-
const cancelled = {
|
|
764
|
-
...job,
|
|
765
|
-
status: "cancelled",
|
|
766
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
767
|
-
error: {
|
|
768
|
-
code: "job_cancelled",
|
|
769
|
-
message: `Job was cancelled: ${job.jobId}`,
|
|
770
|
-
sessionId,
|
|
771
|
-
jobId: job.jobId
|
|
772
|
-
}
|
|
773
|
-
};
|
|
774
|
-
this.replaceJob(sessionId, cancelled);
|
|
775
|
-
this.cancellationRequests.delete(job.jobId);
|
|
776
|
-
this.eventService.emit({
|
|
777
|
-
kind: "job.cancelled",
|
|
778
|
-
sessionId,
|
|
779
|
-
canonicalPathKey: session?.canonicalPathKey ?? job.canonicalPathKey,
|
|
780
|
-
revision: session?.revision ?? job.revision,
|
|
781
|
-
jobId: job.jobId
|
|
782
|
-
});
|
|
783
|
-
return cancelled;
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
//#endregion
|
|
787
|
-
//#region src/services/read-service.ts
|
|
788
|
-
var ReadService = class {
|
|
789
|
-
constructor(context) {
|
|
790
|
-
this.context = context;
|
|
791
|
-
}
|
|
792
|
-
getCapabilities() {
|
|
793
|
-
return success("read", Date.now(), cloneCapabilitiesSnapshot(this.context.capabilities));
|
|
794
|
-
}
|
|
795
|
-
getSession(input) {
|
|
796
|
-
const startedAt = Date.now();
|
|
797
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
798
|
-
if (!session || session.closed) return failure("read", startedAt, createSessionNotFoundError(input.sessionId));
|
|
799
|
-
return success("read", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
800
|
-
sessionId: session.sessionId,
|
|
801
|
-
revision: session.revision
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
};
|
|
805
|
-
//#endregion
|
|
806
|
-
//#region src/services/runtime-service.ts
|
|
807
|
-
function randomId() {
|
|
808
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
809
|
-
}
|
|
810
|
-
var RuntimeService = class {
|
|
811
|
-
constructor(context, cacheService, eventService, jobService) {
|
|
812
|
-
this.context = context;
|
|
813
|
-
this.cacheService = cacheService;
|
|
814
|
-
this.eventService = eventService;
|
|
815
|
-
this.jobService = jobService;
|
|
816
|
-
}
|
|
817
|
-
async openSession(input) {
|
|
818
|
-
const startedAt = Date.now();
|
|
819
|
-
const { fairyPath, canonicalProjectPath, canonicalPathKey } = await resolveCanonicalProjectRoot(this.context.fileSystem, input.projectPath);
|
|
820
|
-
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
821
|
-
const lockFilePath = this.context.fileSystem.join(canonicalProjectPath, ".openfairygui.backend.lock");
|
|
822
|
-
if (existingSessionId) return failure("runtime", startedAt, {
|
|
823
|
-
code: "lock_conflict",
|
|
824
|
-
kind: "in_process_session_exists",
|
|
825
|
-
message: `Project is already open in this backend runtime: ${canonicalProjectPath}`,
|
|
826
|
-
canonicalPathKey,
|
|
827
|
-
holderSessionId: existingSessionId,
|
|
828
|
-
lockFilePath
|
|
829
|
-
});
|
|
830
|
-
let advisoryLock = null;
|
|
831
|
-
try {
|
|
832
|
-
advisoryLock = await this.context.fileSystem.openExclusive(lockFilePath);
|
|
833
|
-
await advisoryLock.writeFile(JSON.stringify({
|
|
834
|
-
pid: process.pid,
|
|
835
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
836
|
-
canonicalPathKey
|
|
837
|
-
}));
|
|
838
|
-
await advisoryLock.close();
|
|
839
|
-
const project = await (0, _openfairygui_core.readProjectAsUam)(new _openfairygui_core.NodeIO(), fairyPath);
|
|
840
|
-
const sessionId = randomId();
|
|
841
|
-
const session = {
|
|
842
|
-
sessionId,
|
|
843
|
-
fairyPath,
|
|
844
|
-
canonicalProjectPath,
|
|
845
|
-
canonicalPathKey,
|
|
846
|
-
lockFilePath,
|
|
847
|
-
project,
|
|
848
|
-
revision: 0,
|
|
849
|
-
lastSavedRevision: 0,
|
|
850
|
-
dirty: false,
|
|
851
|
-
lockHeld: true,
|
|
852
|
-
closed: false
|
|
853
|
-
};
|
|
854
|
-
this.context.sessions.set(sessionId, session);
|
|
855
|
-
this.context.sessionsByPath.set(canonicalPathKey, sessionId);
|
|
856
|
-
this.cacheService.refreshSession(session);
|
|
857
|
-
this.eventService.emit({
|
|
858
|
-
kind: "session.opened",
|
|
859
|
-
sessionId,
|
|
860
|
-
canonicalPathKey,
|
|
861
|
-
revision: session.revision
|
|
862
|
-
});
|
|
863
|
-
return success("runtime", startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
864
|
-
sessionId: session.sessionId,
|
|
865
|
-
revision: session.revision
|
|
866
|
-
});
|
|
867
|
-
} catch (error) {
|
|
868
|
-
if (error?.code === "EEXIST") return failure("runtime", startedAt, {
|
|
869
|
-
code: "lock_conflict",
|
|
870
|
-
kind: "advisory_lock_conflict",
|
|
871
|
-
message: `Advisory lock already exists for project: ${canonicalProjectPath}`,
|
|
872
|
-
canonicalPathKey,
|
|
873
|
-
lockFilePath
|
|
874
|
-
});
|
|
875
|
-
if (advisoryLock) {
|
|
876
|
-
await advisoryLock.close().catch(() => void 0);
|
|
877
|
-
await this.context.fileSystem.unlink(lockFilePath).catch(() => void 0);
|
|
878
|
-
}
|
|
879
|
-
throw error;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
async closeSession(input) {
|
|
883
|
-
const startedAt = Date.now();
|
|
884
|
-
const session = this.context.sessions.get(input.sessionId);
|
|
885
|
-
if (!session || session.closed) return failure("runtime", startedAt, createSessionNotFoundError(input.sessionId));
|
|
886
|
-
this.eventService.emit({
|
|
887
|
-
kind: "session.closeRequested",
|
|
888
|
-
sessionId: session.sessionId,
|
|
889
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
890
|
-
revision: session.revision
|
|
891
|
-
});
|
|
892
|
-
await this.context.fileSystem.unlink(session.lockFilePath).catch(() => void 0);
|
|
893
|
-
session.lockHeld = false;
|
|
894
|
-
session.closed = true;
|
|
895
|
-
this.context.sessions.delete(session.sessionId);
|
|
896
|
-
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
897
|
-
this.cacheService.removeSession(session.sessionId);
|
|
898
|
-
this.jobService.removeSession(session.sessionId);
|
|
899
|
-
this.eventService.emit({
|
|
900
|
-
kind: "session.closed",
|
|
901
|
-
sessionId: session.sessionId,
|
|
902
|
-
canonicalPathKey: session.canonicalPathKey,
|
|
903
|
-
revision: session.revision
|
|
904
|
-
});
|
|
905
|
-
this.eventService.removeSession(session.sessionId);
|
|
906
|
-
return success("runtime", startedAt, {
|
|
907
|
-
sessionId: session.sessionId,
|
|
908
|
-
closed: true
|
|
909
|
-
}, {
|
|
910
|
-
sessionId: session.sessionId,
|
|
911
|
-
revision: session.revision
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
};
|
|
915
|
-
//#endregion
|
|
916
|
-
//#region src/runtime.ts
|
|
917
|
-
const BACKEND_METHODS = [
|
|
918
|
-
"getCapabilities",
|
|
919
|
-
"openSession",
|
|
920
|
-
"getSession",
|
|
921
|
-
"applyTransaction",
|
|
922
|
-
"saveSession",
|
|
923
|
-
"closeSession",
|
|
924
|
-
"getEvents",
|
|
925
|
-
"getJob",
|
|
926
|
-
"listJobs",
|
|
927
|
-
"cancelJob",
|
|
928
|
-
"getCacheSnapshot",
|
|
929
|
-
"refreshCache"
|
|
930
|
-
];
|
|
931
|
-
function createCapabilities() {
|
|
932
|
-
return {
|
|
933
|
-
contractVersion: BACKEND_CONTRACT_VERSION,
|
|
934
|
-
capabilitySchemaVersion: 2,
|
|
935
|
-
transactionKernelOwner: "@openfairygui/core",
|
|
936
|
-
appSeamOwner: "@openfairygui/functions",
|
|
937
|
-
runtimeOwner: "@openfairygui/backend",
|
|
938
|
-
methods: BACKEND_METHODS,
|
|
939
|
-
read: {
|
|
940
|
-
capabilitySnapshot: true,
|
|
941
|
-
sessionSnapshot: true
|
|
942
|
-
},
|
|
943
|
-
authoring: {
|
|
944
|
-
applyTransaction: true,
|
|
945
|
-
saveSession: true,
|
|
946
|
-
resourceKinds: [..._openfairygui_core.UAM_SUPPORTED_MATERIALIZATION_SCOPE.resourceKinds],
|
|
947
|
-
nodeKinds: [..._openfairygui_core.UAM_SUPPORTED_MATERIALIZATION_SCOPE.nodeKinds],
|
|
948
|
-
gearKinds: [..._openfairygui_core.UAM_SUPPORTED_MATERIALIZATION_SCOPE.gearKinds],
|
|
949
|
-
unsupported: ["artifact.publish", "artifact.restore"]
|
|
950
|
-
},
|
|
951
|
-
artifact: createArtifactCapabilities(),
|
|
952
|
-
compatibilityPolicy: BACKEND_COMPATIBILITY_POLICY,
|
|
953
|
-
runtime: {
|
|
954
|
-
sessionRuntime: true,
|
|
955
|
-
advisoryLocking: true,
|
|
956
|
-
coordinatedSave: true,
|
|
957
|
-
atomicSave: false,
|
|
958
|
-
staleRevisionProtection: true,
|
|
959
|
-
pathPolicy: createRuntimePathPolicy(),
|
|
960
|
-
events: {
|
|
961
|
-
polling: true,
|
|
962
|
-
subscriptions: false,
|
|
963
|
-
retentionLimit: 1e3,
|
|
964
|
-
sequenceScope: "runtime"
|
|
965
|
-
},
|
|
966
|
-
jobs: {
|
|
967
|
-
inMemory: true,
|
|
968
|
-
cooperativeCancel: true,
|
|
969
|
-
persistent: false,
|
|
970
|
-
supportedKinds: ["cache.refresh"],
|
|
971
|
-
artifactJobs: false,
|
|
972
|
-
completedRetentionLimit: 100
|
|
973
|
-
},
|
|
974
|
-
cache: {
|
|
975
|
-
derivedReadOnly: true,
|
|
976
|
-
keyedBy: "canonicalPathKey",
|
|
977
|
-
sourceOfTruth: false,
|
|
978
|
-
refreshMethod: "refreshCache"
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
function createNodeBackendFileSystem() {
|
|
984
|
-
return {
|
|
73
|
+
function createBackendStorageFileSystem(storage) {
|
|
74
|
+
const lockedPaths = /* @__PURE__ */ new Set();
|
|
75
|
+
const fileSystem = {
|
|
985
76
|
stat(filePath) {
|
|
986
|
-
return
|
|
77
|
+
return inferStat(storage, fileSystem.resolve(filePath));
|
|
987
78
|
},
|
|
988
79
|
readdir(dirPath) {
|
|
989
|
-
return
|
|
80
|
+
return storage.readdir(fileSystem.resolve(dirPath));
|
|
990
81
|
},
|
|
991
82
|
readFile(filePath) {
|
|
992
|
-
return
|
|
83
|
+
return storage.readFile(fileSystem.resolve(filePath));
|
|
993
84
|
},
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
85
|
+
readFileRaw(filePath) {
|
|
86
|
+
return storage.readFileRaw(fileSystem.resolve(filePath));
|
|
997
87
|
},
|
|
998
88
|
writeFile(filePath, content) {
|
|
999
|
-
return
|
|
89
|
+
return storage.writeFile(fileSystem.resolve(filePath), content);
|
|
1000
90
|
},
|
|
1001
91
|
writeFileRaw(filePath, data) {
|
|
1002
|
-
return
|
|
92
|
+
return storage.writeFileRaw(fileSystem.resolve(filePath), data);
|
|
1003
93
|
},
|
|
1004
|
-
|
|
1005
|
-
|
|
94
|
+
mkdir(dirPath, options) {
|
|
95
|
+
return storage.mkdir(fileSystem.resolve(dirPath), options);
|
|
1006
96
|
},
|
|
1007
|
-
async
|
|
97
|
+
async exists(filePath) {
|
|
98
|
+
if (storage.exists) return storage.exists(fileSystem.resolve(filePath));
|
|
1008
99
|
try {
|
|
1009
|
-
|
|
100
|
+
await fileSystem.stat(filePath);
|
|
101
|
+
return true;
|
|
1010
102
|
} catch {
|
|
1011
|
-
return
|
|
103
|
+
return false;
|
|
1012
104
|
}
|
|
1013
105
|
},
|
|
106
|
+
resolvePath(filePath) {
|
|
107
|
+
const resolved = fileSystem.resolve(filePath);
|
|
108
|
+
return storage.resolvePath ? storage.resolvePath(resolved) : Promise.resolve(resolved);
|
|
109
|
+
},
|
|
1014
110
|
async openExclusive(filePath) {
|
|
1015
|
-
const
|
|
111
|
+
const resolved = fileSystem.resolve(filePath);
|
|
112
|
+
if (storage.openExclusive) return storage.openExclusive(resolved);
|
|
113
|
+
if (lockedPaths.has(resolved) || await fileSystem.exists(resolved)) throw createPathError("EEXIST", `Storage path already exists: ${resolved}`);
|
|
114
|
+
lockedPaths.add(resolved);
|
|
115
|
+
let closed = false;
|
|
1016
116
|
return {
|
|
1017
|
-
writeFile(content) {
|
|
1018
|
-
|
|
117
|
+
async writeFile(content) {
|
|
118
|
+
if (closed) throw createPathError("EBADF", `Storage lock handle is closed: ${resolved}`);
|
|
119
|
+
await fileSystem.mkdir(fileSystem.dirname(resolved), { recursive: true });
|
|
120
|
+
await storage.writeFile(resolved, content);
|
|
1019
121
|
},
|
|
1020
|
-
close() {
|
|
1021
|
-
|
|
122
|
+
async close() {
|
|
123
|
+
closed = true;
|
|
124
|
+
lockedPaths.delete(resolved);
|
|
1022
125
|
}
|
|
1023
126
|
};
|
|
1024
127
|
},
|
|
1025
128
|
unlink(filePath) {
|
|
1026
|
-
|
|
129
|
+
const resolved = fileSystem.resolve(filePath);
|
|
130
|
+
lockedPaths.delete(resolved);
|
|
131
|
+
if (storage.unlink) return storage.unlink(resolved);
|
|
132
|
+
throw createPathError("ENOTSUP", "Storage adapter does not provide unlink().");
|
|
1027
133
|
},
|
|
1028
134
|
join(...paths) {
|
|
1029
|
-
return
|
|
135
|
+
return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
|
|
1030
136
|
},
|
|
1031
137
|
dirname(filePath) {
|
|
1032
|
-
return
|
|
138
|
+
return storage.dirname ? storage.dirname(filePath) : dirnameStoragePath(filePath);
|
|
1033
139
|
},
|
|
1034
140
|
resolve(...paths) {
|
|
1035
|
-
return
|
|
141
|
+
return storage.resolve ? storage.resolve(...paths) : joinStoragePath(...paths);
|
|
1036
142
|
}
|
|
1037
143
|
};
|
|
144
|
+
return fileSystem;
|
|
1038
145
|
}
|
|
1039
|
-
var BackendRuntime = class {
|
|
1040
|
-
fileSystem;
|
|
1041
|
-
capabilities;
|
|
1042
|
-
sessions = /* @__PURE__ */ new Map();
|
|
1043
|
-
sessionsByPath = /* @__PURE__ */ new Map();
|
|
1044
|
-
eventsBySession = /* @__PURE__ */ new Map();
|
|
1045
|
-
jobsBySession = /* @__PURE__ */ new Map();
|
|
1046
|
-
cacheBySession = /* @__PURE__ */ new Map();
|
|
1047
|
-
eventSequence = 0;
|
|
1048
|
-
context;
|
|
1049
|
-
readService;
|
|
1050
|
-
runtimeService;
|
|
1051
|
-
authoringService;
|
|
1052
|
-
cacheService;
|
|
1053
|
-
eventService;
|
|
1054
|
-
jobService;
|
|
1055
|
-
constructor(options = {}) {
|
|
1056
|
-
this.fileSystem = options.fileSystem ?? createNodeBackendFileSystem();
|
|
1057
|
-
this.capabilities = createCapabilities();
|
|
1058
|
-
this.context = {
|
|
1059
|
-
fileSystem: this.fileSystem,
|
|
1060
|
-
capabilities: this.capabilities,
|
|
1061
|
-
sessions: this.sessions,
|
|
1062
|
-
sessionsByPath: this.sessionsByPath,
|
|
1063
|
-
eventsBySession: this.eventsBySession,
|
|
1064
|
-
jobsBySession: this.jobsBySession,
|
|
1065
|
-
cacheBySession: this.cacheBySession,
|
|
1066
|
-
nextEventSequence: () => {
|
|
1067
|
-
this.eventSequence += 1;
|
|
1068
|
-
return this.eventSequence;
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
this.readService = new ReadService(this.context);
|
|
1072
|
-
this.eventService = new EventService(this.context);
|
|
1073
|
-
this.cacheService = new CacheService(this.context);
|
|
1074
|
-
this.jobService = new JobService(this.context, this.cacheService, this.eventService);
|
|
1075
|
-
this.runtimeService = new RuntimeService(this.context, this.cacheService, this.eventService, this.jobService);
|
|
1076
|
-
this.authoringService = new AuthoringService(this.context, this.cacheService, this.eventService);
|
|
1077
|
-
}
|
|
1078
|
-
getCapabilities() {
|
|
1079
|
-
return this.readService.getCapabilities();
|
|
1080
|
-
}
|
|
1081
|
-
async openSession(input) {
|
|
1082
|
-
return this.runtimeService.openSession(input);
|
|
1083
|
-
}
|
|
1084
|
-
getSession(input) {
|
|
1085
|
-
return this.readService.getSession(input);
|
|
1086
|
-
}
|
|
1087
|
-
async applyTransaction(input) {
|
|
1088
|
-
return this.authoringService.applyTransaction(input);
|
|
1089
|
-
}
|
|
1090
|
-
async saveSession(input) {
|
|
1091
|
-
return this.authoringService.saveSession(input);
|
|
1092
|
-
}
|
|
1093
|
-
async closeSession(input) {
|
|
1094
|
-
return this.runtimeService.closeSession(input);
|
|
1095
|
-
}
|
|
1096
|
-
getEvents(input) {
|
|
1097
|
-
return this.eventService.getEvents(input);
|
|
1098
|
-
}
|
|
1099
|
-
getJob(input) {
|
|
1100
|
-
return this.jobService.getJob(input);
|
|
1101
|
-
}
|
|
1102
|
-
listJobs(input) {
|
|
1103
|
-
return this.jobService.listJobs(input);
|
|
1104
|
-
}
|
|
1105
|
-
cancelJob(input) {
|
|
1106
|
-
return this.jobService.cancelJob(input);
|
|
1107
|
-
}
|
|
1108
|
-
getCacheSnapshot(input) {
|
|
1109
|
-
return this.cacheService.getCacheSnapshot(input);
|
|
1110
|
-
}
|
|
1111
|
-
refreshCache(input) {
|
|
1112
|
-
return this.jobService.refreshCache(input);
|
|
1113
|
-
}
|
|
1114
|
-
};
|
|
1115
146
|
//#endregion
|
|
1116
|
-
exports.BACKEND_CAPABILITY_SCHEMA_VERSION = BACKEND_CAPABILITY_SCHEMA_VERSION;
|
|
1117
|
-
exports.BACKEND_COMPATIBILITY_POLICY = BACKEND_COMPATIBILITY_POLICY;
|
|
1118
|
-
exports.BACKEND_CONTRACT_VERSION = BACKEND_CONTRACT_VERSION;
|
|
1119
|
-
exports.BackendRuntime = BackendRuntime;
|
|
1120
|
-
exports.
|
|
147
|
+
exports.BACKEND_CAPABILITY_SCHEMA_VERSION = require_runtime.BACKEND_CAPABILITY_SCHEMA_VERSION;
|
|
148
|
+
exports.BACKEND_COMPATIBILITY_POLICY = require_runtime.BACKEND_COMPATIBILITY_POLICY;
|
|
149
|
+
exports.BACKEND_CONTRACT_VERSION = require_runtime.BACKEND_CONTRACT_VERSION;
|
|
150
|
+
exports.BackendRuntime = require_runtime.BackendRuntime;
|
|
151
|
+
exports.createBackendStorageFileSystem = createBackendStorageFileSystem;
|