@openfairygui/backend 0.2.0-alpha.0 → 0.2.0-alpha.2
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 +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-BL9Pkfc7.cjs +1217 -0
- package/dist/runtime-BPWzx-Tv.mjs +1193 -0
- package/dist/runtime-OO1sHuCl.d.mts +424 -0
- package/dist/runtime-qcSlD_9-.d.cts +424 -0
- package/package.json +65 -53
- package/src/index.ts +6 -1
- package/src/node.ts +96 -0
- package/src/runtime.ts +155 -80
- package/src/services/artifact-service.ts +11 -1
- package/src/services/authoring-service.ts +151 -37
- package/src/services/context.ts +14 -3
- package/src/services/event-service.ts +1 -1
- package/src/services/runtime-service.ts +155 -24
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
|
-
} from '@openfairygui/core';
|
|
5
|
-
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';
|
|
5
|
+
} from '@openfairygui/core/uam';
|
|
6
|
+
import type { ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
|
|
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,44 @@ export interface BackendFileSystem {
|
|
|
43
46
|
resolve(...paths: string[]): string;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
export interface BackendHostAdapter {
|
|
50
|
+
lockMetadata?(input: { canonicalPathKey: string; canonicalProjectPath: string; lockFilePath: string }): unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface BackendArtifactBridgeCapability {
|
|
54
|
+
available: false;
|
|
55
|
+
requiredHost: 'node';
|
|
56
|
+
executionBoundary: 'external-bridge';
|
|
57
|
+
bridgeEntrypoint: '@openfairygui/backend/node';
|
|
58
|
+
reason: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BackendCapabilityManifest {
|
|
62
|
+
browserSafe: true;
|
|
63
|
+
rootEntrypoint: '@openfairygui/backend';
|
|
64
|
+
nodeEntrypoint: '@openfairygui/backend/node';
|
|
65
|
+
adapters: {
|
|
66
|
+
fileSystem: {
|
|
67
|
+
injected: true;
|
|
68
|
+
requiredFor: readonly ['openSession', 'saveSession'];
|
|
69
|
+
};
|
|
70
|
+
host: {
|
|
71
|
+
injected: true;
|
|
72
|
+
requiredFor: readonly ['advisoryLockMetadata'];
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
executionBoundaries: {
|
|
76
|
+
projectSession: 'in-process-browser-safe';
|
|
77
|
+
fileBackedSession: 'adapter-backed';
|
|
78
|
+
artifactPublish: BackendArtifactBridgeCapability;
|
|
79
|
+
artifactRestore: BackendArtifactBridgeCapability;
|
|
80
|
+
};
|
|
81
|
+
diagnostics: {
|
|
82
|
+
stableCodes: true;
|
|
83
|
+
errorDiagnosticMirror: true;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
46
87
|
export interface BackendCapabilities {
|
|
47
88
|
contractVersion: typeof BACKEND_CONTRACT_VERSION;
|
|
48
89
|
capabilitySchemaVersion: typeof BACKEND_CAPABILITY_SCHEMA_VERSION;
|
|
@@ -52,6 +93,7 @@ export interface BackendCapabilities {
|
|
|
52
93
|
methods: readonly [
|
|
53
94
|
'getCapabilities',
|
|
54
95
|
'openSession',
|
|
96
|
+
'openProjectSession',
|
|
55
97
|
'getSession',
|
|
56
98
|
'applyTransaction',
|
|
57
99
|
'saveSession',
|
|
@@ -78,8 +120,11 @@ export interface BackendCapabilities {
|
|
|
78
120
|
artifact: {
|
|
79
121
|
publish: false;
|
|
80
122
|
restore: false;
|
|
81
|
-
status: '
|
|
123
|
+
status: 'bridge-required';
|
|
124
|
+
publishBridge: BackendArtifactBridgeCapability;
|
|
125
|
+
restoreBridge: BackendArtifactBridgeCapability;
|
|
82
126
|
};
|
|
127
|
+
manifest: BackendCapabilityManifest;
|
|
83
128
|
compatibilityPolicy: typeof BACKEND_COMPATIBILITY_POLICY;
|
|
84
129
|
runtime: {
|
|
85
130
|
sessionRuntime: true;
|
|
@@ -140,9 +185,7 @@ export interface BackendFailure<E extends BackendError = BackendError> {
|
|
|
140
185
|
session?: BackendSessionSnapshot;
|
|
141
186
|
}
|
|
142
187
|
|
|
143
|
-
export type BackendResult<T, E extends BackendError = BackendError> =
|
|
144
|
-
| BackendSuccess<T>
|
|
145
|
-
| BackendFailure<E>;
|
|
188
|
+
export type BackendResult<T, E extends BackendError = BackendError> = BackendSuccess<T> | BackendFailure<E>;
|
|
146
189
|
|
|
147
190
|
export interface SessionNotFoundError {
|
|
148
191
|
code: 'session_not_found';
|
|
@@ -319,6 +362,15 @@ export interface CacheRefreshFailedError {
|
|
|
319
362
|
causeCode?: string;
|
|
320
363
|
}
|
|
321
364
|
|
|
365
|
+
export interface BackendCapabilityUnavailableError {
|
|
366
|
+
code: 'capability_unavailable';
|
|
367
|
+
message: string;
|
|
368
|
+
capability: 'fileSystem' | 'artifact.publish' | 'artifact.restore';
|
|
369
|
+
requiredAdapter?: 'BackendFileSystem';
|
|
370
|
+
requiredHost?: 'node';
|
|
371
|
+
bridgeBoundary?: 'external-bridge';
|
|
372
|
+
}
|
|
373
|
+
|
|
322
374
|
export type BackendJobErrors =
|
|
323
375
|
| BackendJobNotFoundError
|
|
324
376
|
| BackendJobNotCancellableError
|
|
@@ -366,6 +418,7 @@ export type BackendError =
|
|
|
366
418
|
| BackendJobNotCancellableError
|
|
367
419
|
| BackendJobCancelledError
|
|
368
420
|
| CacheRefreshFailedError
|
|
421
|
+
| BackendCapabilityUnavailableError
|
|
369
422
|
| ApplyUamTransactionAppError;
|
|
370
423
|
|
|
371
424
|
export interface ApplySessionTransactionInput {
|
|
@@ -374,13 +427,22 @@ export interface ApplySessionTransactionInput {
|
|
|
374
427
|
operations: UamTransactionOperation[];
|
|
375
428
|
}
|
|
376
429
|
|
|
430
|
+
export interface OpenProjectSessionInput {
|
|
431
|
+
project: UamProject;
|
|
432
|
+
sessionId?: string;
|
|
433
|
+
canonicalProjectPath?: string;
|
|
434
|
+
canonicalPathKey?: string;
|
|
435
|
+
}
|
|
436
|
+
|
|
377
437
|
export interface BackendRuntimeOptions {
|
|
378
438
|
fileSystem?: BackendFileSystem;
|
|
439
|
+
host?: BackendHostAdapter;
|
|
379
440
|
}
|
|
380
441
|
|
|
381
442
|
const BACKEND_METHODS = [
|
|
382
443
|
'getCapabilities',
|
|
383
444
|
'openSession',
|
|
445
|
+
'openProjectSession',
|
|
384
446
|
'getSession',
|
|
385
447
|
'applyTransaction',
|
|
386
448
|
'saveSession',
|
|
@@ -393,6 +455,14 @@ const BACKEND_METHODS = [
|
|
|
393
455
|
'refreshCache',
|
|
394
456
|
] as const;
|
|
395
457
|
|
|
458
|
+
const ARTIFACT_BRIDGE_CAPABILITY = {
|
|
459
|
+
available: false,
|
|
460
|
+
requiredHost: 'node',
|
|
461
|
+
executionBoundary: 'external-bridge',
|
|
462
|
+
bridgeEntrypoint: '@openfairygui/backend/node',
|
|
463
|
+
reason: 'publish/restore require explicit Node-hosted filesystem and artifact execution.',
|
|
464
|
+
} as const satisfies BackendArtifactBridgeCapability;
|
|
465
|
+
|
|
396
466
|
function createCapabilities(): BackendCapabilities {
|
|
397
467
|
return {
|
|
398
468
|
contractVersion: BACKEND_CONTRACT_VERSION,
|
|
@@ -414,6 +484,31 @@ function createCapabilities(): BackendCapabilities {
|
|
|
414
484
|
unsupported: ['artifact.publish', 'artifact.restore'],
|
|
415
485
|
},
|
|
416
486
|
artifact: createArtifactCapabilities(),
|
|
487
|
+
manifest: {
|
|
488
|
+
browserSafe: true,
|
|
489
|
+
rootEntrypoint: '@openfairygui/backend',
|
|
490
|
+
nodeEntrypoint: '@openfairygui/backend/node',
|
|
491
|
+
adapters: {
|
|
492
|
+
fileSystem: {
|
|
493
|
+
injected: true,
|
|
494
|
+
requiredFor: ['openSession', 'saveSession'],
|
|
495
|
+
},
|
|
496
|
+
host: {
|
|
497
|
+
injected: true,
|
|
498
|
+
requiredFor: ['advisoryLockMetadata'],
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
executionBoundaries: {
|
|
502
|
+
projectSession: 'in-process-browser-safe',
|
|
503
|
+
fileBackedSession: 'adapter-backed',
|
|
504
|
+
artifactPublish: ARTIFACT_BRIDGE_CAPABILITY,
|
|
505
|
+
artifactRestore: ARTIFACT_BRIDGE_CAPABILITY,
|
|
506
|
+
},
|
|
507
|
+
diagnostics: {
|
|
508
|
+
stableCodes: true,
|
|
509
|
+
errorDiagnosticMirror: true,
|
|
510
|
+
},
|
|
511
|
+
},
|
|
417
512
|
compatibilityPolicy: BACKEND_COMPATIBILITY_POLICY,
|
|
418
513
|
runtime: {
|
|
419
514
|
sessionRuntime: true,
|
|
@@ -446,65 +541,8 @@ function createCapabilities(): BackendCapabilities {
|
|
|
446
541
|
};
|
|
447
542
|
}
|
|
448
543
|
|
|
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
544
|
export class BackendRuntime {
|
|
507
|
-
private readonly fileSystem
|
|
545
|
+
private readonly fileSystem?: BackendFileSystem;
|
|
508
546
|
private readonly capabilities: BackendCapabilities;
|
|
509
547
|
private readonly sessions = new Map<string, BackendSessionState>();
|
|
510
548
|
private readonly sessionsByPath = new Map<string, string>();
|
|
@@ -521,10 +559,11 @@ export class BackendRuntime {
|
|
|
521
559
|
private readonly jobService: JobService;
|
|
522
560
|
|
|
523
561
|
public constructor(options: BackendRuntimeOptions = {}) {
|
|
524
|
-
this.fileSystem = options.fileSystem
|
|
562
|
+
this.fileSystem = options.fileSystem;
|
|
525
563
|
this.capabilities = createCapabilities();
|
|
526
564
|
this.context = {
|
|
527
565
|
fileSystem: this.fileSystem,
|
|
566
|
+
host: options.host,
|
|
528
567
|
capabilities: this.capabilities,
|
|
529
568
|
sessions: this.sessions,
|
|
530
569
|
sessionsByPath: this.sessionsByPath,
|
|
@@ -548,37 +587,68 @@ export class BackendRuntime {
|
|
|
548
587
|
return this.readService.getCapabilities() as BackendSuccess<BackendCapabilities>;
|
|
549
588
|
}
|
|
550
589
|
|
|
551
|
-
public async openSession(input: {
|
|
590
|
+
public async openSession(input: {
|
|
591
|
+
projectPath: string;
|
|
592
|
+
}): Promise<
|
|
593
|
+
BackendResult<
|
|
594
|
+
BackendSessionSnapshot,
|
|
595
|
+
InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError
|
|
596
|
+
>
|
|
597
|
+
> {
|
|
552
598
|
return this.runtimeService.openSession(input);
|
|
553
599
|
}
|
|
554
600
|
|
|
601
|
+
public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
|
|
602
|
+
return this.runtimeService.openProjectSession(input);
|
|
603
|
+
}
|
|
604
|
+
|
|
555
605
|
public getSession(input: { sessionId: string }): BackendResult<BackendSessionSnapshot, SessionNotFoundError> {
|
|
556
606
|
return this.readService.getSession(input);
|
|
557
607
|
}
|
|
558
608
|
|
|
559
609
|
public async applyTransaction(
|
|
560
610
|
input: ApplySessionTransactionInput,
|
|
561
|
-
): Promise<
|
|
611
|
+
): Promise<
|
|
612
|
+
BackendResult<
|
|
613
|
+
BackendSessionSnapshot,
|
|
614
|
+
SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
|
|
615
|
+
>
|
|
616
|
+
> {
|
|
562
617
|
return this.authoringService.applyTransaction(input);
|
|
563
618
|
}
|
|
564
619
|
|
|
565
|
-
public async saveSession(
|
|
566
|
-
|
|
567
|
-
|
|
620
|
+
public async saveSession(input: {
|
|
621
|
+
sessionId: string;
|
|
622
|
+
expectedRevision?: number;
|
|
623
|
+
targetPath?: string;
|
|
624
|
+
}): Promise<
|
|
625
|
+
BackendResult<
|
|
626
|
+
BackendSessionSnapshot,
|
|
627
|
+
| SessionNotFoundError
|
|
628
|
+
| SessionStaleWriteError
|
|
629
|
+
| SavePartialFailureError
|
|
630
|
+
| PathPolicyViolationError
|
|
631
|
+
| BackendCapabilityUnavailableError
|
|
632
|
+
>
|
|
633
|
+
> {
|
|
568
634
|
return this.authoringService.saveSession(input);
|
|
569
635
|
}
|
|
570
636
|
|
|
571
|
-
public async closeSession(
|
|
572
|
-
|
|
573
|
-
): Promise<BackendResult<{ sessionId: string; closed: true }, SessionNotFoundError>> {
|
|
637
|
+
public async closeSession(input: {
|
|
638
|
+
sessionId: string;
|
|
639
|
+
}): Promise<BackendResult<{ sessionId: string; closed: true }, SessionNotFoundError>> {
|
|
574
640
|
return this.runtimeService.closeSession(input);
|
|
575
641
|
}
|
|
576
642
|
|
|
577
|
-
public getEvents(
|
|
643
|
+
public getEvents(
|
|
644
|
+
input: GetEventsInput,
|
|
645
|
+
): BackendResult<GetEventsSnapshot, SessionNotFoundError | EventCursorInvalidError> {
|
|
578
646
|
return this.eventService.getEvents(input);
|
|
579
647
|
}
|
|
580
648
|
|
|
581
|
-
public getJob(
|
|
649
|
+
public getJob(
|
|
650
|
+
input: GetJobInput,
|
|
651
|
+
): BackendResult<BackendJobSnapshot, SessionNotFoundError | BackendJobNotFoundError> {
|
|
582
652
|
return this.jobService.getJob(input);
|
|
583
653
|
}
|
|
584
654
|
|
|
@@ -586,7 +656,12 @@ export class BackendRuntime {
|
|
|
586
656
|
return this.jobService.listJobs(input);
|
|
587
657
|
}
|
|
588
658
|
|
|
589
|
-
public cancelJob(
|
|
659
|
+
public cancelJob(
|
|
660
|
+
input: CancelJobInput,
|
|
661
|
+
): BackendResult<
|
|
662
|
+
BackendJobSnapshot,
|
|
663
|
+
SessionNotFoundError | BackendJobNotFoundError | BackendJobNotCancellableError
|
|
664
|
+
> {
|
|
590
665
|
return this.jobService.cancelJob(input);
|
|
591
666
|
}
|
|
592
667
|
|
|
@@ -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
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ProjectWriter, type FileSystem } from '@openfairygui/core/project-io';
|
|
2
|
+
import { materializeUamProject } from '@openfairygui/core/uam';
|
|
3
|
+
import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
|
|
3
4
|
import type { CacheService } from './cache-service.js';
|
|
4
5
|
import { failure, success, type BackendContext } from './context.js';
|
|
5
6
|
import type { EventService } from './event-service.js';
|
|
6
7
|
import type {
|
|
7
8
|
ApplySessionTransactionInput,
|
|
9
|
+
BackendCapabilityUnavailableError,
|
|
8
10
|
BackendFileSystem,
|
|
9
11
|
BackendResult,
|
|
10
12
|
BackendSessionSnapshot,
|
|
@@ -19,7 +21,7 @@ function createWriterFileSystem(
|
|
|
19
21
|
fileSystem: BackendFileSystem,
|
|
20
22
|
committedPaths: string[],
|
|
21
23
|
failedPaths: string[],
|
|
22
|
-
):
|
|
24
|
+
): FileSystem {
|
|
23
25
|
async function trackWrite<T>(targetPath: string, fn: () => Promise<T>): Promise<T> {
|
|
24
26
|
try {
|
|
25
27
|
const result = await fn();
|
|
@@ -82,18 +84,34 @@ export class AuthoringService {
|
|
|
82
84
|
|
|
83
85
|
public async applyTransaction(
|
|
84
86
|
input: ApplySessionTransactionInput,
|
|
85
|
-
): Promise<
|
|
87
|
+
): Promise<
|
|
88
|
+
BackendResult<
|
|
89
|
+
BackendSessionSnapshot,
|
|
90
|
+
SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
|
|
91
|
+
>
|
|
92
|
+
> {
|
|
86
93
|
const startedAt = Date.now();
|
|
87
94
|
const session = this.context.sessions.get(input.sessionId);
|
|
88
95
|
if (!session || session.closed) {
|
|
89
96
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
90
97
|
}
|
|
91
98
|
if (input.expectedRevision !== session.revision) {
|
|
92
|
-
this.eventService.emit({
|
|
93
|
-
|
|
99
|
+
this.eventService.emit({
|
|
100
|
+
kind: 'transaction.rejected',
|
|
94
101
|
sessionId: session.sessionId,
|
|
102
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
95
103
|
revision: session.revision,
|
|
96
104
|
});
|
|
105
|
+
return failure(
|
|
106
|
+
'authoring',
|
|
107
|
+
startedAt,
|
|
108
|
+
createStaleWriteError(session, input.expectedRevision),
|
|
109
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
110
|
+
{
|
|
111
|
+
sessionId: session.sessionId,
|
|
112
|
+
revision: session.revision,
|
|
113
|
+
},
|
|
114
|
+
);
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
const result = applyUamTransactionApp({
|
|
@@ -101,19 +119,47 @@ export class AuthoringService {
|
|
|
101
119
|
operations: input.operations,
|
|
102
120
|
});
|
|
103
121
|
if (result.ok === false) {
|
|
104
|
-
this.eventService.emit({
|
|
105
|
-
|
|
122
|
+
this.eventService.emit({
|
|
123
|
+
kind: 'transaction.rejected',
|
|
106
124
|
sessionId: session.sessionId,
|
|
125
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
107
126
|
revision: session.revision,
|
|
127
|
+
diagnostics:
|
|
128
|
+
result.error.issues?.map((issue) => ({
|
|
129
|
+
code: result.error.code,
|
|
130
|
+
message: issue.message,
|
|
131
|
+
severity: 'error' as const,
|
|
132
|
+
})) ?? [],
|
|
108
133
|
});
|
|
134
|
+
return failure(
|
|
135
|
+
'authoring',
|
|
136
|
+
startedAt,
|
|
137
|
+
result.error,
|
|
138
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
139
|
+
{
|
|
140
|
+
sessionId: session.sessionId,
|
|
141
|
+
revision: session.revision,
|
|
142
|
+
},
|
|
143
|
+
);
|
|
109
144
|
}
|
|
110
145
|
|
|
111
146
|
session.project = result.project;
|
|
112
147
|
session.revision += 1;
|
|
113
148
|
session.dirty = true;
|
|
114
149
|
const cacheEntry = this.cacheService.invalidateSession(session);
|
|
115
|
-
this.eventService.emit({
|
|
116
|
-
|
|
150
|
+
this.eventService.emit({
|
|
151
|
+
kind: 'transaction.applied',
|
|
152
|
+
sessionId: session.sessionId,
|
|
153
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
154
|
+
revision: session.revision,
|
|
155
|
+
});
|
|
156
|
+
this.eventService.emit({
|
|
157
|
+
kind: 'cache.invalidated',
|
|
158
|
+
sessionId: session.sessionId,
|
|
159
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
160
|
+
revision: session.revision,
|
|
161
|
+
cacheRevision: cacheEntry.revision,
|
|
162
|
+
});
|
|
117
163
|
|
|
118
164
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
119
165
|
sessionId: session.sessionId,
|
|
@@ -121,26 +167,67 @@ export class AuthoringService {
|
|
|
121
167
|
});
|
|
122
168
|
}
|
|
123
169
|
|
|
124
|
-
public async saveSession(
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
public async saveSession(input: {
|
|
171
|
+
sessionId: string;
|
|
172
|
+
expectedRevision?: number;
|
|
173
|
+
targetPath?: string;
|
|
174
|
+
}): Promise<
|
|
175
|
+
BackendResult<
|
|
176
|
+
BackendSessionSnapshot,
|
|
177
|
+
| SessionNotFoundError
|
|
178
|
+
| SessionStaleWriteError
|
|
179
|
+
| SavePartialFailureError
|
|
180
|
+
| PathPolicyViolationError
|
|
181
|
+
| BackendCapabilityUnavailableError
|
|
182
|
+
>
|
|
183
|
+
> {
|
|
127
184
|
const startedAt = Date.now();
|
|
128
185
|
const session = this.context.sessions.get(input.sessionId);
|
|
129
186
|
if (!session || session.closed) {
|
|
130
187
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
131
188
|
}
|
|
189
|
+
if (!this.context.fileSystem) {
|
|
190
|
+
return failure(
|
|
191
|
+
'authoring',
|
|
192
|
+
startedAt,
|
|
193
|
+
{
|
|
194
|
+
code: 'capability_unavailable',
|
|
195
|
+
message: 'saveSession requires an injected BackendFileSystem adapter.',
|
|
196
|
+
capability: 'fileSystem',
|
|
197
|
+
requiredAdapter: 'BackendFileSystem',
|
|
198
|
+
},
|
|
199
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
200
|
+
{
|
|
201
|
+
sessionId: session.sessionId,
|
|
202
|
+
revision: session.revision,
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
}
|
|
132
206
|
if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
|
|
133
|
-
return failure(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
207
|
+
return failure(
|
|
208
|
+
'authoring',
|
|
209
|
+
startedAt,
|
|
210
|
+
createStaleWriteError(session, input.expectedRevision),
|
|
211
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
212
|
+
{
|
|
213
|
+
sessionId: session.sessionId,
|
|
214
|
+
revision: session.revision,
|
|
215
|
+
},
|
|
216
|
+
);
|
|
137
217
|
}
|
|
138
|
-
const
|
|
218
|
+
const fileSystem = this.context.fileSystem;
|
|
219
|
+
const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
|
|
139
220
|
if (targetViolation) {
|
|
140
|
-
return failure(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
221
|
+
return failure(
|
|
222
|
+
'authoring',
|
|
223
|
+
startedAt,
|
|
224
|
+
targetViolation,
|
|
225
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
226
|
+
{
|
|
227
|
+
sessionId: session.sessionId,
|
|
228
|
+
revision: session.revision,
|
|
229
|
+
},
|
|
230
|
+
);
|
|
144
231
|
}
|
|
145
232
|
if (!session.dirty) {
|
|
146
233
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
@@ -151,36 +238,63 @@ export class AuthoringService {
|
|
|
151
238
|
|
|
152
239
|
const committedPaths: string[] = [];
|
|
153
240
|
const failedPaths: string[] = [];
|
|
154
|
-
this.eventService.emit({
|
|
241
|
+
this.eventService.emit({
|
|
242
|
+
kind: 'save.started',
|
|
243
|
+
sessionId: session.sessionId,
|
|
244
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
245
|
+
revision: session.revision,
|
|
246
|
+
});
|
|
155
247
|
try {
|
|
156
|
-
const writer = new ProjectWriter(createWriterFileSystem(
|
|
248
|
+
const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
|
|
157
249
|
await writer.write(materializeUamProject(session.project), session.fairyPath);
|
|
158
250
|
session.lastSavedRevision = session.revision;
|
|
159
251
|
session.dirty = false;
|
|
160
252
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
161
|
-
this.eventService.emit({
|
|
162
|
-
|
|
253
|
+
this.eventService.emit({
|
|
254
|
+
kind: 'save.completed',
|
|
255
|
+
sessionId: session.sessionId,
|
|
256
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
257
|
+
revision: session.revision,
|
|
258
|
+
});
|
|
259
|
+
this.eventService.emit({
|
|
260
|
+
kind: 'cache.updated',
|
|
261
|
+
sessionId: session.sessionId,
|
|
262
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
263
|
+
revision: session.revision,
|
|
264
|
+
cacheRevision: cacheEntry.revision,
|
|
265
|
+
});
|
|
163
266
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
164
267
|
sessionId: session.sessionId,
|
|
165
268
|
revision: session.revision,
|
|
166
269
|
});
|
|
167
270
|
} catch (error) {
|
|
168
271
|
this.cacheService.invalidateSession(session);
|
|
169
|
-
this.eventService.emit({
|
|
170
|
-
|
|
171
|
-
code: 'save_partial_failure',
|
|
172
|
-
message: error instanceof Error ? error.message : String(error),
|
|
272
|
+
this.eventService.emit({
|
|
273
|
+
kind: 'save.failed',
|
|
173
274
|
sessionId: session.sessionId,
|
|
174
275
|
canonicalPathKey: session.canonicalPathKey,
|
|
175
|
-
attemptedRevision: session.revision,
|
|
176
|
-
lastSavedRevision: session.lastSavedRevision,
|
|
177
|
-
committedPaths,
|
|
178
|
-
failedPaths,
|
|
179
|
-
diskMayBePartiallyUpdated: true,
|
|
180
|
-
}, toSessionSnapshot(session, this.context.capabilities), {
|
|
181
|
-
sessionId: session.sessionId,
|
|
182
276
|
revision: session.revision,
|
|
183
277
|
});
|
|
278
|
+
return failure(
|
|
279
|
+
'authoring',
|
|
280
|
+
startedAt,
|
|
281
|
+
{
|
|
282
|
+
code: 'save_partial_failure',
|
|
283
|
+
message: error instanceof Error ? error.message : String(error),
|
|
284
|
+
sessionId: session.sessionId,
|
|
285
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
286
|
+
attemptedRevision: session.revision,
|
|
287
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
288
|
+
committedPaths,
|
|
289
|
+
failedPaths,
|
|
290
|
+
diskMayBePartiallyUpdated: true,
|
|
291
|
+
},
|
|
292
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
293
|
+
{
|
|
294
|
+
sessionId: session.sessionId,
|
|
295
|
+
revision: session.revision,
|
|
296
|
+
},
|
|
297
|
+
);
|
|
184
298
|
}
|
|
185
299
|
}
|
|
186
300
|
}
|