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