@openfairygui/backend 0.2.0-alpha.0 → 0.2.0-alpha.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/backend",
3
- "version": "0.2.0-alpha.0",
3
+ "version": "0.2.0-alpha.1",
4
4
  "description": "FairyGUI Headless Authoring SDK — stateful backend runtime and session services.",
5
5
  "author": "OpenFairyGUI Contributors",
6
6
  "license": "MIT",
@@ -19,18 +19,30 @@
19
19
  "module": "./dist/index.mjs",
20
20
  "types": "./dist/index.d.mts",
21
21
  "exports": {
22
- "require": {
23
- "types": "./dist/index.d.cts",
24
- "default": "./dist/index.cjs"
22
+ ".": {
23
+ "require": {
24
+ "types": "./dist/index.d.cts",
25
+ "default": "./dist/index.cjs"
26
+ },
27
+ "default": {
28
+ "types": "./dist/index.d.mts",
29
+ "default": "./dist/index.mjs"
30
+ }
25
31
  },
26
- "default": {
27
- "types": "./dist/index.d.mts",
28
- "default": "./dist/index.mjs"
32
+ "./node": {
33
+ "require": {
34
+ "types": "./dist/node.d.cts",
35
+ "default": "./dist/node.cjs"
36
+ },
37
+ "default": {
38
+ "types": "./dist/node.d.mts",
39
+ "default": "./dist/node.mjs"
40
+ }
29
41
  }
30
42
  },
31
43
  "scripts": {
32
- "build": "tsdown --format esm,cjs --platform node --external node:fs --external node:fs/promises --external node:path --env.PACKAGE_VERSION=$npm_package_version",
33
- "build:watch": "tsdown --watch --format esm,cjs --platform node --env.PACKAGE_VERSION=$npm_package_version"
44
+ "build": "tsdown src/index.ts src/node.ts --format esm,cjs --platform node --external node:fs --external node:fs/promises --external node:path --env.PACKAGE_VERSION=$npm_package_version",
45
+ "build:watch": "tsdown src/index.ts src/node.ts --watch --format esm,cjs --platform node --external node:fs --external node:fs/promises --external node:path --env.PACKAGE_VERSION=$npm_package_version"
34
46
  },
35
47
  "files": [
36
48
  "dist/",
package/src/index.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  export {
2
2
  BackendRuntime,
3
- createNodeBackendFileSystem,
4
3
  type AdvisoryLockConflictError,
5
4
  type ApplySessionTransactionInput,
5
+ type BackendArtifactBridgeCapability,
6
6
  type BackendCacheEntry,
7
7
  type BackendCacheSnapshot,
8
+ type BackendCapabilityManifest,
9
+ type BackendCapabilityUnavailableError,
8
10
  type BackendCapabilities,
9
11
  type BackendError,
10
12
  type BackendEvent,
11
13
  type BackendEventKind,
12
14
  type BackendFailure,
13
15
  type BackendFileHandle,
16
+ type BackendFileStat,
14
17
  type BackendFileSystem,
18
+ type BackendHostAdapter,
15
19
  type BackendJobKind,
16
20
  type BackendJobListSnapshot,
17
21
  type BackendJobListStatusFilter,
@@ -34,6 +38,7 @@ export {
34
38
  type GetJobInput,
35
39
  type InProcessLockConflictError,
36
40
  type ListJobsInput,
41
+ type OpenProjectSessionInput,
37
42
  type RefreshCacheInput,
38
43
  type SavePartialFailureError,
39
44
  type SessionNotFoundError,
package/src/node.ts ADDED
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ BackendRuntime,
5
+ type BackendFileHandle,
6
+ type BackendFileStat,
7
+ type BackendFileSystem,
8
+ type BackendHostAdapter,
9
+ type BackendRuntimeOptions,
10
+ } from './runtime.js';
11
+
12
+ export function createNodeBackendFileSystem(): BackendFileSystem {
13
+ return {
14
+ stat(filePath: string): Promise<BackendFileStat> {
15
+ return fs.stat(filePath);
16
+ },
17
+ readdir(dirPath: string): Promise<string[]> {
18
+ return fs.readdir(dirPath);
19
+ },
20
+ readFile(filePath: string): Promise<string> {
21
+ return fs.readFile(filePath, 'utf-8');
22
+ },
23
+ async readFileRaw(filePath: string): Promise<Uint8Array> {
24
+ const buffer = await fs.readFile(filePath);
25
+ return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
26
+ },
27
+ writeFile(filePath: string, content: string): Promise<void> {
28
+ return fs.writeFile(filePath, content, 'utf-8');
29
+ },
30
+ writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
31
+ return fs.writeFile(filePath, data);
32
+ },
33
+ async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
34
+ await fs.mkdir(dirPath, { recursive: options?.recursive ?? false });
35
+ },
36
+ async resolvePath(filePath: string): Promise<string> {
37
+ try {
38
+ return await fs.realpath(filePath);
39
+ } catch {
40
+ return path.resolve(filePath);
41
+ }
42
+ },
43
+ async openExclusive(filePath: string): Promise<BackendFileHandle> {
44
+ const handle = await fs.open(filePath, 'wx');
45
+ return {
46
+ writeFile(content: string): Promise<void> {
47
+ return handle.writeFile(content, 'utf-8');
48
+ },
49
+ close(): Promise<void> {
50
+ return handle.close();
51
+ },
52
+ };
53
+ },
54
+ unlink(filePath: string): Promise<void> {
55
+ return fs.unlink(filePath);
56
+ },
57
+ join(...paths: string[]): string {
58
+ return path.join(...paths);
59
+ },
60
+ dirname(filePath: string): string {
61
+ return path.dirname(filePath);
62
+ },
63
+ resolve(...paths: string[]): string {
64
+ return path.resolve(...paths);
65
+ },
66
+ };
67
+ }
68
+
69
+ export function createNodeBackendHostAdapter(): BackendHostAdapter {
70
+ return {
71
+ lockMetadata(input) {
72
+ return {
73
+ pid: process.pid,
74
+ createdAt: new Date().toISOString(),
75
+ canonicalPathKey: input.canonicalPathKey,
76
+ };
77
+ },
78
+ };
79
+ }
80
+
81
+ export function createNodeBackendRuntime(options: BackendRuntimeOptions = {}): BackendRuntime {
82
+ return new BackendRuntime({
83
+ ...options,
84
+ fileSystem: options.fileSystem ?? createNodeBackendFileSystem(),
85
+ host: options.host ?? createNodeBackendHostAdapter(),
86
+ });
87
+ }
88
+
89
+ export { BackendRuntime };
90
+ export type {
91
+ BackendFileHandle,
92
+ BackendFileStat,
93
+ BackendFileSystem,
94
+ BackendHostAdapter,
95
+ BackendRuntimeOptions,
96
+ } from './runtime.js';
package/src/runtime.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import {
2
2
  UAM_SUPPORTED_MATERIALIZATION_SCOPE,
3
+ type UamProject,
3
4
  type UamTransactionOperation,
4
5
  } from '@openfairygui/core';
5
6
  import type { ApplyUamTransactionAppError } from '@openfairygui/functions';
6
- import fs from 'node:fs/promises';
7
- import type { Stats } from 'node:fs';
8
- import path from 'node:path';
9
7
  import {
10
8
  BACKEND_CAPABILITY_SCHEMA_VERSION,
11
9
  BACKEND_COMPATIBILITY_POLICY,
@@ -27,8 +25,13 @@ export interface BackendFileHandle {
27
25
  close(): Promise<void>;
28
26
  }
29
27
 
28
+ export interface BackendFileStat {
29
+ isFile(): boolean;
30
+ isDirectory(): boolean;
31
+ }
32
+
30
33
  export interface BackendFileSystem {
31
- stat(filePath: string): Promise<Stats>;
34
+ stat(filePath: string): Promise<BackendFileStat>;
32
35
  readdir(dirPath: string): Promise<string[]>;
33
36
  readFile(filePath: string): Promise<string>;
34
37
  readFileRaw(filePath: string): Promise<Uint8Array>;
@@ -43,6 +46,48 @@ export interface BackendFileSystem {
43
46
  resolve(...paths: string[]): string;
44
47
  }
45
48
 
49
+ export interface BackendHostAdapter {
50
+ lockMetadata?(input: {
51
+ canonicalPathKey: string;
52
+ canonicalProjectPath: string;
53
+ lockFilePath: string;
54
+ }): unknown;
55
+ }
56
+
57
+ export interface BackendArtifactBridgeCapability {
58
+ available: false;
59
+ requiredHost: 'node';
60
+ executionBoundary: 'external-bridge';
61
+ bridgeEntrypoint: '@openfairygui/backend/node';
62
+ reason: string;
63
+ }
64
+
65
+ export interface BackendCapabilityManifest {
66
+ browserSafe: true;
67
+ rootEntrypoint: '@openfairygui/backend';
68
+ nodeEntrypoint: '@openfairygui/backend/node';
69
+ adapters: {
70
+ fileSystem: {
71
+ injected: true;
72
+ requiredFor: readonly ['openSession', 'saveSession'];
73
+ };
74
+ host: {
75
+ injected: true;
76
+ requiredFor: readonly ['advisoryLockMetadata'];
77
+ };
78
+ };
79
+ executionBoundaries: {
80
+ projectSession: 'in-process-browser-safe';
81
+ fileBackedSession: 'adapter-backed';
82
+ artifactPublish: BackendArtifactBridgeCapability;
83
+ artifactRestore: BackendArtifactBridgeCapability;
84
+ };
85
+ diagnostics: {
86
+ stableCodes: true;
87
+ errorDiagnosticMirror: true;
88
+ };
89
+ }
90
+
46
91
  export interface BackendCapabilities {
47
92
  contractVersion: typeof BACKEND_CONTRACT_VERSION;
48
93
  capabilitySchemaVersion: typeof BACKEND_CAPABILITY_SCHEMA_VERSION;
@@ -52,6 +97,7 @@ export interface BackendCapabilities {
52
97
  methods: readonly [
53
98
  'getCapabilities',
54
99
  'openSession',
100
+ 'openProjectSession',
55
101
  'getSession',
56
102
  'applyTransaction',
57
103
  'saveSession',
@@ -78,8 +124,11 @@ export interface BackendCapabilities {
78
124
  artifact: {
79
125
  publish: false;
80
126
  restore: false;
81
- status: 'deferred';
127
+ status: 'bridge-required';
128
+ publishBridge: BackendArtifactBridgeCapability;
129
+ restoreBridge: BackendArtifactBridgeCapability;
82
130
  };
131
+ manifest: BackendCapabilityManifest;
83
132
  compatibilityPolicy: typeof BACKEND_COMPATIBILITY_POLICY;
84
133
  runtime: {
85
134
  sessionRuntime: true;
@@ -319,6 +368,15 @@ export interface CacheRefreshFailedError {
319
368
  causeCode?: string;
320
369
  }
321
370
 
371
+ export interface BackendCapabilityUnavailableError {
372
+ code: 'capability_unavailable';
373
+ message: string;
374
+ capability: 'fileSystem' | 'artifact.publish' | 'artifact.restore';
375
+ requiredAdapter?: 'BackendFileSystem';
376
+ requiredHost?: 'node';
377
+ bridgeBoundary?: 'external-bridge';
378
+ }
379
+
322
380
  export type BackendJobErrors =
323
381
  | BackendJobNotFoundError
324
382
  | BackendJobNotCancellableError
@@ -366,6 +424,7 @@ export type BackendError =
366
424
  | BackendJobNotCancellableError
367
425
  | BackendJobCancelledError
368
426
  | CacheRefreshFailedError
427
+ | BackendCapabilityUnavailableError
369
428
  | ApplyUamTransactionAppError;
370
429
 
371
430
  export interface ApplySessionTransactionInput {
@@ -374,14 +433,23 @@ export interface ApplySessionTransactionInput {
374
433
  operations: UamTransactionOperation[];
375
434
  }
376
435
 
436
+ export interface OpenProjectSessionInput {
437
+ project: UamProject;
438
+ sessionId?: string;
439
+ canonicalProjectPath?: string;
440
+ canonicalPathKey?: string;
441
+ }
442
+
377
443
  export interface BackendRuntimeOptions {
378
444
  fileSystem?: BackendFileSystem;
445
+ host?: BackendHostAdapter;
379
446
  }
380
447
 
381
448
  const BACKEND_METHODS = [
382
- 'getCapabilities',
383
- 'openSession',
384
- 'getSession',
449
+ 'getCapabilities',
450
+ 'openSession',
451
+ 'openProjectSession',
452
+ 'getSession',
385
453
  'applyTransaction',
386
454
  'saveSession',
387
455
  'closeSession',
@@ -389,10 +457,18 @@ const BACKEND_METHODS = [
389
457
  'getJob',
390
458
  'listJobs',
391
459
  'cancelJob',
392
- 'getCacheSnapshot',
393
- 'refreshCache',
460
+ 'getCacheSnapshot',
461
+ 'refreshCache',
394
462
  ] as const;
395
463
 
464
+ const ARTIFACT_BRIDGE_CAPABILITY = {
465
+ available: false,
466
+ requiredHost: 'node',
467
+ executionBoundary: 'external-bridge',
468
+ bridgeEntrypoint: '@openfairygui/backend/node',
469
+ reason: 'publish/restore require explicit Node-hosted filesystem and artifact execution.',
470
+ } as const satisfies BackendArtifactBridgeCapability;
471
+
396
472
  function createCapabilities(): BackendCapabilities {
397
473
  return {
398
474
  contractVersion: BACKEND_CONTRACT_VERSION,
@@ -414,6 +490,31 @@ function createCapabilities(): BackendCapabilities {
414
490
  unsupported: ['artifact.publish', 'artifact.restore'],
415
491
  },
416
492
  artifact: createArtifactCapabilities(),
493
+ manifest: {
494
+ browserSafe: true,
495
+ rootEntrypoint: '@openfairygui/backend',
496
+ nodeEntrypoint: '@openfairygui/backend/node',
497
+ adapters: {
498
+ fileSystem: {
499
+ injected: true,
500
+ requiredFor: ['openSession', 'saveSession'],
501
+ },
502
+ host: {
503
+ injected: true,
504
+ requiredFor: ['advisoryLockMetadata'],
505
+ },
506
+ },
507
+ executionBoundaries: {
508
+ projectSession: 'in-process-browser-safe',
509
+ fileBackedSession: 'adapter-backed',
510
+ artifactPublish: ARTIFACT_BRIDGE_CAPABILITY,
511
+ artifactRestore: ARTIFACT_BRIDGE_CAPABILITY,
512
+ },
513
+ diagnostics: {
514
+ stableCodes: true,
515
+ errorDiagnosticMirror: true,
516
+ },
517
+ },
417
518
  compatibilityPolicy: BACKEND_COMPATIBILITY_POLICY,
418
519
  runtime: {
419
520
  sessionRuntime: true,
@@ -446,65 +547,8 @@ function createCapabilities(): BackendCapabilities {
446
547
  };
447
548
  }
448
549
 
449
- export function createNodeBackendFileSystem(): BackendFileSystem {
450
- return {
451
- stat(filePath: string): Promise<Stats> {
452
- return fs.stat(filePath);
453
- },
454
- readdir(dirPath: string): Promise<string[]> {
455
- return fs.readdir(dirPath);
456
- },
457
- readFile(filePath: string): Promise<string> {
458
- return fs.readFile(filePath, 'utf-8');
459
- },
460
- async readFileRaw(filePath: string): Promise<Uint8Array> {
461
- const buffer = await fs.readFile(filePath);
462
- return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
463
- },
464
- writeFile(filePath: string, content: string): Promise<void> {
465
- return fs.writeFile(filePath, content, 'utf-8');
466
- },
467
- writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
468
- return fs.writeFile(filePath, data);
469
- },
470
- async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
471
- await fs.mkdir(dirPath, { recursive: options?.recursive ?? false });
472
- },
473
- async resolvePath(filePath: string): Promise<string> {
474
- try {
475
- return await fs.realpath(filePath);
476
- } catch {
477
- return path.resolve(filePath);
478
- }
479
- },
480
- async openExclusive(filePath: string): Promise<BackendFileHandle> {
481
- const handle = await fs.open(filePath, 'wx');
482
- return {
483
- writeFile(content: string): Promise<void> {
484
- return handle.writeFile(content, 'utf-8');
485
- },
486
- close(): Promise<void> {
487
- return handle.close();
488
- },
489
- };
490
- },
491
- unlink(filePath: string): Promise<void> {
492
- return fs.unlink(filePath);
493
- },
494
- join(...paths: string[]): string {
495
- return path.join(...paths);
496
- },
497
- dirname(filePath: string): string {
498
- return path.dirname(filePath);
499
- },
500
- resolve(...paths: string[]): string {
501
- return path.resolve(...paths);
502
- },
503
- };
504
- }
505
-
506
550
  export class BackendRuntime {
507
- private readonly fileSystem: BackendFileSystem;
551
+ private readonly fileSystem?: BackendFileSystem;
508
552
  private readonly capabilities: BackendCapabilities;
509
553
  private readonly sessions = new Map<string, BackendSessionState>();
510
554
  private readonly sessionsByPath = new Map<string, string>();
@@ -521,10 +565,11 @@ export class BackendRuntime {
521
565
  private readonly jobService: JobService;
522
566
 
523
567
  public constructor(options: BackendRuntimeOptions = {}) {
524
- this.fileSystem = options.fileSystem ?? createNodeBackendFileSystem();
568
+ this.fileSystem = options.fileSystem;
525
569
  this.capabilities = createCapabilities();
526
570
  this.context = {
527
571
  fileSystem: this.fileSystem,
572
+ host: options.host,
528
573
  capabilities: this.capabilities,
529
574
  sessions: this.sessions,
530
575
  sessionsByPath: this.sessionsByPath,
@@ -548,10 +593,14 @@ export class BackendRuntime {
548
593
  return this.readService.getCapabilities() as BackendSuccess<BackendCapabilities>;
549
594
  }
550
595
 
551
- public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError>> {
596
+ public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError>> {
552
597
  return this.runtimeService.openSession(input);
553
598
  }
554
599
 
600
+ public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
601
+ return this.runtimeService.openProjectSession(input);
602
+ }
603
+
555
604
  public getSession(input: { sessionId: string }): BackendResult<BackendSessionSnapshot, SessionNotFoundError> {
556
605
  return this.readService.getSession(input);
557
606
  }
@@ -564,7 +613,7 @@ export class BackendRuntime {
564
613
 
565
614
  public async saveSession(
566
615
  input: { sessionId: string; expectedRevision?: number; targetPath?: string },
567
- ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError>> {
616
+ ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>> {
568
617
  return this.authoringService.saveSession(input);
569
618
  }
570
619
 
@@ -1,9 +1,19 @@
1
1
  import type { BackendCapabilities } from '../runtime.js';
2
2
 
3
3
  export function createArtifactCapabilities(): BackendCapabilities['artifact'] {
4
+ const bridge = {
5
+ available: false,
6
+ requiredHost: 'node',
7
+ executionBoundary: 'external-bridge',
8
+ bridgeEntrypoint: '@openfairygui/backend/node',
9
+ reason: 'publish/restore require explicit Node-hosted filesystem and artifact execution.',
10
+ } as const;
11
+
4
12
  return {
5
13
  publish: false,
6
14
  restore: false,
7
- status: 'deferred',
15
+ status: 'bridge-required',
16
+ publishBridge: bridge,
17
+ restoreBridge: bridge,
8
18
  };
9
19
  }
@@ -5,6 +5,7 @@ import { failure, success, type BackendContext } from './context.js';
5
5
  import type { EventService } from './event-service.js';
6
6
  import type {
7
7
  ApplySessionTransactionInput,
8
+ BackendCapabilityUnavailableError,
8
9
  BackendFileSystem,
9
10
  BackendResult,
10
11
  BackendSessionSnapshot,
@@ -123,19 +124,31 @@ export class AuthoringService {
123
124
 
124
125
  public async saveSession(
125
126
  input: { sessionId: string; expectedRevision?: number; targetPath?: string },
126
- ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError>> {
127
+ ): Promise<BackendResult<BackendSessionSnapshot, SessionNotFoundError | SessionStaleWriteError | SavePartialFailureError | PathPolicyViolationError | BackendCapabilityUnavailableError>> {
127
128
  const startedAt = Date.now();
128
129
  const session = this.context.sessions.get(input.sessionId);
129
130
  if (!session || session.closed) {
130
131
  return failure('authoring', startedAt, createSessionNotFoundError(input.sessionId));
131
132
  }
133
+ if (!this.context.fileSystem) {
134
+ return failure('authoring', startedAt, {
135
+ code: 'capability_unavailable',
136
+ message: 'saveSession requires an injected BackendFileSystem adapter.',
137
+ capability: 'fileSystem',
138
+ requiredAdapter: 'BackendFileSystem',
139
+ }, toSessionSnapshot(session, this.context.capabilities), {
140
+ sessionId: session.sessionId,
141
+ revision: session.revision,
142
+ });
143
+ }
132
144
  if (input.expectedRevision !== undefined && input.expectedRevision !== session.revision) {
133
145
  return failure('authoring', startedAt, createStaleWriteError(session, input.expectedRevision), toSessionSnapshot(session, this.context.capabilities), {
134
146
  sessionId: session.sessionId,
135
147
  revision: session.revision,
136
148
  });
137
149
  }
138
- const targetViolation = await validateSaveTarget(this.context.fileSystem, session.fairyPath, input.targetPath);
150
+ const fileSystem = this.context.fileSystem;
151
+ const targetViolation = await validateSaveTarget(fileSystem, session.fairyPath, input.targetPath);
139
152
  if (targetViolation) {
140
153
  return failure('authoring', startedAt, targetViolation, toSessionSnapshot(session, this.context.capabilities), {
141
154
  sessionId: session.sessionId,
@@ -153,7 +166,7 @@ export class AuthoringService {
153
166
  const failedPaths: string[] = [];
154
167
  this.eventService.emit({ kind: 'save.started', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
155
168
  try {
156
- const writer = new ProjectWriter(createWriterFileSystem(this.context.fileSystem, committedPaths, failedPaths));
169
+ const writer = new ProjectWriter(createWriterFileSystem(fileSystem, committedPaths, failedPaths));
157
170
  await writer.write(materializeUamProject(session.project), session.fairyPath);
158
171
  session.lastSavedRevision = session.revision;
159
172
  session.dirty = false;
@@ -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,
@@ -33,7 +34,8 @@ export interface BackendSessionState {
33
34
  }
34
35
 
35
36
  export interface BackendContext {
36
- fileSystem: BackendFileSystem;
37
+ fileSystem?: BackendFileSystem;
38
+ host?: BackendHostAdapter;
37
39
  capabilities: BackendCapabilities;
38
40
  sessions: Map<string, BackendSessionState>;
39
41
  sessionsByPath: Map<string, string>;
@@ -43,6 +45,14 @@ export interface BackendContext {
43
45
  nextEventSequence: () => number;
44
46
  }
45
47
 
48
+ function diagnosticFromError(error: BackendError): BackendDiagnostic {
49
+ return {
50
+ code: error.code,
51
+ message: error.message,
52
+ severity: 'error',
53
+ };
54
+ }
55
+
46
56
  function randomId(): string {
47
57
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
48
58
  }
@@ -103,9 +113,10 @@ export function failure<E extends BackendError>(
103
113
  diagnostics?: BackendDiagnostic[];
104
114
  },
105
115
  ): BackendFailure<E> {
116
+ const diagnostics = options?.diagnostics ?? [diagnosticFromError(error)];
106
117
  return {
107
118
  ok: false,
108
- meta: createMeta(stage, startedAt, options),
119
+ meta: createMeta(stage, startedAt, { ...options, diagnostics }),
109
120
  error,
110
121
  session,
111
122
  };
@@ -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}`,