@rigkit/sdk 0.1.8
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/README.md +19 -0
- package/package.json +41 -0
- package/src/cli.ts +116 -0
- package/src/host.ts +46 -0
- package/src/index.test.ts +53 -0
- package/src/index.ts +84 -0
- package/src/runtime/api-handlers.ts +166 -0
- package/src/runtime/api.ts +69 -0
- package/src/runtime/app.test.ts +924 -0
- package/src/runtime/app.ts +63 -0
- package/src/runtime/cli.ts +115 -0
- package/src/runtime/control.ts +163 -0
- package/src/runtime/errors.ts +108 -0
- package/src/runtime/index.ts +81 -0
- package/src/runtime/openapi.ts +5 -0
- package/src/runtime/operations.ts +267 -0
- package/src/runtime/protocol.ts +193 -0
- package/src/runtime/runs.ts +292 -0
- package/src/runtime/server.ts +182 -0
- package/src/runtime/sessions.ts +257 -0
- package/src/runtime/state.ts +12 -0
- package/src/runtime/token.ts +16 -0
- package/src/runtime/types.ts +35 -0
- package/src/runtime/version.ts +1 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { RuntimeEvent, RuntimeOperation } from "./protocol.ts";
|
|
2
|
+
import {
|
|
3
|
+
RuntimeHostRequestError,
|
|
4
|
+
runtimeFailureBody,
|
|
5
|
+
} from "./errors.ts";
|
|
6
|
+
|
|
7
|
+
export type RunStatus = "running" | "completed" | "failed";
|
|
8
|
+
|
|
9
|
+
export type PendingHostResponse = {
|
|
10
|
+
resolve(value: unknown): void;
|
|
11
|
+
reject(error: Error): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type PendingHostCapabilityResource = {
|
|
15
|
+
resolveClosed(): void;
|
|
16
|
+
rejectClosed(error: Error): void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type HostCapabilitySessionResult<Result = unknown> = {
|
|
20
|
+
result: Result;
|
|
21
|
+
closed: Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type RunRecord = {
|
|
25
|
+
id: string;
|
|
26
|
+
operation: string;
|
|
27
|
+
operationDefinition?: RuntimeOperation;
|
|
28
|
+
input: unknown;
|
|
29
|
+
status: RunStatus;
|
|
30
|
+
events: RuntimeEvent[];
|
|
31
|
+
result?: unknown;
|
|
32
|
+
error?: { code: string; message: string };
|
|
33
|
+
pendingHostRequestIds: Set<string>;
|
|
34
|
+
pendingHostCapabilityResourceIds: Set<string>;
|
|
35
|
+
subscribers: Set<ReadableStreamDefaultController<Uint8Array>>;
|
|
36
|
+
eventSubscribers: Set<(event: RuntimeEvent) => void>;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type RuntimeRunSummary = {
|
|
42
|
+
runId: string;
|
|
43
|
+
operation: string;
|
|
44
|
+
input: unknown;
|
|
45
|
+
status: RunStatus;
|
|
46
|
+
result?: unknown;
|
|
47
|
+
error?: { code: string; message: string };
|
|
48
|
+
createdAt: string;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type RunStore = {
|
|
53
|
+
runs: Map<string, RunRecord>;
|
|
54
|
+
hostResponses: Map<string, PendingHostResponse>;
|
|
55
|
+
hostCapabilityResources: Map<string, PendingHostCapabilityResource>;
|
|
56
|
+
activeSessions: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const encoder = new TextEncoder();
|
|
60
|
+
|
|
61
|
+
export function createRunStore(): RunStore {
|
|
62
|
+
return {
|
|
63
|
+
runs: new Map(),
|
|
64
|
+
hostResponses: new Map(),
|
|
65
|
+
hostCapabilityResources: new Map(),
|
|
66
|
+
activeSessions: 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createRun(operation: string, input: unknown, operationDefinition?: RuntimeOperation): RunRecord {
|
|
71
|
+
const now = new Date().toISOString();
|
|
72
|
+
return {
|
|
73
|
+
id: `run_${crypto.randomUUID()}`,
|
|
74
|
+
operation,
|
|
75
|
+
operationDefinition,
|
|
76
|
+
input,
|
|
77
|
+
status: "running",
|
|
78
|
+
events: [],
|
|
79
|
+
pendingHostRequestIds: new Set(),
|
|
80
|
+
pendingHostCapabilityResourceIds: new Set(),
|
|
81
|
+
subscribers: new Set(),
|
|
82
|
+
eventSubscribers: new Set(),
|
|
83
|
+
createdAt: now,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function emitRunEvent(run: RunRecord, event: RuntimeEvent): void {
|
|
89
|
+
run.events.push(event);
|
|
90
|
+
run.updatedAt = new Date().toISOString();
|
|
91
|
+
const payload = encodeSse(event);
|
|
92
|
+
for (const subscriber of run.subscribers) {
|
|
93
|
+
subscriber.enqueue(payload);
|
|
94
|
+
}
|
|
95
|
+
for (const subscriber of run.eventSubscribers) {
|
|
96
|
+
subscriber(event);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function completeRun(run: RunRecord, result: unknown, store?: RunStore): void {
|
|
101
|
+
if (run.status !== "running") return;
|
|
102
|
+
run.status = "completed";
|
|
103
|
+
run.result = result;
|
|
104
|
+
run.updatedAt = new Date().toISOString();
|
|
105
|
+
if (store) {
|
|
106
|
+
settleHostCapabilityResources(run, store, (pending) => pending.resolveClosed());
|
|
107
|
+
}
|
|
108
|
+
emitRunEvent(run, { type: "run.completed", runId: run.id, result });
|
|
109
|
+
closeRunSubscribers(run);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function failRun(run: RunRecord, error: unknown, store?: RunStore): void {
|
|
113
|
+
if (run.status !== "running") return;
|
|
114
|
+
run.status = "failed";
|
|
115
|
+
run.error = runtimeFailureBody(error);
|
|
116
|
+
run.updatedAt = new Date().toISOString();
|
|
117
|
+
if (store) {
|
|
118
|
+
for (const requestId of run.pendingHostRequestIds) {
|
|
119
|
+
const pending = store.hostResponses.get(requestId);
|
|
120
|
+
if (pending) {
|
|
121
|
+
store.hostResponses.delete(requestId);
|
|
122
|
+
pending.reject(new RuntimeHostRequestError({
|
|
123
|
+
message: run.error.message,
|
|
124
|
+
requestId,
|
|
125
|
+
cause: error,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
run.pendingHostRequestIds.clear();
|
|
130
|
+
settleHostCapabilityResources(run, store, (pending) =>
|
|
131
|
+
pending.rejectClosed(new RuntimeHostRequestError({
|
|
132
|
+
message: run.error?.message ?? "Run failed",
|
|
133
|
+
cause: error,
|
|
134
|
+
}))
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
emitRunEvent(run, { type: "run.failed", runId: run.id, error: run.error });
|
|
138
|
+
closeRunSubscribers(run);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function summarizeRun(run: RunRecord): RuntimeRunSummary {
|
|
142
|
+
return {
|
|
143
|
+
runId: run.id,
|
|
144
|
+
operation: run.operation,
|
|
145
|
+
input: run.input,
|
|
146
|
+
status: run.status,
|
|
147
|
+
result: run.result,
|
|
148
|
+
error: run.error,
|
|
149
|
+
createdAt: run.createdAt,
|
|
150
|
+
updatedAt: run.updatedAt,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function sseResponse(run: RunRecord): Response {
|
|
155
|
+
let controllerRef: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
156
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
157
|
+
start(controller) {
|
|
158
|
+
controllerRef = controller;
|
|
159
|
+
for (const event of run.events) controller.enqueue(encodeSse(event));
|
|
160
|
+
if (run.status !== "running") {
|
|
161
|
+
controller.close();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
run.subscribers.add(controller);
|
|
165
|
+
},
|
|
166
|
+
cancel() {
|
|
167
|
+
if (controllerRef) run.subscribers.delete(controllerRef);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return new Response(stream, {
|
|
172
|
+
headers: {
|
|
173
|
+
"content-type": "text/event-stream",
|
|
174
|
+
"cache-control": "no-cache",
|
|
175
|
+
connection: "keep-alive",
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function requestHost(store: RunStore, run: RunRecord, method: string, params: unknown): Promise<unknown> {
|
|
181
|
+
if (run.status !== "running") {
|
|
182
|
+
return Promise.reject(new RuntimeHostRequestError({
|
|
183
|
+
message: `Run ${run.id} is ${run.status}`,
|
|
184
|
+
method,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
const requestId = `host_req_${crypto.randomUUID()}`;
|
|
188
|
+
run.pendingHostRequestIds.add(requestId);
|
|
189
|
+
emitRunEvent(run, { type: "host.request", requestId, id: requestId, method, params });
|
|
190
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
191
|
+
store.hostResponses.set(requestId, { resolve, reject });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function requestHostCapability(
|
|
196
|
+
store: RunStore,
|
|
197
|
+
run: RunRecord,
|
|
198
|
+
capability: string,
|
|
199
|
+
params: unknown,
|
|
200
|
+
): Promise<unknown> {
|
|
201
|
+
const { requestId } = emitHostCapabilityRequest(run, capability, params);
|
|
202
|
+
return waitForHostResponse(store, requestId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function requestHostCapabilitySession<Result = unknown>(
|
|
206
|
+
store: RunStore,
|
|
207
|
+
run: RunRecord,
|
|
208
|
+
capability: string,
|
|
209
|
+
params: unknown,
|
|
210
|
+
): Promise<HostCapabilitySessionResult<Result>> {
|
|
211
|
+
const { requestId } = emitHostCapabilityRequest(run, capability, params);
|
|
212
|
+
const closed = new Promise<void>((resolveClosed, rejectClosed) => {
|
|
213
|
+
store.hostCapabilityResources.set(requestId, { resolveClosed, rejectClosed });
|
|
214
|
+
run.pendingHostCapabilityResourceIds.add(requestId);
|
|
215
|
+
});
|
|
216
|
+
const result = await waitForHostResponse(store, requestId).catch((error) => {
|
|
217
|
+
closeHostCapabilityResource(store, requestId, error instanceof Error ? error : new Error(String(error)));
|
|
218
|
+
throw error;
|
|
219
|
+
});
|
|
220
|
+
return { result: result as Result, closed };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function closeHostCapabilityResource(store: RunStore, requestId: string, error?: Error): boolean {
|
|
224
|
+
const pending = store.hostCapabilityResources.get(requestId);
|
|
225
|
+
if (!pending) return false;
|
|
226
|
+
store.hostCapabilityResources.delete(requestId);
|
|
227
|
+
clearPendingHostCapabilityResource(store, requestId);
|
|
228
|
+
if (error) pending.rejectClosed(error);
|
|
229
|
+
else pending.resolveClosed();
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function emitHostCapabilityRequest(
|
|
234
|
+
run: RunRecord,
|
|
235
|
+
capability: string,
|
|
236
|
+
params: unknown,
|
|
237
|
+
): { requestId: string } {
|
|
238
|
+
if (run.status !== "running") {
|
|
239
|
+
throw new RuntimeHostRequestError({
|
|
240
|
+
message: `Run ${run.id} is ${run.status}`,
|
|
241
|
+
method: capability,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const requestId = `cap_req_${crypto.randomUUID()}`;
|
|
245
|
+
run.pendingHostRequestIds.add(requestId);
|
|
246
|
+
emitRunEvent(run, { type: "host.capability.request", requestId, id: requestId, capability, params });
|
|
247
|
+
return { requestId };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function waitForHostResponse(store: RunStore, requestId: string): Promise<unknown> {
|
|
251
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
252
|
+
store.hostResponses.set(requestId, { resolve, reject });
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function subscribeRunEvents(run: RunRecord, handler: (event: RuntimeEvent) => void): () => void {
|
|
257
|
+
run.eventSubscribers.add(handler);
|
|
258
|
+
return () => {
|
|
259
|
+
run.eventSubscribers.delete(handler);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function closeRunSubscribers(run: RunRecord): void {
|
|
264
|
+
for (const subscriber of run.subscribers) {
|
|
265
|
+
subscriber.close();
|
|
266
|
+
}
|
|
267
|
+
run.subscribers.clear();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function settleHostCapabilityResources(
|
|
271
|
+
run: RunRecord,
|
|
272
|
+
store: RunStore,
|
|
273
|
+
settle: (pending: PendingHostCapabilityResource) => void,
|
|
274
|
+
): void {
|
|
275
|
+
for (const requestId of run.pendingHostCapabilityResourceIds) {
|
|
276
|
+
const pending = store.hostCapabilityResources.get(requestId);
|
|
277
|
+
if (!pending) continue;
|
|
278
|
+
store.hostCapabilityResources.delete(requestId);
|
|
279
|
+
settle(pending);
|
|
280
|
+
}
|
|
281
|
+
run.pendingHostCapabilityResourceIds.clear();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function clearPendingHostCapabilityResource(store: RunStore, requestId: string): void {
|
|
285
|
+
for (const run of store.runs.values()) {
|
|
286
|
+
run.pendingHostCapabilityResourceIds.delete(requestId);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function encodeSse(event: RuntimeEvent): Uint8Array {
|
|
291
|
+
return encoder.encode(`data: ${JSON.stringify(event)}\n\n`);
|
|
292
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
HttpApp,
|
|
5
|
+
HttpServer,
|
|
6
|
+
HttpServerRequest,
|
|
7
|
+
HttpServerResponse,
|
|
8
|
+
} from "@effect/platform";
|
|
9
|
+
import * as BunHttpServer from "@effect/platform-bun/BunHttpServer";
|
|
10
|
+
import { RIGKIT_ENGINE_VERSION } from "@rigkit/engine";
|
|
11
|
+
import { Effect, Exit, Scope } from "effect";
|
|
12
|
+
import { RIGKIT_RUNTIME_VERSION } from "./version.ts";
|
|
13
|
+
import { runtimeJsonError, sessionRunIdFor } from "./app.ts";
|
|
14
|
+
import { createRuntimeControlApiHandler } from "./api-handlers.ts";
|
|
15
|
+
import type { RuntimeAppState } from "./control.ts";
|
|
16
|
+
import { runSessionSocketEffect } from "./sessions.ts";
|
|
17
|
+
import { DEFAULT_IDLE_MS } from "./protocol.ts";
|
|
18
|
+
import { createRunStore } from "./runs.ts";
|
|
19
|
+
import { readOrCreateToken } from "./token.ts";
|
|
20
|
+
import type { RuntimeContext, RuntimeServer, ServeRuntimeOptions } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
export class RuntimeServerError extends Error {
|
|
23
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
this.name = "RuntimeServerError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function serveRuntimeEffect(options: ServeRuntimeOptions): Effect.Effect<RuntimeServer, RuntimeServerError, Scope.Scope> {
|
|
30
|
+
return Effect.acquireRelease(
|
|
31
|
+
Effect.tryPromise({
|
|
32
|
+
try: () => serveRuntime(options),
|
|
33
|
+
catch: (cause) => new RuntimeServerError("Failed to start Rigkit runtime server", { cause }),
|
|
34
|
+
}),
|
|
35
|
+
(server) => Effect.sync(() => server.stop()),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function serveRuntime(options: ServeRuntimeOptions): Promise<RuntimeServer> {
|
|
40
|
+
const projectDir = resolve(options.projectDir);
|
|
41
|
+
const configPath = resolve(options.configPath);
|
|
42
|
+
const statePath = options.statePath ? resolve(options.statePath) : undefined;
|
|
43
|
+
const host = options.host ?? "127.0.0.1";
|
|
44
|
+
const token = options.token ?? readOrCreateToken(options.tokenPath);
|
|
45
|
+
const idleMs = options.idleMs ?? DEFAULT_IDLE_MS;
|
|
46
|
+
const startedAt = new Date().toISOString();
|
|
47
|
+
const store = createRunStore();
|
|
48
|
+
let expiresAt = new Date(Date.now() + idleMs).toISOString();
|
|
49
|
+
let url = "";
|
|
50
|
+
let stopServer = () => {};
|
|
51
|
+
let idleTimer: ReturnType<typeof setInterval> | undefined;
|
|
52
|
+
let resolveClosed!: () => void;
|
|
53
|
+
const closed = new Promise<void>((resolve) => {
|
|
54
|
+
resolveClosed = resolve;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const writeHandle = () => {
|
|
58
|
+
mkdirSync(dirname(options.handlePath), { recursive: true });
|
|
59
|
+
writeFileSync(
|
|
60
|
+
options.handlePath,
|
|
61
|
+
`${JSON.stringify({
|
|
62
|
+
projectId: options.projectId,
|
|
63
|
+
projectDir,
|
|
64
|
+
configPath,
|
|
65
|
+
statePath,
|
|
66
|
+
pid: process.pid,
|
|
67
|
+
url,
|
|
68
|
+
tokenPath: options.tokenPath,
|
|
69
|
+
engineVersion: RIGKIT_ENGINE_VERSION,
|
|
70
|
+
runtimeVersion: RIGKIT_RUNTIME_VERSION,
|
|
71
|
+
startedAt,
|
|
72
|
+
expiresAt,
|
|
73
|
+
}, null, 2)}\n`,
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const context: RuntimeContext = {
|
|
78
|
+
projectId: options.projectId,
|
|
79
|
+
projectDir,
|
|
80
|
+
configPath,
|
|
81
|
+
statePath,
|
|
82
|
+
source: options.source,
|
|
83
|
+
token,
|
|
84
|
+
startedAt,
|
|
85
|
+
getExpiresAt: () => expiresAt,
|
|
86
|
+
touch: () => {
|
|
87
|
+
expiresAt = new Date(Date.now() + idleMs).toISOString();
|
|
88
|
+
if (url) writeHandle();
|
|
89
|
+
},
|
|
90
|
+
stop: () => stopServer(),
|
|
91
|
+
};
|
|
92
|
+
const state: RuntimeAppState = { context, store };
|
|
93
|
+
const controlApi = createRuntimeControlApiHandler(context, store);
|
|
94
|
+
const app = createRuntimeHttpApp(state, controlApi);
|
|
95
|
+
const scope = Effect.runSync(Scope.make());
|
|
96
|
+
const server = await Effect.runPromise(Scope.extend(BunHttpServer.make({
|
|
97
|
+
hostname: host,
|
|
98
|
+
port: options.port ?? 0,
|
|
99
|
+
}), scope));
|
|
100
|
+
await Effect.runPromise(Scope.extend(
|
|
101
|
+
HttpServer.serveEffect(app).pipe(Effect.provideService(HttpServer.HttpServer, server)),
|
|
102
|
+
scope,
|
|
103
|
+
));
|
|
104
|
+
|
|
105
|
+
let stopped = false;
|
|
106
|
+
stopServer = () => {
|
|
107
|
+
if (stopped) return;
|
|
108
|
+
stopped = true;
|
|
109
|
+
if (idleTimer) clearInterval(idleTimer);
|
|
110
|
+
void controlApi.dispose();
|
|
111
|
+
void Effect.runPromise(Scope.close(scope, Exit.void))
|
|
112
|
+
.catch(() => {})
|
|
113
|
+
.finally(resolveClosed);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const port = server.address._tag === "TcpAddress" ? server.address.port : options.port ?? 0;
|
|
117
|
+
url = `http://${host}:${port}`;
|
|
118
|
+
writeHandle();
|
|
119
|
+
|
|
120
|
+
idleTimer = setInterval(() => {
|
|
121
|
+
if ([...store.runs.values()].some((run) => run.status === "running") || store.activeSessions > 0) return;
|
|
122
|
+
if (Date.now() <= Date.parse(expiresAt)) return;
|
|
123
|
+
stopServer();
|
|
124
|
+
}, Math.min(idleMs, 30_000));
|
|
125
|
+
idleTimer.unref?.();
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
url,
|
|
129
|
+
token,
|
|
130
|
+
closed,
|
|
131
|
+
stop() {
|
|
132
|
+
stopServer();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createRuntimeHttpApp(
|
|
138
|
+
state: RuntimeAppState,
|
|
139
|
+
controlApi: ReturnType<typeof createRuntimeControlApiHandler>,
|
|
140
|
+
): HttpApp.Default<unknown> {
|
|
141
|
+
const controlApp = HttpApp.fromWebHandler(controlApi.handler);
|
|
142
|
+
return Effect.gen(function* () {
|
|
143
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
144
|
+
const sessionRunId = sessionRunIdFor(requestPathname(request.url));
|
|
145
|
+
if (sessionRunId) return yield* handleRunSessionRequest(state, sessionRunId);
|
|
146
|
+
return yield* controlApp;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function handleRunSessionRequest(
|
|
151
|
+
state: RuntimeAppState,
|
|
152
|
+
runId: string,
|
|
153
|
+
) {
|
|
154
|
+
return Effect.gen(function* () {
|
|
155
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
156
|
+
if (request.headers.authorization !== `Bearer ${state.context.token}`) {
|
|
157
|
+
return HttpServerResponse.fromWeb(runtimeJsonError(401, "Unauthorized"));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!state.store.runs.has(runId)) {
|
|
161
|
+
return HttpServerResponse.fromWeb(runtimeJsonError(404, `Unknown run ${runId}`));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
state.context.touch();
|
|
165
|
+
const socket = yield* Effect.either(HttpServerRequest.upgrade);
|
|
166
|
+
if (socket._tag === "Left") {
|
|
167
|
+
return HttpServerResponse.fromWeb(runtimeJsonError(400, "WebSocket upgrade failed"));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
yield* Effect.forkDaemon(
|
|
171
|
+
runSessionSocketEffect(state, runId, socket.right).pipe(Effect.scoped),
|
|
172
|
+
);
|
|
173
|
+
return HttpServerResponse.empty({ status: 101 });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requestPathname(url: string): string {
|
|
178
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) return new URL(url).pathname;
|
|
179
|
+
return url.split("?", 1)[0] || "/";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type { RuntimeServer, ServeRuntimeOptions } from "./types.ts";
|