@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.
- package/README.md +9 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- 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-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-GKzsXJdO.cjs → runtime-C5N7GLsG.cjs} +159 -4
- package/dist/{runtime-DFatY9W0.mjs → runtime-CxH7aT_v.mjs} +160 -5
- package/package.json +3 -3
- package/src/index.ts +17 -16
- package/src/runtime.ts +17 -2
- package/src/services/authoring-service.ts +201 -41
- package/src/services/context.ts +11 -8
- package/src/services/runtime-service.ts +103 -15
- package/src/services/session-utils.ts +5 -1
- package/src/storage.ts +5 -3
|
@@ -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(
|
|
@@ -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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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;
|
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,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,
|
|
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,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 =
|
|
181
|
-
??
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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);
|