@openfairygui/backend 0.2.0-alpha.0 → 0.2.0-alpha.10
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 +62 -1
- package/dist/index.cjs +102 -1071
- package/dist/index.d.cts +24 -355
- package/dist/index.d.mts +24 -355
- package/dist/index.mjs +99 -1044
- package/dist/node.cjs +107 -0
- package/dist/node.d.cts +8 -0
- package/dist/node.d.mts +8 -0
- package/dist/node.mjs +78 -0
- package/dist/runtime-DFatY9W0.mjs +1417 -0
- package/dist/runtime-GKzsXJdO.cjs +1441 -0
- package/dist/runtime-GyNVxAQ0.d.mts +494 -0
- package/dist/runtime-Jec6FcF5.d.cts +494 -0
- package/package.json +65 -53
- package/src/contracts.ts +8 -0
- package/src/index.ts +18 -1
- package/src/node.ts +96 -0
- package/src/runtime.ts +256 -80
- package/src/services/artifact-service.ts +11 -1
- package/src/services/authoring-service.ts +466 -37
- package/src/services/context.ts +15 -3
- package/src/services/event-service.ts +1 -1
- package/src/services/runtime-service.ts +164 -25
- package/src/storage.ts +192 -0
|
@@ -1,25 +1,34 @@
|
|
|
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';
|
|
6
7
|
import type {
|
|
7
8
|
ApplySessionTransactionInput,
|
|
9
|
+
BackendCapabilityUnavailableError,
|
|
8
10
|
BackendFileSystem,
|
|
9
11
|
BackendResult,
|
|
10
12
|
BackendSessionSnapshot,
|
|
13
|
+
InProcessLockConflictError,
|
|
14
|
+
MaterializeSessionInput,
|
|
15
|
+
MaterializeSessionSnapshot,
|
|
16
|
+
MaterializeValidationFailedError,
|
|
17
|
+
MaterializeWriteFailedError,
|
|
18
|
+
SaveSessionInput,
|
|
11
19
|
SavePartialFailureError,
|
|
12
20
|
SessionNotFoundError,
|
|
13
21
|
SessionStaleWriteError,
|
|
14
22
|
} from '../runtime.js';
|
|
15
|
-
import {
|
|
23
|
+
import type { BackendDiagnostic } from '../contracts.js';
|
|
24
|
+
import { normalizeComparablePath, validateSaveTarget, type PathPolicyViolationError } from '../path-policy.js';
|
|
16
25
|
import { createSessionNotFoundError, createStaleWriteError, toSessionSnapshot } from './session-utils.js';
|
|
17
26
|
|
|
18
27
|
function createWriterFileSystem(
|
|
19
28
|
fileSystem: BackendFileSystem,
|
|
20
29
|
committedPaths: string[],
|
|
21
30
|
failedPaths: string[],
|
|
22
|
-
):
|
|
31
|
+
): FileSystem {
|
|
23
32
|
async function trackWrite<T>(targetPath: string, fn: () => Promise<T>): Promise<T> {
|
|
24
33
|
try {
|
|
25
34
|
const result = await fn();
|
|
@@ -73,6 +82,78 @@ function createWriterFileSystem(
|
|
|
73
82
|
};
|
|
74
83
|
}
|
|
75
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
|
+
|
|
76
157
|
export class AuthoringService {
|
|
77
158
|
public constructor(
|
|
78
159
|
private readonly context: BackendContext,
|
|
@@ -82,18 +163,34 @@ export class AuthoringService {
|
|
|
82
163
|
|
|
83
164
|
public async applyTransaction(
|
|
84
165
|
input: ApplySessionTransactionInput,
|
|
85
|
-
): Promise<
|
|
166
|
+
): Promise<
|
|
167
|
+
BackendResult<
|
|
168
|
+
BackendSessionSnapshot,
|
|
169
|
+
SessionNotFoundError | SessionStaleWriteError | ApplyUamTransactionAppError
|
|
170
|
+
>
|
|
171
|
+
> {
|
|
86
172
|
const startedAt = Date.now();
|
|
87
173
|
const session = this.context.sessions.get(input.sessionId);
|
|
88
174
|
if (!session || session.closed) {
|
|
89
175
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
90
176
|
}
|
|
91
177
|
if (input.expectedRevision !== session.revision) {
|
|
92
|
-
this.eventService.emit({
|
|
93
|
-
|
|
178
|
+
this.eventService.emit({
|
|
179
|
+
kind: 'transaction.rejected',
|
|
94
180
|
sessionId: session.sessionId,
|
|
181
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
95
182
|
revision: session.revision,
|
|
96
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
|
+
);
|
|
97
194
|
}
|
|
98
195
|
|
|
99
196
|
const result = applyUamTransactionApp({
|
|
@@ -101,19 +198,44 @@ export class AuthoringService {
|
|
|
101
198
|
operations: input.operations,
|
|
102
199
|
});
|
|
103
200
|
if (result.ok === false) {
|
|
104
|
-
|
|
105
|
-
|
|
201
|
+
const diagnostics = toBackendDiagnostics(result.error);
|
|
202
|
+
this.eventService.emit({
|
|
203
|
+
kind: 'transaction.rejected',
|
|
106
204
|
sessionId: session.sessionId,
|
|
205
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
107
206
|
revision: session.revision,
|
|
207
|
+
diagnostics,
|
|
108
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
|
+
);
|
|
109
220
|
}
|
|
110
221
|
|
|
111
222
|
session.project = result.project;
|
|
112
223
|
session.revision += 1;
|
|
113
224
|
session.dirty = true;
|
|
114
225
|
const cacheEntry = this.cacheService.invalidateSession(session);
|
|
115
|
-
this.eventService.emit({
|
|
116
|
-
|
|
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
|
+
});
|
|
117
239
|
|
|
118
240
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
119
241
|
sessionId: session.sessionId,
|
|
@@ -121,26 +243,73 @@ export class AuthoringService {
|
|
|
121
243
|
});
|
|
122
244
|
}
|
|
123
245
|
|
|
124
|
-
public async saveSession(
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
127
269
|
const startedAt = Date.now();
|
|
128
270
|
const session = this.context.sessions.get(input.sessionId);
|
|
129
271
|
if (!session || session.closed) {
|
|
130
272
|
return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
|
|
131
273
|
}
|
|
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
|
+
);
|
|
288
|
+
}
|
|
132
289
|
if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
|
|
133
|
-
return failure(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
);
|
|
137
300
|
}
|
|
138
|
-
const targetViolation = await validateSaveTarget(
|
|
301
|
+
const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
|
|
139
302
|
if (targetViolation) {
|
|
140
|
-
return failure(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
);
|
|
144
313
|
}
|
|
145
314
|
if (!session.dirty) {
|
|
146
315
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
@@ -151,36 +320,296 @@ export class AuthoringService {
|
|
|
151
320
|
|
|
152
321
|
const committedPaths: string[] = [];
|
|
153
322
|
const failedPaths: string[] = [];
|
|
154
|
-
this.eventService.emit({
|
|
323
|
+
this.eventService.emit({
|
|
324
|
+
kind: 'save.started',
|
|
325
|
+
sessionId: session.sessionId,
|
|
326
|
+
canonicalPathKey: session.canonicalPathKey,
|
|
327
|
+
revision: session.revision,
|
|
328
|
+
});
|
|
155
329
|
try {
|
|
156
|
-
const writer = new ProjectWriter(createWriterFileSystem(
|
|
330
|
+
const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
|
|
157
331
|
await writer.write(materializeUamProject(session.project), session.fairyPath);
|
|
158
332
|
session.lastSavedRevision = session.revision;
|
|
159
333
|
session.dirty = false;
|
|
160
334
|
const cacheEntry = this.cacheService.refreshSession(session);
|
|
161
|
-
this.eventService.emit({
|
|
162
|
-
|
|
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
|
+
});
|
|
163
348
|
return success('authoring', startedAt, toSessionSnapshot(session, this.context.capabilities), {
|
|
164
349
|
sessionId: session.sessionId,
|
|
165
350
|
revision: session.revision,
|
|
166
351
|
});
|
|
167
352
|
} catch (error) {
|
|
168
353
|
this.cacheService.invalidateSession(session);
|
|
169
|
-
this.eventService.emit({
|
|
170
|
-
|
|
171
|
-
code: 'save_partial_failure',
|
|
172
|
-
message: error instanceof Error ? error.message : String(error),
|
|
354
|
+
this.eventService.emit({
|
|
355
|
+
kind: 'save.failed',
|
|
173
356
|
sessionId: session.sessionId,
|
|
174
357
|
canonicalPathKey: session.canonicalPathKey,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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).`,
|
|
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',
|
|
181
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,
|
|
182
587
|
revision: session.revision,
|
|
588
|
+
diagnostics: diagnosticsFromError,
|
|
183
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
|
+
);
|
|
184
613
|
}
|
|
185
614
|
}
|
|
186
615
|
}
|
package/src/services/context.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
BackendEvent,
|
|
6
6
|
BackendFailure,
|
|
7
7
|
BackendFileSystem,
|
|
8
|
+
BackendHostAdapter,
|
|
8
9
|
BackendJobSnapshot,
|
|
9
10
|
BackendSessionSnapshot,
|
|
10
11
|
BackendSuccess,
|
|
@@ -24,7 +25,8 @@ export interface BackendSessionState {
|
|
|
24
25
|
canonicalProjectPath: string;
|
|
25
26
|
canonicalPathKey: string;
|
|
26
27
|
lockFilePath: string;
|
|
27
|
-
|
|
28
|
+
fileSystem?: BackendFileSystem;
|
|
29
|
+
project: import('@openfairygui/core/uam').UamProject;
|
|
28
30
|
revision: number;
|
|
29
31
|
lastSavedRevision: number;
|
|
30
32
|
dirty: boolean;
|
|
@@ -33,7 +35,8 @@ export interface BackendSessionState {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export interface BackendContext {
|
|
36
|
-
fileSystem
|
|
38
|
+
fileSystem?: BackendFileSystem;
|
|
39
|
+
host?: BackendHostAdapter;
|
|
37
40
|
capabilities: BackendCapabilities;
|
|
38
41
|
sessions: Map<string, BackendSessionState>;
|
|
39
42
|
sessionsByPath: Map<string, string>;
|
|
@@ -43,6 +46,14 @@ export interface BackendContext {
|
|
|
43
46
|
nextEventSequence: () => number;
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function diagnosticFromError(error: BackendError): BackendDiagnostic {
|
|
50
|
+
return {
|
|
51
|
+
code: error.code,
|
|
52
|
+
message: error.message,
|
|
53
|
+
severity: 'error',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
function randomId(): string {
|
|
47
58
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
48
59
|
}
|
|
@@ -103,9 +114,10 @@ export function failure<E extends BackendError>(
|
|
|
103
114
|
diagnostics?: BackendDiagnostic[];
|
|
104
115
|
},
|
|
105
116
|
): BackendFailure<E> {
|
|
117
|
+
const diagnostics = options?.diagnostics ?? [diagnosticFromError(error)];
|
|
106
118
|
return {
|
|
107
119
|
ok: false,
|
|
108
|
-
meta: createMeta(stage, startedAt, options),
|
|
120
|
+
meta: createMeta(stage, startedAt, { ...options, diagnostics }),
|
|
109
121
|
error,
|
|
110
122
|
session,
|
|
111
123
|
};
|
|
@@ -51,7 +51,7 @@ export class EventService {
|
|
|
51
51
|
after: String(input.after),
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
-
if (events.length > 0 && after < oldestSequence - 1) {
|
|
54
|
+
if (events.length > 0 && after !== 0 && after < oldestSequence - 1) {
|
|
55
55
|
return failure('runtime', startedAt, {
|
|
56
56
|
code: 'event_cursor_invalid',
|
|
57
57
|
message: `Event cursor has expired: ${after}`,
|