@openfairygui/backend 0.2.0-alpha.14 → 0.2.0-alpha.16
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 +3 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.mts +1 -1
- package/dist/node.mjs +1 -1
- package/dist/{runtime-B9z0n2yF.mjs → runtime-B017UgBl.mjs} +137 -14
- package/dist/{runtime-GyNVxAQ0.d.mts → runtime-B6UjcyIt.d.cts} +11 -4
- package/dist/{runtime-Jec6FcF5.d.cts → runtime-BihiQW-H.d.mts} +11 -4
- package/dist/{runtime-DqApG30C.cjs → runtime-P0Nhxk2f.cjs} +137 -14
- package/package.json +3 -3
- package/src/index.ts +17 -16
- package/src/runtime.ts +17 -2
- package/src/services/authoring-service.ts +174 -49
- package/src/services/context.ts +10 -9
- package/src/services/runtime-service.ts +101 -15
- package/src/services/session-utils.ts +5 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
24
|
-
import {
|
|
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(
|
|
@@ -85,17 +91,36 @@ function createWriterFileSystem(
|
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
function
|
|
94
|
+
function projectSourceFiles(project: UamProject): Map<string, ProjectSourceFile> {
|
|
89
95
|
const result = new Map<string, ProjectSourceFile>();
|
|
90
96
|
for (const pkg of project.packages) {
|
|
97
|
+
result.set(`${pkg.id}/package.xml`, {
|
|
98
|
+
packageName: pkg.name,
|
|
99
|
+
branch: '',
|
|
100
|
+
path: '',
|
|
101
|
+
fileName: 'package.xml',
|
|
102
|
+
});
|
|
103
|
+
const branches = new Set<string>();
|
|
91
104
|
for (const resource of pkg.resources) {
|
|
92
|
-
if (resource.
|
|
93
|
-
const fileName = resource.
|
|
105
|
+
if (resource.branch) branches.add(resource.branch);
|
|
106
|
+
const fileName = resource.kind === 'component'
|
|
107
|
+
? `${resource.name}.xml`
|
|
108
|
+
: resource.fileName ?? resource.file ?? '';
|
|
94
109
|
if (!fileName) continue;
|
|
95
|
-
result.set(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
result.set(`${pkg.id}/${resource.id}`, {
|
|
111
|
+
packageName: pkg.name,
|
|
112
|
+
branch: resource.branch,
|
|
113
|
+
path: resource.path,
|
|
114
|
+
fileName,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
for (const branch of branches) {
|
|
118
|
+
result.set(`${pkg.id}/branch/${branch}`, {
|
|
119
|
+
packageName: pkg.name,
|
|
120
|
+
branch,
|
|
121
|
+
path: '',
|
|
122
|
+
fileName: 'package_branch.xml',
|
|
123
|
+
});
|
|
99
124
|
}
|
|
100
125
|
}
|
|
101
126
|
return result;
|
|
@@ -105,14 +130,14 @@ function sourceFileKey(source: ProjectSourceFile): string {
|
|
|
105
130
|
return [source.branch, source.packageName, source.path, source.fileName].join('\0');
|
|
106
131
|
}
|
|
107
132
|
|
|
108
|
-
function
|
|
133
|
+
function recordStaleProjectFiles(
|
|
109
134
|
session: Parameters<typeof toSessionSnapshot>[0],
|
|
110
135
|
previousProject: UamProject,
|
|
111
136
|
nextProject: UamProject,
|
|
112
137
|
): void {
|
|
113
138
|
if (!session.fileSystem) return;
|
|
114
|
-
const previousSources =
|
|
115
|
-
const nextSourceKeys = new Set([...
|
|
139
|
+
const previousSources = projectSourceFiles(previousProject);
|
|
140
|
+
const nextSourceKeys = new Set([...projectSourceFiles(nextProject).values()].map(sourceFileKey));
|
|
116
141
|
for (const source of previousSources.values()) {
|
|
117
142
|
const key = sourceFileKey(source);
|
|
118
143
|
if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
|
|
@@ -126,15 +151,15 @@ function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagno
|
|
|
126
151
|
return error.diagnostics.length > 0
|
|
127
152
|
? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
128
153
|
: [
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
154
|
+
{
|
|
155
|
+
code: error.code,
|
|
156
|
+
message: error.message,
|
|
157
|
+
severity: 'error',
|
|
158
|
+
operationKind: error.operationKind,
|
|
159
|
+
opIndex: error.opIndex,
|
|
160
|
+
opId: error.opId,
|
|
161
|
+
},
|
|
162
|
+
];
|
|
138
163
|
}
|
|
139
164
|
|
|
140
165
|
function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
|
|
@@ -146,6 +171,17 @@ function createCapabilityUnavailableError(message: string): BackendCapabilityUna
|
|
|
146
171
|
};
|
|
147
172
|
}
|
|
148
173
|
|
|
174
|
+
function createUamFidelityUnsupportedError(
|
|
175
|
+
session: Parameters<typeof toSessionSnapshot>[0],
|
|
176
|
+
): UamFidelityUnsupportedError {
|
|
177
|
+
return {
|
|
178
|
+
code: 'uam_fidelity_unsupported',
|
|
179
|
+
message: 'The source project contains formal properties that the current UAM cannot preserve.',
|
|
180
|
+
sessionId: session.sessionId,
|
|
181
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
149
185
|
function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
|
|
150
186
|
const issues = validateUamProject(sessionProject);
|
|
151
187
|
return issues.map((issue) => ({
|
|
@@ -195,12 +231,31 @@ function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['stor
|
|
|
195
231
|
}
|
|
196
232
|
|
|
197
233
|
export class AuthoringService {
|
|
234
|
+
private readonly sessionOperations = new Map<string, Promise<void>>();
|
|
235
|
+
|
|
198
236
|
public constructor(
|
|
199
237
|
private readonly context: BackendContext,
|
|
200
238
|
private readonly cacheService: CacheService,
|
|
201
239
|
private readonly eventService: EventService,
|
|
202
240
|
) {}
|
|
203
241
|
|
|
242
|
+
private async runSessionExclusive<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
|
|
243
|
+
const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
|
|
244
|
+
let release = (): void => undefined;
|
|
245
|
+
const current = new Promise<void>((resolve) => {
|
|
246
|
+
release = resolve;
|
|
247
|
+
});
|
|
248
|
+
const tail = previous.then(() => current);
|
|
249
|
+
this.sessionOperations.set(sessionId, tail);
|
|
250
|
+
await previous;
|
|
251
|
+
try {
|
|
252
|
+
return await operation();
|
|
253
|
+
} finally {
|
|
254
|
+
release();
|
|
255
|
+
if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
204
259
|
public async applyTransaction(
|
|
205
260
|
input: ApplySessionTransactionInput,
|
|
206
261
|
): Promise<
|
|
@@ -208,6 +263,17 @@ export class AuthoringService {
|
|
|
208
263
|
BackendSessionSnapshot,
|
|
209
264
|
SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
|
|
210
265
|
>
|
|
266
|
+
> {
|
|
267
|
+
return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private async applyTransactionExclusive(
|
|
271
|
+
input: ApplySessionTransactionInput,
|
|
272
|
+
): Promise<
|
|
273
|
+
BackendResult<
|
|
274
|
+
BackendSessionSnapshot,
|
|
275
|
+
SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
|
|
276
|
+
>
|
|
211
277
|
> {
|
|
212
278
|
const startedAt = Date.now();
|
|
213
279
|
const session = this.context.sessions.get(input.sessionId);
|
|
@@ -259,7 +325,7 @@ export class AuthoringService {
|
|
|
259
325
|
);
|
|
260
326
|
}
|
|
261
327
|
|
|
262
|
-
|
|
328
|
+
recordStaleProjectFiles(session, session.project, result.project);
|
|
263
329
|
session.project = result.project;
|
|
264
330
|
session.revision += 1;
|
|
265
331
|
session.dirty = true;
|
|
@@ -284,12 +350,15 @@ export class AuthoringService {
|
|
|
284
350
|
});
|
|
285
351
|
}
|
|
286
352
|
|
|
287
|
-
public async saveSession(
|
|
353
|
+
public async saveSession(
|
|
354
|
+
input: SaveSessionInput,
|
|
355
|
+
): Promise<
|
|
288
356
|
BackendResult<
|
|
289
357
|
BackendSessionSnapshot | MaterializeSessionSnapshot,
|
|
290
358
|
| SessionNotFoundError
|
|
291
359
|
| SessionStaleWriteError
|
|
292
360
|
| SavePartialFailureError
|
|
361
|
+
| UamFidelityUnsupportedError
|
|
293
362
|
| MaterializeValidationFailedError
|
|
294
363
|
| MaterializeWriteFailedError
|
|
295
364
|
| PathPolicyViolationError
|
|
@@ -307,6 +376,25 @@ export class AuthoringService {
|
|
|
307
376
|
reason: 'force_save',
|
|
308
377
|
});
|
|
309
378
|
}
|
|
379
|
+
return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private async saveSessionExclusive(
|
|
383
|
+
input: SaveSessionInput,
|
|
384
|
+
): Promise<
|
|
385
|
+
BackendResult<
|
|
386
|
+
BackendSessionSnapshot | MaterializeSessionSnapshot,
|
|
387
|
+
| SessionNotFoundError
|
|
388
|
+
| SessionStaleWriteError
|
|
389
|
+
| SavePartialFailureError
|
|
390
|
+
| UamFidelityUnsupportedError
|
|
391
|
+
| MaterializeValidationFailedError
|
|
392
|
+
| MaterializeWriteFailedError
|
|
393
|
+
| PathPolicyViolationError
|
|
394
|
+
| InProcessLockConflictError
|
|
395
|
+
| BackendCapabilityUnavailableError
|
|
396
|
+
>
|
|
397
|
+
> {
|
|
310
398
|
const startedAt = Date.now();
|
|
311
399
|
const session = this.context.sessions.get(input.sessionId);
|
|
312
400
|
if (!session || session.closed) {
|
|
@@ -317,9 +405,7 @@ export class AuthoringService {
|
|
|
317
405
|
return failure(
|
|
318
406
|
'authoring',
|
|
319
407
|
startedAt,
|
|
320
|
-
createCapabilityUnavailableError(
|
|
321
|
-
'saveSession requires an injected BackendFileSystem adapter.',
|
|
322
|
-
),
|
|
408
|
+
createCapabilityUnavailableError('saveSession requires an injected BackendFileSystem adapter.'),
|
|
323
409
|
toSessionSnapshot(session, this.context.capabilities),
|
|
324
410
|
{
|
|
325
411
|
sessionId: session.sessionId,
|
|
@@ -358,6 +444,18 @@ export class AuthoringService {
|
|
|
358
444
|
revision: session.revision,
|
|
359
445
|
});
|
|
360
446
|
}
|
|
447
|
+
if (session.uamFidelity === 'unsupported') {
|
|
448
|
+
return failure(
|
|
449
|
+
'authoring',
|
|
450
|
+
startedAt,
|
|
451
|
+
createUamFidelityUnsupportedError(session),
|
|
452
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
453
|
+
{
|
|
454
|
+
sessionId: session.sessionId,
|
|
455
|
+
revision: session.revision,
|
|
456
|
+
},
|
|
457
|
+
);
|
|
458
|
+
}
|
|
361
459
|
|
|
362
460
|
const committedPaths: string[] = [];
|
|
363
461
|
const failedPaths: string[] = [];
|
|
@@ -426,11 +524,32 @@ export class AuthoringService {
|
|
|
426
524
|
}
|
|
427
525
|
}
|
|
428
526
|
|
|
429
|
-
public async materializeSession(
|
|
527
|
+
public async materializeSession(
|
|
528
|
+
input: MaterializeSessionInput,
|
|
529
|
+
): Promise<
|
|
530
|
+
BackendResult<
|
|
531
|
+
MaterializeSessionSnapshot,
|
|
532
|
+
| SessionNotFoundError
|
|
533
|
+
| SessionStaleWriteError
|
|
534
|
+
| UamFidelityUnsupportedError
|
|
535
|
+
| MaterializeValidationFailedError
|
|
536
|
+
| MaterializeWriteFailedError
|
|
537
|
+
| PathPolicyViolationError
|
|
538
|
+
| InProcessLockConflictError
|
|
539
|
+
| BackendCapabilityUnavailableError
|
|
540
|
+
>
|
|
541
|
+
> {
|
|
542
|
+
return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private async materializeSessionExclusive(
|
|
546
|
+
input: MaterializeSessionInput,
|
|
547
|
+
): Promise<
|
|
430
548
|
BackendResult<
|
|
431
549
|
MaterializeSessionSnapshot,
|
|
432
550
|
| SessionNotFoundError
|
|
433
551
|
| SessionStaleWriteError
|
|
552
|
+
| UamFidelityUnsupportedError
|
|
434
553
|
| MaterializeValidationFailedError
|
|
435
554
|
| MaterializeWriteFailedError
|
|
436
555
|
| PathPolicyViolationError
|
|
@@ -457,14 +576,13 @@ export class AuthoringService {
|
|
|
457
576
|
}
|
|
458
577
|
|
|
459
578
|
const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
|
|
460
|
-
const fileSystem =
|
|
579
|
+
const fileSystem =
|
|
580
|
+
storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
|
|
461
581
|
if (!fileSystem) {
|
|
462
582
|
return failure(
|
|
463
583
|
'authoring',
|
|
464
584
|
startedAt,
|
|
465
|
-
createCapabilityUnavailableError(
|
|
466
|
-
'materializeSession requires an injected BackendFileSystem adapter.',
|
|
467
|
-
),
|
|
585
|
+
createCapabilityUnavailableError('materializeSession requires an injected BackendFileSystem adapter.'),
|
|
468
586
|
toSessionSnapshot(session, this.context.capabilities),
|
|
469
587
|
{
|
|
470
588
|
sessionId: session.sessionId,
|
|
@@ -510,6 +628,19 @@ export class AuthoringService {
|
|
|
510
628
|
}
|
|
511
629
|
}
|
|
512
630
|
|
|
631
|
+
if (session.uamFidelity === 'unsupported') {
|
|
632
|
+
return failure(
|
|
633
|
+
'authoring',
|
|
634
|
+
startedAt,
|
|
635
|
+
createUamFidelityUnsupportedError(session),
|
|
636
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
637
|
+
{
|
|
638
|
+
sessionId: session.sessionId,
|
|
639
|
+
revision: session.revision,
|
|
640
|
+
},
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
513
644
|
const diagnostics = validationDiagnostics(session.project);
|
|
514
645
|
if (diagnostics.length > 0) {
|
|
515
646
|
const error: MaterializeValidationFailedError = {
|
|
@@ -520,17 +651,11 @@ export class AuthoringService {
|
|
|
520
651
|
issueCount: diagnostics.length,
|
|
521
652
|
diagnostics,
|
|
522
653
|
};
|
|
523
|
-
return failure(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
{
|
|
529
|
-
sessionId: session.sessionId,
|
|
530
|
-
revision: session.revision,
|
|
531
|
-
diagnostics,
|
|
532
|
-
},
|
|
533
|
-
);
|
|
654
|
+
return failure('authoring', startedAt, error, toSessionSnapshot(session, this.context.capabilities), {
|
|
655
|
+
sessionId: session.sessionId,
|
|
656
|
+
revision: session.revision,
|
|
657
|
+
diagnostics,
|
|
658
|
+
});
|
|
534
659
|
}
|
|
535
660
|
|
|
536
661
|
let document: ReturnType<typeof materializeUamProject>;
|
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,
|
|
@@ -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,9 +27,10 @@ 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;
|
|
32
|
-
/**
|
|
33
|
+
/** Package-controlled files deferred until a successful replacement project write. */
|
|
33
34
|
pendingStaleSourceFiles: Map<string, import('@openfairygui/core/project-io').ProjectSourceFile>;
|
|
34
35
|
dirty: boolean;
|
|
35
36
|
lockHeld: boolean;
|
|
@@ -1,20 +1,25 @@
|
|
|
1
|
-
import { ProjectReader,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
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
|
|
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,6 +221,7 @@ 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,
|
|
144
227
|
pendingStaleSourceFiles: new Map(),
|
|
@@ -178,12 +261,14 @@ export class RuntimeService {
|
|
|
178
261
|
const sessionId = input.sessionId ?? randomId();
|
|
179
262
|
const storage = input.storage;
|
|
180
263
|
const memoryProjectPath = `memory://${sessionId}`;
|
|
181
|
-
const canonicalProjectPath =
|
|
182
|
-
??
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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());
|
|
187
272
|
const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
|
|
188
273
|
if (existingSessionId) {
|
|
189
274
|
return failure('runtime', startedAt, {
|
|
@@ -203,6 +288,7 @@ export class RuntimeService {
|
|
|
203
288
|
lockFilePath: '',
|
|
204
289
|
fileSystem: storage?.fileSystem,
|
|
205
290
|
project: normalizeUamProject(input.project),
|
|
291
|
+
uamFidelity: 'full',
|
|
206
292
|
revision: 0,
|
|
207
293
|
lastSavedRevision: 0,
|
|
208
294
|
pendingStaleSourceFiles: new Map(),
|
|
@@ -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(
|
|
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
|
};
|