@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
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.1",
|
|
4
4
|
"description": "FairyGUI Headless Authoring SDK — stateful backend runtime and session services.",
|
|
5
5
|
"author": "OpenFairyGUI Contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,18 +19,30 @@
|
|
|
19
19
|
"module": "./dist/index.mjs",
|
|
20
20
|
"types": "./dist/index.d.mts",
|
|
21
21
|
"exports": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
22
|
+
".": {
|
|
23
|
+
"require": {
|
|
24
|
+
"types": "./dist/index.d.cts",
|
|
25
|
+
"default": "./dist/index.cjs"
|
|
26
|
+
},
|
|
27
|
+
"default": {
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
}
|
|
25
31
|
},
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
32
|
+
"./node": {
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./dist/node.d.cts",
|
|
35
|
+
"default": "./dist/node.cjs"
|
|
36
|
+
},
|
|
37
|
+
"default": {
|
|
38
|
+
"types": "./dist/node.d.mts",
|
|
39
|
+
"default": "./dist/node.mjs"
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
42
|
},
|
|
31
43
|
"scripts": {
|
|
32
|
-
"build": "tsdown --format esm,cjs --platform node --external node:fs --external node:fs/promises --external node:path --env.PACKAGE_VERSION=$npm_package_version",
|
|
33
|
-
"build:watch": "tsdown --watch --format esm,cjs --platform node --env.PACKAGE_VERSION=$npm_package_version"
|
|
44
|
+
"build": "tsdown src/index.ts src/node.ts --format esm,cjs --platform node --external node:fs --external node:fs/promises --external node:path --env.PACKAGE_VERSION=$npm_package_version",
|
|
45
|
+
"build:watch": "tsdown src/index.ts src/node.ts --watch --format esm,cjs --platform node --external node:fs --external node:fs/promises --external node:path --env.PACKAGE_VERSION=$npm_package_version"
|
|
34
46
|
},
|
|
35
47
|
"files": [
|
|
36
48
|
"dist/",
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
export {
|
|
2
2
|
BackendRuntime,
|
|
3
|
-
createNodeBackendFileSystem,
|
|
4
3
|
type AdvisoryLockConflictError,
|
|
5
4
|
type ApplySessionTransactionInput,
|
|
5
|
+
type BackendArtifactBridgeCapability,
|
|
6
6
|
type BackendCacheEntry,
|
|
7
7
|
type BackendCacheSnapshot,
|
|
8
|
+
type BackendCapabilityManifest,
|
|
9
|
+
type BackendCapabilityUnavailableError,
|
|
8
10
|
type BackendCapabilities,
|
|
9
11
|
type BackendError,
|
|
10
12
|
type BackendEvent,
|
|
11
13
|
type BackendEventKind,
|
|
12
14
|
type BackendFailure,
|
|
13
15
|
type BackendFileHandle,
|
|
16
|
+
type BackendFileStat,
|
|
14
17
|
type BackendFileSystem,
|
|
18
|
+
type BackendHostAdapter,
|
|
15
19
|
type BackendJobKind,
|
|
16
20
|
type BackendJobListSnapshot,
|
|
17
21
|
type BackendJobListStatusFilter,
|
|
@@ -34,6 +38,7 @@ export {
|
|
|
34
38
|
type GetJobInput,
|
|
35
39
|
type InProcessLockConflictError,
|
|
36
40
|
type ListJobsInput,
|
|
41
|
+
type OpenProjectSessionInput,
|
|
37
42
|
type RefreshCacheInput,
|
|
38
43
|
type SavePartialFailureError,
|
|
39
44
|
type SessionNotFoundError,
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
BackendRuntime,
|
|
5
|
+
type BackendFileHandle,
|
|
6
|
+
type BackendFileStat,
|
|
7
|
+
type BackendFileSystem,
|
|
8
|
+
type BackendHostAdapter,
|
|
9
|
+
type BackendRuntimeOptions,
|
|
10
|
+
} from './runtime.js';
|
|
11
|
+
|
|
12
|
+
export function createNodeBackendFileSystem(): BackendFileSystem {
|
|
13
|
+
return {
|
|
14
|
+
stat(filePath: string): Promise<BackendFileStat> {
|
|
15
|
+
return fs.stat(filePath);
|
|
16
|
+
},
|
|
17
|
+
readdir(dirPath: string): Promise<string[]> {
|
|
18
|
+
return fs.readdir(dirPath);
|
|
19
|
+
},
|
|
20
|
+
readFile(filePath: string): Promise<string> {
|
|
21
|
+
return fs.readFile(filePath, 'utf-8');
|
|
22
|
+
},
|
|
23
|
+
async readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
24
|
+
const buffer = await fs.readFile(filePath);
|
|
25
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
26
|
+
},
|
|
27
|
+
writeFile(filePath: string, content: string): Promise<void> {
|
|
28
|
+
return fs.writeFile(filePath, content, 'utf-8');
|
|
29
|
+
},
|
|
30
|
+
writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
31
|
+
return fs.writeFile(filePath, data);
|
|
32
|
+
},
|
|
33
|
+
async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
|
|
34
|
+
await fs.mkdir(dirPath, { recursive: options?.recursive ?? false });
|
|
35
|
+
},
|
|
36
|
+
async resolvePath(filePath: string): Promise<string> {
|
|
37
|
+
try {
|
|
38
|
+
return await fs.realpath(filePath);
|
|
39
|
+
} catch {
|
|
40
|
+
return path.resolve(filePath);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async openExclusive(filePath: string): Promise<BackendFileHandle> {
|
|
44
|
+
const handle = await fs.open(filePath, 'wx');
|
|
45
|
+
return {
|
|
46
|
+
writeFile(content: string): Promise<void> {
|
|
47
|
+
return handle.writeFile(content, 'utf-8');
|
|
48
|
+
},
|
|
49
|
+
close(): Promise<void> {
|
|
50
|
+
return handle.close();
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
unlink(filePath: string): Promise<void> {
|
|
55
|
+
return fs.unlink(filePath);
|
|
56
|
+
},
|
|
57
|
+
join(...paths: string[]): string {
|
|
58
|
+
return path.join(...paths);
|
|
59
|
+
},
|
|
60
|
+
dirname(filePath: string): string {
|
|
61
|
+
return path.dirname(filePath);
|
|
62
|
+
},
|
|
63
|
+
resolve(...paths: string[]): string {
|
|
64
|
+
return path.resolve(...paths);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createNodeBackendHostAdapter(): BackendHostAdapter {
|
|
70
|
+
return {
|
|
71
|
+
lockMetadata(input) {
|
|
72
|
+
return {
|
|
73
|
+
pid: process.pid,
|
|
74
|
+
createdAt: new Date().toISOString(),
|
|
75
|
+
canonicalPathKey: input.canonicalPathKey,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createNodeBackendRuntime(options: BackendRuntimeOptions = {}): BackendRuntime {
|
|
82
|
+
return new BackendRuntime({
|
|
83
|
+
...options,
|
|
84
|
+
fileSystem: options.fileSystem ?? createNodeBackendFileSystem(),
|
|
85
|
+
host: options.host ?? createNodeBackendHostAdapter(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { BackendRuntime };
|
|
90
|
+
export type {
|
|
91
|
+
BackendFileHandle,
|
|
92
|
+
BackendFileStat,
|
|
93
|
+
BackendFileSystem,
|
|
94
|
+
BackendHostAdapter,
|
|
95
|
+
BackendRuntimeOptions,
|
|
96
|
+
} from './runtime.js';
|
package/src/runtime.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
UAM_SUPPORTED_MATERIALIZATION_SCOPE,
|
|
3
|
+
type UamProject,
|
|
3
4
|
type UamTransactionOperation,
|
|
4
5
|
} from '@openfairygui/core';
|
|
5
6
|
import type { ApplyUamTransactionAppError } from '@openfairygui/functions';
|
|
6
|
-
import fs from 'node:fs/promises';
|
|
7
|
-
import type { Stats } from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
7
|
import {
|
|
10
8
|
BACKEND_CAPABILITY_SCHEMA_VERSION,
|
|
11
9
|
BACKEND_COMPATIBILITY_POLICY,
|
|
@@ -27,8 +25,13 @@ export interface BackendFileHandle {
|
|
|
27
25
|
close(): Promise<void>;
|
|
28
26
|
}
|
|
29
27
|
|
|
28
|
+
export interface BackendFileStat {
|
|
29
|
+
isFile(): boolean;
|
|
30
|
+
isDirectory(): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
30
33
|
export interface BackendFileSystem {
|
|
31
|
-
stat(filePath: string): Promise<
|
|
34
|
+
stat(filePath: string): Promise<BackendFileStat>;
|
|
32
35
|
readdir(dirPath: string): Promise<string[]>;
|
|
33
36
|
readFile(filePath: string): Promise<string>;
|
|
34
37
|
readFileRaw(filePath: string): Promise<Uint8Array>;
|
|
@@ -43,6 +46,48 @@ export interface BackendFileSystem {
|
|
|
43
46
|
resolve(...paths: string[]): string;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
export interface BackendHostAdapter {
|
|
50
|
+
lockMetadata?(input: {
|
|
51
|
+
canonicalPathKey: string;
|
|
52
|
+
canonicalProjectPath: string;
|
|
53
|
+
lockFilePath: string;
|
|
54
|
+
}): unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface BackendArtifactBridgeCapability {
|
|
58
|
+
available: false;
|
|
59
|
+
requiredHost: 'node';
|
|
60
|
+
executionBoundary: 'external-bridge';
|
|
61
|
+
bridgeEntrypoint: '@openfairygui/backend/node';
|
|
62
|
+
reason: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface BackendCapabilityManifest {
|
|
66
|
+
browserSafe: true;
|
|
67
|
+
rootEntrypoint: '@openfairygui/backend';
|
|
68
|
+
nodeEntrypoint: '@openfairygui/backend/node';
|
|
69
|
+
adapters: {
|
|
70
|
+
fileSystem: {
|
|
71
|
+
injected: true;
|
|
72
|
+
requiredFor: readonly ['openSession', 'saveSession'];
|
|
73
|
+
};
|
|
74
|
+
host: {
|
|
75
|
+
injected: true;
|
|
76
|
+
requiredFor: readonly ['advisoryLockMetadata'];
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
executionBoundaries: {
|
|
80
|
+
projectSession: 'in-process-browser-safe';
|
|
81
|
+
fileBackedSession: 'adapter-backed';
|
|
82
|
+
artifactPublish: BackendArtifactBridgeCapability;
|
|
83
|
+
artifactRestore: BackendArtifactBridgeCapability;
|
|
84
|
+
};
|
|
85
|
+
diagnostics: {
|
|
86
|
+
stableCodes: true;
|
|
87
|
+
errorDiagnosticMirror: true;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
46
91
|
export interface BackendCapabilities {
|
|
47
92
|
contractVersion: typeof BACKEND_CONTRACT_VERSION;
|
|
48
93
|
capabilitySchemaVersion: typeof BACKEND_CAPABILITY_SCHEMA_VERSION;
|
|
@@ -52,6 +97,7 @@ export interface BackendCapabilities {
|
|
|
52
97
|
methods: readonly [
|
|
53
98
|
'getCapabilities',
|
|
54
99
|
'openSession',
|
|
100
|
+
'openProjectSession',
|
|
55
101
|
'getSession',
|
|
56
102
|
'applyTransaction',
|
|
57
103
|
'saveSession',
|
|
@@ -78,8 +124,11 @@ export interface BackendCapabilities {
|
|
|
78
124
|
artifact: {
|
|
79
125
|
publish: false;
|
|
80
126
|
restore: false;
|
|
81
|
-
status: '
|
|
127
|
+
status: 'bridge-required';
|
|
128
|
+
publishBridge: BackendArtifactBridgeCapability;
|
|
129
|
+
restoreBridge: BackendArtifactBridgeCapability;
|
|
82
130
|
};
|
|
131
|
+
manifest: BackendCapabilityManifest;
|
|
83
132
|
compatibilityPolicy: typeof BACKEND_COMPATIBILITY_POLICY;
|
|
84
133
|
runtime: {
|
|
85
134
|
sessionRuntime: true;
|
|
@@ -319,6 +368,15 @@ export interface CacheRefreshFailedError {
|
|
|
319
368
|
causeCode?: string;
|
|
320
369
|
}
|
|
321
370
|
|
|
371
|
+
export interface BackendCapabilityUnavailableError {
|
|
372
|
+
code: 'capability_unavailable';
|
|
373
|
+
message: string;
|
|
374
|
+
capability: 'fileSystem' | 'artifact.publish' | 'artifact.restore';
|
|
375
|
+
requiredAdapter?: 'BackendFileSystem';
|
|
376
|
+
requiredHost?: 'node';
|
|
377
|
+
bridgeBoundary?: 'external-bridge';
|
|
378
|
+
}
|
|
379
|
+
|
|
322
380
|
export type BackendJobErrors =
|
|
323
381
|
| BackendJobNotFoundError
|
|
324
382
|
| BackendJobNotCancellableError
|
|
@@ -366,6 +424,7 @@ export type BackendError =
|
|
|
366
424
|
| BackendJobNotCancellableError
|
|
367
425
|
| BackendJobCancelledError
|
|
368
426
|
| CacheRefreshFailedError
|
|
427
|
+
| BackendCapabilityUnavailableError
|
|
369
428
|
| ApplyUamTransactionAppError;
|
|
370
429
|
|
|
371
430
|
export interface ApplySessionTransactionInput {
|
|
@@ -374,14 +433,23 @@ export interface ApplySessionTransactionInput {
|
|
|
374
433
|
operations: UamTransactionOperation[];
|
|
375
434
|
}
|
|
376
435
|
|
|
436
|
+
export interface OpenProjectSessionInput {
|
|
437
|
+
project: UamProject;
|
|
438
|
+
sessionId?: string;
|
|
439
|
+
canonicalProjectPath?: string;
|
|
440
|
+
canonicalPathKey?: string;
|
|
441
|
+
}
|
|
442
|
+
|
|
377
443
|
export interface BackendRuntimeOptions {
|
|
378
444
|
fileSystem?: BackendFileSystem;
|
|
445
|
+
host?: BackendHostAdapter;
|
|
379
446
|
}
|
|
380
447
|
|
|
381
448
|
const BACKEND_METHODS = [
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
449
|
+
'getCapabilities',
|
|
450
|
+
'openSession',
|
|
451
|
+
'openProjectSession',
|
|
452
|
+
'getSession',
|
|
385
453
|
'applyTransaction',
|
|
386
454
|
'saveSession',
|
|
387
455
|
'closeSession',
|
|
@@ -389,10 +457,18 @@ const BACKEND_METHODS = [
|
|
|
389
457
|
'getJob',
|
|
390
458
|
'listJobs',
|
|
391
459
|
'cancelJob',
|
|
392
|
-
|
|
393
|
-
|
|
460
|
+
'getCacheSnapshot',
|
|
461
|
+
'refreshCache',
|
|
394
462
|
] as const;
|
|
395
463
|
|
|
464
|
+
const ARTIFACT_BRIDGE_CAPABILITY = {
|
|
465
|
+
available: false,
|
|
466
|
+
requiredHost: 'node',
|
|
467
|
+
executionBoundary: 'external-bridge',
|
|
468
|
+
bridgeEntrypoint: '@openfairygui/backend/node',
|
|
469
|
+
reason: 'publish/restore require explicit Node-hosted filesystem and artifact execution.',
|
|
470
|
+
} as const satisfies BackendArtifactBridgeCapability;
|
|
471
|
+
|
|
396
472
|
function createCapabilities(): BackendCapabilities {
|
|
397
473
|
return {
|
|
398
474
|
contractVersion: BACKEND_CONTRACT_VERSION,
|
|
@@ -414,6 +490,31 @@ function createCapabilities(): BackendCapabilities {
|
|
|
414
490
|
unsupported: ['artifact.publish', 'artifact.restore'],
|
|
415
491
|
},
|
|
416
492
|
artifact: createArtifactCapabilities(),
|
|
493
|
+
manifest: {
|
|
494
|
+
browserSafe: true,
|
|
495
|
+
rootEntrypoint: '@openfairygui/backend',
|
|
496
|
+
nodeEntrypoint: '@openfairygui/backend/node',
|
|
497
|
+
adapters: {
|
|
498
|
+
fileSystem: {
|
|
499
|
+
injected: true,
|
|
500
|
+
requiredFor: ['openSession', 'saveSession'],
|
|
501
|
+
},
|
|
502
|
+
host: {
|
|
503
|
+
injected: true,
|
|
504
|
+
requiredFor: ['advisoryLockMetadata'],
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
executionBoundaries: {
|
|
508
|
+
projectSession: 'in-process-browser-safe',
|
|
509
|
+
fileBackedSession: 'adapter-backed',
|
|
510
|
+
artifactPublish: ARTIFACT_BRIDGE_CAPABILITY,
|
|
511
|
+
artifactRestore: ARTIFACT_BRIDGE_CAPABILITY,
|
|
512
|
+
},
|
|
513
|
+
diagnostics: {
|
|
514
|
+
stableCodes: true,
|
|
515
|
+
errorDiagnosticMirror: true,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
417
518
|
compatibilityPolicy: BACKEND_COMPATIBILITY_POLICY,
|
|
418
519
|
runtime: {
|
|
419
520
|
sessionRuntime: true,
|
|
@@ -446,65 +547,8 @@ function createCapabilities(): BackendCapabilities {
|
|
|
446
547
|
};
|
|
447
548
|
}
|
|
448
549
|
|
|
449
|
-
export function createNodeBackendFileSystem(): BackendFileSystem {
|
|
450
|
-
return {
|
|
451
|
-
stat(filePath: string): Promise<Stats> {
|
|
452
|
-
return fs.stat(filePath);
|
|
453
|
-
},
|
|
454
|
-
readdir(dirPath: string): Promise<string[]> {
|
|
455
|
-
return fs.readdir(dirPath);
|
|
456
|
-
},
|
|
457
|
-
readFile(filePath: string): Promise<string> {
|
|
458
|
-
return fs.readFile(filePath, 'utf-8');
|
|
459
|
-
},
|
|
460
|
-
async readFileRaw(filePath: string): Promise<Uint8Array> {
|
|
461
|
-
const buffer = await fs.readFile(filePath);
|
|
462
|
-
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
463
|
-
},
|
|
464
|
-
writeFile(filePath: string, content: string): Promise<void> {
|
|
465
|
-
return fs.writeFile(filePath, content, 'utf-8');
|
|
466
|
-
},
|
|
467
|
-
writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
|
|
468
|
-
return fs.writeFile(filePath, data);
|
|
469
|
-
},
|
|
470
|
-
async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
|
|
471
|
-
await fs.mkdir(dirPath, { recursive: options?.recursive ?? false });
|
|
472
|
-
},
|
|
473
|
-
async resolvePath(filePath: string): Promise<string> {
|
|
474
|
-
try {
|
|
475
|
-
return await fs.realpath(filePath);
|
|
476
|
-
} catch {
|
|
477
|
-
return path.resolve(filePath);
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
async openExclusive(filePath: string): Promise<BackendFileHandle> {
|
|
481
|
-
const handle = await fs.open(filePath, 'wx');
|
|
482
|
-
return {
|
|
483
|
-
writeFile(content: string): Promise<void> {
|
|
484
|
-
return handle.writeFile(content, 'utf-8');
|
|
485
|
-
},
|
|
486
|
-
close(): Promise<void> {
|
|
487
|
-
return handle.close();
|
|
488
|
-
},
|
|
489
|
-
};
|
|
490
|
-
},
|
|
491
|
-
unlink(filePath: string): Promise<void> {
|
|
492
|
-
return fs.unlink(filePath);
|
|
493
|
-
},
|
|
494
|
-
join(...paths: string[]): string {
|
|
495
|
-
return path.join(...paths);
|
|
496
|
-
},
|
|
497
|
-
dirname(filePath: string): string {
|
|
498
|
-
return path.dirname(filePath);
|
|
499
|
-
},
|
|
500
|
-
resolve(...paths: string[]): string {
|
|
501
|
-
return path.resolve(...paths);
|
|
502
|
-
},
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
|
|
506
550
|
export class BackendRuntime {
|
|
507
|
-
private readonly fileSystem
|
|
551
|
+
private readonly fileSystem?: BackendFileSystem;
|
|
508
552
|
private readonly capabilities: BackendCapabilities;
|
|
509
553
|
private readonly sessions = new Map<string, BackendSessionState>();
|
|
510
554
|
private readonly sessionsByPath = new Map<string, string>();
|
|
@@ -521,10 +565,11 @@ export class BackendRuntime {
|
|
|
521
565
|
private readonly jobService: JobService;
|
|
522
566
|
|
|
523
567
|
public constructor(options: BackendRuntimeOptions = {}) {
|
|
524
|
-
this.fileSystem = options.fileSystem
|
|
568
|
+
this.fileSystem = options.fileSystem;
|
|
525
569
|
this.capabilities = createCapabilities();
|
|
526
570
|
this.context = {
|
|
527
571
|
fileSystem: this.fileSystem,
|
|
572
|
+
host: options.host,
|
|
528
573
|
capabilities: this.capabilities,
|
|
529
574
|
sessions: this.sessions,
|
|
530
575
|
sessionsByPath: this.sessionsByPath,
|
|
@@ -548,10 +593,14 @@ export class BackendRuntime {
|
|
|
548
593
|
return this.readService.getCapabilities() as BackendSuccess<BackendCapabilities>;
|
|
549
594
|
}
|
|
550
595
|
|
|
551
|
-
public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError>> {
|
|
596
|
+
public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError>> {
|
|
552
597
|
return this.runtimeService.openSession(input);
|
|
553
598
|
}
|
|
554
599
|
|
|
600
|
+
public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
|
|
601
|
+
return this.runtimeService.openProjectSession(input);
|
|
602
|
+
}
|
|
603
|
+
|
|
555
604
|
public getSession(input: { sessionId: string }): BackendResult<BackendSessionSnapshot, SessionNotFoundError> {
|
|
556
605
|
return this.readService.getSession(input);
|
|
557
606
|
}
|
|
@@ -564,7 +613,7 @@ export class BackendRuntime {
|
|
|
564
613
|
|
|
565
614
|
public async saveSession(
|
|
566
615
|
input: { sessionId: string; expectedRevision?: number; targetPath?: string },
|
|
567
|
-
): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError>> {
|
|
616
|
+
): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>> {
|
|
568
617
|
return this.authoringService.saveSession(input);
|
|
569
618
|
}
|
|
570
619
|
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import type { BackendCapabilities } from '../runtime.js';
|
|
2
2
|
|
|
3
3
|
export function createArtifactCapabilities(): BackendCapabilities['artifact'] {
|
|
4
|
+
const bridge = {
|
|
5
|
+
available: false,
|
|
6
|
+
requiredHost: 'node',
|
|
7
|
+
executionBoundary: 'external-bridge',
|
|
8
|
+
bridgeEntrypoint: '@openfairygui/backend/node',
|
|
9
|
+
reason: 'publish/restore require explicit Node-hosted filesystem and artifact execution.',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
4
12
|
return {
|
|
5
13
|
publish: false,
|
|
6
14
|
restore: false,
|
|
7
|
-
status: '
|
|
15
|
+
status: 'bridge-required',
|
|
16
|
+
publishBridge: bridge,
|
|
17
|
+
restoreBridge: bridge,
|
|
8
18
|
};
|
|
9
19
|
}
|
|
@@ -5,6 +5,7 @@ import { failure, success, type BackendContext } from './context.js';
|
|
|
5
5
|
import type { EventService } from './event-service.js';
|
|
6
6
|
import type {
|
|
7
7
|
ApplySessionTransactionInput,
|
|
8
|
+
BackendCapabilityUnavailableError,
|
|
8
9
|
BackendFileSystem,
|
|
9
10
|
BackendResult,
|
|
10
11
|
BackendSessionSnapshot,
|
|
@@ -123,19 +124,31 @@ export class AuthoringService {
|
|
|
123
124
|
|
|
124
125
|
public async saveSession(
|
|
125
126
|
input: { sessionId: string; expectedRevision?: number; targetPath?: string },
|
|
126
|
-
): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError>> {
|
|
127
|
+
): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>> {
|
|
127
128
|
const startedAt = Date.now();
|
|
128
129
|
const session = this.context.sessions.get(input.sessionId);
|
|
129
130
|
if (!session || session.closed) {
|
|
130
131
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
131
132
|
}
|
|
133
|
+
if (!this.context.fileSystem) {
|
|
134
|
+
return failure('authoring', startedAt, {
|
|
135
|
+
code: 'capability_unavailable',
|
|
136
|
+
message: 'saveSession requires an injected BackendFileSystem adapter.',
|
|
137
|
+
capability: 'fileSystem',
|
|
138
|
+
requiredAdapter: 'BackendFileSystem',
|
|
139
|
+
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
140
|
+
sessionId: session.sessionId,
|
|
141
|
+
revision: session.revision,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
132
144
|
if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
|
|
133
145
|
return failure('authoring', startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
|
|
134
146
|
sessionId: session.sessionId,
|
|
135
147
|
revision: session.revision,
|
|
136
148
|
});
|
|
137
149
|
}
|
|
138
|
-
const
|
|
150
|
+
const fileSystem = this.context.fileSystem;
|
|
151
|
+
const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
|
|
139
152
|
if (targetViolation) {
|
|
140
153
|
return failure('authoring', startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
|
|
141
154
|
sessionId: session.sessionId,
|
|
@@ -153,7 +166,7 @@ export class AuthoringService {
|
|
|
153
166
|
const failedPaths: string[] = [];
|
|
154
167
|
this.eventService.emit({ kind: 'save.started', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
|
|
155
168
|
try {
|
|
156
|
-
const writer = new ProjectWriter(createWriterFileSystem(
|
|
169
|
+
const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
|
|
157
170
|
await writer.write(materializeUamProject(session.project), session.fairyPath);
|
|
158
171
|
session.lastSavedRevision = session.revision;
|
|
159
172
|
session.dirty = false;
|
package/src/services/context.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
BackendEvent,
|
|
6
6
|
BackendFailure,
|
|
7
7
|
BackendFileSystem,
|
|
8
|
+
BackendHostAdapter,
|
|
8
9
|
BackendJobSnapshot,
|
|
9
10
|
BackendSessionSnapshot,
|
|
10
11
|
BackendSuccess,
|
|
@@ -33,7 +34,8 @@ export interface BackendSessionState {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export interface BackendContext {
|
|
36
|
-
fileSystem
|
|
37
|
+
fileSystem?: BackendFileSystem;
|
|
38
|
+
host?: BackendHostAdapter;
|
|
37
39
|
capabilities: BackendCapabilities;
|
|
38
40
|
sessions: Map<string, BackendSessionState>;
|
|
39
41
|
sessionsByPath: Map<string, string>;
|
|
@@ -43,6 +45,14 @@ export interface BackendContext {
|
|
|
43
45
|
nextEventSequence: () => number;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function diagnosticFromError(error: BackendError): BackendDiagnostic {
|
|
49
|
+
return {
|
|
50
|
+
code: error.code,
|
|
51
|
+
message: error.message,
|
|
52
|
+
severity: 'error',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
function randomId(): string {
|
|
47
57
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
48
58
|
}
|
|
@@ -103,9 +113,10 @@ export function failure<E extends BackendError>(
|
|
|
103
113
|
diagnostics?: BackendDiagnostic[];
|
|
104
114
|
},
|
|
105
115
|
): BackendFailure<E> {
|
|
116
|
+
const diagnostics = options?.diagnostics ?? [diagnosticFromError(error)];
|
|
106
117
|
return {
|
|
107
118
|
ok: false,
|
|
108
|
-
meta: createMeta(stage, startedAt, options),
|
|
119
|
+
meta: createMeta(stage, startedAt, { ...options, diagnostics }),
|
|
109
120
|
error,
|
|
110
121
|
session,
|
|
111
122
|
};
|
|
@@ -51,7 +51,7 @@ export class EventService {
|
|
|
51
51
|
after: String(input.after),
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
-
if (events.length > 0 && after < oldestSequence - 1) {
|
|
54
|
+
if (events.length > 0 && after !== 0 && after < oldestSequence - 1) {
|
|
55
55
|
return failure('runtime', startedAt, {
|
|
56
56
|
code: 'event_cursor_invalid',
|
|
57
57
|
message: `Event cursor has expired: ${after}`,
|