@openfairygui/backend 0.2.0-alpha.1 → 0.2.0-alpha.13
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/LICENSE +21 -0
- package/README.md +40 -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-DLWLghmH.mjs → runtime-DFatY9W0.mjs} +244 -19
- package/dist/{runtime-BXbt135W.cjs → runtime-GKzsXJdO.cjs} +251 -26
- package/dist/{runtime-C5JY_kRI.d.cts → runtime-GyNVxAQ0.d.mts} +81 -11
- package/dist/{runtime-C5ZsWMnH.d.mts → runtime-Jec6FcF5.d.cts} +81 -11
- package/package.json +65 -65
- package/src/contracts.ts +8 -0
- package/src/index.ts +12 -0
- package/src/runtime.ts +156 -29
- package/src/services/authoring-service.ts +462 -46
- package/src/services/context.ts +2 -1
- package/src/services/runtime-service.ts +62 -25
- package/src/storage.ts +192 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ProjectWriter, type FileSystem } from '@openfairygui/core/project-io';
|
|
2
|
+
import { materializeUamProject, validateUamProject } from '@openfairygui/core/uam';
|
|
3
|
+
import { applyUamTransactionApp, type ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
|
|
3
4
|
import type { CacheService } from './cache-service.js';
|
|
4
5
|
import { failure, success, type BackendContext } from './context.js';
|
|
5
6
|
import type { EventService } from './event-service.js';
|
|
@@ -9,18 +10,25 @@ import type {
|
|
|
9
10
|
BackendFileSystem,
|
|
10
11
|
BackendResult,
|
|
11
12
|
BackendSessionSnapshot,
|
|
13
|
+
InProcessLockConflictError,
|
|
14
|
+
MaterializeSessionInput,
|
|
15
|
+
MaterializeSessionSnapshot,
|
|
16
|
+
MaterializeValidationFailedError,
|
|
17
|
+
MaterializeWriteFailedError,
|
|
18
|
+
SaveSessionInput,
|
|
12
19
|
SavePartialFailureError,
|
|
13
20
|
SessionNotFoundError,
|
|
14
21
|
SessionStaleWriteError,
|
|
15
22
|
} from '../runtime.js';
|
|
16
|
-
import {
|
|
23
|
+
import type { BackendDiagnostic } from '../contracts.js';
|
|
24
|
+
import { normalizeComparablePath, validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
|
|
17
25
|
import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
|
|
18
26
|
|
|
19
27
|
function createWriterFileSystem(
|
|
20
28
|
fileSystem: BackendFileSystem,
|
|
21
29
|
committedPaths: string[],
|
|
22
30
|
failedPaths: string[],
|
|
23
|
-
):
|
|
31
|
+
): FileSystem {
|
|
24
32
|
async function trackWrite<T>(targetPath: string, fn: () => Promise<T>): Promise<T> {
|
|
25
33
|
try {
|
|
26
34
|
const result = await fn();
|
|
@@ -74,6 +82,78 @@ function createWriterFileSystem(
|
|
|
74
82
|
};
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
function toBackendDiagnostics(error: ApplyUamTransactionAppError): BackendDiagnostic[] {
|
|
86
|
+
return error.diagnostics.length > 0
|
|
87
|
+
? error.diagnostics.map((diagnostic) => ({ ...diagnostic }))
|
|
88
|
+
: [
|
|
89
|
+
{
|
|
90
|
+
code: error.code,
|
|
91
|
+
message: error.message,
|
|
92
|
+
severity: 'error',
|
|
93
|
+
operationKind: error.operationKind,
|
|
94
|
+
opIndex: error.opIndex,
|
|
95
|
+
opId: error.opId,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
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
|
+
|
|
77
157
|
export class AuthoringService {
|
|
78
158
|
public constructor(
|
|
79
159
|
private readonly context: BackendContext,
|
|
@@ -83,18 +163,34 @@ export class AuthoringService {
|
|
|
83
163
|
|
|
84
164
|
public async applyTransaction(
|
|
85
165
|
input: ApplySessionTransactionInput,
|
|
86
|
-
): Promise<
|
|
166
|
+
): Promise<
|
|
167
|
+
BackendResult<
|
|
168
|
+
BackendSessionSnapshot,
|
|
169
|
+
SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
|
|
170
|
+
>
|
|
171
|
+
> {
|
|
87
172
|
const startedAt = Date.now();
|
|
88
173
|
const session = this.context.sessions.get(input.sessionId);
|
|
89
174
|
if (!session || session.closed) {
|
|
90
175
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
91
176
|
}
|
|
92
177
|
if (input.expectedRevision !== session.revision) {
|
|
93
|
-
this.eventService.emit({
|
|
94
|
-
|
|
178
|
+
this.eventService.emit({
|
|
179
|
+
kind: 'transaction.rejected',
|
|
95
180
|
sessionId: session.sessionId,
|
|
181
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
96
182
|
revision: session.revision,
|
|
97
183
|
});
|
|
184
|
+
return failure(
|
|
185
|
+
'authoring',
|
|
186
|
+
startedAt,
|
|
187
|
+
createStaleWriteError(session, input.expectedRevision),
|
|
188
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
189
|
+
{
|
|
190
|
+
sessionId: session.sessionId,
|
|
191
|
+
revision: session.revision,
|
|
192
|
+
},
|
|
193
|
+
);
|
|
98
194
|
}
|
|
99
195
|
|
|
100
196
|
const result = applyUamTransactionApp({
|
|
@@ -102,19 +198,44 @@ export class AuthoringService {
|
|
|
102
198
|
operations: input.operations,
|
|
103
199
|
});
|
|
104
200
|
if (result.ok === false) {
|
|
105
|
-
|
|
106
|
-
|
|
201
|
+
const diagnostics = toBackendDiagnostics(result.error);
|
|
202
|
+
this.eventService.emit({
|
|
203
|
+
kind: 'transaction.rejected',
|
|
107
204
|
sessionId: session.sessionId,
|
|
205
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
108
206
|
revision: session.revision,
|
|
207
|
+
diagnostics,
|
|
109
208
|
});
|
|
209
|
+
return failure(
|
|
210
|
+
'authoring',
|
|
211
|
+
startedAt,
|
|
212
|
+
result.error,
|
|
213
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
214
|
+
{
|
|
215
|
+
sessionId: session.sessionId,
|
|
216
|
+
revision: session.revision,
|
|
217
|
+
diagnostics,
|
|
218
|
+
},
|
|
219
|
+
);
|
|
110
220
|
}
|
|
111
221
|
|
|
112
222
|
session.project = result.project;
|
|
113
223
|
session.revision += 1;
|
|
114
224
|
session.dirty = true;
|
|
115
225
|
const cacheEntry = this.cacheService.invalidateSession(session);
|
|
116
|
-
this.eventService.emit({
|
|
117
|
-
|
|
226
|
+
this.eventService.emit({
|
|
227
|
+
kind: 'transaction.applied',
|
|
228
|
+
sessionId: session.sessionId,
|
|
229
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
230
|
+
revision: session.revision,
|
|
231
|
+
});
|
|
232
|
+
this.eventService.emit({
|
|
233
|
+
kind: 'cache.invalidated',
|
|
234
|
+
sessionId: session.sessionId,
|
|
235
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
236
|
+
revision: session.revision,
|
|
237
|
+
cacheRevision: cacheEntry.revision,
|
|
238
|
+
});
|
|
118
239
|
|
|
119
240
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
120
241
|
sessionId: session.sessionId,
|
|
@@ -122,38 +243,73 @@ export class AuthoringService {
|
|
|
122
243
|
});
|
|
123
244
|
}
|
|
124
245
|
|
|
125
|
-
public async saveSession(
|
|
126
|
-
|
|
127
|
-
|
|
246
|
+
public async saveSession(input: SaveSessionInput): Promise<
|
|
247
|
+
BackendResult<
|
|
248
|
+
BackendSessionSnapshot | MaterializeSessionSnapshot,
|
|
249
|
+
| SessionNotFoundError
|
|
250
|
+
| SessionStaleWriteError
|
|
251
|
+
| SavePartialFailureError
|
|
252
|
+
| MaterializeValidationFailedError
|
|
253
|
+
| MaterializeWriteFailedError
|
|
254
|
+
| PathPolicyViolationError
|
|
255
|
+
| InProcessLockConflictError
|
|
256
|
+
| BackendCapabilityUnavailableError
|
|
257
|
+
>
|
|
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
|
+
}
|
|
128
269
|
const startedAt = Date.now();
|
|
129
270
|
const session = this.context.sessions.get(input.sessionId);
|
|
130
271
|
if (!session || session.closed) {
|
|
131
272
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
132
273
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
274
|
+
const fileSystem = input.fileSystem ?? session.fileSystem ?? this.context.fileSystem;
|
|
275
|
+
if (!fileSystem) {
|
|
276
|
+
return failure(
|
|
277
|
+
'authoring',
|
|
278
|
+
startedAt,
|
|
279
|
+
createCapabilityUnavailableError(
|
|
280
|
+
'saveSession requires an injected BackendFileSystem adapter.',
|
|
281
|
+
),
|
|
282
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
283
|
+
{
|
|
284
|
+
sessionId: session.sessionId,
|
|
285
|
+
revision: session.revision,
|
|
286
|
+
},
|
|
287
|
+
);
|
|
143
288
|
}
|
|
144
289
|
if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
|
|
145
|
-
return failure(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
290
|
+
return failure(
|
|
291
|
+
'authoring',
|
|
292
|
+
startedAt,
|
|
293
|
+
createStaleWriteError(session, input.expectedRevision),
|
|
294
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
295
|
+
{
|
|
296
|
+
sessionId: session.sessionId,
|
|
297
|
+
revision: session.revision,
|
|
298
|
+
},
|
|
299
|
+
);
|
|
149
300
|
}
|
|
150
|
-
const fileSystem = this.context.fileSystem;
|
|
151
301
|
const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
|
|
152
302
|
if (targetViolation) {
|
|
153
|
-
return failure(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
303
|
+
return failure(
|
|
304
|
+
'authoring',
|
|
305
|
+
startedAt,
|
|
306
|
+
targetViolation,
|
|
307
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
308
|
+
{
|
|
309
|
+
sessionId: session.sessionId,
|
|
310
|
+
revision: session.revision,
|
|
311
|
+
},
|
|
312
|
+
);
|
|
157
313
|
}
|
|
158
314
|
if (!session.dirty) {
|
|
159
315
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
@@ -164,36 +320,296 @@ export class AuthoringService {
|
|
|
164
320
|
|
|
165
321
|
const committedPaths: string[] = [];
|
|
166
322
|
const failedPaths: string[] = [];
|
|
167
|
-
this.eventService.emit({
|
|
323
|
+
this.eventService.emit({
|
|
324
|
+
kind: 'save.started',
|
|
325
|
+
sessionId: session.sessionId,
|
|
326
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
327
|
+
revision: session.revision,
|
|
328
|
+
});
|
|
168
329
|
try {
|
|
169
330
|
const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
|
|
170
331
|
await writer.write(materializeUamProject(session.project), session.fairyPath);
|
|
171
332
|
session.lastSavedRevision = session.revision;
|
|
172
333
|
session.dirty = false;
|
|
173
334
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
174
|
-
this.eventService.emit({
|
|
175
|
-
|
|
335
|
+
this.eventService.emit({
|
|
336
|
+
kind: 'save.completed',
|
|
337
|
+
sessionId: session.sessionId,
|
|
338
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
339
|
+
revision: session.revision,
|
|
340
|
+
});
|
|
341
|
+
this.eventService.emit({
|
|
342
|
+
kind: 'cache.updated',
|
|
343
|
+
sessionId: session.sessionId,
|
|
344
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
345
|
+
revision: session.revision,
|
|
346
|
+
cacheRevision: cacheEntry.revision,
|
|
347
|
+
});
|
|
176
348
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
177
349
|
sessionId: session.sessionId,
|
|
178
350
|
revision: session.revision,
|
|
179
351
|
});
|
|
180
352
|
} catch (error) {
|
|
181
353
|
this.cacheService.invalidateSession(session);
|
|
182
|
-
this.eventService.emit({
|
|
183
|
-
|
|
184
|
-
code: 'save_partial_failure',
|
|
185
|
-
message: error instanceof Error ? error.message : String(error),
|
|
354
|
+
this.eventService.emit({
|
|
355
|
+
kind: 'save.failed',
|
|
186
356
|
sessionId: session.sessionId,
|
|
187
357
|
canonicalPathKey: session.canonicalPathKey,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
358
|
+
revision: session.revision,
|
|
359
|
+
});
|
|
360
|
+
return failure(
|
|
361
|
+
'authoring',
|
|
362
|
+
startedAt,
|
|
363
|
+
{
|
|
364
|
+
code: 'save_partial_failure',
|
|
365
|
+
message: error instanceof Error ? error.message : String(error),
|
|
366
|
+
sessionId: session.sessionId,
|
|
367
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
368
|
+
attemptedRevision: session.revision,
|
|
369
|
+
lastSavedRevision: session.lastSavedRevision,
|
|
370
|
+
committedPaths,
|
|
371
|
+
failedPaths,
|
|
372
|
+
diskMayBePartiallyUpdated: true,
|
|
373
|
+
},
|
|
374
|
+
toSessionSnapshot(session, this.context.capabilities),
|
|
375
|
+
{
|
|
376
|
+
sessionId: session.sessionId,
|
|
377
|
+
revision: session.revision,
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
}
|
|
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).`,
|
|
194
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,
|
|
195
587
|
revision: session.revision,
|
|
588
|
+
diagnostics: diagnosticsFromError,
|
|
196
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
|
+
);
|
|
197
613
|
}
|
|
198
614
|
}
|
|
199
615
|
}
|
package/src/services/context.ts
CHANGED
|
@@ -25,7 +25,8 @@ export interface BackendSessionState {
|
|
|
25
25
|
canonicalProjectPath: string;
|
|
26
26
|
canonicalPathKey: string;
|
|
27
27
|
lockFilePath: string;
|
|
28
|
-
|
|
28
|
+
fileSystem?: BackendFileSystem;
|
|
29
|
+
project: import('@openfairygui/core/uam').UamProject;
|
|
29
30
|
revision: number;
|
|
30
31
|
lastSavedRevision: number;
|
|
31
32
|
dirty: boolean;
|