@openfairygui/backend 0.2.0-alpha.7 → 0.2.0-alpha.8
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 -2
- 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-Cn0O-wZt.mjs → runtime-DFatY9W0.mjs} +210 -9
- package/dist/{runtime-YK3ACpuQ.cjs → runtime-GKzsXJdO.cjs} +209 -8
- package/dist/{runtime-Bydxr_SN.d.mts → runtime-GyNVxAQ0.d.mts} +48 -6
- package/dist/{runtime-DziTGugy.d.cts → runtime-Jec6FcF5.d.cts} +48 -6
- package/package.json +3 -3
- package/src/index.ts +4 -0
- package/src/runtime.ts +72 -5
- package/src/services/authoring-service.ts +314 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProjectWriter, type FileSystem } from '@openfairygui/core/project-io';
|
|
2
|
-
import { materializeUamProject } from '@openfairygui/core/uam';
|
|
2
|
+
import { materializeUamProject, validateUamProject } from '@openfairygui/core/uam';
|
|
3
3
|
import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
|
|
4
4
|
import type { CacheService } from './cache-service.js';
|
|
5
5
|
import { failure, success, type BackendContext } from './context.js';
|
|
@@ -10,13 +10,18 @@ import type {
|
|
|
10
10
|
BackendFileSystem,
|
|
11
11
|
BackendResult,
|
|
12
12
|
BackendSessionSnapshot,
|
|
13
|
+
InProcessLockConflictError,
|
|
14
|
+
MaterializeSessionInput,
|
|
15
|
+
MaterializeSessionSnapshot,
|
|
16
|
+
MaterializeValidationFailedError,
|
|
17
|
+
MaterializeWriteFailedError,
|
|
13
18
|
SaveSessionInput,
|
|
14
19
|
SavePartialFailureError,
|
|
15
20
|
SessionNotFoundError,
|
|
16
21
|
SessionStaleWriteError,
|
|
17
22
|
} from '../runtime.js';
|
|
18
23
|
import type { BackendDiagnostic } from '../contracts.js';
|
|
19
|
-
import { validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
|
|
24
|
+
import { normalizeComparablePath, validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
|
|
20
25
|
import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
|
|
21
26
|
|
|
22
27
|
function createWriterFileSystem(
|
|
@@ -92,6 +97,63 @@ function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagno
|
|
|
92
97
|
];
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
|
|
101
|
+
return {
|
|
102
|
+
code: 'capability_unavailable',
|
|
103
|
+
message,
|
|
104
|
+
capability: 'fileSystem',
|
|
105
|
+
requiredAdapter: 'BackendFileSystem',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
|
|
110
|
+
const issues = validateUamProject(sessionProject);
|
|
111
|
+
return issues.map((issue) => ({
|
|
112
|
+
code: 'materialize_validation_failed',
|
|
113
|
+
message: issue.message,
|
|
114
|
+
severity: 'error',
|
|
115
|
+
path: issue.path,
|
|
116
|
+
operationKind: 'materializeSession',
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toMaterializeSnapshot(
|
|
121
|
+
session: Parameters<typeof toSessionSnapshot>[0],
|
|
122
|
+
capabilities: Parameters<typeof toSessionSnapshot>[1],
|
|
123
|
+
input: {
|
|
124
|
+
reason?: string;
|
|
125
|
+
writtenPaths: string[];
|
|
126
|
+
skippedPaths: string[];
|
|
127
|
+
diagnostics: BackendDiagnostic[];
|
|
128
|
+
},
|
|
129
|
+
): MaterializeSessionSnapshot {
|
|
130
|
+
return {
|
|
131
|
+
...toSessionSnapshot(session, capabilities),
|
|
132
|
+
mode: 'fullProject',
|
|
133
|
+
reason: input.reason,
|
|
134
|
+
materializeRevision: session.revision,
|
|
135
|
+
saveRevision: session.lastSavedRevision,
|
|
136
|
+
writtenPaths: [...input.writtenPaths],
|
|
137
|
+
skippedPaths: [...input.skippedPaths],
|
|
138
|
+
diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic })),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['storage']>): {
|
|
143
|
+
fileSystem: BackendFileSystem;
|
|
144
|
+
fairyPath: string;
|
|
145
|
+
canonicalProjectPath: string;
|
|
146
|
+
canonicalPathKey: string;
|
|
147
|
+
} {
|
|
148
|
+
const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || '.');
|
|
149
|
+
return {
|
|
150
|
+
fileSystem: input.fileSystem,
|
|
151
|
+
fairyPath: input.fairyPath,
|
|
152
|
+
canonicalProjectPath,
|
|
153
|
+
canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
95
157
|
export class AuthoringService {
|
|
96
158
|
public constructor(
|
|
97
159
|
private readonly context: BackendContext,
|
|
@@ -183,14 +245,27 @@ export class AuthoringService {
|
|
|
183
245
|
|
|
184
246
|
public async saveSession(input: SaveSessionInput): Promise<
|
|
185
247
|
BackendResult<
|
|
186
|
-
BackendSessionSnapshot,
|
|
248
|
+
BackendSessionSnapshot | MaterializeSessionSnapshot,
|
|
187
249
|
| SessionNotFoundError
|
|
188
250
|
| SessionStaleWriteError
|
|
189
251
|
| SavePartialFailureError
|
|
252
|
+
| MaterializeValidationFailedError
|
|
253
|
+
| MaterializeWriteFailedError
|
|
190
254
|
| PathPolicyViolationError
|
|
255
|
+
| InProcessLockConflictError
|
|
191
256
|
| BackendCapabilityUnavailableError
|
|
192
257
|
>
|
|
193
258
|
> {
|
|
259
|
+
if (input.force === true || input.mode === 'materializeCleanSession') {
|
|
260
|
+
return this.materializeSession({
|
|
261
|
+
sessionId: input.sessionId,
|
|
262
|
+
expectedRevision: input.expectedRevision,
|
|
263
|
+
targetPath: input.targetPath,
|
|
264
|
+
fileSystem: input.fileSystem,
|
|
265
|
+
mode: 'fullProject',
|
|
266
|
+
reason: 'force_save',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
194
269
|
const startedAt = Date.now();
|
|
195
270
|
const session = this.context.sessions.get(input.sessionId);
|
|
196
271
|
if (!session || session.closed) {
|
|
@@ -201,12 +276,9 @@ export class AuthoringService {
|
|
|
201
276
|
return failure(
|
|
202
277
|
'authoring',
|
|
203
278
|
startedAt,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
capability: 'fileSystem',
|
|
208
|
-
requiredAdapter: 'BackendFileSystem',
|
|
209
|
-
},
|
|
279
|
+
createCapabilityUnavailableError(
|
|
280
|
+
'saveSession requires an injected BackendFileSystem adapter.',
|
|
281
|
+
),
|
|
210
282
|
toSessionSnapshot(session, this.context.capabilities),
|
|
211
283
|
{
|
|
212
284
|
sessionId: session.sessionId,
|
|
@@ -307,4 +379,237 @@ export class AuthoringService {
|
|
|
307
379
|
);
|
|
308
380
|
}
|
|
309
381
|
}
|
|
382
|
+
|
|
383
|
+
public async materializeSession(input: MaterializeSessionInput): Promise<
|
|
384
|
+
BackendResult<
|
|
385
|
+
MaterializeSessionSnapshot,
|
|
386
|
+
| SessionNotFoundError
|
|
387
|
+
| SessionStaleWriteError
|
|
388
|
+
| MaterializeValidationFailedError
|
|
389
|
+
| MaterializeWriteFailedError
|
|
390
|
+
| PathPolicyViolationError
|
|
391
|
+
| InProcessLockConflictError
|
|
392
|
+
| BackendCapabilityUnavailableError
|
|
393
|
+
>
|
|
394
|
+
> {
|
|
395
|
+
const startedAt = Date.now();
|
|
396
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
397
|
+
if (!session || session.closed) {
|
|
398
|
+
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
399
|
+
}
|
|
400
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
|
|
401
|
+
return failure(
|
|
402
|
+
'authoring',
|
|
403
|
+
startedAt,
|
|
404
|
+
createStaleWriteError(session, input.expectedRevision),
|
|
405
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
406
|
+
{
|
|
407
|
+
sessionId: session.sessionId,
|
|
408
|
+
revision: session.revision,
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
|
|
414
|
+
const fileSystem = storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
|
|
415
|
+
if (!fileSystem) {
|
|
416
|
+
return failure(
|
|
417
|
+
'authoring',
|
|
418
|
+
startedAt,
|
|
419
|
+
createCapabilityUnavailableError(
|
|
420
|
+
'materializeSession requires an injected BackendFileSystem adapter.',
|
|
421
|
+
),
|
|
422
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
423
|
+
{
|
|
424
|
+
sessionId: session.sessionId,
|
|
425
|
+
revision: session.revision,
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
|
|
431
|
+
const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
|
|
432
|
+
if (targetViolation) {
|
|
433
|
+
return failure(
|
|
434
|
+
'authoring',
|
|
435
|
+
startedAt,
|
|
436
|
+
targetViolation,
|
|
437
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
438
|
+
{
|
|
439
|
+
sessionId: session.sessionId,
|
|
440
|
+
revision: session.revision,
|
|
441
|
+
},
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (storageTarget) {
|
|
446
|
+
const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
|
|
447
|
+
if (holderSessionId && holderSessionId !== session.sessionId) {
|
|
448
|
+
return failure(
|
|
449
|
+
'authoring',
|
|
450
|
+
startedAt,
|
|
451
|
+
{
|
|
452
|
+
code: 'lock_conflict',
|
|
453
|
+
kind: 'in_process_session_exists',
|
|
454
|
+
message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
|
|
455
|
+
canonicalPathKey: storageTarget.canonicalPathKey,
|
|
456
|
+
holderSessionId,
|
|
457
|
+
},
|
|
458
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
459
|
+
{
|
|
460
|
+
sessionId: session.sessionId,
|
|
461
|
+
revision: session.revision,
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const diagnostics = validationDiagnostics(session.project);
|
|
468
|
+
if (diagnostics.length > 0) {
|
|
469
|
+
const error: MaterializeValidationFailedError = {
|
|
470
|
+
code: 'materialize_validation_failed',
|
|
471
|
+
message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
|
|
472
|
+
sessionId: session.sessionId,
|
|
473
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
474
|
+
issueCount: diagnostics.length,
|
|
475
|
+
diagnostics,
|
|
476
|
+
};
|
|
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
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let document: ReturnType<typeof materializeUamProject>;
|
|
491
|
+
try {
|
|
492
|
+
document = materializeUamProject(session.project);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
const diagnosticsFromError: BackendDiagnostic[] = [
|
|
495
|
+
{
|
|
496
|
+
code: 'materialize_validation_failed',
|
|
497
|
+
message: error instanceof Error ? error.message : String(error),
|
|
498
|
+
severity: 'error',
|
|
499
|
+
operationKind: 'materializeSession',
|
|
500
|
+
},
|
|
501
|
+
];
|
|
502
|
+
return failure(
|
|
503
|
+
'authoring',
|
|
504
|
+
startedAt,
|
|
505
|
+
{
|
|
506
|
+
code: 'materialize_validation_failed',
|
|
507
|
+
message: error instanceof Error ? error.message : String(error),
|
|
508
|
+
sessionId: session.sessionId,
|
|
509
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
510
|
+
issueCount: diagnosticsFromError.length,
|
|
511
|
+
diagnostics: diagnosticsFromError,
|
|
512
|
+
},
|
|
513
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
514
|
+
{
|
|
515
|
+
sessionId: session.sessionId,
|
|
516
|
+
revision: session.revision,
|
|
517
|
+
diagnostics: diagnosticsFromError,
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const writtenPaths: string[] = [];
|
|
523
|
+
const failedPaths: string[] = [];
|
|
524
|
+
const skippedPaths: string[] = [];
|
|
525
|
+
this.eventService.emit({
|
|
526
|
+
kind: 'save.started',
|
|
527
|
+
sessionId: session.sessionId,
|
|
528
|
+
canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
|
|
529
|
+
revision: session.revision,
|
|
530
|
+
});
|
|
531
|
+
try {
|
|
532
|
+
const writer = new ProjectWriter(createWriterFileSystem(fileSystem, writtenPaths, failedPaths));
|
|
533
|
+
await writer.write(document, fairyPath);
|
|
534
|
+
if (storageTarget) {
|
|
535
|
+
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
536
|
+
session.fileSystem = storageTarget.fileSystem;
|
|
537
|
+
session.fairyPath = storageTarget.fairyPath;
|
|
538
|
+
session.canonicalProjectPath = storageTarget.canonicalProjectPath;
|
|
539
|
+
session.canonicalPathKey = storageTarget.canonicalPathKey;
|
|
540
|
+
this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
|
|
541
|
+
}
|
|
542
|
+
session.lastSavedRevision = session.revision;
|
|
543
|
+
session.dirty = false;
|
|
544
|
+
const cacheEntry = this.cacheService.refreshSession(session);
|
|
545
|
+
this.eventService.emit({
|
|
546
|
+
kind: 'save.completed',
|
|
547
|
+
sessionId: session.sessionId,
|
|
548
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
549
|
+
revision: session.revision,
|
|
550
|
+
});
|
|
551
|
+
this.eventService.emit({
|
|
552
|
+
kind: 'cache.updated',
|
|
553
|
+
sessionId: session.sessionId,
|
|
554
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
555
|
+
revision: session.revision,
|
|
556
|
+
cacheRevision: cacheEntry.revision,
|
|
557
|
+
});
|
|
558
|
+
return success(
|
|
559
|
+
'authoring',
|
|
560
|
+
startedAt,
|
|
561
|
+
toMaterializeSnapshot(session, this.context.capabilities, {
|
|
562
|
+
reason: input.reason,
|
|
563
|
+
writtenPaths,
|
|
564
|
+
skippedPaths,
|
|
565
|
+
diagnostics: [],
|
|
566
|
+
}),
|
|
567
|
+
{
|
|
568
|
+
sessionId: session.sessionId,
|
|
569
|
+
revision: session.revision,
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
const diagnosticsFromError: BackendDiagnostic[] = [
|
|
574
|
+
{
|
|
575
|
+
code: 'write_failed',
|
|
576
|
+
message: error instanceof Error ? error.message : String(error),
|
|
577
|
+
severity: 'error',
|
|
578
|
+
path: failedPaths[0],
|
|
579
|
+
operationKind: 'materializeSession',
|
|
580
|
+
},
|
|
581
|
+
];
|
|
582
|
+
this.cacheService.invalidateSession(session);
|
|
583
|
+
this.eventService.emit({
|
|
584
|
+
kind: 'save.failed',
|
|
585
|
+
sessionId: session.sessionId,
|
|
586
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
587
|
+
revision: session.revision,
|
|
588
|
+
diagnostics: diagnosticsFromError,
|
|
589
|
+
});
|
|
590
|
+
return failure(
|
|
591
|
+
'authoring',
|
|
592
|
+
startedAt,
|
|
593
|
+
{
|
|
594
|
+
code: 'write_failed',
|
|
595
|
+
message: error instanceof Error ? error.message : String(error),
|
|
596
|
+
sessionId: session.sessionId,
|
|
597
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
598
|
+
attemptedRevision: session.revision,
|
|
599
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
600
|
+
writtenPaths,
|
|
601
|
+
failedPaths,
|
|
602
|
+
skippedPaths,
|
|
603
|
+
diagnostics: diagnosticsFromError,
|
|
604
|
+
diskMayBePartiallyUpdated: true,
|
|
605
|
+
},
|
|
606
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
607
|
+
{
|
|
608
|
+
sessionId: session.sessionId,
|
|
609
|
+
revision: session.revision,
|
|
610
|
+
diagnostics: diagnosticsFromError,
|
|
611
|
+
},
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
310
615
|
}
|