@openfairygui/backend 0.2.0-alpha.13 → 0.2.0-alpha.15

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,9 +1,13 @@
1
- import { ProjectWriter, type FileSystem } from '@openfairygui/core/project-io';
2
- import { materializeUamProject, validateUamProject } 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 FileSystem, type ProjectSourceFile, ProjectWriter } 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,
@@ -15,13 +19,15 @@ import type {
15
19
  MaterializeSessionSnapshot,
16
20
  MaterializeValidationFailedError,
17
21
  MaterializeWriteFailedError,
18
- SaveSessionInput,
19
22
  SavePartialFailureError,
23
+ SaveSessionInput,
20
24
  SessionNotFoundError,
21
25
  SessionStaleWriteError,
26
+ UamFidelityUnsupportedError,
22
27
  } from '../runtime.js';
23
- import type { BackendDiagnostic } from '../contracts.js';
24
- import { normalizeComparablePath, 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';
25
31
  import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
26
32
 
27
33
  function createWriterFileSystem(
@@ -79,22 +85,64 @@ function createWriterFileSystem(
79
85
  dirname(filePath: string): string {
80
86
  return fileSystem.dirname(filePath);
81
87
  },
88
+ async unlink(filePath: string): Promise<void> {
89
+ await trackWrite(filePath, () => fileSystem.unlink(filePath));
90
+ },
82
91
  };
83
92
  }
84
93
 
94
+ function projectAssetSourceFiles(project: UamProject): Map<string, ProjectSourceFile> {
95
+ const result = new Map<string, ProjectSourceFile>();
96
+ for (const pkg of project.packages) {
97
+ for (const resource of pkg.resources) {
98
+ if (resource.kind === 'component') continue;
99
+ const fileName = resource.fileName ?? resource.file ?? '';
100
+ if (!fileName) continue;
101
+ result.set(`${pkg.id}/${resource.id}`, {
102
+ packageName: pkg.name,
103
+ branch: resource.branch,
104
+ path: resource.path,
105
+ fileName,
106
+ });
107
+ }
108
+ }
109
+ return result;
110
+ }
111
+
112
+ function sourceFileKey(source: ProjectSourceFile): string {
113
+ return [source.branch, source.packageName, source.path, source.fileName].join('\0');
114
+ }
115
+
116
+ function recordStaleResourceSources(
117
+ session: Parameters<typeof toSessionSnapshot>[0],
118
+ previousProject: UamProject,
119
+ nextProject: UamProject,
120
+ ): void {
121
+ if (!session.fileSystem) return;
122
+ const previousSources = projectAssetSourceFiles(previousProject);
123
+ const nextSourceKeys = new Set([...projectAssetSourceFiles(nextProject).values()].map(sourceFileKey));
124
+ for (const source of previousSources.values()) {
125
+ const key = sourceFileKey(source);
126
+ if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
127
+ }
128
+ for (const key of nextSourceKeys) {
129
+ session.pendingStaleSourceFiles.delete(key);
130
+ }
131
+ }
132
+
85
133
  function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagnostic[] {
86
134
  return error.diagnostics.length > 0
87
135
  ? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
88
136
  : [
89
- {
90
- code: error.code,
91
- message: error.message,
92
- severity: 'error',
93
- operationKind: error.operationKind,
94
- opIndex: error.opIndex,
95
- opId: error.opId,
96
- },
97
- ];
137
+ {
138
+ code: error.code,
139
+ message: error.message,
140
+ severity: 'error',
141
+ operationKind: error.operationKind,
142
+ opIndex: error.opIndex,
143
+ opId: error.opId,
144
+ },
145
+ ];
98
146
  }
99
147
 
100
148
  function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
@@ -106,6 +154,17 @@ function createCapabilityUnavailableError(message: string): BackendCapabilityUna
106
154
  };
107
155
  }
108
156
 
157
+ function createUamFidelityUnsupportedError(
158
+ session: Parameters<typeof toSessionSnapshot>[0],
159
+ ): UamFidelityUnsupportedError {
160
+ return {
161
+ code: 'uam_fidelity_unsupported',
162
+ message: 'The source project contains formal properties that the current UAM cannot preserve.',
163
+ sessionId: session.sessionId,
164
+ canonicalPathKey: session.canonicalPathKey,
165
+ };
166
+ }
167
+
109
168
  function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
110
169
  const issues = validateUamProject(sessionProject);
111
170
  return issues.map((issue) => ({
@@ -155,12 +214,31 @@ function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['stor
155
214
  }
156
215
 
157
216
  export class AuthoringService {
217
+ private readonly sessionOperations = new Map<string, Promise<void>>();
218
+
158
219
  public constructor(
159
220
  private readonly context: BackendContext,
160
221
  private readonly cacheService: CacheService,
161
222
  private readonly eventService: EventService,
162
223
  ) {}
163
224
 
225
+ private async runSessionExclusive<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
226
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
227
+ let release = (): void => undefined;
228
+ const current = new Promise<void>((resolve) => {
229
+ release = resolve;
230
+ });
231
+ const tail = previous.then(() => current);
232
+ this.sessionOperations.set(sessionId, tail);
233
+ await previous;
234
+ try {
235
+ return await operation();
236
+ } finally {
237
+ release();
238
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
239
+ }
240
+ }
241
+
164
242
  public async applyTransaction(
165
243
  input: ApplySessionTransactionInput,
166
244
  ): Promise<
@@ -168,6 +246,17 @@ export class AuthoringService {
168
246
  BackendSessionSnapshot,
169
247
  SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
170
248
  >
249
+ > {
250
+ return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
251
+ }
252
+
253
+ private async applyTransactionExclusive(
254
+ input: ApplySessionTransactionInput,
255
+ ): Promise<
256
+ BackendResult<
257
+ BackendSessionSnapshot,
258
+ SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
259
+ >
171
260
  > {
172
261
  const startedAt = Date.now();
173
262
  const session = this.context.sessions.get(input.sessionId);
@@ -219,6 +308,7 @@ export class AuthoringService {
219
308
  );
220
309
  }
221
310
 
311
+ recordStaleResourceSources(session, session.project, result.project);
222
312
  session.project = result.project;
223
313
  session.revision += 1;
224
314
  session.dirty = true;
@@ -243,12 +333,15 @@ export class AuthoringService {
243
333
  });
244
334
  }
245
335
 
246
- public async saveSession(input: SaveSessionInput): Promise<
336
+ public async saveSession(
337
+ input: SaveSessionInput,
338
+ ): Promise<
247
339
  BackendResult<
248
340
  BackendSessionSnapshot | MaterializeSessionSnapshot,
249
341
  | SessionNotFoundError
250
342
  | SessionStaleWriteError
251
343
  | SavePartialFailureError
344
+ | UamFidelityUnsupportedError
252
345
  | MaterializeValidationFailedError
253
346
  | MaterializeWriteFailedError
254
347
  | PathPolicyViolationError
@@ -266,19 +359,36 @@ export class AuthoringService {
266
359
  reason: 'force_save',
267
360
  });
268
361
  }
362
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
363
+ }
364
+
365
+ private async saveSessionExclusive(
366
+ input: SaveSessionInput,
367
+ ): Promise<
368
+ BackendResult<
369
+ BackendSessionSnapshot | MaterializeSessionSnapshot,
370
+ | SessionNotFoundError
371
+ | SessionStaleWriteError
372
+ | SavePartialFailureError
373
+ | UamFidelityUnsupportedError
374
+ | MaterializeValidationFailedError
375
+ | MaterializeWriteFailedError
376
+ | PathPolicyViolationError
377
+ | InProcessLockConflictError
378
+ | BackendCapabilityUnavailableError
379
+ >
380
+ > {
269
381
  const startedAt = Date.now();
270
382
  const session = this.context.sessions.get(input.sessionId);
271
383
  if (!session || session.closed) {
272
384
  return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
273
385
  }
274
- const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
386
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
275
387
  if (!fileSystem) {
276
388
  return failure(
277
389
  'authoring',
278
390
  startedAt,
279
- createCapabilityUnavailableError(
280
- 'saveSession requires an injected BackendFileSystem adapter.',
281
- ),
391
+ createCapabilityUnavailableError('saveSession requires an injected BackendFileSystem adapter.'),
282
392
  toSessionSnapshot(session, this.context.capabilities),
283
393
  {
284
394
  sessionId: session.sessionId,
@@ -317,6 +427,18 @@ export class AuthoringService {
317
427
  revision: session.revision,
318
428
  });
319
429
  }
430
+ if (session.uamFidelity === 'unsupported') {
431
+ return failure(
432
+ 'authoring',
433
+ startedAt,
434
+ createUamFidelityUnsupportedError(session),
435
+ toSessionSnapshot(session, this.context.capabilities),
436
+ {
437
+ sessionId: session.sessionId,
438
+ revision: session.revision,
439
+ },
440
+ );
441
+ }
320
442
 
321
443
  const committedPaths: string[] = [];
322
444
  const failedPaths: string[] = [];
@@ -328,7 +450,12 @@ export class AuthoringService {
328
450
  });
329
451
  try {
330
452
  const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
331
- await writer.write(materializeUamProject(session.project), session.fairyPath);
453
+ await writer.write(materializeUamProject(session.project), session.fairyPath, {
454
+ staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
455
+ });
456
+ session.fileSystem ??= fileSystem;
457
+ session.pendingStaleSourceFiles.clear();
458
+ commitUamProjectSourcePaths(session.project);
332
459
  session.lastSavedRevision = session.revision;
333
460
  session.dirty = false;
334
461
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -380,11 +507,32 @@ export class AuthoringService {
380
507
  }
381
508
  }
382
509
 
383
- public async materializeSession(input: MaterializeSessionInput): Promise<
510
+ public async materializeSession(
511
+ input: MaterializeSessionInput,
512
+ ): Promise<
513
+ BackendResult<
514
+ MaterializeSessionSnapshot,
515
+ | SessionNotFoundError
516
+ | SessionStaleWriteError
517
+ | UamFidelityUnsupportedError
518
+ | MaterializeValidationFailedError
519
+ | MaterializeWriteFailedError
520
+ | PathPolicyViolationError
521
+ | InProcessLockConflictError
522
+ | BackendCapabilityUnavailableError
523
+ >
524
+ > {
525
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
526
+ }
527
+
528
+ private async materializeSessionExclusive(
529
+ input: MaterializeSessionInput,
530
+ ): Promise<
384
531
  BackendResult<
385
532
  MaterializeSessionSnapshot,
386
533
  | SessionNotFoundError
387
534
  | SessionStaleWriteError
535
+ | UamFidelityUnsupportedError
388
536
  | MaterializeValidationFailedError
389
537
  | MaterializeWriteFailedError
390
538
  | PathPolicyViolationError
@@ -411,14 +559,13 @@ export class AuthoringService {
411
559
  }
412
560
 
413
561
  const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
414
- const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
562
+ const fileSystem =
563
+ storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
415
564
  if (!fileSystem) {
416
565
  return failure(
417
566
  'authoring',
418
567
  startedAt,
419
- createCapabilityUnavailableError(
420
- 'materializeSession requires an injected BackendFileSystem adapter.',
421
- ),
568
+ createCapabilityUnavailableError('materializeSession requires an injected BackendFileSystem adapter.'),
422
569
  toSessionSnapshot(session, this.context.capabilities),
423
570
  {
424
571
  sessionId: session.sessionId,
@@ -464,6 +611,19 @@ export class AuthoringService {
464
611
  }
465
612
  }
466
613
 
614
+ if (session.uamFidelity === 'unsupported') {
615
+ return failure(
616
+ 'authoring',
617
+ startedAt,
618
+ createUamFidelityUnsupportedError(session),
619
+ toSessionSnapshot(session, this.context.capabilities),
620
+ {
621
+ sessionId: session.sessionId,
622
+ revision: session.revision,
623
+ },
624
+ );
625
+ }
626
+
467
627
  const diagnostics = validationDiagnostics(session.project);
468
628
  if (diagnostics.length > 0) {
469
629
  const error: MaterializeValidationFailedError = {
@@ -474,17 +634,11 @@ export class AuthoringService {
474
634
  issueCount: diagnostics.length,
475
635
  diagnostics,
476
636
  };
477
- return failure(
478
- 'authoring',
479
- startedAt,
480
- error,
481
- toSessionSnapshot(session, this.context.capabilities),
482
- {
483
- sessionId: session.sessionId,
484
- revision: session.revision,
485
- diagnostics,
486
- },
487
- );
637
+ return failure('authoring', startedAt, error, toSessionSnapshot(session, this.context.capabilities), {
638
+ sessionId: session.sessionId,
639
+ revision: session.revision,
640
+ diagnostics,
641
+ });
488
642
  }
489
643
 
490
644
  let document: ReturnType<typeof materializeUamProject>;
@@ -530,7 +684,13 @@ export class AuthoringService {
530
684
  });
531
685
  try {
532
686
  const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
533
- await writer.write(document, fairyPath);
687
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
688
+ await writer.write(document, fairyPath, {
689
+ staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
690
+ });
691
+ if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
692
+ if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
693
+ if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
534
694
  if (storageTarget) {
535
695
  this.context.sessionsByPath.delete(session.canonicalPathKey);
536
696
  session.fileSystem = storageTarget.fileSystem;
@@ -1,3 +1,11 @@
1
+ import {
2
+ BACKEND_CAPABILITY_SCHEMA_VERSION,
3
+ BACKEND_CONTRACT_VERSION,
4
+ type BackendDiagnostic,
5
+ type BackendMessage,
6
+ type BackendResponseMeta,
7
+ type BackendStage,
8
+ } from '../contracts.js';
1
9
  import type {
2
10
  BackendCacheEntry,
3
11
  BackendCapabilities,
@@ -10,14 +18,6 @@ import type {
10
18
  BackendSessionSnapshot,
11
19
  BackendSuccess,
12
20
  } from '../runtime.js';
13
- import {
14
- BACKEND_CAPABILITY_SCHEMA_VERSION,
15
- BACKEND_CONTRACT_VERSION,
16
- type BackendDiagnostic,
17
- type BackendMessage,
18
- type BackendResponseMeta,
19
- type BackendStage,
20
- } from '../contracts.js';
21
21
 
22
22
  export interface BackendSessionState {
23
23
  sessionId: string;
@@ -27,8 +27,11 @@ export interface BackendSessionState {
27
27
  lockFilePath: string;
28
28
  fileSystem?: BackendFileSystem;
29
29
  project: import('@openfairygui/core/uam').UamProject;
30
+ uamFidelity: 'full' | 'unsupported';
30
31
  revision: number;
31
32
  lastSavedRevision: number;
33
+ /** Source files deferred until a successful replacement project write. */
34
+ pendingStaleSourceFiles: Map<string, import('@openfairygui/core/project-io').ProjectSourceFile>;
32
35
  dirty: boolean;
33
36
  lockHeld: boolean;
34
37
  closed: boolean;
@@ -1,20 +1,25 @@
1
- import { ProjectReader, type FileSystem } from '@openfairygui/core/project-io';
2
- import { liftDocumentToUamProject, normalizeUamProject } from '@openfairygui/core/uam';
3
- import { failure, success, type BackendContext, type BackendSessionState } from './context.js';
4
- import type { CacheService } from './cache-service.js';
5
- import type { EventService } from './event-service.js';
6
- import type { JobService } from './job-service.js';
1
+ import { type FileSystem, ProjectReader, ProjectWriter } from '@openfairygui/core/project-io';
2
+ import {
3
+ liftDocumentToUamProject,
4
+ materializeUamProject,
5
+ normalizeUamProject,
6
+ type UamProject,
7
+ } from '@openfairygui/core/uam';
8
+ import { normalizeComparablePath, resolveCanonicalProjectRoot } from '../path-policy.js';
7
9
  import type {
8
10
  AdvisoryLockConflictError,
11
+ BackendCapabilityUnavailableError,
9
12
  BackendFileHandle,
10
13
  BackendResult,
11
14
  BackendSessionSnapshot,
12
- BackendCapabilityUnavailableError,
13
15
  InProcessLockConflictError,
14
16
  OpenProjectSessionInput,
15
17
  SessionNotFoundError,
16
18
  } from '../runtime.js';
17
- import { normalizeComparablePath, resolveCanonicalProjectRoot } from '../path-policy.js';
19
+ import type { CacheService } from './cache-service.js';
20
+ import { type BackendContext, type BackendSessionState, failure, success } from './context.js';
21
+ import type { EventService } from './event-service.js';
22
+ import type { JobService } from './job-service.js';
18
23
  import { createSessionNotFoundError, toSessionSnapshot } from './session-utils.js';
19
24
 
20
25
  function randomId(): string {
@@ -74,6 +79,82 @@ function createProjectReaderFileSystem(fileSystem: NonNullable<BackendContext['f
74
79
  };
75
80
  }
76
81
 
82
+ function createCaptureFileSystem(files: Map<string, string | Uint8Array>): FileSystem {
83
+ const normalize = (filePath: string): string => filePath.replace(/\\/g, '/').replace(/\/+/g, '/');
84
+ return {
85
+ async readFile(filePath: string): Promise<string> {
86
+ const value = files.get(normalize(filePath));
87
+ if (typeof value !== 'string') throw new Error(`Captured text file was not found: ${filePath}`);
88
+ return value;
89
+ },
90
+ async readFileRaw(filePath: string): Promise<Uint8Array> {
91
+ const value = files.get(normalize(filePath));
92
+ if (!(value instanceof Uint8Array)) throw new Error(`Captured binary file was not found: ${filePath}`);
93
+ return value.slice();
94
+ },
95
+ async writeFile(filePath: string, content: string): Promise<void> {
96
+ files.set(normalize(filePath), content);
97
+ },
98
+ async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
99
+ files.set(normalize(filePath), data.slice());
100
+ },
101
+ async mkdir(): Promise<void> {},
102
+ async readdir(): Promise<string[]> {
103
+ return [];
104
+ },
105
+ async exists(filePath: string): Promise<boolean> {
106
+ return files.has(normalize(filePath));
107
+ },
108
+ join(...paths: string[]): string {
109
+ return normalize(paths.filter(Boolean).join('/'));
110
+ },
111
+ dirname(filePath: string): string {
112
+ const normalized = normalize(filePath);
113
+ const separator = normalized.lastIndexOf('/');
114
+ return separator < 0 ? '' : normalized.slice(0, separator);
115
+ },
116
+ async unlink(filePath: string): Promise<void> {
117
+ files.delete(normalize(filePath));
118
+ },
119
+ };
120
+ }
121
+
122
+ function capturedFilesEqual(left: Map<string, string | Uint8Array>, right: Map<string, string | Uint8Array>): boolean {
123
+ if (left.size !== right.size) return false;
124
+ for (const [filePath, leftValue] of left) {
125
+ const rightValue = right.get(filePath);
126
+ if (typeof leftValue === 'string') {
127
+ if (leftValue !== rightValue) return false;
128
+ continue;
129
+ }
130
+ if (!(rightValue instanceof Uint8Array) || leftValue.length !== rightValue.length) return false;
131
+ for (let index = 0; index < leftValue.length; index += 1) {
132
+ if (leftValue[index] !== rightValue[index]) return false;
133
+ }
134
+ }
135
+ return true;
136
+ }
137
+
138
+ async function hasFullUamFidelity(
139
+ document: Awaited<ReturnType<ProjectReader['read']>>,
140
+ project: UamProject,
141
+ ): Promise<boolean> {
142
+ const sourceFiles = new Map<string, string | Uint8Array>();
143
+ const materializedFiles = new Map<string, string | Uint8Array>();
144
+ try {
145
+ await Promise.all([
146
+ new ProjectWriter(createCaptureFileSystem(sourceFiles)).write(document, 'Project.fairy'),
147
+ new ProjectWriter(createCaptureFileSystem(materializedFiles)).write(
148
+ materializeUamProject(project),
149
+ 'Project.fairy',
150
+ ),
151
+ ]);
152
+ } catch {
153
+ return false;
154
+ }
155
+ return capturedFilesEqual(sourceFiles, materializedFiles);
156
+ }
157
+
77
158
  export class RuntimeService {
78
159
  public constructor(
79
160
  private readonly context: BackendContext,
@@ -129,7 +210,8 @@ export class RuntimeService {
129
210
  await advisoryLock.close();
130
211
 
131
212
  const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
132
- const project = liftDocumentToUamProject(await reader.read(fairyPath));
213
+ const document = await reader.read(fairyPath, { hydrateResourceBytes: true });
214
+ const project = liftDocumentToUamProject(document);
133
215
  const sessionId = randomId();
134
216
  const session: BackendSessionState = {
135
217
  sessionId,
@@ -139,8 +221,10 @@ export class RuntimeService {
139
221
  lockFilePath,
140
222
  fileSystem,
141
223
  project,
224
+ uamFidelity: (await hasFullUamFidelity(document, project)) ? 'full' : 'unsupported',
142
225
  revision: 0,
143
226
  lastSavedRevision: 0,
227
+ pendingStaleSourceFiles: new Map(),
144
228
  dirty: false,
145
229
  lockHeld: true,
146
230
  closed: false,
@@ -177,12 +261,14 @@ export class RuntimeService {
177
261
  const sessionId = input.sessionId ?? randomId();
178
262
  const storage = input.storage;
179
263
  const memoryProjectPath = `memory://${sessionId}`;
180
- const canonicalProjectPath = storage?.canonicalProjectPath
181
- ?? input.canonicalProjectPath
182
- ?? (storage ? storage.fileSystem.dirname(storage.fairyPath) || '.' : memoryProjectPath);
183
- const canonicalPathKey = storage?.canonicalPathKey
184
- ?? input.canonicalPathKey
185
- ?? (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
264
+ const canonicalProjectPath =
265
+ storage?.canonicalProjectPath ??
266
+ input.canonicalProjectPath ??
267
+ (storage ? storage.fileSystem.dirname(storage.fairyPath) || '.' : memoryProjectPath);
268
+ const canonicalPathKey =
269
+ storage?.canonicalPathKey ??
270
+ input.canonicalPathKey ??
271
+ (storage ? normalizeComparablePath(canonicalProjectPath) : canonicalProjectPath.toLowerCase());
186
272
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
187
273
  if (existingSessionId) {
188
274
  return failure('runtime', startedAt, {
@@ -202,8 +288,10 @@ export class RuntimeService {
202
288
  lockFilePath: '',
203
289
  fileSystem: storage?.fileSystem,
204
290
  project: normalizeUamProject(input.project),
291
+ uamFidelity: 'full',
205
292
  revision: 0,
206
293
  lastSavedRevision: 0,
294
+ pendingStaleSourceFiles: new Map(),
207
295
  dirty: false,
208
296
  lockHeld: false,
209
297
  closed: false,
@@ -7,13 +7,17 @@ import type {
7
7
  import type { BackendSessionState } from './context.js';
8
8
  import { cloneCapabilitiesSnapshot } from './snapshot-utils.js';
9
9
 
10
- export function toSessionSnapshot(session: BackendSessionState, capabilities: BackendCapabilities): BackendSessionSnapshot {
10
+ export function toSessionSnapshot(
11
+ session: BackendSessionState,
12
+ capabilities: BackendCapabilities,
13
+ ): BackendSessionSnapshot {
11
14
  return {
12
15
  sessionId: session.sessionId,
13
16
  canonicalProjectPath: session.canonicalProjectPath,
14
17
  revision: session.revision,
15
18
  lastSavedRevision: session.lastSavedRevision,
16
19
  dirty: session.dirty,
20
+ uamFidelity: session.uamFidelity,
17
21
  lockHeld: session.lockHeld,
18
22
  capabilities: cloneCapabilitiesSnapshot(capabilities),
19
23
  };
package/src/storage.ts CHANGED
@@ -21,7 +21,7 @@ export interface BackendAsyncStorageAdapter {
21
21
  stat?(filePath: string): Promise<BackendStorageStatLike>;
22
22
  resolvePath?(filePath: string): Promise<string>;
23
23
  openExclusive?(filePath: string): Promise<BackendFileHandle>;
24
- unlink?(filePath: string): Promise<void>;
24
+ unlink(filePath: string): Promise<void>;
25
25
  join?(...paths: string[]): string;
26
26
  dirname?(filePath: string): string;
27
27
  resolve?(...paths: string[]): string;
@@ -114,6 +114,9 @@ async function inferStat(storage: BackendAsyncStorageAdapter, filePath: string):
114
114
  export type BackendStorageFileSystem = BackendFileSystem & CoreProjectFileSystem;
115
115
 
116
116
  export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapter): BackendStorageFileSystem {
117
+ if (typeof storage.unlink !== 'function') {
118
+ throw createPathError('ENOTSUP', 'Storage adapter must provide unlink() for project resource lifecycle writes.');
119
+ }
117
120
  const lockedPaths = new Set<string>();
118
121
 
119
122
  const fileSystem: BackendStorageFileSystem = {
@@ -174,8 +177,7 @@ export function createBackendStorageFileSystem(storage: BackendAsyncStorageAdapt
174
177
  unlink(filePath: string): Promise<void> {
175
178
  const resolved = fileSystem.resolve(filePath);
176
179
  lockedPaths.delete(resolved);
177
- if (storage.unlink) return storage.unlink(resolved);
178
- throw createPathError('ENOTSUP', 'Storage adapter does not provide unlink().');
180
+ return storage.unlink(resolved);
179
181
  },
180
182
  join(...paths: string[]): string {
181
183
  return storage.join ? storage.join(...paths) : joinStoragePath(...paths);