@openfairygui/backend 0.2.0-alpha.13 → 0.2.0-alpha.14
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 +6 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/node.cjs +1 -1
- package/dist/node.mjs +1 -1
- package/dist/{runtime-DFatY9W0.mjs → runtime-B9z0n2yF.mjs} +52 -5
- package/dist/{runtime-GKzsXJdO.cjs → runtime-DqApG30C.cjs} +51 -4
- package/package.json +3 -3
- package/src/services/authoring-service.ts +57 -5
- package/src/services/context.ts +2 -0
- package/src/services/runtime-service.ts +3 -1
- package/src/storage.ts +5 -3
package/README.md
CHANGED
|
@@ -34,7 +34,11 @@ It does **not** redefine transaction grammar or expose `Document`.
|
|
|
34
34
|
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
|
-
or File System Access API bridges.
|
|
37
|
+
or File System Access API bridges. Storage adapters must implement `unlink()` so resource rename/move/remove
|
|
38
|
+
can clean up stale source files. The default Node filesystem/runtime lives under `@openfairygui/backend/node`.
|
|
39
|
+
File-backed `openSession` hydrates primary resource bytes so browser-safe transactions can rename/move
|
|
40
|
+
assets or add/replace/remove binary resources. `saveSession` writes replacement bytes before it removes
|
|
41
|
+
stale source files, preserving the prior file when a write fails.
|
|
38
42
|
|
|
39
43
|
## Relationship to other packages
|
|
40
44
|
|
|
@@ -73,6 +77,7 @@ const fileSystem = createBackendStorageFileSystem({
|
|
|
73
77
|
async mkdir(dirPath) { await storage.mkdir(dirPath); },
|
|
74
78
|
async readdir(dirPath) { return storage.readdir(dirPath); },
|
|
75
79
|
async exists(filePath) { return storage.exists(filePath); },
|
|
80
|
+
async unlink(filePath) { await storage.remove(filePath); },
|
|
76
81
|
});
|
|
77
82
|
|
|
78
83
|
const runtime = new BackendRuntime();
|
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-DqApG30C.cjs");
|
|
3
3
|
//#region src/storage.ts
|
|
4
4
|
var StorageFileStat = class {
|
|
5
5
|
constructor(kind) {
|
|
@@ -71,6 +71,7 @@ async function inferStat(storage, filePath) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
function createBackendStorageFileSystem(storage) {
|
|
74
|
+
if (typeof storage.unlink !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide unlink() for project resource lifecycle writes.");
|
|
74
75
|
const lockedPaths = /* @__PURE__ */ new Set();
|
|
75
76
|
const fileSystem = {
|
|
76
77
|
stat(filePath) {
|
|
@@ -128,8 +129,7 @@ function createBackendStorageFileSystem(storage) {
|
|
|
128
129
|
unlink(filePath) {
|
|
129
130
|
const resolved = fileSystem.resolve(filePath);
|
|
130
131
|
lockedPaths.delete(resolved);
|
|
131
|
-
|
|
132
|
-
throw createPathError("ENOTSUP", "Storage adapter does not provide unlink().");
|
|
132
|
+
return storage.unlink(resolved);
|
|
133
133
|
},
|
|
134
134
|
join(...paths) {
|
|
135
135
|
return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
|
package/dist/index.d.cts
CHANGED
|
@@ -22,7 +22,7 @@ interface BackendAsyncStorageAdapter {
|
|
|
22
22
|
stat?(filePath: string): Promise<BackendStorageStatLike>;
|
|
23
23
|
resolvePath?(filePath: string): Promise<string>;
|
|
24
24
|
openExclusive?(filePath: string): Promise<BackendFileHandle>;
|
|
25
|
-
unlink
|
|
25
|
+
unlink(filePath: string): Promise<void>;
|
|
26
26
|
join?(...paths: string[]): string;
|
|
27
27
|
dirname?(filePath: string): string;
|
|
28
28
|
resolve?(...paths: string[]): string;
|
package/dist/index.d.mts
CHANGED
|
@@ -22,7 +22,7 @@ interface BackendAsyncStorageAdapter {
|
|
|
22
22
|
stat?(filePath: string): Promise<BackendStorageStatLike>;
|
|
23
23
|
resolvePath?(filePath: string): Promise<string>;
|
|
24
24
|
openExclusive?(filePath: string): Promise<BackendFileHandle>;
|
|
25
|
-
unlink
|
|
25
|
+
unlink(filePath: string): Promise<void>;
|
|
26
26
|
join?(...paths: string[]): string;
|
|
27
27
|
dirname?(filePath: string): string;
|
|
28
28
|
resolve?(...paths: string[]): string;
|
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-B9z0n2yF.mjs";
|
|
2
2
|
//#region src/storage.ts
|
|
3
3
|
var StorageFileStat = class {
|
|
4
4
|
constructor(kind) {
|
|
@@ -70,6 +70,7 @@ async function inferStat(storage, filePath) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
function createBackendStorageFileSystem(storage) {
|
|
73
|
+
if (typeof storage.unlink !== "function") throw createPathError("ENOTSUP", "Storage adapter must provide unlink() for project resource lifecycle writes.");
|
|
73
74
|
const lockedPaths = /* @__PURE__ */ new Set();
|
|
74
75
|
const fileSystem = {
|
|
75
76
|
stat(filePath) {
|
|
@@ -127,8 +128,7 @@ function createBackendStorageFileSystem(storage) {
|
|
|
127
128
|
unlink(filePath) {
|
|
128
129
|
const resolved = fileSystem.resolve(filePath);
|
|
129
130
|
lockedPaths.delete(resolved);
|
|
130
|
-
|
|
131
|
-
throw createPathError("ENOTSUP", "Storage adapter does not provide unlink().");
|
|
131
|
+
return storage.unlink(resolved);
|
|
132
132
|
},
|
|
133
133
|
join(...paths) {
|
|
134
134
|
return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
|
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-DqApG30C.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");
|
package/dist/node.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, liftDocumentToUamProject, materializeUamProject, normalizeUamProject, validateUamProject } from "@openfairygui/core/uam";
|
|
1
|
+
import { UAM_SUPPORTED_MATERIALIZATION_SCOPE, UAM_SUPPORTED_TRANSACTION_SCOPE, commitUamProjectSourcePaths, liftDocumentToUamProject, materializeUamProject, normalizeUamProject, validateUamProject } from "@openfairygui/core/uam";
|
|
2
2
|
import { ProjectReader, ProjectWriter } from "@openfairygui/core/project-io";
|
|
3
3
|
import { applyUamTransactionApp } from "@openfairygui/functions/uam";
|
|
4
4
|
//#region src/contracts.ts
|
|
@@ -251,9 +251,45 @@ function createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
|
|
|
251
251
|
},
|
|
252
252
|
dirname(filePath) {
|
|
253
253
|
return fileSystem.dirname(filePath);
|
|
254
|
+
},
|
|
255
|
+
async unlink(filePath) {
|
|
256
|
+
await trackWrite(filePath, () => fileSystem.unlink(filePath));
|
|
254
257
|
}
|
|
255
258
|
};
|
|
256
259
|
}
|
|
260
|
+
function projectAssetSourceFiles(project) {
|
|
261
|
+
const result = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const pkg of project.packages) for (const resource of pkg.resources) {
|
|
263
|
+
if (resource.kind === "component") continue;
|
|
264
|
+
const fileName = resource.fileName ?? resource.file ?? "";
|
|
265
|
+
if (!fileName) continue;
|
|
266
|
+
result.set(`${pkg.id}/${resource.id}`, {
|
|
267
|
+
packageName: pkg.name,
|
|
268
|
+
branch: resource.branch,
|
|
269
|
+
path: resource.path,
|
|
270
|
+
fileName
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
function sourceFileKey(source) {
|
|
276
|
+
return [
|
|
277
|
+
source.branch,
|
|
278
|
+
source.packageName,
|
|
279
|
+
source.path,
|
|
280
|
+
source.fileName
|
|
281
|
+
].join("\0");
|
|
282
|
+
}
|
|
283
|
+
function recordStaleResourceSources(session, previousProject, nextProject) {
|
|
284
|
+
if (!session.fileSystem) return;
|
|
285
|
+
const previousSources = projectAssetSourceFiles(previousProject);
|
|
286
|
+
const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
|
|
287
|
+
for (const source of previousSources.values()) {
|
|
288
|
+
const key = sourceFileKey(source);
|
|
289
|
+
if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
|
|
290
|
+
}
|
|
291
|
+
for (const key of nextSourceKeys) session.pendingStaleSourceFiles.delete(key);
|
|
292
|
+
}
|
|
257
293
|
function toBackendDiagnostics(error) {
|
|
258
294
|
return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
|
|
259
295
|
code: error.code,
|
|
@@ -343,6 +379,7 @@ var AuthoringService = class {
|
|
|
343
379
|
diagnostics
|
|
344
380
|
});
|
|
345
381
|
}
|
|
382
|
+
recordStaleResourceSources(session, session.project, result.project);
|
|
346
383
|
session.project = result.project;
|
|
347
384
|
session.revision += 1;
|
|
348
385
|
session.dirty = true;
|
|
@@ -377,7 +414,7 @@ var AuthoringService = class {
|
|
|
377
414
|
const startedAt = Date.now();
|
|
378
415
|
const session = this.context.sessions.get(input.sessionId);
|
|
379
416
|
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
380
|
-
const fileSystem =
|
|
417
|
+
const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
|
|
381
418
|
if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
|
|
382
419
|
sessionId: session.sessionId,
|
|
383
420
|
revision: session.revision
|
|
@@ -404,7 +441,10 @@ var AuthoringService = class {
|
|
|
404
441
|
revision: session.revision
|
|
405
442
|
});
|
|
406
443
|
try {
|
|
407
|
-
await new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write(materializeUamProject(session.project), session.fairyPath);
|
|
444
|
+
await new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write(materializeUamProject(session.project), session.fairyPath, { staleSourceFiles: [...session.pendingStaleSourceFiles.values()] });
|
|
445
|
+
session.fileSystem ??= fileSystem;
|
|
446
|
+
session.pendingStaleSourceFiles.clear();
|
|
447
|
+
commitUamProjectSourcePaths(session.project);
|
|
408
448
|
session.lastSavedRevision = session.revision;
|
|
409
449
|
session.dirty = false;
|
|
410
450
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
@@ -528,7 +568,12 @@ var AuthoringService = class {
|
|
|
528
568
|
revision: session.revision
|
|
529
569
|
});
|
|
530
570
|
try {
|
|
531
|
-
|
|
571
|
+
const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
|
|
572
|
+
const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
|
|
573
|
+
await writer.write(document, fairyPath, { staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [] });
|
|
574
|
+
if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
575
|
+
if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
576
|
+
if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
|
|
532
577
|
if (storageTarget) {
|
|
533
578
|
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
534
579
|
session.fileSystem = storageTarget.fileSystem;
|
|
@@ -1089,7 +1134,7 @@ var RuntimeService = class {
|
|
|
1089
1134
|
canonicalPathKey
|
|
1090
1135
|
}));
|
|
1091
1136
|
await advisoryLock.close();
|
|
1092
|
-
const project = liftDocumentToUamProject(await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath));
|
|
1137
|
+
const project = liftDocumentToUamProject(await new ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true }));
|
|
1093
1138
|
const sessionId = randomId();
|
|
1094
1139
|
const session = {
|
|
1095
1140
|
sessionId,
|
|
@@ -1101,6 +1146,7 @@ var RuntimeService = class {
|
|
|
1101
1146
|
project,
|
|
1102
1147
|
revision: 0,
|
|
1103
1148
|
lastSavedRevision: 0,
|
|
1149
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
1104
1150
|
dirty: false,
|
|
1105
1151
|
lockHeld: true,
|
|
1106
1152
|
closed: false
|
|
@@ -1158,6 +1204,7 @@ var RuntimeService = class {
|
|
|
1158
1204
|
project: normalizeUamProject(input.project),
|
|
1159
1205
|
revision: 0,
|
|
1160
1206
|
lastSavedRevision: 0,
|
|
1207
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
1161
1208
|
dirty: false,
|
|
1162
1209
|
lockHeld: false,
|
|
1163
1210
|
closed: false
|
|
@@ -252,9 +252,45 @@ function createWriterFileSystem(fileSystem, committedPaths, failedPaths) {
|
|
|
252
252
|
},
|
|
253
253
|
dirname(filePath) {
|
|
254
254
|
return fileSystem.dirname(filePath);
|
|
255
|
+
},
|
|
256
|
+
async unlink(filePath) {
|
|
257
|
+
await trackWrite(filePath, () => fileSystem.unlink(filePath));
|
|
255
258
|
}
|
|
256
259
|
};
|
|
257
260
|
}
|
|
261
|
+
function projectAssetSourceFiles(project) {
|
|
262
|
+
const result = /* @__PURE__ */ new Map();
|
|
263
|
+
for (const pkg of project.packages) for (const resource of pkg.resources) {
|
|
264
|
+
if (resource.kind === "component") continue;
|
|
265
|
+
const fileName = resource.fileName ?? resource.file ?? "";
|
|
266
|
+
if (!fileName) continue;
|
|
267
|
+
result.set(`${pkg.id}/${resource.id}`, {
|
|
268
|
+
packageName: pkg.name,
|
|
269
|
+
branch: resource.branch,
|
|
270
|
+
path: resource.path,
|
|
271
|
+
fileName
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
function sourceFileKey(source) {
|
|
277
|
+
return [
|
|
278
|
+
source.branch,
|
|
279
|
+
source.packageName,
|
|
280
|
+
source.path,
|
|
281
|
+
source.fileName
|
|
282
|
+
].join("\0");
|
|
283
|
+
}
|
|
284
|
+
function recordStaleResourceSources(session, previousProject, nextProject) {
|
|
285
|
+
if (!session.fileSystem) return;
|
|
286
|
+
const previousSources = projectAssetSourceFiles(previousProject);
|
|
287
|
+
const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
|
|
288
|
+
for (const source of previousSources.values()) {
|
|
289
|
+
const key = sourceFileKey(source);
|
|
290
|
+
if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
|
|
291
|
+
}
|
|
292
|
+
for (const key of nextSourceKeys) session.pendingStaleSourceFiles.delete(key);
|
|
293
|
+
}
|
|
258
294
|
function toBackendDiagnostics(error) {
|
|
259
295
|
return error.diagnostics.length > 0 ? error.diagnostics.map((diagnostic) => ({ ...diagnostic })) : [{
|
|
260
296
|
code: error.code,
|
|
@@ -344,6 +380,7 @@ var AuthoringService = class {
|
|
|
344
380
|
diagnostics
|
|
345
381
|
});
|
|
346
382
|
}
|
|
383
|
+
recordStaleResourceSources(session, session.project, result.project);
|
|
347
384
|
session.project = result.project;
|
|
348
385
|
session.revision += 1;
|
|
349
386
|
session.dirty = true;
|
|
@@ -378,7 +415,7 @@ var AuthoringService = class {
|
|
|
378
415
|
const startedAt = Date.now();
|
|
379
416
|
const session = this.context.sessions.get(input.sessionId);
|
|
380
417
|
if (!session || session.closed) return failure("authoring", startedAt, createSessionNotFoundError(input.sessionId));
|
|
381
|
-
const fileSystem =
|
|
418
|
+
const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
|
|
382
419
|
if (!fileSystem) return failure("authoring", startedAt, createCapabilityUnavailableError$1("saveSession requires an injected BackendFileSystem adapter."), toSessionSnapshot(session, this.context.capabilities), {
|
|
383
420
|
sessionId: session.sessionId,
|
|
384
421
|
revision: session.revision
|
|
@@ -405,7 +442,10 @@ var AuthoringService = class {
|
|
|
405
442
|
revision: session.revision
|
|
406
443
|
});
|
|
407
444
|
try {
|
|
408
|
-
await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write((0, _openfairygui_core_uam.materializeUamProject)(session.project), session.fairyPath);
|
|
445
|
+
await new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths)).write((0, _openfairygui_core_uam.materializeUamProject)(session.project), session.fairyPath, { staleSourceFiles: [...session.pendingStaleSourceFiles.values()] });
|
|
446
|
+
session.fileSystem ??= fileSystem;
|
|
447
|
+
session.pendingStaleSourceFiles.clear();
|
|
448
|
+
(0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
|
|
409
449
|
session.lastSavedRevision = session.revision;
|
|
410
450
|
session.dirty = false;
|
|
411
451
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
@@ -529,7 +569,12 @@ var AuthoringService = class {
|
|
|
529
569
|
revision: session.revision
|
|
530
570
|
});
|
|
531
571
|
try {
|
|
532
|
-
|
|
572
|
+
const writer = new _openfairygui_core_project_io.ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
|
|
573
|
+
const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
|
|
574
|
+
await writer.write(document, fairyPath, { staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [] });
|
|
575
|
+
if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
576
|
+
if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
577
|
+
if (isSessionStorageTarget || storageTarget) (0, _openfairygui_core_uam.commitUamProjectSourcePaths)(session.project);
|
|
533
578
|
if (storageTarget) {
|
|
534
579
|
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
535
580
|
session.fileSystem = storageTarget.fileSystem;
|
|
@@ -1090,7 +1135,7 @@ var RuntimeService = class {
|
|
|
1090
1135
|
canonicalPathKey
|
|
1091
1136
|
}));
|
|
1092
1137
|
await advisoryLock.close();
|
|
1093
|
-
const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath));
|
|
1138
|
+
const project = (0, _openfairygui_core_uam.liftDocumentToUamProject)(await new _openfairygui_core_project_io.ProjectReader(createProjectReaderFileSystem(fileSystem)).read(fairyPath, { hydrateResourceBytes: true }));
|
|
1094
1139
|
const sessionId = randomId();
|
|
1095
1140
|
const session = {
|
|
1096
1141
|
sessionId,
|
|
@@ -1102,6 +1147,7 @@ var RuntimeService = class {
|
|
|
1102
1147
|
project,
|
|
1103
1148
|
revision: 0,
|
|
1104
1149
|
lastSavedRevision: 0,
|
|
1150
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
1105
1151
|
dirty: false,
|
|
1106
1152
|
lockHeld: true,
|
|
1107
1153
|
closed: false
|
|
@@ -1159,6 +1205,7 @@ var RuntimeService = class {
|
|
|
1159
1205
|
project: (0, _openfairygui_core_uam.normalizeUamProject)(input.project),
|
|
1160
1206
|
revision: 0,
|
|
1161
1207
|
lastSavedRevision: 0,
|
|
1208
|
+
pendingStaleSourceFiles: /* @__PURE__ */ new Map(),
|
|
1162
1209
|
dirty: false,
|
|
1163
1210
|
lockHeld: false,
|
|
1164
1211
|
closed: false
|
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.14",
|
|
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/
|
|
56
|
-
"@openfairygui/
|
|
55
|
+
"@openfairygui/functions": "0.2.0-alpha.14",
|
|
56
|
+
"@openfairygui/core": "0.2.0-alpha.14"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"ava": "^7.0.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ProjectWriter, type FileSystem } from '@openfairygui/core/project-io';
|
|
2
|
-
import { materializeUamProject, validateUamProject } from '@openfairygui/core/uam';
|
|
1
|
+
import { ProjectWriter, type FileSystem, type ProjectSourceFile } from '@openfairygui/core/project-io';
|
|
2
|
+
import { commitUamProjectSourcePaths, materializeUamProject, validateUamProject, type UamProject } from '@openfairygui/core/uam';
|
|
3
3
|
import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
|
|
4
4
|
import type { CacheService } from './cache-service.js';
|
|
5
5
|
import { failure, success, type BackendContext } from './context.js';
|
|
@@ -79,9 +79,49 @@ function createWriterFileSystem(
|
|
|
79
79
|
dirname(filePath: string): string {
|
|
80
80
|
return fileSystem.dirname(filePath);
|
|
81
81
|
},
|
|
82
|
+
async unlink(filePath: string): Promise<void> {
|
|
83
|
+
await trackWrite(filePath, () => fileSystem.unlink(filePath));
|
|
84
|
+
},
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
function projectAssetSourceFiles(project: UamProject): Map<string, ProjectSourceFile> {
|
|
89
|
+
const result = new Map<string, ProjectSourceFile>();
|
|
90
|
+
for (const pkg of project.packages) {
|
|
91
|
+
for (const resource of pkg.resources) {
|
|
92
|
+
if (resource.kind === 'component') continue;
|
|
93
|
+
const fileName = resource.fileName ?? resource.file ?? '';
|
|
94
|
+
if (!fileName) continue;
|
|
95
|
+
result.set(
|
|
96
|
+
`${pkg.id}/${resource.id}`,
|
|
97
|
+
{ packageName: pkg.name, branch: resource.branch, path: resource.path, fileName },
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sourceFileKey(source: ProjectSourceFile): string {
|
|
105
|
+
return [source.branch, source.packageName, source.path, source.fileName].join('\0');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function recordStaleResourceSources(
|
|
109
|
+
session: Parameters<typeof toSessionSnapshot>[0],
|
|
110
|
+
previousProject: UamProject,
|
|
111
|
+
nextProject: UamProject,
|
|
112
|
+
): void {
|
|
113
|
+
if (!session.fileSystem) return;
|
|
114
|
+
const previousSources = projectAssetSourceFiles(previousProject);
|
|
115
|
+
const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
|
|
116
|
+
for (const source of previousSources.values()) {
|
|
117
|
+
const key = sourceFileKey(source);
|
|
118
|
+
if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
|
|
119
|
+
}
|
|
120
|
+
for (const key of nextSourceKeys) {
|
|
121
|
+
session.pendingStaleSourceFiles.delete(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagnostic[] {
|
|
86
126
|
return error.diagnostics.length > 0
|
|
87
127
|
? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
@@ -219,6 +259,7 @@ export class AuthoringService {
|
|
|
219
259
|
);
|
|
220
260
|
}
|
|
221
261
|
|
|
262
|
+
recordStaleResourceSources(session, session.project, result.project);
|
|
222
263
|
session.project = result.project;
|
|
223
264
|
session.revision += 1;
|
|
224
265
|
session.dirty = true;
|
|
@@ -271,7 +312,7 @@ export class AuthoringService {
|
|
|
271
312
|
if (!session || session.closed) {
|
|
272
313
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
273
314
|
}
|
|
274
|
-
const fileSystem =
|
|
315
|
+
const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
|
|
275
316
|
if (!fileSystem) {
|
|
276
317
|
return failure(
|
|
277
318
|
'authoring',
|
|
@@ -328,7 +369,12 @@ export class AuthoringService {
|
|
|
328
369
|
});
|
|
329
370
|
try {
|
|
330
371
|
const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
|
|
331
|
-
await writer.write(materializeUamProject(session.project), session.fairyPath
|
|
372
|
+
await writer.write(materializeUamProject(session.project), session.fairyPath, {
|
|
373
|
+
staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
|
|
374
|
+
});
|
|
375
|
+
session.fileSystem ??= fileSystem;
|
|
376
|
+
session.pendingStaleSourceFiles.clear();
|
|
377
|
+
commitUamProjectSourcePaths(session.project);
|
|
332
378
|
session.lastSavedRevision = session.revision;
|
|
333
379
|
session.dirty = false;
|
|
334
380
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
@@ -530,7 +576,13 @@ export class AuthoringService {
|
|
|
530
576
|
});
|
|
531
577
|
try {
|
|
532
578
|
const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
|
|
533
|
-
|
|
579
|
+
const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
|
|
580
|
+
await writer.write(document, fairyPath, {
|
|
581
|
+
staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
|
|
582
|
+
});
|
|
583
|
+
if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
584
|
+
if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
585
|
+
if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
|
|
534
586
|
if (storageTarget) {
|
|
535
587
|
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
536
588
|
session.fileSystem = storageTarget.fileSystem;
|
package/src/services/context.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface BackendSessionState {
|
|
|
29
29
|
project: import('@openfairygui/core/uam').UamProject;
|
|
30
30
|
revision: number;
|
|
31
31
|
lastSavedRevision: number;
|
|
32
|
+
/** Source files deferred until a successful replacement project write. */
|
|
33
|
+
pendingStaleSourceFiles: Map<string, import('@openfairygui/core/project-io').ProjectSourceFile>;
|
|
32
34
|
dirty: boolean;
|
|
33
35
|
lockHeld: boolean;
|
|
34
36
|
closed: boolean;
|
|
@@ -129,7 +129,7 @@ export class RuntimeService {
|
|
|
129
129
|
await advisoryLock.close();
|
|
130
130
|
|
|
131
131
|
const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
|
|
132
|
-
const project = liftDocumentToUamProject(await reader.read(fairyPath));
|
|
132
|
+
const project = liftDocumentToUamProject(await reader.read(fairyPath, { hydrateResourceBytes: true }));
|
|
133
133
|
const sessionId = randomId();
|
|
134
134
|
const session: BackendSessionState = {
|
|
135
135
|
sessionId,
|
|
@@ -141,6 +141,7 @@ export class RuntimeService {
|
|
|
141
141
|
project,
|
|
142
142
|
revision: 0,
|
|
143
143
|
lastSavedRevision: 0,
|
|
144
|
+
pendingStaleSourceFiles: new Map(),
|
|
144
145
|
dirty: false,
|
|
145
146
|
lockHeld: true,
|
|
146
147
|
closed: false,
|
|
@@ -204,6 +205,7 @@ export class RuntimeService {
|
|
|
204
205
|
project: normalizeUamProject(input.project),
|
|
205
206
|
revision: 0,
|
|
206
207
|
lastSavedRevision: 0,
|
|
208
|
+
pendingStaleSourceFiles: new Map(),
|
|
207
209
|
dirty: false,
|
|
208
210
|
lockHeld: false,
|
|
209
211
|
closed: false,
|
package/src/storage.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface BackendAsyncStorageAdapter {
|
|
|
21
21
|
stat?(filePath: string): Promise<BackendStorageStatLike>;
|
|
22
22
|
resolvePath?(filePath: string): Promise<string>;
|
|
23
23
|
openExclusive?(filePath: string): Promise<BackendFileHandle>;
|
|
24
|
-
unlink
|
|
24
|
+
unlink(filePath: string): Promise<void>;
|
|
25
25
|
join?(...paths: string[]): string;
|
|
26
26
|
dirname?(filePath: string): string;
|
|
27
27
|
resolve?(...paths: string[]): string;
|
|
@@ -114,6 +114,9 @@ async function inferStat(storage: BackendAsyncStorageAdapter, filePath: string):
|
|
|
114
114
|
export type BackendStorageFileSystem = BackendFileSystem & CoreProjectFileSystem;
|
|
115
115
|
|
|
116
116
|
export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem {
|
|
117
|
+
if (typeof storage.unlink !== 'function') {
|
|
118
|
+
throw createPathError('ENOTSUP', 'Storage adapter must provide unlink() for project resource lifecycle writes.');
|
|
119
|
+
}
|
|
117
120
|
const lockedPaths = new Set<string>();
|
|
118
121
|
|
|
119
122
|
const fileSystem: BackendStorageFileSystem = {
|
|
@@ -174,8 +177,7 @@ export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapt
|
|
|
174
177
|
unlink(filePath: string): Promise<void> {
|
|
175
178
|
const resolved = fileSystem.resolve(filePath);
|
|
176
179
|
lockedPaths.delete(resolved);
|
|
177
|
-
|
|
178
|
-
throw createPathError('ENOTSUP', 'Storage adapter does not provide unlink().');
|
|
180
|
+
return storage.unlink(resolved);
|
|
179
181
|
},
|
|
180
182
|
join(...paths: string[]): string {
|
|
181
183
|
return storage.join ? storage.join(...paths) : joinStoragePath(...paths);
|