@openfairygui/backend 0.2.0-alpha.2 → 0.2.0-alpha.21
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 +48 -2
- package/dist/index.cjs +146 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.mts +33 -2
- package/dist/index.mjs +146 -2
- 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-OO1sHuCl.d.mts → runtime-B4SwHTRC.d.mts} +89 -10
- package/dist/{runtime-qcSlD_9-.d.cts → runtime-BB_3Ws5y.d.cts} +90 -11
- package/dist/{runtime-BPWzx-Tv.mjs → runtime-CqGK_D9A.mjs} +483 -87
- package/dist/{runtime-BL9Pkfc7.cjs → runtime-D3bShb1X.cjs} +482 -86
- package/package.json +5 -4
- package/src/contracts.ts +8 -0
- package/src/index.ts +25 -12
- package/src/runtime/capabilities.ts +126 -0
- package/src/runtime/contracts.ts +512 -0
- package/src/runtime.ts +67 -540
- package/src/services/authoring-service.ts +520 -80
- package/src/services/context.ts +12 -8
- package/src/services/runtime-service.ts +108 -12
- package/src/services/session-project-writer.ts +64 -0
- package/src/services/session-utils.ts +5 -1
- package/src/storage.ts +194 -0
|
@@ -1,87 +1,201 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import type { ProjectSourceFile } 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,
|
|
10
14
|
BackendFileSystem,
|
|
11
15
|
BackendResult,
|
|
12
16
|
BackendSessionSnapshot,
|
|
17
|
+
InProcessLockConflictError,
|
|
18
|
+
MaterializeSessionInput,
|
|
19
|
+
MaterializeSessionSnapshot,
|
|
20
|
+
MaterializeValidationFailedError,
|
|
21
|
+
MaterializeWriteFailedError,
|
|
13
22
|
SavePartialFailureError,
|
|
23
|
+
SaveSessionInput,
|
|
14
24
|
SessionNotFoundError,
|
|
15
25
|
SessionStaleWriteError,
|
|
26
|
+
UamFidelityUnsupportedError,
|
|
16
27
|
} from '../runtime.js';
|
|
17
|
-
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';
|
|
31
|
+
import { writeSessionProject } from './session-project-writer.js';
|
|
18
32
|
import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
|
|
19
33
|
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
function projectSourceFiles(project: UamProject): Map<string, ProjectSourceFile> {
|
|
35
|
+
const result = new Map<string, ProjectSourceFile>();
|
|
36
|
+
for (const pkg of project.packages) {
|
|
37
|
+
result.set(`${pkg.id}/package.xml`, {
|
|
38
|
+
packageName: pkg.name,
|
|
39
|
+
branch: '',
|
|
40
|
+
path: '',
|
|
41
|
+
fileName: 'package.xml',
|
|
42
|
+
});
|
|
43
|
+
const branches = new Set<string>();
|
|
44
|
+
for (const resource of pkg.resources) {
|
|
45
|
+
if (resource.branch) branches.add(resource.branch);
|
|
46
|
+
const fileName = resource.kind === 'component'
|
|
47
|
+
? `${resource.name}.xml`
|
|
48
|
+
: resource.fileName ?? resource.file ?? '';
|
|
49
|
+
if (!fileName) continue;
|
|
50
|
+
result.set(`${pkg.id}/${resource.id}`, {
|
|
51
|
+
packageName: pkg.name,
|
|
52
|
+
branch: resource.branch,
|
|
53
|
+
path: resource.path,
|
|
54
|
+
fileName,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (const branch of branches) {
|
|
58
|
+
result.set(`${pkg.id}/branch/${branch}`, {
|
|
59
|
+
packageName: pkg.name,
|
|
60
|
+
branch,
|
|
61
|
+
path: '',
|
|
62
|
+
fileName: 'package_branch.xml',
|
|
63
|
+
});
|
|
33
64
|
}
|
|
34
65
|
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sourceFileKey(source: ProjectSourceFile): string {
|
|
70
|
+
return [source.branch, source.packageName, source.path, source.fileName].join('\0');
|
|
71
|
+
}
|
|
35
72
|
|
|
73
|
+
function recordStaleProjectFiles(
|
|
74
|
+
session: Parameters<typeof toSessionSnapshot>[0],
|
|
75
|
+
previousProject: UamProject,
|
|
76
|
+
nextProject: UamProject,
|
|
77
|
+
): void {
|
|
78
|
+
if (!session.fileSystem) return;
|
|
79
|
+
const previousSources = projectSourceFiles(previousProject);
|
|
80
|
+
const nextSourceKeys = new Set([...projectSourceFiles(nextProject).values()].map(sourceFileKey));
|
|
81
|
+
for (const source of previousSources.values()) {
|
|
82
|
+
const key = sourceFileKey(source);
|
|
83
|
+
if (!nextSourceKeys.has(key)) session.pendingStaleSourceFiles.set(key, source);
|
|
84
|
+
}
|
|
85
|
+
for (const key of nextSourceKeys) {
|
|
86
|
+
session.pendingStaleSourceFiles.delete(key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagnostic[] {
|
|
91
|
+
return error.diagnostics.length > 0
|
|
92
|
+
? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
93
|
+
: [
|
|
94
|
+
{
|
|
95
|
+
code: error.code,
|
|
96
|
+
message: error.message,
|
|
97
|
+
severity: 'error',
|
|
98
|
+
operationKind: error.operationKind,
|
|
99
|
+
opIndex: error.opIndex,
|
|
100
|
+
opId: error.opId,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createCapabilityUnavailableError(message: string): BackendCapabilityUnavailableError {
|
|
36
106
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
107
|
+
code: 'capability_unavailable',
|
|
108
|
+
message,
|
|
109
|
+
capability: 'fileSystem',
|
|
110
|
+
requiredAdapter: 'BackendFileSystem',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createUamFidelityUnsupportedError(
|
|
115
|
+
session: Parameters<typeof toSessionSnapshot>[0],
|
|
116
|
+
): UamFidelityUnsupportedError {
|
|
117
|
+
return {
|
|
118
|
+
code: 'uam_fidelity_unsupported',
|
|
119
|
+
message: 'The source project contains formal properties that the current UAM cannot preserve.',
|
|
120
|
+
sessionId: session.sessionId,
|
|
121
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validationDiagnostics(sessionProject: Parameters<typeof validateUamProject>[0]): BackendDiagnostic[] {
|
|
126
|
+
const issues = validateUamProject(sessionProject);
|
|
127
|
+
return issues.map((issue) => ({
|
|
128
|
+
code: 'materialize_validation_failed',
|
|
129
|
+
message: issue.message,
|
|
130
|
+
severity: 'error',
|
|
131
|
+
path: issue.path,
|
|
132
|
+
operationKind: 'materializeSession',
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function toMaterializeSnapshot(
|
|
137
|
+
session: Parameters<typeof toSessionSnapshot>[0],
|
|
138
|
+
capabilities: Parameters<typeof toSessionSnapshot>[1],
|
|
139
|
+
input: {
|
|
140
|
+
reason?: string;
|
|
141
|
+
writtenPaths: string[];
|
|
142
|
+
skippedPaths: string[];
|
|
143
|
+
diagnostics: BackendDiagnostic[];
|
|
144
|
+
},
|
|
145
|
+
): MaterializeSessionSnapshot {
|
|
146
|
+
return {
|
|
147
|
+
...toSessionSnapshot(session, capabilities),
|
|
148
|
+
mode: 'fullProject',
|
|
149
|
+
reason: input.reason,
|
|
150
|
+
materializeRevision: session.revision,
|
|
151
|
+
saveRevision: session.lastSavedRevision,
|
|
152
|
+
writtenPaths: [...input.writtenPaths],
|
|
153
|
+
skippedPaths: [...input.skippedPaths],
|
|
154
|
+
diagnostics: input.diagnostics.map((diagnostic) => ({ ...diagnostic })),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function storageCanonicalTarget(input: NonNullable<MaterializeSessionInput['storage']>): {
|
|
159
|
+
fileSystem: BackendFileSystem;
|
|
160
|
+
fairyPath: string;
|
|
161
|
+
canonicalProjectPath: string;
|
|
162
|
+
canonicalPathKey: string;
|
|
163
|
+
} {
|
|
164
|
+
const canonicalProjectPath = input.canonicalProjectPath ?? (input.fileSystem.dirname(input.fairyPath) || '.');
|
|
165
|
+
return {
|
|
166
|
+
fileSystem: input.fileSystem,
|
|
167
|
+
fairyPath: input.fairyPath,
|
|
168
|
+
canonicalProjectPath,
|
|
169
|
+
canonicalPathKey: input.canonicalPathKey ?? normalizeComparablePath(canonicalProjectPath),
|
|
75
170
|
};
|
|
76
171
|
}
|
|
77
172
|
|
|
78
173
|
export class AuthoringService {
|
|
174
|
+
private readonly sessionOperations = new Map<string, Promise<void>>();
|
|
175
|
+
|
|
79
176
|
public constructor(
|
|
80
177
|
private readonly context: BackendContext,
|
|
81
178
|
private readonly cacheService: CacheService,
|
|
82
179
|
private readonly eventService: EventService,
|
|
83
180
|
) {}
|
|
84
181
|
|
|
182
|
+
private async runSessionExclusive<T>(sessionId: string, operation: () => Promise<T>): Promise<T> {
|
|
183
|
+
const previous = this.sessionOperations.get(sessionId) ?? Promise.resolve();
|
|
184
|
+
let release = (): void => undefined;
|
|
185
|
+
const current = new Promise<void>((resolve) => {
|
|
186
|
+
release = resolve;
|
|
187
|
+
});
|
|
188
|
+
const tail = previous.then(() => current);
|
|
189
|
+
this.sessionOperations.set(sessionId, tail);
|
|
190
|
+
await previous;
|
|
191
|
+
try {
|
|
192
|
+
return await operation();
|
|
193
|
+
} finally {
|
|
194
|
+
release();
|
|
195
|
+
if (this.sessionOperations.get(sessionId) === tail) this.sessionOperations.delete(sessionId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
85
199
|
public async applyTransaction(
|
|
86
200
|
input: ApplySessionTransactionInput,
|
|
87
201
|
): Promise<
|
|
@@ -89,6 +203,17 @@ export class AuthoringService {
|
|
|
89
203
|
BackendSessionSnapshot,
|
|
90
204
|
SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
|
|
91
205
|
>
|
|
206
|
+
> {
|
|
207
|
+
return this.runSessionExclusive(input.sessionId, () => this.applyTransactionExclusive(input));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async applyTransactionExclusive(
|
|
211
|
+
input: ApplySessionTransactionInput,
|
|
212
|
+
): Promise<
|
|
213
|
+
BackendResult<
|
|
214
|
+
BackendSessionSnapshot,
|
|
215
|
+
SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
|
|
216
|
+
>
|
|
92
217
|
> {
|
|
93
218
|
const startedAt = Date.now();
|
|
94
219
|
const session = this.context.sessions.get(input.sessionId);
|
|
@@ -119,17 +244,13 @@ export class AuthoringService {
|
|
|
119
244
|
operations: input.operations,
|
|
120
245
|
});
|
|
121
246
|
if (result.ok === false) {
|
|
247
|
+
const diagnostics = toBackendDiagnostics(result.error);
|
|
122
248
|
this.eventService.emit({
|
|
123
249
|
kind: 'transaction.rejected',
|
|
124
250
|
sessionId: session.sessionId,
|
|
125
251
|
canonicalPathKey: session.canonicalPathKey,
|
|
126
252
|
revision: session.revision,
|
|
127
|
-
diagnostics
|
|
128
|
-
result.error.issues?.map((issue) => ({
|
|
129
|
-
code: result.error.code,
|
|
130
|
-
message: issue.message,
|
|
131
|
-
severity: 'error' as const,
|
|
132
|
-
})) ?? [],
|
|
253
|
+
diagnostics,
|
|
133
254
|
});
|
|
134
255
|
return failure(
|
|
135
256
|
'authoring',
|
|
@@ -139,10 +260,12 @@ export class AuthoringService {
|
|
|
139
260
|
{
|
|
140
261
|
sessionId: session.sessionId,
|
|
141
262
|
revision: session.revision,
|
|
263
|
+
diagnostics,
|
|
142
264
|
},
|
|
143
265
|
);
|
|
144
266
|
}
|
|
145
267
|
|
|
268
|
+
recordStaleProjectFiles(session, session.project, result.project);
|
|
146
269
|
session.project = result.project;
|
|
147
270
|
session.revision += 1;
|
|
148
271
|
session.dirty = true;
|
|
@@ -167,17 +290,48 @@ export class AuthoringService {
|
|
|
167
290
|
});
|
|
168
291
|
}
|
|
169
292
|
|
|
170
|
-
public async saveSession(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
targetPath?: string;
|
|
174
|
-
}): Promise<
|
|
293
|
+
public async saveSession(
|
|
294
|
+
input: SaveSessionInput,
|
|
295
|
+
): Promise<
|
|
175
296
|
BackendResult<
|
|
176
|
-
BackendSessionSnapshot,
|
|
297
|
+
BackendSessionSnapshot | MaterializeSessionSnapshot,
|
|
298
|
+
| SessionNotFoundError
|
|
299
|
+
| SessionStaleWriteError
|
|
300
|
+
| SavePartialFailureError
|
|
301
|
+
| UamFidelityUnsupportedError
|
|
302
|
+
| MaterializeValidationFailedError
|
|
303
|
+
| MaterializeWriteFailedError
|
|
304
|
+
| PathPolicyViolationError
|
|
305
|
+
| InProcessLockConflictError
|
|
306
|
+
| BackendCapabilityUnavailableError
|
|
307
|
+
>
|
|
308
|
+
> {
|
|
309
|
+
if (input.force === true || input.mode === 'materializeCleanSession') {
|
|
310
|
+
return this.materializeSession({
|
|
311
|
+
sessionId: input.sessionId,
|
|
312
|
+
expectedRevision: input.expectedRevision,
|
|
313
|
+
targetPath: input.targetPath,
|
|
314
|
+
fileSystem: input.fileSystem,
|
|
315
|
+
mode: 'fullProject',
|
|
316
|
+
reason: 'force_save',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return this.runSessionExclusive(input.sessionId, () => this.saveSessionExclusive(input));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private async saveSessionExclusive(
|
|
323
|
+
input: SaveSessionInput,
|
|
324
|
+
): Promise<
|
|
325
|
+
BackendResult<
|
|
326
|
+
BackendSessionSnapshot | MaterializeSessionSnapshot,
|
|
177
327
|
| SessionNotFoundError
|
|
178
328
|
| SessionStaleWriteError
|
|
179
329
|
| SavePartialFailureError
|
|
330
|
+
| UamFidelityUnsupportedError
|
|
331
|
+
| MaterializeValidationFailedError
|
|
332
|
+
| MaterializeWriteFailedError
|
|
180
333
|
| PathPolicyViolationError
|
|
334
|
+
| InProcessLockConflictError
|
|
181
335
|
| BackendCapabilityUnavailableError
|
|
182
336
|
>
|
|
183
337
|
> {
|
|
@@ -186,16 +340,12 @@ export class AuthoringService {
|
|
|
186
340
|
if (!session || session.closed) {
|
|
187
341
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
188
342
|
}
|
|
189
|
-
|
|
343
|
+
const fileSystem = session.fileSystem ?? input.fileSystem ?? this.context.fileSystem;
|
|
344
|
+
if (!fileSystem) {
|
|
190
345
|
return failure(
|
|
191
346
|
'authoring',
|
|
192
347
|
startedAt,
|
|
193
|
-
|
|
194
|
-
code: 'capability_unavailable',
|
|
195
|
-
message: 'saveSession requires an injected BackendFileSystem adapter.',
|
|
196
|
-
capability: 'fileSystem',
|
|
197
|
-
requiredAdapter: 'BackendFileSystem',
|
|
198
|
-
},
|
|
348
|
+
createCapabilityUnavailableError('saveSession requires an injected BackendFileSystem adapter.'),
|
|
199
349
|
toSessionSnapshot(session, this.context.capabilities),
|
|
200
350
|
{
|
|
201
351
|
sessionId: session.sessionId,
|
|
@@ -215,7 +365,6 @@ export class AuthoringService {
|
|
|
215
365
|
},
|
|
216
366
|
);
|
|
217
367
|
}
|
|
218
|
-
const fileSystem = this.context.fileSystem;
|
|
219
368
|
const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
|
|
220
369
|
if (targetViolation) {
|
|
221
370
|
return failure(
|
|
@@ -235,6 +384,18 @@ export class AuthoringService {
|
|
|
235
384
|
revision: session.revision,
|
|
236
385
|
});
|
|
237
386
|
}
|
|
387
|
+
if (session.uamFidelity === 'unsupported') {
|
|
388
|
+
return failure(
|
|
389
|
+
'authoring',
|
|
390
|
+
startedAt,
|
|
391
|
+
createUamFidelityUnsupportedError(session),
|
|
392
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
393
|
+
{
|
|
394
|
+
sessionId: session.sessionId,
|
|
395
|
+
revision: session.revision,
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
}
|
|
238
399
|
|
|
239
400
|
const committedPaths: string[] = [];
|
|
240
401
|
const failedPaths: string[] = [];
|
|
@@ -245,8 +406,17 @@ export class AuthoringService {
|
|
|
245
406
|
revision: session.revision,
|
|
246
407
|
});
|
|
247
408
|
try {
|
|
248
|
-
|
|
249
|
-
|
|
409
|
+
await writeSessionProject({
|
|
410
|
+
fileSystem,
|
|
411
|
+
document: materializeUamProject(session.project),
|
|
412
|
+
fairyPath: session.fairyPath,
|
|
413
|
+
staleSourceFiles: [...session.pendingStaleSourceFiles.values()],
|
|
414
|
+
writtenPaths: committedPaths,
|
|
415
|
+
failedPaths,
|
|
416
|
+
});
|
|
417
|
+
session.fileSystem ??= fileSystem;
|
|
418
|
+
session.pendingStaleSourceFiles.clear();
|
|
419
|
+
commitUamProjectSourcePaths(session.project);
|
|
250
420
|
session.lastSavedRevision = session.revision;
|
|
251
421
|
session.dirty = false;
|
|
252
422
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
@@ -297,4 +467,274 @@ export class AuthoringService {
|
|
|
297
467
|
);
|
|
298
468
|
}
|
|
299
469
|
}
|
|
470
|
+
|
|
471
|
+
public async materializeSession(
|
|
472
|
+
input: MaterializeSessionInput,
|
|
473
|
+
): Promise<
|
|
474
|
+
BackendResult<
|
|
475
|
+
MaterializeSessionSnapshot,
|
|
476
|
+
| SessionNotFoundError
|
|
477
|
+
| SessionStaleWriteError
|
|
478
|
+
| UamFidelityUnsupportedError
|
|
479
|
+
| MaterializeValidationFailedError
|
|
480
|
+
| MaterializeWriteFailedError
|
|
481
|
+
| PathPolicyViolationError
|
|
482
|
+
| InProcessLockConflictError
|
|
483
|
+
| BackendCapabilityUnavailableError
|
|
484
|
+
>
|
|
485
|
+
> {
|
|
486
|
+
return this.runSessionExclusive(input.sessionId, () => this.materializeSessionExclusive(input));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private async materializeSessionExclusive(
|
|
490
|
+
input: MaterializeSessionInput,
|
|
491
|
+
): Promise<
|
|
492
|
+
BackendResult<
|
|
493
|
+
MaterializeSessionSnapshot,
|
|
494
|
+
| SessionNotFoundError
|
|
495
|
+
| SessionStaleWriteError
|
|
496
|
+
| UamFidelityUnsupportedError
|
|
497
|
+
| MaterializeValidationFailedError
|
|
498
|
+
| MaterializeWriteFailedError
|
|
499
|
+
| PathPolicyViolationError
|
|
500
|
+
| InProcessLockConflictError
|
|
501
|
+
| BackendCapabilityUnavailableError
|
|
502
|
+
>
|
|
503
|
+
> {
|
|
504
|
+
const startedAt = Date.now();
|
|
505
|
+
const session = this.context.sessions.get(input.sessionId);
|
|
506
|
+
if (!session || session.closed) {
|
|
507
|
+
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
508
|
+
}
|
|
509
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
|
|
510
|
+
return failure(
|
|
511
|
+
'authoring',
|
|
512
|
+
startedAt,
|
|
513
|
+
createStaleWriteError(session, input.expectedRevision),
|
|
514
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
515
|
+
{
|
|
516
|
+
sessionId: session.sessionId,
|
|
517
|
+
revision: session.revision,
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const storageTarget = input.storage ? storageCanonicalTarget(input.storage) : null;
|
|
523
|
+
const fileSystem =
|
|
524
|
+
storageTarget?.fileSystem ?? input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
|
|
525
|
+
if (!fileSystem) {
|
|
526
|
+
return failure(
|
|
527
|
+
'authoring',
|
|
528
|
+
startedAt,
|
|
529
|
+
createCapabilityUnavailableError('materializeSession requires an injected BackendFileSystem adapter.'),
|
|
530
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
531
|
+
{
|
|
532
|
+
sessionId: session.sessionId,
|
|
533
|
+
revision: session.revision,
|
|
534
|
+
},
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const fairyPath = storageTarget?.fairyPath ?? session.fairyPath;
|
|
539
|
+
const targetViolation = await validateSaveTarget(fileSystem, fairyPath, input.targetPath);
|
|
540
|
+
if (targetViolation) {
|
|
541
|
+
return failure(
|
|
542
|
+
'authoring',
|
|
543
|
+
startedAt,
|
|
544
|
+
targetViolation,
|
|
545
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
546
|
+
{
|
|
547
|
+
sessionId: session.sessionId,
|
|
548
|
+
revision: session.revision,
|
|
549
|
+
},
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (storageTarget) {
|
|
554
|
+
const holderSessionId = this.context.sessionsByPath.get(storageTarget.canonicalPathKey);
|
|
555
|
+
if (holderSessionId && holderSessionId !== session.sessionId) {
|
|
556
|
+
return failure(
|
|
557
|
+
'authoring',
|
|
558
|
+
startedAt,
|
|
559
|
+
{
|
|
560
|
+
code: 'lock_conflict',
|
|
561
|
+
kind: 'in_process_session_exists',
|
|
562
|
+
message: `Project is already open in this backend runtime: ${storageTarget.canonicalProjectPath}`,
|
|
563
|
+
canonicalPathKey: storageTarget.canonicalPathKey,
|
|
564
|
+
holderSessionId,
|
|
565
|
+
},
|
|
566
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
567
|
+
{
|
|
568
|
+
sessionId: session.sessionId,
|
|
569
|
+
revision: session.revision,
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (session.uamFidelity === 'unsupported') {
|
|
576
|
+
return failure(
|
|
577
|
+
'authoring',
|
|
578
|
+
startedAt,
|
|
579
|
+
createUamFidelityUnsupportedError(session),
|
|
580
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
581
|
+
{
|
|
582
|
+
sessionId: session.sessionId,
|
|
583
|
+
revision: session.revision,
|
|
584
|
+
},
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const diagnostics = validationDiagnostics(session.project);
|
|
589
|
+
if (diagnostics.length > 0) {
|
|
590
|
+
const error: MaterializeValidationFailedError = {
|
|
591
|
+
code: 'materialize_validation_failed',
|
|
592
|
+
message: `UAM materialize validation failed with ${diagnostics.length} issue(s).`,
|
|
593
|
+
sessionId: session.sessionId,
|
|
594
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
595
|
+
issueCount: diagnostics.length,
|
|
596
|
+
diagnostics,
|
|
597
|
+
};
|
|
598
|
+
return failure('authoring', startedAt, error, toSessionSnapshot(session, this.context.capabilities), {
|
|
599
|
+
sessionId: session.sessionId,
|
|
600
|
+
revision: session.revision,
|
|
601
|
+
diagnostics,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let document: ReturnType<typeof materializeUamProject>;
|
|
606
|
+
try {
|
|
607
|
+
document = materializeUamProject(session.project);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
const diagnosticsFromError: BackendDiagnostic[] = [
|
|
610
|
+
{
|
|
611
|
+
code: 'materialize_validation_failed',
|
|
612
|
+
message: error instanceof Error ? error.message : String(error),
|
|
613
|
+
severity: 'error',
|
|
614
|
+
operationKind: 'materializeSession',
|
|
615
|
+
},
|
|
616
|
+
];
|
|
617
|
+
return failure(
|
|
618
|
+
'authoring',
|
|
619
|
+
startedAt,
|
|
620
|
+
{
|
|
621
|
+
code: 'materialize_validation_failed',
|
|
622
|
+
message: error instanceof Error ? error.message : String(error),
|
|
623
|
+
sessionId: session.sessionId,
|
|
624
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
625
|
+
issueCount: diagnosticsFromError.length,
|
|
626
|
+
diagnostics: diagnosticsFromError,
|
|
627
|
+
},
|
|
628
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
629
|
+
{
|
|
630
|
+
sessionId: session.sessionId,
|
|
631
|
+
revision: session.revision,
|
|
632
|
+
diagnostics: diagnosticsFromError,
|
|
633
|
+
},
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const writtenPaths: string[] = [];
|
|
638
|
+
const failedPaths: string[] = [];
|
|
639
|
+
const skippedPaths: string[] = [];
|
|
640
|
+
this.eventService.emit({
|
|
641
|
+
kind: 'save.started',
|
|
642
|
+
sessionId: session.sessionId,
|
|
643
|
+
canonicalPathKey: storageTarget?.canonicalPathKey ?? session.canonicalPathKey,
|
|
644
|
+
revision: session.revision,
|
|
645
|
+
});
|
|
646
|
+
try {
|
|
647
|
+
const isSessionStorageTarget = fileSystem === session.fileSystem && fairyPath === session.fairyPath;
|
|
648
|
+
await writeSessionProject({
|
|
649
|
+
fileSystem,
|
|
650
|
+
document,
|
|
651
|
+
fairyPath,
|
|
652
|
+
staleSourceFiles: isSessionStorageTarget ? [...session.pendingStaleSourceFiles.values()] : [],
|
|
653
|
+
writtenPaths,
|
|
654
|
+
failedPaths,
|
|
655
|
+
});
|
|
656
|
+
if (isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
657
|
+
if (storageTarget && !isSessionStorageTarget) session.pendingStaleSourceFiles.clear();
|
|
658
|
+
if (isSessionStorageTarget || storageTarget) commitUamProjectSourcePaths(session.project);
|
|
659
|
+
if (storageTarget) {
|
|
660
|
+
this.context.sessionsByPath.delete(session.canonicalPathKey);
|
|
661
|
+
session.fileSystem = storageTarget.fileSystem;
|
|
662
|
+
session.fairyPath = storageTarget.fairyPath;
|
|
663
|
+
session.canonicalProjectPath = storageTarget.canonicalProjectPath;
|
|
664
|
+
session.canonicalPathKey = storageTarget.canonicalPathKey;
|
|
665
|
+
this.context.sessionsByPath.set(session.canonicalPathKey, session.sessionId);
|
|
666
|
+
}
|
|
667
|
+
session.lastSavedRevision = session.revision;
|
|
668
|
+
session.dirty = false;
|
|
669
|
+
const cacheEntry = this.cacheService.refreshSession(session);
|
|
670
|
+
this.eventService.emit({
|
|
671
|
+
kind: 'save.completed',
|
|
672
|
+
sessionId: session.sessionId,
|
|
673
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
674
|
+
revision: session.revision,
|
|
675
|
+
});
|
|
676
|
+
this.eventService.emit({
|
|
677
|
+
kind: 'cache.updated',
|
|
678
|
+
sessionId: session.sessionId,
|
|
679
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
680
|
+
revision: session.revision,
|
|
681
|
+
cacheRevision: cacheEntry.revision,
|
|
682
|
+
});
|
|
683
|
+
return success(
|
|
684
|
+
'authoring',
|
|
685
|
+
startedAt,
|
|
686
|
+
toMaterializeSnapshot(session, this.context.capabilities, {
|
|
687
|
+
reason: input.reason,
|
|
688
|
+
writtenPaths,
|
|
689
|
+
skippedPaths,
|
|
690
|
+
diagnostics: [],
|
|
691
|
+
}),
|
|
692
|
+
{
|
|
693
|
+
sessionId: session.sessionId,
|
|
694
|
+
revision: session.revision,
|
|
695
|
+
},
|
|
696
|
+
);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
const diagnosticsFromError: BackendDiagnostic[] = [
|
|
699
|
+
{
|
|
700
|
+
code: 'write_failed',
|
|
701
|
+
message: error instanceof Error ? error.message : String(error),
|
|
702
|
+
severity: 'error',
|
|
703
|
+
path: failedPaths[0],
|
|
704
|
+
operationKind: 'materializeSession',
|
|
705
|
+
},
|
|
706
|
+
];
|
|
707
|
+
this.cacheService.invalidateSession(session);
|
|
708
|
+
this.eventService.emit({
|
|
709
|
+
kind: 'save.failed',
|
|
710
|
+
sessionId: session.sessionId,
|
|
711
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
712
|
+
revision: session.revision,
|
|
713
|
+
diagnostics: diagnosticsFromError,
|
|
714
|
+
});
|
|
715
|
+
return failure(
|
|
716
|
+
'authoring',
|
|
717
|
+
startedAt,
|
|
718
|
+
{
|
|
719
|
+
code: 'write_failed',
|
|
720
|
+
message: error instanceof Error ? error.message : String(error),
|
|
721
|
+
sessionId: session.sessionId,
|
|
722
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
723
|
+
attemptedRevision: session.revision,
|
|
724
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
725
|
+
writtenPaths,
|
|
726
|
+
failedPaths,
|
|
727
|
+
skippedPaths,
|
|
728
|
+
diagnostics: diagnosticsFromError,
|
|
729
|
+
diskMayBePartiallyUpdated: true,
|
|
730
|
+
},
|
|
731
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
732
|
+
{
|
|
733
|
+
sessionId: session.sessionId,
|
|
734
|
+
revision: session.revision,
|
|
735
|
+
diagnostics: diagnosticsFromError,
|
|
736
|
+
},
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
300
740
|
}
|