@openfairygui/backend 0.2.0-alpha.2 → 0.2.0-alpha.21

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.
@@ -1,87 +1,201 @@
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';
4
- import type { CacheService } from './cache-service.js';
5
- import { failure, success, type BackendContext } from './context.js';
6
- import type { EventService } from './event-service.js';
1
+ import type { ProjectSourceFile } from '@openfairygui/core/project-io';
2
+ import {
3
+ commitUamProjectSourcePaths,
4
+ materializeUamProject,
5
+ type UamProject,
6
+ validateUamProject,
7
+ } from '@openfairygui/core/uam';
8
+ import { type ApplyUamTransactionAppError, applyUamTransactionApp } from '@openfairygui/functions/uam';
9
+ import type { BackendDiagnostic } from '../contracts.js';
10
+ import { normalizeComparablePath, type PathPolicyViolationError, validateSaveTarget } from '../path-policy.js';
7
11
  import type {
8
12
  ApplySessionTransactionInput,
9
13
  BackendCapabilityUnavailableError,
10
14
  BackendFileSystem,
11
15
  BackendResult,
12
16
  BackendSessionSnapshot,
17
+ InProcessLockConflictError,
18
+ MaterializeSessionInput,
19
+ MaterializeSessionSnapshot,
20
+ MaterializeValidationFailedError,
21
+ MaterializeWriteFailedError,
13
22
  SavePartialFailureError,
23
+ SaveSessionInput,
14
24
  SessionNotFoundError,
15
25
  SessionStaleWriteError,
26
+ UamFidelityUnsupportedError,
16
27
  } from '../runtime.js';
17
- import { validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
28
+ import type { CacheService } from './cache-service.js';
29
+ import { type BackendContext, failure, success } from './context.js';
30
+ import type { EventService } from './event-service.js';
31
+ import { writeSessionProject } from './session-project-writer.js';
18
32
  import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
19
33
 
20
- function createWriterFileSystem(
21
- fileSystem: BackendFileSystem,
22
- committedPaths: string[],
23
- failedPaths: string[],
24
- ): FileSystem {
25
- async function trackWrite<T>(targetPath: string, fn: () => Promise<T>): Promise<T> {
26
- try {
27
- const result = await fn();
28
- committedPaths.push(targetPath);
29
- return result;
30
- } catch (error) {
31
- failedPaths.push(targetPath);
32
- throw error;
34
+ function projectSourceFiles(project: UamProject): Map<string, ProjectSourceFile> {
35
+ const result = new Map<string, ProjectSourceFile>();
36
+ for (const pkg of project.packages) {
37
+ result.set(`${pkg.id}/package.xml`, {
38
+ packageName: pkg.name,
39
+ branch: '',
40
+ path: '',
41
+ fileName: 'package.xml',
42
+ });
43
+ const branches = new Set<string>();
44
+ for (const resource of pkg.resources) {
45
+ if (resource.branch) branches.add(resource.branch);
46
+ const fileName = resource.kind === 'component'
47
+ ? `${resource.name}.xml`
48
+ : resource.fileName ?? resource.file ?? '';
49
+ if (!fileName) continue;
50
+ result.set(`${pkg.id}/${resource.id}`, {
51
+ packageName: pkg.name,
52
+ branch: resource.branch,
53
+ path: resource.path,
54
+ fileName,
55
+ });
56
+ }
57
+ for (const branch of branches) {
58
+ result.set(`${pkg.id}/branch/${branch}`, {
59
+ packageName: pkg.name,
60
+ branch,
61
+ path: '',
62
+ fileName: 'package_branch.xml',
63
+ });
33
64
  }
34
65
  }
66
+ return result;
67
+ }
68
+
69
+ function sourceFileKey(source: ProjectSourceFile): string {
70
+ return [source.branch, source.packageName, source.path, source.fileName].join('\0');
71
+ }
35
72
 
73
+ function recordStaleProjectFiles(
74
+ session: Parameters<typeof toSessionSnapshot>[0],
75
+ previousProject: UamProject,
76
+ nextProject: UamProject,
77
+ ): void {
78
+ if (!session.fileSystem) return;
79
+ const previousSources = projectSourceFiles(previousProject);
80
+ const nextSourceKeys = new Set([...projectSourceFiles(nextProject).values()].map(sourceFileKey));
81
+ for (const source of previousSources.values()) {
82
+ const key = sourceFileKey(source);
83
+ if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
84
+ }
85
+ for (const key of nextSourceKeys) {
86
+ session.pendingStaleSourceFiles.delete(key);
87
+ }
88
+ }
89
+
90
+ function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagnostic[] {
91
+ return error.diagnostics.length > 0
92
+ ? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
93
+ : [
94
+ {
95
+ code: error.code,
96
+ message: error.message,
97
+ severity: 'error',
98
+ operationKind: error.operationKind,
99
+ opIndex: error.opIndex,
100
+ opId: error.opId,
101
+ },
102
+ ];
103
+ }
104
+
105
+ function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
36
106
  return {
37
- async readFile(filePath: string): Promise<string> {
38
- return fileSystem.readFile(filePath);
39
- },
40
- async readFileRaw(filePath: string): Promise<Uint8Array> {
41
- return fileSystem.readFileRaw(filePath);
42
- },
43
- async writeFile(filePath: string, content: string): Promise<void> {
44
- await trackWrite(filePath, async () => {
45
- await fileSystem.mkdir(fileSystem.dirname(filePath), { recursive: true });
46
- await fileSystem.writeFile(filePath, content);
47
- });
48
- },
49
- async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
50
- await trackWrite(filePath, async () => {
51
- await fileSystem.mkdir(fileSystem.dirname(filePath), { recursive: true });
52
- await fileSystem.writeFileRaw(filePath, data);
53
- });
54
- },
55
- async mkdir(dirPath: string): Promise<void> {
56
- await fileSystem.mkdir(dirPath, { recursive: true });
57
- },
58
- async readdir(dirPath: string): Promise<string[]> {
59
- return fileSystem.readdir(dirPath);
60
- },
61
- async exists(filePath: string): Promise<boolean> {
62
- try {
63
- await fileSystem.stat(filePath);
64
- return true;
65
- } catch {
66
- return false;
67
- }
68
- },
69
- join(...paths: string[]): string {
70
- return fileSystem.join(...paths);
71
- },
72
- dirname(filePath: string): string {
73
- return fileSystem.dirname(filePath);
74
- },
107
+ code: 'capability_unavailable',
108
+ message,
109
+ capability: 'fileSystem',
110
+ requiredAdapter: 'BackendFileSystem',
111
+ };
112
+ }
113
+
114
+ function createUamFidelityUnsupportedError(
115
+ session: Parameters<typeof toSessionSnapshot>[0],
116
+ ): UamFidelityUnsupportedError {
117
+ return {
118
+ code: 'uam_fidelity_unsupported',
119
+ message: 'The source project contains formal properties that the current UAM cannot preserve.',
120
+ sessionId: session.sessionId,
121
+ canonicalPathKey: session.canonicalPathKey,
122
+ };
123
+ }
124
+
125
+ function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
126
+ const issues = validateUamProject(sessionProject);
127
+ return issues.map((issue) => ({
128
+ code: 'materialize_validation_failed',
129
+ message: issue.message,
130
+ severity: 'error',
131
+ path: issue.path,
132
+ operationKind: 'materializeSession',
133
+ }));
134
+ }
135
+
136
+ function toMaterializeSnapshot(
137
+ session: Parameters<typeof toSessionSnapshot>[0],
138
+ capabilities: Parameters<typeof toSessionSnapshot>[1],
139
+ input: {
140
+ reason?: string;
141
+ writtenPaths: string[];
142
+ skippedPaths: string[];
143
+ diagnostics: BackendDiagnostic[];
144
+ },
145
+ ): MaterializeSessionSnapshot {
146
+ return {
147
+ ...toSessionSnapshot(session, capabilities),
148
+ mode: 'fullProject',
149
+ reason: input.reason,
150
+ materializeRevision: session.revision,
151
+ saveRevision: session.lastSavedRevision,
152
+ writtenPaths: [...input.writtenPaths],
153
+ skippedPaths: [...input.skippedPaths],
154
+ diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic })),
155
+ };
156
+ }
157
+
158
+ function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['storage']>): {
159
+ fileSystem: BackendFileSystem;
160
+ fairyPath: string;
161
+ canonicalProjectPath: string;
162
+ canonicalPathKey: string;
163
+ } {
164
+ const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || '.');
165
+ return {
166
+ fileSystem: input.fileSystem,
167
+ fairyPath: input.fairyPath,
168
+ canonicalProjectPath,
169
+ canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath),
75
170
  };
76
171
  }
77
172
 
78
173
  export class AuthoringService {
174
+ private readonly sessionOperations = new Map<string, Promise<void>>();
175
+
79
176
  public constructor(
80
177
  private readonly context: BackendContext,
81
178
  private readonly cacheService: CacheService,
82
179
  private readonly eventService: EventService,
83
180
  ) {}
84
181
 
182
+ private async runSessionExclusive<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
183
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
184
+ let release = (): void => undefined;
185
+ const current = new Promise<void>((resolve) => {
186
+ release = resolve;
187
+ });
188
+ const tail = previous.then(() => current);
189
+ this.sessionOperations.set(sessionId, tail);
190
+ await previous;
191
+ try {
192
+ return await operation();
193
+ } finally {
194
+ release();
195
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
196
+ }
197
+ }
198
+
85
199
  public async applyTransaction(
86
200
  input: ApplySessionTransactionInput,
87
201
  ): Promise<
@@ -89,6 +203,17 @@ export class AuthoringService {
89
203
  BackendSessionSnapshot,
90
204
  SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
91
205
  >
206
+ > {
207
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
208
+ }
209
+
210
+ private async applyTransactionExclusive(
211
+ input: ApplySessionTransactionInput,
212
+ ): Promise<
213
+ BackendResult<
214
+ BackendSessionSnapshot,
215
+ SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
216
+ >
92
217
  > {
93
218
  const startedAt = Date.now();
94
219
  const session = this.context.sessions.get(input.sessionId);
@@ -119,17 +244,13 @@ export class AuthoringService {
119
244
  operations: input.operations,
120
245
  });
121
246
  if (result.ok === false) {
247
+ const diagnostics = toBackendDiagnostics(result.error);
122
248
  this.eventService.emit({
123
249
  kind: 'transaction.rejected',
124
250
  sessionId: session.sessionId,
125
251
  canonicalPathKey: session.canonicalPathKey,
126
252
  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
- })) ?? [],
253
+ diagnostics,
133
254
  });
134
255
  return failure(
135
256
  'authoring',
@@ -139,10 +260,12 @@ export class AuthoringService {
139
260
  {
140
261
  sessionId: session.sessionId,
141
262
  revision: session.revision,
263
+ diagnostics,
142
264
  },
143
265
  );
144
266
  }
145
267
 
268
+ recordStaleProjectFiles(session, session.project, result.project);
146
269
  session.project = result.project;
147
270
  session.revision += 1;
148
271
  session.dirty = true;
@@ -167,17 +290,48 @@ export class AuthoringService {
167
290
  });
168
291
  }
169
292
 
170
- public async saveSession(input: {
171
- sessionId: string;
172
- expectedRevision?: number;
173
- targetPath?: string;
174
- }): Promise<
293
+ public async saveSession(
294
+ input: SaveSessionInput,
295
+ ): Promise<
175
296
  BackendResult<
176
- BackendSessionSnapshot,
297
+ BackendSessionSnapshot | MaterializeSessionSnapshot,
298
+ | SessionNotFoundError
299
+ | SessionStaleWriteError
300
+ | SavePartialFailureError
301
+ | UamFidelityUnsupportedError
302
+ | MaterializeValidationFailedError
303
+ | MaterializeWriteFailedError
304
+ | PathPolicyViolationError
305
+ | InProcessLockConflictError
306
+ | BackendCapabilityUnavailableError
307
+ >
308
+ > {
309
+ if (input.force === true || input.mode === 'materializeCleanSession') {
310
+ return this.materializeSession({
311
+ sessionId: input.sessionId,
312
+ expectedRevision: input.expectedRevision,
313
+ targetPath: input.targetPath,
314
+ fileSystem: input.fileSystem,
315
+ mode: 'fullProject',
316
+ reason: 'force_save',
317
+ });
318
+ }
319
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
320
+ }
321
+
322
+ private async saveSessionExclusive(
323
+ input: SaveSessionInput,
324
+ ): Promise<
325
+ BackendResult<
326
+ BackendSessionSnapshot | MaterializeSessionSnapshot,
177
327
  | SessionNotFoundError
178
328
  | SessionStaleWriteError
179
329
  | SavePartialFailureError
330
+ | UamFidelityUnsupportedError
331
+ | MaterializeValidationFailedError
332
+ | MaterializeWriteFailedError
180
333
  | PathPolicyViolationError
334
+ | InProcessLockConflictError
181
335
  | BackendCapabilityUnavailableError
182
336
  >
183
337
  > {
@@ -186,16 +340,12 @@ export class AuthoringService {
186
340
  if (!session || session.closed) {
187
341
  return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
188
342
  }
189
- if (!this.context.fileSystem) {
343
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
344
+ if (!fileSystem) {
190
345
  return failure(
191
346
  'authoring',
192
347
  startedAt,
193
- {
194
- code: 'capability_unavailable',
195
- message: 'saveSession requires an injected BackendFileSystem adapter.',
196
- capability: 'fileSystem',
197
- requiredAdapter: 'BackendFileSystem',
198
- },
348
+ createCapabilityUnavailableError('saveSession requires an injected BackendFileSystem adapter.'),
199
349
  toSessionSnapshot(session, this.context.capabilities),
200
350
  {
201
351
  sessionId: session.sessionId,
@@ -215,7 +365,6 @@ export class AuthoringService {
215
365
  },
216
366
  );
217
367
  }
218
- const fileSystem = this.context.fileSystem;
219
368
  const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
220
369
  if (targetViolation) {
221
370
  return failure(
@@ -235,6 +384,18 @@ export class AuthoringService {
235
384
  revision: session.revision,
236
385
  });
237
386
  }
387
+ if (session.uamFidelity === 'unsupported') {
388
+ return failure(
389
+ 'authoring',
390
+ startedAt,
391
+ createUamFidelityUnsupportedError(session),
392
+ toSessionSnapshot(session, this.context.capabilities),
393
+ {
394
+ sessionId: session.sessionId,
395
+ revision: session.revision,
396
+ },
397
+ );
398
+ }
238
399
 
239
400
  const committedPaths: string[] = [];
240
401
  const failedPaths: string[] = [];
@@ -245,8 +406,17 @@ export class AuthoringService {
245
406
  revision: session.revision,
246
407
  });
247
408
  try {
248
- const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
249
- await writer.write(materializeUamProject(session.project), session.fairyPath);
409
+ await writeSessionProject({
410
+ fileSystem,
411
+ document: materializeUamProject(session.project),
412
+ fairyPath: session.fairyPath,
413
+ staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
414
+ writtenPaths: committedPaths,
415
+ failedPaths,
416
+ });
417
+ session.fileSystem ??= fileSystem;
418
+ session.pendingStaleSourceFiles.clear();
419
+ commitUamProjectSourcePaths(session.project);
250
420
  session.lastSavedRevision = session.revision;
251
421
  session.dirty = false;
252
422
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -297,4 +467,274 @@ export class AuthoringService {
297
467
  );
298
468
  }
299
469
  }
470
+
471
+ public async materializeSession(
472
+ input: MaterializeSessionInput,
473
+ ): Promise<
474
+ BackendResult<
475
+ MaterializeSessionSnapshot,
476
+ | SessionNotFoundError
477
+ | SessionStaleWriteError
478
+ | UamFidelityUnsupportedError
479
+ | MaterializeValidationFailedError
480
+ | MaterializeWriteFailedError
481
+ | PathPolicyViolationError
482
+ | InProcessLockConflictError
483
+ | BackendCapabilityUnavailableError
484
+ >
485
+ > {
486
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
487
+ }
488
+
489
+ private async materializeSessionExclusive(
490
+ input: MaterializeSessionInput,
491
+ ): Promise<
492
+ BackendResult<
493
+ MaterializeSessionSnapshot,
494
+ | SessionNotFoundError
495
+ | SessionStaleWriteError
496
+ | UamFidelityUnsupportedError
497
+ | MaterializeValidationFailedError
498
+ | MaterializeWriteFailedError
499
+ | PathPolicyViolationError
500
+ | InProcessLockConflictError
501
+ | BackendCapabilityUnavailableError
502
+ >
503
+ > {
504
+ const startedAt = Date.now();
505
+ const session = this.context.sessions.get(input.sessionId);
506
+ if (!session || session.closed) {
507
+ return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
508
+ }
509
+ if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
510
+ return failure(
511
+ 'authoring',
512
+ startedAt,
513
+ createStaleWriteError(session, input.expectedRevision),
514
+ toSessionSnapshot(session, this.context.capabilities),
515
+ {
516
+ sessionId: session.sessionId,
517
+ revision: session.revision,
518
+ },
519
+ );
520
+ }
521
+
522
+ const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
523
+ const fileSystem =
524
+ storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
525
+ if (!fileSystem) {
526
+ return failure(
527
+ 'authoring',
528
+ startedAt,
529
+ createCapabilityUnavailableError('materializeSession requires an injected BackendFileSystem adapter.'),
530
+ toSessionSnapshot(session, this.context.capabilities),
531
+ {
532
+ sessionId: session.sessionId,
533
+ revision: session.revision,
534
+ },
535
+ );
536
+ }
537
+
538
+ const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
539
+ const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
540
+ if (targetViolation) {
541
+ return failure(
542
+ 'authoring',
543
+ startedAt,
544
+ targetViolation,
545
+ toSessionSnapshot(session, this.context.capabilities),
546
+ {
547
+ sessionId: session.sessionId,
548
+ revision: session.revision,
549
+ },
550
+ );
551
+ }
552
+
553
+ if (storageTarget) {
554
+ const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
555
+ if (holderSessionId && holderSessionId !== session.sessionId) {
556
+ return failure(
557
+ 'authoring',
558
+ startedAt,
559
+ {
560
+ code: 'lock_conflict',
561
+ kind: 'in_process_session_exists',
562
+ message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
563
+ canonicalPathKey: storageTarget.canonicalPathKey,
564
+ holderSessionId,
565
+ },
566
+ toSessionSnapshot(session, this.context.capabilities),
567
+ {
568
+ sessionId: session.sessionId,
569
+ revision: session.revision,
570
+ },
571
+ );
572
+ }
573
+ }
574
+
575
+ if (session.uamFidelity === 'unsupported') {
576
+ return failure(
577
+ 'authoring',
578
+ startedAt,
579
+ createUamFidelityUnsupportedError(session),
580
+ toSessionSnapshot(session, this.context.capabilities),
581
+ {
582
+ sessionId: session.sessionId,
583
+ revision: session.revision,
584
+ },
585
+ );
586
+ }
587
+
588
+ const diagnostics = validationDiagnostics(session.project);
589
+ if (diagnostics.length > 0) {
590
+ const error: MaterializeValidationFailedError = {
591
+ code: 'materialize_validation_failed',
592
+ message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
593
+ sessionId: session.sessionId,
594
+ canonicalPathKey: session.canonicalPathKey,
595
+ issueCount: diagnostics.length,
596
+ diagnostics,
597
+ };
598
+ return failure('authoring', startedAt, error, toSessionSnapshot(session, this.context.capabilities), {
599
+ sessionId: session.sessionId,
600
+ revision: session.revision,
601
+ diagnostics,
602
+ });
603
+ }
604
+
605
+ let document: ReturnType<typeof materializeUamProject>;
606
+ try {
607
+ document = materializeUamProject(session.project);
608
+ } catch (error) {
609
+ const diagnosticsFromError: BackendDiagnostic[] = [
610
+ {
611
+ code: 'materialize_validation_failed',
612
+ message: error instanceof Error ? error.message : String(error),
613
+ severity: 'error',
614
+ operationKind: 'materializeSession',
615
+ },
616
+ ];
617
+ return failure(
618
+ 'authoring',
619
+ startedAt,
620
+ {
621
+ code: 'materialize_validation_failed',
622
+ message: error instanceof Error ? error.message : String(error),
623
+ sessionId: session.sessionId,
624
+ canonicalPathKey: session.canonicalPathKey,
625
+ issueCount: diagnosticsFromError.length,
626
+ diagnostics: diagnosticsFromError,
627
+ },
628
+ toSessionSnapshot(session, this.context.capabilities),
629
+ {
630
+ sessionId: session.sessionId,
631
+ revision: session.revision,
632
+ diagnostics: diagnosticsFromError,
633
+ },
634
+ );
635
+ }
636
+
637
+ const writtenPaths: string[] = [];
638
+ const failedPaths: string[] = [];
639
+ const skippedPaths: string[] = [];
640
+ this.eventService.emit({
641
+ kind: 'save.started',
642
+ sessionId: session.sessionId,
643
+ canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
644
+ revision: session.revision,
645
+ });
646
+ try {
647
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
648
+ await writeSessionProject({
649
+ fileSystem,
650
+ document,
651
+ fairyPath,
652
+ staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
653
+ writtenPaths,
654
+ failedPaths,
655
+ });
656
+ if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
657
+ if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
658
+ if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
659
+ if (storageTarget) {
660
+ this.context.sessionsByPath.delete(session.canonicalPathKey);
661
+ session.fileSystem = storageTarget.fileSystem;
662
+ session.fairyPath = storageTarget.fairyPath;
663
+ session.canonicalProjectPath = storageTarget.canonicalProjectPath;
664
+ session.canonicalPathKey = storageTarget.canonicalPathKey;
665
+ this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
666
+ }
667
+ session.lastSavedRevision = session.revision;
668
+ session.dirty = false;
669
+ const cacheEntry = this.cacheService.refreshSession(session);
670
+ this.eventService.emit({
671
+ kind: 'save.completed',
672
+ sessionId: session.sessionId,
673
+ canonicalPathKey: session.canonicalPathKey,
674
+ revision: session.revision,
675
+ });
676
+ this.eventService.emit({
677
+ kind: 'cache.updated',
678
+ sessionId: session.sessionId,
679
+ canonicalPathKey: session.canonicalPathKey,
680
+ revision: session.revision,
681
+ cacheRevision: cacheEntry.revision,
682
+ });
683
+ return success(
684
+ 'authoring',
685
+ startedAt,
686
+ toMaterializeSnapshot(session, this.context.capabilities, {
687
+ reason: input.reason,
688
+ writtenPaths,
689
+ skippedPaths,
690
+ diagnostics: [],
691
+ }),
692
+ {
693
+ sessionId: session.sessionId,
694
+ revision: session.revision,
695
+ },
696
+ );
697
+ } catch (error) {
698
+ const diagnosticsFromError: BackendDiagnostic[] = [
699
+ {
700
+ code: 'write_failed',
701
+ message: error instanceof Error ? error.message : String(error),
702
+ severity: 'error',
703
+ path: failedPaths[0],
704
+ operationKind: 'materializeSession',
705
+ },
706
+ ];
707
+ this.cacheService.invalidateSession(session);
708
+ this.eventService.emit({
709
+ kind: 'save.failed',
710
+ sessionId: session.sessionId,
711
+ canonicalPathKey: session.canonicalPathKey,
712
+ revision: session.revision,
713
+ diagnostics: diagnosticsFromError,
714
+ });
715
+ return failure(
716
+ 'authoring',
717
+ startedAt,
718
+ {
719
+ code: 'write_failed',
720
+ message: error instanceof Error ? error.message : String(error),
721
+ sessionId: session.sessionId,
722
+ canonicalPathKey: session.canonicalPathKey,
723
+ attemptedRevision: session.revision,
724
+ lastSavedRevision: session.lastSavedRevision,
725
+ writtenPaths,
726
+ failedPaths,
727
+ skippedPaths,
728
+ diagnostics: diagnosticsFromError,
729
+ diskMayBePartiallyUpdated: true,
730
+ },
731
+ toSessionSnapshot(session, this.context.capabilities),
732
+ {
733
+ sessionId: session.sessionId,
734
+ revision: session.revision,
735
+ diagnostics: diagnosticsFromError,
736
+ },
737
+ );
738
+ }
739
+ }
300
740
  }