@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/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<Stats>;
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: 'deferred';
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: BackendFileSystem;
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 ?? createNodeBackendFileSystem();
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: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError>> {
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<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError>> {
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
- input: { sessionId: string; expectedRevision?: number; targetPath?: string },
567
- ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError>> {
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
- input: { sessionId: string },
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(input: GetEventsInput): BackendResult<GetEventsSnapshot, SessionNotFoundError | EventCursorInvalidError> {
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(input: GetJobInput): BackendResult<BackendJobSnapshot, SessionNotFoundError | BackendJobNotFoundError> {
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(input: CancelJobInput): BackendResult<BackendJobSnapshot, SessionNotFoundError | BackendJobNotFoundError | BackendJobNotCancellableError> {
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: 'deferred',
15
+ status: 'bridge-required',
16
+ publishBridge: bridge,
17
+ restoreBridge: bridge,
8
18
  };
9
19
  }
@@ -1,10 +1,12 @@
1
- import { materializeUamProject, ProjectWriter } from '@openfairygui/core';
2
- import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions';
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
- ): import('@openfairygui/core').FileSystem {
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<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError>> {
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({ kind: 'transaction.rejected', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
93
- return failure('authoring', startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
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({ kind: 'transaction.rejected', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision, diagnostics: result.error.issues?.map((issue) => ({ code: result.error.code, message: issue.message, severity: 'error' as const })) ?? [] });
105
- return failure('authoring', startedAt, result.error, toSessionSnapshot(session, this.context.capabilities), {
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({ kind: 'transaction.applied', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
116
- this.eventService.emit({ kind: 'cache.invalidated', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision, cacheRevision: cacheEntry.revision });
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
- input: { sessionId: string; expectedRevision?: number; targetPath?: string },
126
- ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError>> {
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('authoring', startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
134
- sessionId: session.sessionId,
135
- revision: session.revision,
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 targetViolation = await validateSaveTarget(this.context.fileSystem, session.fairyPath, input.targetPath);
218
+ const fileSystem = this.context.fileSystem;
219
+ const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
139
220
  if (targetViolation) {
140
- return failure('authoring', startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
141
- sessionId: session.sessionId,
142
- revision: session.revision,
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({ kind: 'save.started', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
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(this.context.fileSystem, committedPaths, failedPaths));
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({ kind: 'save.completed', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
162
- this.eventService.emit({ kind: 'cache.updated', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision, cacheRevision: cacheEntry.revision });
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({ kind: 'save.failed', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
170
- return failure('authoring', startedAt, {
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
  }