@openfairygui/backend 0.2.0-alpha.9 → 0.2.0

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.
@@ -0,0 +1,518 @@
1
+ import type { UamProject, UamTransactionOperation } from '@openfairygui/core/uam';
2
+ import type { ApplyUamTransactionAppError } from '@openfairygui/functions/uam';
3
+ import type {
4
+ BACKEND_CAPABILITY_SCHEMA_VERSION,
5
+ BACKEND_COMPATIBILITY_POLICY,
6
+ BACKEND_CONTRACT_VERSION,
7
+ BackendDiagnostic,
8
+ BackendResponseMeta,
9
+ } from '../contracts.js';
10
+ import type { PathPolicyViolationError } from '../path-policy.js';
11
+
12
+ /** An exclusive lock owned for the lifetime of one backend session. */
13
+ export interface BackendSessionLock {
14
+ /** Persist optional host metadata without changing lock ownership. */
15
+ writeMetadata(content: string): Promise<void>;
16
+ /** Release ownership. Browser implementations must also release when their document terminates. */
17
+ release(): Promise<void>;
18
+ }
19
+
20
+ export interface BackendFileStat {
21
+ isFile(): boolean;
22
+ isDirectory(): boolean;
23
+ }
24
+
25
+ export interface BackendFileSystem {
26
+ stat(filePath: string): Promise<BackendFileStat>;
27
+ readdir(dirPath: string): Promise<string[]>;
28
+ readFile(filePath: string): Promise<string>;
29
+ readFileRaw(filePath: string): Promise<Uint8Array>;
30
+ writeFile(filePath: string, content: string): Promise<void>;
31
+ writeFileRaw(filePath: string, data: Uint8Array): Promise<void>;
32
+ mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void>;
33
+ resolvePath(filePath: string): Promise<string>;
34
+ acquireSessionLock(lockPath: string): Promise<BackendSessionLock>;
35
+ unlink(filePath: string): Promise<void>;
36
+ rmdir(dirPath: string): Promise<void>;
37
+ join(...paths: string[]): string;
38
+ dirname(filePath: string): string;
39
+ resolve(...paths: string[]): string;
40
+ }
41
+
42
+ export interface BackendHostAdapter {
43
+ lockMetadata?(input: { canonicalPathKey: string; canonicalProjectPath: string; lockFilePath: string }): unknown;
44
+ }
45
+
46
+ export interface BackendArtifactBridgeCapability {
47
+ available: false;
48
+ requiredHost: 'node';
49
+ executionBoundary: 'external-bridge';
50
+ bridgeEntrypoint: '@openfairygui/backend/node';
51
+ reason: string;
52
+ }
53
+
54
+ export interface BackendCapabilityManifest {
55
+ browserSafe: true;
56
+ rootEntrypoint: '@openfairygui/backend';
57
+ nodeEntrypoint: '@openfairygui/backend/node';
58
+ adapters: {
59
+ fileSystem: {
60
+ injected: true;
61
+ requiredFor: readonly ['openSession', 'saveSession', 'materializeSession'];
62
+ };
63
+ projectStorage: {
64
+ injected: true;
65
+ browserSafe: true;
66
+ requiredFor: readonly ['openProjectSession.writeback', 'saveSession', 'materializeSession'];
67
+ adapterFactory: 'createBackendStorageFileSystem';
68
+ };
69
+ host: {
70
+ injected: true;
71
+ requiredFor: readonly ['advisoryLockMetadata'];
72
+ };
73
+ };
74
+ executionBoundaries: {
75
+ projectSession: 'in-process-browser-safe';
76
+ fileBackedSession: 'adapter-backed';
77
+ artifactPublish: BackendArtifactBridgeCapability;
78
+ artifactRestore: BackendArtifactBridgeCapability;
79
+ };
80
+ diagnostics: {
81
+ stableCodes: true;
82
+ errorDiagnosticMirror: true;
83
+ };
84
+ }
85
+
86
+ export interface BackendCapabilities {
87
+ contractVersion: typeof BACKEND_CONTRACT_VERSION;
88
+ capabilitySchemaVersion: typeof BACKEND_CAPABILITY_SCHEMA_VERSION;
89
+ transactionKernelOwner: '@openfairygui/core';
90
+ appSeamOwner: '@openfairygui/functions';
91
+ runtimeOwner: '@openfairygui/backend';
92
+ methods: readonly [
93
+ 'getCapabilities',
94
+ 'openSession',
95
+ 'openProjectSession',
96
+ 'getSession',
97
+ 'applyTransaction',
98
+ 'saveSession',
99
+ 'materializeSession',
100
+ 'closeSession',
101
+ 'getEvents',
102
+ 'getJob',
103
+ 'listJobs',
104
+ 'cancelJob',
105
+ 'getCacheSnapshot',
106
+ 'refreshCache',
107
+ ];
108
+ read: {
109
+ capabilitySnapshot: true;
110
+ sessionSnapshot: true;
111
+ };
112
+ authoring: {
113
+ applyTransaction: true;
114
+ saveSession: true;
115
+ resourceKinds: readonly string[];
116
+ nodeKinds: readonly string[];
117
+ gearKinds: readonly string[];
118
+ transactionScope: {
119
+ resourceKinds: readonly string[];
120
+ nodeKinds: readonly string[];
121
+ gearKinds: readonly string[];
122
+ };
123
+ unsupported: readonly ['artifact.publish', 'artifact.restore'];
124
+ };
125
+ artifact: {
126
+ publish: false;
127
+ restore: false;
128
+ status: 'bridge-required';
129
+ publishBridge: BackendArtifactBridgeCapability;
130
+ restoreBridge: BackendArtifactBridgeCapability;
131
+ };
132
+ manifest: BackendCapabilityManifest;
133
+ compatibilityPolicy: typeof BACKEND_COMPATIBILITY_POLICY;
134
+ runtime: {
135
+ sessionRuntime: true;
136
+ advisoryLocking: true;
137
+ coordinatedSave: true;
138
+ atomicSave: false;
139
+ staleRevisionProtection: true;
140
+ pathPolicy: {
141
+ canonicalization: 'realpath+normalized-casefold';
142
+ sessionIdentity: 'project-root';
143
+ saveTarget: 'opened-project-only';
144
+ outputTargets: 'deferred';
145
+ workspaceBoundary: 'project-root-only';
146
+ };
147
+ events: {
148
+ polling: true;
149
+ subscriptions: false;
150
+ retentionLimit: 1000;
151
+ sequenceScope: 'runtime';
152
+ };
153
+ jobs: {
154
+ inMemory: true;
155
+ cooperativeCancel: true;
156
+ persistent: false;
157
+ supportedKinds: readonly ['cache.refresh'];
158
+ artifactJobs: false;
159
+ completedRetentionLimit: 100;
160
+ };
161
+ cache: {
162
+ derivedReadOnly: true;
163
+ keyedBy: 'canonicalPathKey';
164
+ sourceOfTruth: false;
165
+ refreshMethod: 'refreshCache';
166
+ };
167
+ };
168
+ }
169
+
170
+ export interface BackendSessionSnapshot {
171
+ sessionId: string;
172
+ canonicalProjectPath: string;
173
+ revision: number;
174
+ lastSavedRevision: number;
175
+ dirty: boolean;
176
+ uamFidelity: 'full' | 'unsupported';
177
+ lockHeld: boolean;
178
+ capabilities: BackendCapabilities;
179
+ }
180
+
181
+ export interface MaterializeSessionSnapshot extends BackendSessionSnapshot {
182
+ mode: 'fullProject';
183
+ reason?: string;
184
+ materializeRevision: number;
185
+ saveRevision: number;
186
+ writtenPaths: string[];
187
+ skippedPaths: string[];
188
+ diagnostics: BackendDiagnostic[];
189
+ }
190
+
191
+ export interface BackendSuccess<T> {
192
+ ok: true;
193
+ meta: BackendResponseMeta;
194
+ data: T;
195
+ }
196
+
197
+ export interface BackendFailure<E extends BackendError = BackendError> {
198
+ ok: false;
199
+ meta: BackendResponseMeta;
200
+ error: E;
201
+ session?: BackendSessionSnapshot;
202
+ }
203
+
204
+ export type BackendResult<T, E extends BackendError = BackendError> = BackendSuccess<T> | BackendFailure<E>;
205
+
206
+ export interface SessionNotFoundError {
207
+ code: 'session_not_found';
208
+ message: string;
209
+ sessionId: string;
210
+ }
211
+
212
+ export interface SessionStaleWriteError {
213
+ code: 'stale_write';
214
+ message: string;
215
+ sessionId: string;
216
+ canonicalPathKey: string;
217
+ expectedRevision: number;
218
+ actualRevision: number;
219
+ }
220
+
221
+ export interface InProcessLockConflictError {
222
+ code: 'lock_conflict';
223
+ kind: 'in_process_session_exists';
224
+ message: string;
225
+ canonicalPathKey: string;
226
+ holderSessionId: string;
227
+ lockFilePath?: string;
228
+ }
229
+
230
+ export interface AdvisoryLockConflictError {
231
+ code: 'lock_conflict';
232
+ kind: 'advisory_lock_conflict';
233
+ message: string;
234
+ canonicalPathKey: string;
235
+ holderSessionId?: string;
236
+ lockFilePath: string;
237
+ }
238
+
239
+ export interface SavePartialFailureError {
240
+ code: 'save_partial_failure';
241
+ message: string;
242
+ sessionId: string;
243
+ canonicalPathKey: string;
244
+ attemptedRevision: number;
245
+ lastSavedRevision: number;
246
+ committedPaths: string[];
247
+ failedPaths: string[];
248
+ diskMayBePartiallyUpdated: true;
249
+ }
250
+
251
+ export interface UamFidelityUnsupportedError {
252
+ code: 'uam_fidelity_unsupported';
253
+ message: string;
254
+ sessionId: string;
255
+ canonicalPathKey: string;
256
+ }
257
+
258
+ export interface MaterializeValidationFailedError {
259
+ code: 'materialize_validation_failed';
260
+ message: string;
261
+ sessionId: string;
262
+ canonicalPathKey: string;
263
+ issueCount: number;
264
+ diagnostics: BackendDiagnostic[];
265
+ }
266
+
267
+ export interface MaterializeWriteFailedError {
268
+ code: 'write_failed';
269
+ message: string;
270
+ sessionId: string;
271
+ canonicalPathKey: string;
272
+ attemptedRevision: number;
273
+ lastSavedRevision: number;
274
+ writtenPaths: string[];
275
+ failedPaths: string[];
276
+ skippedPaths: string[];
277
+ diagnostics: BackendDiagnostic[];
278
+ diskMayBePartiallyUpdated: true;
279
+ }
280
+
281
+ export type BackendEventKind =
282
+ | 'session.opened'
283
+ | 'transaction.applied'
284
+ | 'transaction.rejected'
285
+ | 'save.started'
286
+ | 'save.completed'
287
+ | 'save.failed'
288
+ | 'session.closeRequested'
289
+ | 'session.closed'
290
+ | 'cache.invalidated'
291
+ | 'cache.updated'
292
+ | 'job.created'
293
+ | 'job.started'
294
+ | 'job.progress'
295
+ | 'job.cancelRequested'
296
+ | 'job.cancelled'
297
+ | 'job.completed'
298
+ | 'job.failed';
299
+
300
+ export interface BackendEvent {
301
+ sequence: number;
302
+ kind: BackendEventKind;
303
+ timestamp: string;
304
+ sessionId?: string;
305
+ canonicalPathKey?: string;
306
+ revision?: number;
307
+ cacheRevision?: number;
308
+ jobId?: string;
309
+ diagnostics: BackendDiagnostic[];
310
+ payload?: unknown;
311
+ }
312
+
313
+ export interface GetEventsInput {
314
+ sessionId: string;
315
+ after?: string;
316
+ limit?: number;
317
+ }
318
+
319
+ export interface GetEventsSnapshot {
320
+ events: BackendEvent[];
321
+ oldestSequence: number;
322
+ currentSequence: number;
323
+ cursorExpired: boolean;
324
+ }
325
+
326
+ export interface EventCursorInvalidError {
327
+ code: 'event_cursor_invalid';
328
+ message: string;
329
+ sessionId: string;
330
+ after: string;
331
+ }
332
+
333
+ export type BackendJobStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
334
+ export type BackendJobKind = 'cache.refresh';
335
+ export type BackendJobListStatusFilter = BackendJobStatus | 'active' | 'terminal';
336
+
337
+ export interface BackendJobProgress {
338
+ completed: number;
339
+ total?: number;
340
+ message?: string;
341
+ }
342
+
343
+ export interface BackendJobSnapshot {
344
+ jobId: string;
345
+ kind: BackendJobKind;
346
+ status: BackendJobStatus;
347
+ createdAt: string;
348
+ startedAt?: string;
349
+ finishedAt?: string;
350
+ sessionId?: string;
351
+ canonicalPathKey?: string;
352
+ revision?: number;
353
+ cacheRevision?: number;
354
+ diagnostics: BackendDiagnostic[];
355
+ progress?: BackendJobProgress;
356
+ result?: unknown;
357
+ error?: BackendError;
358
+ }
359
+
360
+ export interface BackendJobListSnapshot {
361
+ jobs: BackendJobSnapshot[];
362
+ }
363
+
364
+ export interface GetJobInput {
365
+ sessionId: string;
366
+ jobId: string;
367
+ }
368
+
369
+ export interface ListJobsInput {
370
+ sessionId: string;
371
+ status?: BackendJobListStatusFilter;
372
+ kind?: BackendJobKind;
373
+ limit?: number;
374
+ }
375
+
376
+ export interface CancelJobInput {
377
+ sessionId: string;
378
+ jobId: string;
379
+ }
380
+
381
+ export interface BackendJobNotFoundError {
382
+ code: 'job_not_found';
383
+ message: string;
384
+ sessionId: string;
385
+ jobId: string;
386
+ }
387
+
388
+ export interface BackendJobNotCancellableError {
389
+ code: 'job_not_cancellable';
390
+ message: string;
391
+ sessionId: string;
392
+ jobId: string;
393
+ status: 'completed' | 'failed' | 'cancelled';
394
+ }
395
+
396
+ export interface BackendJobCancelledError {
397
+ code: 'job_cancelled';
398
+ message: string;
399
+ sessionId: string;
400
+ jobId: string;
401
+ }
402
+
403
+ export interface CacheRefreshFailedError {
404
+ code: 'cache_refresh_failed';
405
+ message: string;
406
+ sessionId: string;
407
+ jobId: string;
408
+ causeCode?: string;
409
+ }
410
+
411
+ export interface BackendCapabilityUnavailableError {
412
+ code: 'capability_unavailable';
413
+ message: string;
414
+ capability: 'fileSystem' | 'artifact.publish' | 'artifact.restore';
415
+ requiredAdapter?: 'BackendFileSystem';
416
+ requiredHost?: 'node';
417
+ bridgeBoundary?: 'external-bridge';
418
+ }
419
+
420
+ export type BackendJobErrors =
421
+ | BackendJobNotFoundError
422
+ | BackendJobNotCancellableError
423
+ | BackendJobCancelledError
424
+ | CacheRefreshFailedError;
425
+
426
+ export interface BackendCacheSnapshot {
427
+ cacheRevision: number;
428
+ entries: BackendCacheEntry[];
429
+ }
430
+
431
+ export interface BackendCacheEntry {
432
+ canonicalPathKey: string;
433
+ sessionId?: string;
434
+ revision: number;
435
+ lastSavedRevision: number;
436
+ dirty: boolean;
437
+ valid: boolean;
438
+ indexedAt: string;
439
+ summary: {
440
+ resourceCount: number;
441
+ packageCount?: number;
442
+ diagnostics: BackendDiagnostic[];
443
+ };
444
+ }
445
+
446
+ export interface GetCacheSnapshotInput {
447
+ sessionId: string;
448
+ }
449
+
450
+ export interface RefreshCacheInput {
451
+ sessionId: string;
452
+ reason?: 'manual' | 'session_open' | 'after_save';
453
+ }
454
+
455
+ export type BackendError =
456
+ | SessionNotFoundError
457
+ | SessionStaleWriteError
458
+ | InProcessLockConflictError
459
+ | AdvisoryLockConflictError
460
+ | SavePartialFailureError
461
+ | UamFidelityUnsupportedError
462
+ | MaterializeValidationFailedError
463
+ | MaterializeWriteFailedError
464
+ | PathPolicyViolationError
465
+ | EventCursorInvalidError
466
+ | BackendJobNotFoundError
467
+ | BackendJobNotCancellableError
468
+ | BackendJobCancelledError
469
+ | CacheRefreshFailedError
470
+ | BackendCapabilityUnavailableError
471
+ | ApplyUamTransactionAppError;
472
+
473
+ export interface ApplySessionTransactionInput {
474
+ sessionId: string;
475
+ expectedRevision: number;
476
+ operations: UamTransactionOperation[];
477
+ }
478
+
479
+ export interface OpenProjectSessionInput {
480
+ /** Authoritative UAM project. Use BackendRuntime.openSession() when importing an existing project from storage. */
481
+ project: UamProject;
482
+ sessionId?: string;
483
+ canonicalProjectPath?: string;
484
+ canonicalPathKey?: string;
485
+ /** Optional writeback target for the authoritative UAM project; this is not an import source. */
486
+ storage?: BackendProjectSessionStorage;
487
+ }
488
+
489
+ export interface BackendProjectSessionStorage {
490
+ fileSystem: BackendFileSystem;
491
+ fairyPath: string;
492
+ canonicalProjectPath?: string;
493
+ canonicalPathKey?: string;
494
+ }
495
+
496
+ export interface SaveSessionInput {
497
+ sessionId: string;
498
+ expectedRevision?: number;
499
+ targetPath?: string;
500
+ fileSystem?: BackendFileSystem;
501
+ force?: boolean;
502
+ mode?: 'materializeCleanSession';
503
+ }
504
+
505
+ export interface MaterializeSessionInput {
506
+ sessionId: string;
507
+ expectedRevision?: number;
508
+ storage?: BackendProjectSessionStorage;
509
+ targetPath?: string;
510
+ fileSystem?: BackendFileSystem;
511
+ mode?: 'fullProject';
512
+ reason?: string;
513
+ }
514
+
515
+ export interface BackendRuntimeOptions {
516
+ fileSystem?: BackendFileSystem;
517
+ host?: BackendHostAdapter;
518
+ }