@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.
- package/README.md +29 -8
- package/dist/index.cjs +1633 -27
- package/dist/index.d.cts +516 -4
- package/dist/index.d.mts +516 -4
- package/dist/index.mjs +1629 -23
- package/dist/node.cjs +1603 -9
- package/dist/node.d.cts +507 -2
- package/dist/node.d.mts +507 -2
- package/dist/node.mjs +1601 -6
- package/package.json +7 -6
- package/src/index.ts +18 -17
- package/src/node.ts +24 -8
- package/src/runtime/capabilities.ts +126 -0
- package/src/runtime/contracts.ts +518 -0
- package/src/runtime.ts +49 -623
- package/src/services/authoring-service.ts +242 -99
- package/src/services/cache-service.ts +1 -2
- package/src/services/context.ts +17 -8
- package/src/services/event-service.ts +1 -2
- package/src/services/job-service.ts +4 -5
- package/src/services/read-service.ts +1 -2
- package/src/services/runtime-service.ts +133 -27
- package/src/services/session-project-writer.ts +69 -0
- package/src/services/session-utils.ts +6 -3
- package/src/storage.ts +67 -27
- package/dist/runtime-DFatY9W0.mjs +0 -1417
- package/dist/runtime-GKzsXJdO.cjs +0 -1441
- package/dist/runtime-GyNVxAQ0.d.mts +0 -494
- package/dist/runtime-Jec6FcF5.d.cts +0 -494
- package/src/services/snapshot-utils.ts +0 -43
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
24
|
-
import {
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
331
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
533
|
-
await
|
|
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 ? [
|
|
40
|
+
entries: entry ? [structuredClone(entry)] : [],
|
|
42
41
|
}, { sessionId: session.sessionId, revision: session.revision });
|
|
43
42
|
}
|
|
44
43
|
|
package/src/services/context.ts
CHANGED
|
@@ -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(
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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(),
|
|
9
|
+
return success('read', Date.now(), structuredClone(this.context.capabilities));
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
public getSession(input: { sessionId: string }): BackendResult<BackendSessionSnapshot, SessionNotFoundError> {
|