@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.
@@ -1,4 +1,4 @@
1
- import { NodeIO, readProjectAsUam } from '@openfairygui/core';
1
+ import { ProjectReader, liftDocumentToUamProject, normalizeUamProject } from '@openfairygui/core';
2
2
  import { failure, success, type BackendContext, type BackendSessionState } from './context.js';
3
3
  import type { CacheService } from './cache-service.js';
4
4
  import type { EventService } from './event-service.js';
@@ -8,7 +8,9 @@ import type {
8
8
  BackendFileHandle,
9
9
  BackendResult,
10
10
  BackendSessionSnapshot,
11
+ BackendCapabilityUnavailableError,
11
12
  InProcessLockConflictError,
13
+ OpenProjectSessionInput,
12
14
  SessionNotFoundError,
13
15
  } from '../runtime.js';
14
16
  import { resolveCanonicalProjectRoot } from '../path-policy.js';
@@ -18,6 +20,57 @@ function randomId(): string {
18
20
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
19
21
  }
20
22
 
23
+ function createCapabilityUnavailableError(capability: BackendCapabilityUnavailableError['capability']): BackendCapabilityUnavailableError {
24
+ const artifactCapability = capability === 'artifact.publish' || capability === 'artifact.restore';
25
+ return {
26
+ code: 'capability_unavailable',
27
+ message: artifactCapability
28
+ ? `${capability} requires the Node bridge boundary exposed by @openfairygui/backend/node.`
29
+ : `${capability} requires an injected BackendFileSystem adapter.`,
30
+ capability,
31
+ requiredAdapter: capability === 'fileSystem' ? 'BackendFileSystem' : undefined,
32
+ requiredHost: artifactCapability ? 'node' : undefined,
33
+ bridgeBoundary: artifactCapability ? 'external-bridge' : undefined,
34
+ };
35
+ }
36
+
37
+ function createProjectReaderFileSystem(fileSystem: NonNullable<BackendContext['fileSystem']>): import('@openfairygui/core').FileSystem {
38
+ return {
39
+ readFile(filePath: string): Promise<string> {
40
+ return fileSystem.readFile(filePath);
41
+ },
42
+ readFileRaw(filePath: string): Promise<Uint8Array> {
43
+ return fileSystem.readFileRaw(filePath);
44
+ },
45
+ writeFile(filePath: string, content: string): Promise<void> {
46
+ return fileSystem.writeFile(filePath, content);
47
+ },
48
+ writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
49
+ return fileSystem.writeFileRaw(filePath, data);
50
+ },
51
+ async mkdir(dirPath: string): Promise<void> {
52
+ await fileSystem.mkdir(dirPath, { recursive: true });
53
+ },
54
+ readdir(dirPath: string): Promise<string[]> {
55
+ return fileSystem.readdir(dirPath);
56
+ },
57
+ async exists(filePath: string): Promise<boolean> {
58
+ try {
59
+ await fileSystem.stat(filePath);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ },
65
+ join(...paths: string[]): string {
66
+ return fileSystem.join(...paths);
67
+ },
68
+ dirname(filePath: string): string {
69
+ return fileSystem.dirname(filePath);
70
+ },
71
+ };
72
+ }
73
+
21
74
  export class RuntimeService {
22
75
  public constructor(
23
76
  private readonly context: BackendContext,
@@ -26,12 +79,16 @@ export class RuntimeService {
26
79
  private readonly jobService: JobService,
27
80
  ) {}
28
81
 
29
- public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError>> {
82
+ public async openSession(input: { projectPath: string }): Promise<BackendResult<BackendSessionSnapshot, InProcessLockConflictError | AdvisoryLockConflictError | BackendCapabilityUnavailableError>> {
30
83
  const startedAt = Date.now();
31
- const resolved = await resolveCanonicalProjectRoot(this.context.fileSystem, input.projectPath);
84
+ if (!this.context.fileSystem) {
85
+ return failure('runtime', startedAt, createCapabilityUnavailableError('fileSystem'));
86
+ }
87
+ const fileSystem = this.context.fileSystem;
88
+ const resolved = await resolveCanonicalProjectRoot(fileSystem, input.projectPath);
32
89
  const { fairyPath, canonicalProjectPath, canonicalPathKey } = resolved;
33
90
  const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
34
- const lockFilePath = this.context.fileSystem.join(canonicalProjectPath, '.openfairygui.backend.lock');
91
+ const lockFilePath = fileSystem.join(canonicalProjectPath, '.openfairygui.backend.lock');
35
92
 
36
93
  if (existingSessionId) {
37
94
  return failure('runtime', startedAt, {
@@ -46,16 +103,19 @@ export class RuntimeService {
46
103
 
47
104
  let advisoryLock: BackendFileHandle | null = null;
48
105
  try {
49
- advisoryLock = await this.context.fileSystem.openExclusive(lockFilePath);
50
- await advisoryLock.writeFile(JSON.stringify({
51
- pid: process.pid,
106
+ advisoryLock = await fileSystem.openExclusive(lockFilePath);
107
+ await advisoryLock.writeFile(JSON.stringify(this.context.host?.lockMetadata?.({
108
+ canonicalPathKey,
109
+ canonicalProjectPath,
110
+ lockFilePath,
111
+ }) ?? {
52
112
  createdAt: new Date().toISOString(),
53
113
  canonicalPathKey,
54
114
  }));
55
115
  await advisoryLock.close();
56
116
 
57
- const io = new NodeIO();
58
- const project = await readProjectAsUam(io, fairyPath);
117
+ const reader = new ProjectReader(createProjectReaderFileSystem(fileSystem));
118
+ const project = liftDocumentToUamProject(await reader.read(fairyPath));
59
119
  const sessionId = randomId();
60
120
  const session: BackendSessionState = {
61
121
  sessionId,
@@ -80,7 +140,7 @@ export class RuntimeService {
80
140
  revision: session.revision,
81
141
  });
82
142
  } catch (error) {
83
- if ((error as NodeJS.ErrnoException)?.code === 'EEXIST') {
143
+ if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST') {
84
144
  return failure('runtime', startedAt, {
85
145
  code: 'lock_conflict',
86
146
  kind: 'advisory_lock_conflict',
@@ -91,12 +151,52 @@ export class RuntimeService {
91
151
  }
92
152
  if (advisoryLock) {
93
153
  await advisoryLock.close().catch(() => undefined);
94
- await this.context.fileSystem.unlink(lockFilePath).catch(() => undefined);
154
+ await fileSystem.unlink(lockFilePath).catch(() => undefined);
95
155
  }
96
156
  throw error;
97
157
  }
98
158
  }
99
159
 
160
+ public openProjectSession(input: OpenProjectSessionInput): BackendResult<BackendSessionSnapshot> {
161
+ const startedAt = Date.now();
162
+ const sessionId = input.sessionId ?? randomId();
163
+ const canonicalProjectPath = input.canonicalProjectPath ?? `memory://${sessionId}`;
164
+ const canonicalPathKey = input.canonicalPathKey ?? canonicalProjectPath.toLowerCase();
165
+ const existingSessionId = this.context.sessionsByPath.get(canonicalPathKey);
166
+ if (existingSessionId) {
167
+ return failure('runtime', startedAt, {
168
+ code: 'lock_conflict',
169
+ kind: 'in_process_session_exists',
170
+ message: `Project is already open in this backend runtime: ${canonicalProjectPath}`,
171
+ canonicalPathKey,
172
+ holderSessionId: existingSessionId,
173
+ });
174
+ }
175
+
176
+ const session: BackendSessionState = {
177
+ sessionId,
178
+ fairyPath: canonicalProjectPath,
179
+ canonicalProjectPath,
180
+ canonicalPathKey,
181
+ lockFilePath: '',
182
+ project: normalizeUamProject(input.project),
183
+ revision: 0,
184
+ lastSavedRevision: 0,
185
+ dirty: false,
186
+ lockHeld: false,
187
+ closed: false,
188
+ };
189
+ this.context.sessions.set(sessionId, session);
190
+ this.context.sessionsByPath.set(canonicalPathKey, sessionId);
191
+ this.cacheService.refreshSession(session);
192
+ this.eventService.emit({ kind: 'session.opened', sessionId, canonicalPathKey, revision: session.revision });
193
+
194
+ return success('runtime', startedAt, toSessionSnapshot(session, this.context.capabilities), {
195
+ sessionId: session.sessionId,
196
+ revision: session.revision,
197
+ });
198
+ }
199
+
100
200
  public async closeSession(
101
201
  input: { sessionId: string },
102
202
  ): Promise<BackendResult<{ sessionId: string; closed: true }, SessionNotFoundError>> {
@@ -107,7 +207,9 @@ export class RuntimeService {
107
207
  }
108
208
 
109
209
  this.eventService.emit({ kind: 'session.closeRequested', sessionId: session.sessionId, canonicalPathKey: session.canonicalPathKey, revision: session.revision });
110
- await this.context.fileSystem.unlink(session.lockFilePath).catch(() => undefined);
210
+ if (this.context.fileSystem && session.lockFilePath) {
211
+ await this.context.fileSystem.unlink(session.lockFilePath).catch(() => undefined);
212
+ }
111
213
  session.lockHeld = false;
112
214
  session.closed = true;
113
215
  this.context.sessions.delete(session.sessionId);