@openfairygui/backend 0.2.0-alpha.9 → 0.2.0

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,16 @@
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 { ProjectBranchDirectory, ProjectResourceFolder, ProjectSourceFile } from '@openfairygui/core/project-io';
2
+ import {
3
+ commitUamProjectSourcePaths,
4
+ materializeUamProject,
5
+ staleBranchDirectories,
6
+ staleResourceFolders,
7
+ staleSourceFiles,
8
+ type UamProject,
9
+ validateUamProject,
10
+ } from '@openfairygui/core/uam';
11
+ import { type ApplyUamTransactionAppError, applyUamTransactionAppAsync } from '@openfairygui/functions/uam';
12
+ import type { BackendDiagnostic } from '../contracts.js';
13
+ import { normalizeComparablePath, type PathPolicyViolationError, validateSaveTarget } from '../path-policy.js';
7
14
  import type {
8
15
  ApplySessionTransactionInput,
9
16
  BackendCapabilityUnavailableError,
@@ -15,86 +22,69 @@ import type {
15
22
  MaterializeSessionSnapshot,
16
23
  MaterializeValidationFailedError,
17
24
  MaterializeWriteFailedError,
18
- SaveSessionInput,
19
25
  SavePartialFailureError,
26
+ SaveSessionInput,
20
27
  SessionNotFoundError,
21
28
  SessionStaleWriteError,
29
+ UamFidelityUnsupportedError,
22
30
  } from '../runtime.js';
23
- import type { BackendDiagnostic } from '../contracts.js';
24
- import { normalizeComparablePath, validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
31
+ import type { CacheService } from './cache-service.js';
32
+ import { type BackendContext, failure, success } from './context.js';
33
+ import type { EventService } from './event-service.js';
34
+ import { writeSessionProject } from './session-project-writer.js';
25
35
  import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
26
36
 
27
- function createWriterFileSystem(
28
- fileSystem: BackendFileSystem,
29
- committedPaths: string[],
30
- failedPaths: string[],
31
- ): FileSystem {
32
- async function trackWrite<T>(targetPath: string, fn: () => Promise<T>): Promise<T> {
33
- try {
34
- const result = await fn();
35
- committedPaths.push(targetPath);
36
- return result;
37
- } catch (error) {
38
- failedPaths.push(targetPath);
39
- throw error;
40
- }
41
- }
37
+ function sourceFileKey(source: ProjectSourceFile): string {
38
+ return [source.branch, source.packageName, source.path, source.fileName].join('\0');
39
+ }
42
40
 
43
- return {
44
- async readFile(filePath: string): Promise<string> {
45
- return fileSystem.readFile(filePath);
46
- },
47
- async readFileRaw(filePath: string): Promise<Uint8Array> {
48
- return fileSystem.readFileRaw(filePath);
49
- },
50
- async writeFile(filePath: string, content: string): Promise<void> {
51
- await trackWrite(filePath, async () => {
52
- await fileSystem.mkdir(fileSystem.dirname(filePath), { recursive: true });
53
- await fileSystem.writeFile(filePath, content);
54
- });
55
- },
56
- async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
57
- await trackWrite(filePath, async () => {
58
- await fileSystem.mkdir(fileSystem.dirname(filePath), { recursive: true });
59
- await fileSystem.writeFileRaw(filePath, data);
60
- });
61
- },
62
- async mkdir(dirPath: string): Promise<void> {
63
- await fileSystem.mkdir(dirPath, { recursive: true });
64
- },
65
- async readdir(dirPath: string): Promise<string[]> {
66
- return fileSystem.readdir(dirPath);
67
- },
68
- async exists(filePath: string): Promise<boolean> {
69
- try {
70
- await fileSystem.stat(filePath);
71
- return true;
72
- } catch {
73
- return false;
74
- }
75
- },
76
- join(...paths: string[]): string {
77
- return fileSystem.join(...paths);
78
- },
79
- dirname(filePath: string): string {
80
- return fileSystem.dirname(filePath);
81
- },
82
- };
41
+ function resourceFolderKey(folder: ProjectResourceFolder): string {
42
+ return [folder.branch, folder.packageName, folder.path].join('\0');
43
+ }
44
+
45
+ function branchDirectoryKey(directory: ProjectBranchDirectory): string {
46
+ return [directory.branch, directory.packageName ?? ''].join('\0');
47
+ }
48
+
49
+ function recordStaleProjectFiles(
50
+ session: Parameters<typeof toSessionSnapshot>[0],
51
+ previousProject: UamProject,
52
+ nextProject: UamProject,
53
+ ): void {
54
+ if (!session.fileSystem) return;
55
+ for (const source of staleSourceFiles(previousProject, nextProject)) {
56
+ session.pendingStaleSourceFiles.set(sourceFileKey(source), source);
57
+ }
58
+ for (const source of staleSourceFiles(nextProject, previousProject)) {
59
+ session.pendingStaleSourceFiles.delete(sourceFileKey(source));
60
+ }
61
+ for (const folder of staleResourceFolders(previousProject, nextProject)) {
62
+ session.pendingStaleResourceFolders.set(resourceFolderKey(folder), folder);
63
+ }
64
+ for (const folder of staleResourceFolders(nextProject, previousProject)) {
65
+ session.pendingStaleResourceFolders.delete(resourceFolderKey(folder));
66
+ }
67
+ for (const directory of staleBranchDirectories(previousProject, nextProject)) {
68
+ session.pendingStaleBranchDirectories.set(branchDirectoryKey(directory), directory);
69
+ }
70
+ for (const directory of staleBranchDirectories(nextProject, previousProject)) {
71
+ session.pendingStaleBranchDirectories.delete(branchDirectoryKey(directory));
72
+ }
83
73
  }
84
74
 
85
75
  function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagnostic[] {
86
76
  return error.diagnostics.length > 0
87
77
  ? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
88
78
  : [
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
- ];
79
+ {
80
+ code: error.code,
81
+ message: error.message,
82
+ severity: 'error',
83
+ operationKind: error.operationKind,
84
+ opIndex: error.opIndex,
85
+ opId: error.opId,
86
+ },
87
+ ];
98
88
  }
99
89
 
100
90
  function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
@@ -106,6 +96,17 @@ function createCapabilityUnavailableError(message: string): BackendCapabilityUna
106
96
  };
107
97
  }
108
98
 
99
+ function createUamFidelityUnsupportedError(
100
+ session: Parameters<typeof toSessionSnapshot>[0],
101
+ ): UamFidelityUnsupportedError {
102
+ return {
103
+ code: 'uam_fidelity_unsupported',
104
+ message: 'The source project contains formal properties that the current UAM cannot preserve.',
105
+ sessionId: session.sessionId,
106
+ canonicalPathKey: session.canonicalPathKey,
107
+ };
108
+ }
109
+
109
110
  function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
110
111
  const issues = validateUamProject(sessionProject);
111
112
  return issues.map((issue) => ({
@@ -154,13 +155,46 @@ function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['stor
154
155
  };
155
156
  }
156
157
 
158
+ function detachSharedByteViews(value: unknown, seen = new WeakSet<object>()): void {
159
+ if (!value || typeof value !== 'object' || seen.has(value)) return;
160
+ seen.add(value);
161
+ for (const [key, child] of Object.entries(value)) {
162
+ if (child instanceof Uint8Array) {
163
+ if (typeof SharedArrayBuffer !== 'undefined' && child.buffer instanceof SharedArrayBuffer) {
164
+ (value as Record<string, unknown>)[key] = new Uint8Array(child);
165
+ }
166
+ continue;
167
+ }
168
+ detachSharedByteViews(child, seen);
169
+ }
170
+ }
171
+
157
172
  export class AuthoringService {
173
+ private readonly sessionOperations = new Map<string, Promise<void>>();
174
+
158
175
  public constructor(
159
176
  private readonly context: BackendContext,
160
177
  private readonly cacheService: CacheService,
161
178
  private readonly eventService: EventService,
162
179
  ) {}
163
180
 
181
+ private async runSessionExclusive<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
182
+ const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
183
+ let release = (): void => undefined;
184
+ const current = new Promise<void>((resolve) => {
185
+ release = resolve;
186
+ });
187
+ const tail = previous.then(() => current);
188
+ this.sessionOperations.set(sessionId, tail);
189
+ await previous;
190
+ try {
191
+ return await operation();
192
+ } finally {
193
+ release();
194
+ if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
195
+ }
196
+ }
197
+
164
198
  public async applyTransaction(
165
199
  input: ApplySessionTransactionInput,
166
200
  ): Promise<
@@ -168,6 +202,19 @@ export class AuthoringService {
168
202
  BackendSessionSnapshot,
169
203
  SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
170
204
  >
205
+ > {
206
+ const queuedInput = structuredClone(input);
207
+ detachSharedByteViews(queuedInput);
208
+ return this.runSessionExclusive(queuedInput.sessionId, () => this.applyTransactionExclusive(queuedInput));
209
+ }
210
+
211
+ private async applyTransactionExclusive(
212
+ input: ApplySessionTransactionInput,
213
+ ): Promise<
214
+ BackendResult<
215
+ BackendSessionSnapshot,
216
+ SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
217
+ >
171
218
  > {
172
219
  const startedAt = Date.now();
173
220
  const session = this.context.sessions.get(input.sessionId);
@@ -193,10 +240,13 @@ export class AuthoringService {
193
240
  );
194
241
  }
195
242
 
196
- const result = applyUamTransactionApp({
243
+ const result = await applyUamTransactionAppAsync({
197
244
  project: session.project,
198
245
  operations: input.operations,
199
246
  });
247
+ if (this.context.sessions.get(input.sessionId) !== session || session.closed) {
248
+ return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
249
+ }
200
250
  if (result.ok === false) {
201
251
  const diagnostics = toBackendDiagnostics(result.error);
202
252
  this.eventService.emit({
@@ -219,6 +269,7 @@ export class AuthoringService {
219
269
  );
220
270
  }
221
271
 
272
+ recordStaleProjectFiles(session, session.project, result.project);
222
273
  session.project = result.project;
223
274
  session.revision += 1;
224
275
  session.dirty = true;
@@ -243,12 +294,15 @@ export class AuthoringService {
243
294
  });
244
295
  }
245
296
 
246
- public async saveSession(input: SaveSessionInput): Promise<
297
+ public async saveSession(
298
+ input: SaveSessionInput,
299
+ ): Promise<
247
300
  BackendResult<
248
301
  BackendSessionSnapshot | MaterializeSessionSnapshot,
249
302
  | SessionNotFoundError
250
303
  | SessionStaleWriteError
251
304
  | SavePartialFailureError
305
+ | UamFidelityUnsupportedError
252
306
  | MaterializeValidationFailedError
253
307
  | MaterializeWriteFailedError
254
308
  | PathPolicyViolationError
@@ -266,19 +320,36 @@ export class AuthoringService {
266
320
  reason: 'force_save',
267
321
  });
268
322
  }
323
+ return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
324
+ }
325
+
326
+ private async saveSessionExclusive(
327
+ input: SaveSessionInput,
328
+ ): Promise<
329
+ BackendResult<
330
+ BackendSessionSnapshot | MaterializeSessionSnapshot,
331
+ | SessionNotFoundError
332
+ | SessionStaleWriteError
333
+ | SavePartialFailureError
334
+ | UamFidelityUnsupportedError
335
+ | MaterializeValidationFailedError
336
+ | MaterializeWriteFailedError
337
+ | PathPolicyViolationError
338
+ | InProcessLockConflictError
339
+ | BackendCapabilityUnavailableError
340
+ >
341
+ > {
269
342
  const startedAt = Date.now();
270
343
  const session = this.context.sessions.get(input.sessionId);
271
344
  if (!session || session.closed) {
272
345
  return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
273
346
  }
274
- const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
347
+ const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
275
348
  if (!fileSystem) {
276
349
  return failure(
277
350
  'authoring',
278
351
  startedAt,
279
- createCapabilityUnavailableError(
280
- 'saveSession requires an injected BackendFileSystem adapter.',
281
- ),
352
+ createCapabilityUnavailableError('saveSession requires an injected BackendFileSystem adapter.'),
282
353
  toSessionSnapshot(session, this.context.capabilities),
283
354
  {
284
355
  sessionId: session.sessionId,
@@ -317,6 +388,18 @@ export class AuthoringService {
317
388
  revision: session.revision,
318
389
  });
319
390
  }
391
+ if (session.uamFidelity === 'unsupported') {
392
+ return failure(
393
+ 'authoring',
394
+ startedAt,
395
+ createUamFidelityUnsupportedError(session),
396
+ toSessionSnapshot(session, this.context.capabilities),
397
+ {
398
+ sessionId: session.sessionId,
399
+ revision: session.revision,
400
+ },
401
+ );
402
+ }
320
403
 
321
404
  const committedPaths: string[] = [];
322
405
  const failedPaths: string[] = [];
@@ -327,8 +410,21 @@ export class AuthoringService {
327
410
  revision: session.revision,
328
411
  });
329
412
  try {
330
- const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
331
- await writer.write(materializeUamProject(session.project), session.fairyPath);
413
+ await writeSessionProject({
414
+ fileSystem,
415
+ document: materializeUamProject(session.project),
416
+ fairyPath: session.fairyPath,
417
+ staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
418
+ staleResourceFolders: [...session.pendingStaleResourceFolders.values()],
419
+ staleBranchDirectories: [...session.pendingStaleBranchDirectories.values()],
420
+ writtenPaths: committedPaths,
421
+ failedPaths,
422
+ });
423
+ session.fileSystem ??= fileSystem;
424
+ session.pendingStaleSourceFiles.clear();
425
+ session.pendingStaleResourceFolders.clear();
426
+ session.pendingStaleBranchDirectories.clear();
427
+ commitUamProjectSourcePaths(session.project);
332
428
  session.lastSavedRevision = session.revision;
333
429
  session.dirty = false;
334
430
  const cacheEntry = this.cacheService.refreshSession(session);
@@ -380,11 +476,32 @@ export class AuthoringService {
380
476
  }
381
477
  }
382
478
 
383
- public async materializeSession(input: MaterializeSessionInput): Promise<
479
+ public async materializeSession(
480
+ input: MaterializeSessionInput,
481
+ ): Promise<
384
482
  BackendResult<
385
483
  MaterializeSessionSnapshot,
386
484
  | SessionNotFoundError
387
485
  | SessionStaleWriteError
486
+ | UamFidelityUnsupportedError
487
+ | MaterializeValidationFailedError
488
+ | MaterializeWriteFailedError
489
+ | PathPolicyViolationError
490
+ | InProcessLockConflictError
491
+ | BackendCapabilityUnavailableError
492
+ >
493
+ > {
494
+ return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
495
+ }
496
+
497
+ private async materializeSessionExclusive(
498
+ input: MaterializeSessionInput,
499
+ ): Promise<
500
+ BackendResult<
501
+ MaterializeSessionSnapshot,
502
+ | SessionNotFoundError
503
+ | SessionStaleWriteError
504
+ | UamFidelityUnsupportedError
388
505
  | MaterializeValidationFailedError
389
506
  | MaterializeWriteFailedError
390
507
  | PathPolicyViolationError
@@ -411,14 +528,13 @@ export class AuthoringService {
411
528
  }
412
529
 
413
530
  const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
414
- const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
531
+ const fileSystem =
532
+ storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
415
533
  if (!fileSystem) {
416
534
  return failure(
417
535
  'authoring',
418
536
  startedAt,
419
- createCapabilityUnavailableError(
420
- 'materializeSession requires an injected BackendFileSystem adapter.',
421
- ),
537
+ createCapabilityUnavailableError('materializeSession requires an injected BackendFileSystem adapter.'),
422
538
  toSessionSnapshot(session, this.context.capabilities),
423
539
  {
424
540
  sessionId: session.sessionId,
@@ -464,6 +580,19 @@ export class AuthoringService {
464
580
  }
465
581
  }
466
582
 
583
+ if (session.uamFidelity === 'unsupported') {
584
+ return failure(
585
+ 'authoring',
586
+ startedAt,
587
+ createUamFidelityUnsupportedError(session),
588
+ toSessionSnapshot(session, this.context.capabilities),
589
+ {
590
+ sessionId: session.sessionId,
591
+ revision: session.revision,
592
+ },
593
+ );
594
+ }
595
+
467
596
  const diagnostics = validationDiagnostics(session.project);
468
597
  if (diagnostics.length > 0) {
469
598
  const error: MaterializeValidationFailedError = {
@@ -474,17 +603,11 @@ export class AuthoringService {
474
603
  issueCount: diagnostics.length,
475
604
  diagnostics,
476
605
  };
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
- );
606
+ return failure('authoring', startedAt, error, toSessionSnapshot(session, this.context.capabilities), {
607
+ sessionId: session.sessionId,
608
+ revision: session.revision,
609
+ diagnostics,
610
+ });
488
611
  }
489
612
 
490
613
  let document: ReturnType<typeof materializeUamProject>;
@@ -529,8 +652,28 @@ export class AuthoringService {
529
652
  revision: session.revision,
530
653
  });
531
654
  try {
532
- const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
533
- await writer.write(document, fairyPath);
655
+ const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
656
+ await writeSessionProject({
657
+ fileSystem,
658
+ document,
659
+ fairyPath,
660
+ staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
661
+ staleResourceFolders: isSessionStorageTarget ? [...session.pendingStaleResourceFolders.values()] : [],
662
+ staleBranchDirectories: isSessionStorageTarget ? [...session.pendingStaleBranchDirectories.values()] : [],
663
+ writtenPaths,
664
+ failedPaths,
665
+ });
666
+ if (isSessionStorageTarget) {
667
+ session.pendingStaleSourceFiles.clear();
668
+ session.pendingStaleResourceFolders.clear();
669
+ session.pendingStaleBranchDirectories.clear();
670
+ }
671
+ if (storageTarget && !isSessionStorageTarget) {
672
+ session.pendingStaleSourceFiles.clear();
673
+ session.pendingStaleResourceFolders.clear();
674
+ session.pendingStaleBranchDirectories.clear();
675
+ }
676
+ if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
534
677
  if (storageTarget) {
535
678
  this.context.sessionsByPath.delete(session.canonicalPathKey);
536
679
  session.fileSystem = storageTarget.fileSystem;
@@ -7,7 +7,6 @@ import type {
7
7
  GetCacheSnapshotInput,
8
8
  SessionNotFoundError,
9
9
  } from '../runtime.js';
10
- import { cloneCacheEntrySnapshot } from './snapshot-utils.js';
11
10
 
12
11
  function createCacheEntry(session: BackendSessionState, valid: boolean): BackendCacheEntry {
13
12
  return {
@@ -38,7 +37,7 @@ export class CacheService {
38
37
  const entry = this.context.cacheBySession.get(input.sessionId);
39
38
  return success('read', startedAt, {
40
39
  cacheRevision: entry?.revision ?? session.revision,
41
- entries: entry ? [cloneCacheEntrySnapshot(entry)] : [],
40
+ entries: entry ? [structuredClone(entry)] : [],
42
41
  }, { sessionId: session.sessionId, revision: session.revision });
43
42
  }
44
43
 
@@ -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,
@@ -7,17 +15,10 @@ import type {
7
15
  BackendFileSystem,
8
16
  BackendHostAdapter,
9
17
  BackendJobSnapshot,
18
+ BackendSessionLock,
10
19
  BackendSessionSnapshot,
11
20
  BackendSuccess,
12
21
  } 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
22
 
22
23
  export interface BackendSessionState {
23
24
  sessionId: string;
@@ -25,10 +26,18 @@ export interface BackendSessionState {
25
26
  canonicalProjectPath: string;
26
27
  canonicalPathKey: string;
27
28
  lockFilePath: string;
29
+ sessionLock: BackendSessionLock | null;
28
30
  fileSystem?: BackendFileSystem;
29
31
  project: import('@openfairygui/core/uam').UamProject;
32
+ uamFidelity: 'full' | 'unsupported';
30
33
  revision: number;
31
34
  lastSavedRevision: number;
35
+ /** Package-controlled files deferred until a successful replacement project write. */
36
+ pendingStaleSourceFiles: Map<string, import('@openfairygui/core/project-io').ProjectSourceFile>;
37
+ /** Empty resource directories deferred until a successful replacement project write. */
38
+ pendingStaleResourceFolders: Map<string, import('@openfairygui/core/project-io').ProjectResourceFolder>;
39
+ /** Removed package-branch and root-branch directories deferred until controlled files are replaced. */
40
+ pendingStaleBranchDirectories: Map<string, import('@openfairygui/core/project-io').ProjectBranchDirectory>;
32
41
  dirty: boolean;
33
42
  lockHeld: boolean;
34
43
  closed: boolean;
@@ -8,7 +8,6 @@ import type {
8
8
  GetEventsSnapshot,
9
9
  SessionNotFoundError,
10
10
  } from '../runtime.js';
11
- import { cloneEventSnapshot } from './snapshot-utils.js';
12
11
 
13
12
  const DEFAULT_EVENT_RETENTION_LIMIT = 1000;
14
13
 
@@ -71,7 +70,7 @@ export class EventService {
71
70
  const filtered = events.filter((event) => event.sequence > after);
72
71
  const limit = input.limit === undefined ? filtered.length : Math.max(0, input.limit);
73
72
  return success('runtime', startedAt, {
74
- events: filtered.slice(0, limit).map(cloneEventSnapshot),
73
+ events: filtered.slice(0, limit).map((event) => structuredClone(event)),
75
74
  oldestSequence,
76
75
  currentSequence,
77
76
  cursorExpired: false,
@@ -2,7 +2,6 @@ import { failure, success, type BackendContext } from './context.js';
2
2
  import type { CacheService } from './cache-service.js';
3
3
  import type { EventService } from './event-service.js';
4
4
  import { createSessionNotFoundError } from './session-utils.js';
5
- import { cloneJobSnapshot } from './snapshot-utils.js';
6
5
  import type {
7
6
  BackendJobListSnapshot,
8
7
  BackendJobNotCancellableError,
@@ -76,7 +75,7 @@ export class JobService {
76
75
  this.eventService.emit({ kind: 'job.created', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision, jobId });
77
76
  this.scheduleRefreshJob(session.sessionId, jobId);
78
77
 
79
- return success('runtime', startedAt, cloneJobSnapshot(job), { sessionId: session.sessionId, revision: session.revision });
78
+ return success('runtime', startedAt, structuredClone(job), { sessionId: session.sessionId, revision: session.revision });
80
79
  }
81
80
 
82
81
  public getJob(input: GetJobInput): BackendResult<BackendJobSnapshot, SessionNotFoundError | BackendJobNotFoundError> {
@@ -92,7 +91,7 @@ export class JobService {
92
91
  jobId: input.jobId,
93
92
  }, undefined, { sessionId: session.sessionId, revision: session.revision });
94
93
  }
95
- return success('runtime', startedAt, cloneJobSnapshot(job), { sessionId: session.sessionId, revision: session.revision });
94
+ return success('runtime', startedAt, structuredClone(job), { sessionId: session.sessionId, revision: session.revision });
96
95
  }
97
96
 
98
97
  public listJobs(input: ListJobsInput): BackendResult<BackendJobListSnapshot, SessionNotFoundError> {
@@ -107,7 +106,7 @@ export class JobService {
107
106
  else jobs = jobs.filter((job) => job.status === input.status);
108
107
  }
109
108
  if (input.limit !== undefined) jobs = jobs.slice(0, Math.max(0, input.limit));
110
- return success('runtime', startedAt, { jobs: jobs.map(cloneJobSnapshot) }, { sessionId: session.sessionId, revision: session.revision });
109
+ return success('runtime', startedAt, { jobs: jobs.map((job) => structuredClone(job)) }, { sessionId: session.sessionId, revision: session.revision });
111
110
  }
112
111
 
113
112
  public cancelJob(input: CancelJobInput): BackendResult<BackendJobSnapshot, SessionNotFoundError | BackendJobNotFoundError | BackendJobNotCancellableError> {
@@ -135,7 +134,7 @@ export class JobService {
135
134
  this.cancellationRequests.add(job.jobId);
136
135
  this.eventService.emit({ kind: 'job.cancelRequested', sessionId: input.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision, jobId: job.jobId });
137
136
  const cancelled = this.cancelRefreshJob(session.sessionId, job);
138
- return success('runtime', startedAt, cloneJobSnapshot(cancelled), { sessionId: session.sessionId, revision: session.revision });
137
+ return success('runtime', startedAt, structuredClone(cancelled), { sessionId: session.sessionId, revision: session.revision });
139
138
  }
140
139
 
141
140
  public removeSession(sessionId: string): void {
@@ -1,13 +1,12 @@
1
1
  import { failure, success, type BackendContext } from './context.js';
2
2
  import type { BackendCapabilities, BackendResult, BackendSessionSnapshot, SessionNotFoundError } from '../runtime.js';
3
3
  import { createSessionNotFoundError, toSessionSnapshot } from './session-utils.js';
4
- import { cloneCapabilitiesSnapshot } from './snapshot-utils.js';
5
4
 
6
5
  export class ReadService {
7
6
  public constructor(private readonly context: BackendContext) {}
8
7
 
9
8
  public getCapabilities(): BackendResult<BackendCapabilities> {
10
- return success('read', Date.now(), cloneCapabilitiesSnapshot(this.context.capabilities));
9
+ return success('read', Date.now(), structuredClone(this.context.capabilities));
11
10
  }
12
11
 
13
12
  public getSession(input: { sessionId: string }): BackendResult<BackendSessionSnapshot, SessionNotFoundError> {