robodev 0.34.0 → 0.36.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.
- package/package.json +4 -2
- package/src/dev-clients.ts +31 -26
- package/src/dev-inspect.test.ts +136 -7
- package/src/dev-inspect.ts +685 -106
- package/src/dev-server.ts +301 -24
- package/src/dev.ts +43 -6
- package/templates/auth-chat/package.json +1 -1
- package/templates/backend/package.json +1 -1
- package/templates/empty/package.json +1 -1
- package/templates/marketplace/package.json +1 -1
- package/templates/space/package.json +1 -1
package/src/dev-server.ts
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import cors from "@fastify/cors";
|
|
3
|
-
import
|
|
3
|
+
import websocket from "@fastify/websocket";
|
|
4
|
+
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify";
|
|
5
|
+
import type { WebSocket } from "ws";
|
|
4
6
|
import {
|
|
7
|
+
SOCKET_CLOSE_POLICY,
|
|
8
|
+
SOCKET_IDLE_PING_MS,
|
|
9
|
+
bindSocketSession,
|
|
5
10
|
buildOpenApi,
|
|
6
11
|
handleLocalAuth,
|
|
7
12
|
isLocalAuthPath,
|
|
13
|
+
isReservedStoragePath,
|
|
8
14
|
matchProjectRoute,
|
|
15
|
+
matchSocketRoute,
|
|
16
|
+
parseLogicalKey,
|
|
17
|
+
resolveLocalObject,
|
|
9
18
|
resolveLocalUser,
|
|
10
19
|
runRoute,
|
|
11
20
|
scalarHtml,
|
|
12
21
|
swaggerHtml,
|
|
22
|
+
verifyStorageServeToken,
|
|
13
23
|
type AuthQueryable,
|
|
14
24
|
type RuntimeRoute,
|
|
25
|
+
type RuntimeSocket,
|
|
15
26
|
} from "@robodev-ai/runtime";
|
|
16
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
JobEnqueueInput,
|
|
29
|
+
RobodevDb,
|
|
30
|
+
SocketDefinition,
|
|
31
|
+
SocketHandlerContext,
|
|
32
|
+
} from "@robodev-ai/sdk";
|
|
17
33
|
import type { RuntimeJob } from "@robodev-ai/runtime";
|
|
18
34
|
import { devRouteClients } from "./dev-clients.js";
|
|
19
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
buildDashboardState,
|
|
37
|
+
isInspectPath,
|
|
38
|
+
shouldCountRequestHistogram,
|
|
39
|
+
type DevInspectStore,
|
|
40
|
+
} from "./dev-inspect.js";
|
|
20
41
|
|
|
21
42
|
/** One compiled generation of the project. Swapped wholesale on reload. */
|
|
22
43
|
export type DevRuntime = {
|
|
@@ -24,6 +45,7 @@ export type DevRuntime = {
|
|
|
24
45
|
pool: AuthQueryable & { end: () => Promise<void> };
|
|
25
46
|
routes: RuntimeRoute[];
|
|
26
47
|
jobs: RuntimeJob[];
|
|
48
|
+
sockets: RuntimeSocket[];
|
|
27
49
|
openapi: Record<string, unknown>;
|
|
28
50
|
};
|
|
29
51
|
|
|
@@ -39,6 +61,7 @@ export type DevServerOptions = {
|
|
|
39
61
|
brokerBaseUrl: string;
|
|
40
62
|
googleClientId?: string;
|
|
41
63
|
googleClientSecret?: string;
|
|
64
|
+
storageDir: string;
|
|
42
65
|
};
|
|
43
66
|
|
|
44
67
|
const SWAGGER_PATHS = new Set(["/_robodev/docs", "/_robodev/swagger", "/docs", "/swagger"]);
|
|
@@ -57,10 +80,242 @@ function rawBodyOf(request: FastifyRequest): Buffer | undefined {
|
|
|
57
80
|
return (request as FastifyRequest & { rawBody?: Buffer }).rawBody;
|
|
58
81
|
}
|
|
59
82
|
|
|
83
|
+
function queryRecord(query: unknown): Record<string, string> {
|
|
84
|
+
if (!query || typeof query !== "object") return {};
|
|
85
|
+
const out: Record<string, string> = {};
|
|
86
|
+
for (const [key, value] of Object.entries(query as Record<string, unknown>)) {
|
|
87
|
+
if (typeof value === "string") out[key] = value;
|
|
88
|
+
else if (Array.isArray(value) && typeof value[0] === "string") out[key] = value[0];
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function socketAuthorization(request: FastifyRequest): string | undefined {
|
|
94
|
+
const header = request.headers.authorization;
|
|
95
|
+
if (typeof header === "string" && header.length > 0) return header;
|
|
96
|
+
const token = queryRecord(request.query).accessToken;
|
|
97
|
+
if (token) return `Bearer ${token}`;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function codedErrorBody(error: unknown): { status: number; body: Record<string, unknown> } | null {
|
|
102
|
+
if (!error || typeof error !== "object" || !("statusCode" in error)) return null;
|
|
103
|
+
const statusCode = (error as { statusCode: unknown }).statusCode;
|
|
104
|
+
if (typeof statusCode !== "number") return null;
|
|
105
|
+
const code = (error as { code?: unknown }).code;
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
return {
|
|
108
|
+
status: statusCode,
|
|
109
|
+
body: {
|
|
110
|
+
error: typeof code === "string" ? code : message,
|
|
111
|
+
message,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function routeClients(options: DevServerOptions, getRuntime: () => DevRuntime | null) {
|
|
117
|
+
return devRouteClients({
|
|
118
|
+
env: options.env,
|
|
119
|
+
log: options.log,
|
|
120
|
+
enqueue: options.enqueue,
|
|
121
|
+
inspect: options.inspect,
|
|
122
|
+
query: (sql, values) => {
|
|
123
|
+
const runtime = getRuntime();
|
|
124
|
+
if (!runtime) throw new Error("dev runtime not ready");
|
|
125
|
+
return runtime.pool.query(sql, values);
|
|
126
|
+
},
|
|
127
|
+
objectsDir: options.storageDir,
|
|
128
|
+
publicBaseUrl: options.listenOrigin,
|
|
129
|
+
projectId: options.projectId,
|
|
130
|
+
jwtSecret: options.jwtSecret,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function resolveSocketUser(
|
|
135
|
+
options: DevServerOptions,
|
|
136
|
+
def: SocketDefinition,
|
|
137
|
+
request: FastifyRequest,
|
|
138
|
+
): Promise<{ ok: true; user: SocketHandlerContext["user"] } | { ok: false }> {
|
|
139
|
+
const auth = def.auth ?? false;
|
|
140
|
+
if (auth === false) return { ok: true, user: null };
|
|
141
|
+
if (typeof auth === "function") {
|
|
142
|
+
try {
|
|
143
|
+
const user = await auth({ headers: request.headers });
|
|
144
|
+
if (!user) return { ok: false };
|
|
145
|
+
return { ok: true, user };
|
|
146
|
+
} catch {
|
|
147
|
+
return { ok: false };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (auth !== "required" && auth !== "optional") return { ok: false };
|
|
151
|
+
const user = await resolveLocalUser(
|
|
152
|
+
{ projectId: options.projectId, jwtSecret: options.jwtSecret },
|
|
153
|
+
socketAuthorization(request),
|
|
154
|
+
);
|
|
155
|
+
if (auth === "required" && !user) return { ok: false };
|
|
156
|
+
return { ok: true, user };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function handleLocalSocket(
|
|
160
|
+
socket: WebSocket,
|
|
161
|
+
request: FastifyRequest,
|
|
162
|
+
options: DevServerOptions,
|
|
163
|
+
getRuntime: () => DevRuntime | null,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
const runtime = getRuntime();
|
|
166
|
+
if (!runtime) {
|
|
167
|
+
socket.close(SOCKET_CLOSE_POLICY, "not_found");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const pathname = pathnameOf(request.url);
|
|
171
|
+
const matched = matchSocketRoute(pathname, runtime.sockets);
|
|
172
|
+
if (!matched) {
|
|
173
|
+
socket.close(SOCKET_CLOSE_POLICY, "not_found");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const auth = await resolveSocketUser(options, matched.socket.def, request);
|
|
178
|
+
if (!auth.ok) {
|
|
179
|
+
socket.close(SOCKET_CLOSE_POLICY, "unauthorized");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const clients = routeClients(options, getRuntime);
|
|
184
|
+
const session = await bindSocketSession({
|
|
185
|
+
projectId: options.projectId,
|
|
186
|
+
name: matched.socket.name,
|
|
187
|
+
socket: {
|
|
188
|
+
send: (data) => socket.send(data),
|
|
189
|
+
close: (code, reason) => socket.close(code, reason),
|
|
190
|
+
get readyState() {
|
|
191
|
+
return socket.readyState;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
timeoutMs: matched.socket.def.timeoutMs,
|
|
195
|
+
createContext: ({ send, close, room }) => ({
|
|
196
|
+
db: runtime.db,
|
|
197
|
+
email: clients.email,
|
|
198
|
+
llm: clients.llm,
|
|
199
|
+
agent: clients.agent,
|
|
200
|
+
storage: clients.storage,
|
|
201
|
+
env: clients.env,
|
|
202
|
+
params: matched.params,
|
|
203
|
+
query: queryRecord(request.query),
|
|
204
|
+
headers: request.headers,
|
|
205
|
+
user: auth.user,
|
|
206
|
+
push: clients.push,
|
|
207
|
+
jobs: clients.jobs,
|
|
208
|
+
sockets: clients.sockets,
|
|
209
|
+
send,
|
|
210
|
+
close,
|
|
211
|
+
room,
|
|
212
|
+
}),
|
|
213
|
+
onConnect: matched.socket.def.onConnect,
|
|
214
|
+
onMessage: matched.socket.def.onMessage,
|
|
215
|
+
onClose: matched.socket.def.onClose,
|
|
216
|
+
});
|
|
217
|
+
if (!session.ok) {
|
|
218
|
+
if (socket.readyState === socket.OPEN) {
|
|
219
|
+
socket.close(session.code, session.reason);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let alive = true;
|
|
225
|
+
const pingTimer = setInterval(() => {
|
|
226
|
+
if (socket.readyState !== socket.OPEN) {
|
|
227
|
+
clearInterval(pingTimer);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (!alive) {
|
|
231
|
+
socket.close(SOCKET_CLOSE_POLICY, "idle");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
alive = false;
|
|
235
|
+
try {
|
|
236
|
+
socket.ping();
|
|
237
|
+
} catch {
|
|
238
|
+
socket.close(SOCKET_CLOSE_POLICY, "idle");
|
|
239
|
+
}
|
|
240
|
+
}, SOCKET_IDLE_PING_MS);
|
|
241
|
+
pingTimer.unref?.();
|
|
242
|
+
|
|
243
|
+
socket.on("pong", () => {
|
|
244
|
+
alive = true;
|
|
245
|
+
});
|
|
246
|
+
socket.on("message", (data, isBinary) => {
|
|
247
|
+
if (isBinary) {
|
|
248
|
+
session.onBinary();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const text = typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8");
|
|
252
|
+
session.onText(text);
|
|
253
|
+
});
|
|
254
|
+
socket.on("close", () => {
|
|
255
|
+
clearInterval(pingTimer);
|
|
256
|
+
session.onClose();
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function serveLocalStorage(
|
|
261
|
+
request: FastifyRequest,
|
|
262
|
+
reply: FastifyReply,
|
|
263
|
+
options: DevServerOptions,
|
|
264
|
+
getRuntime: () => DevRuntime | null,
|
|
265
|
+
pathname: string,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
const runtime = getRuntime();
|
|
268
|
+
if (!runtime) {
|
|
269
|
+
await reply.code(503).send({ error: "not_ready" });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const encodedKey = pathname.slice("/storage/objects/".length);
|
|
273
|
+
let key: string;
|
|
274
|
+
try {
|
|
275
|
+
key = decodeURIComponent(encodedKey);
|
|
276
|
+
parseLogicalKey(key);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
const mapped = codedErrorBody(error);
|
|
279
|
+
if (mapped) {
|
|
280
|
+
await reply.code(mapped.status).send(mapped.body);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await reply.code(400).send({ error: "invalid_key" });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const resolved = await resolveLocalObject({
|
|
288
|
+
query: (sql, values) => runtime.pool.query(sql, values),
|
|
289
|
+
objectsDir: options.storageDir,
|
|
290
|
+
key,
|
|
291
|
+
});
|
|
292
|
+
if (!resolved) {
|
|
293
|
+
await reply.code(404).send({ error: "not_found" });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const user = await resolveLocalUser(
|
|
298
|
+
{ projectId: options.projectId, jwtSecret: options.jwtSecret },
|
|
299
|
+
request.headers.authorization,
|
|
300
|
+
);
|
|
301
|
+
const signed = verifyStorageServeToken(
|
|
302
|
+
options.jwtSecret,
|
|
303
|
+
options.projectId,
|
|
304
|
+
key,
|
|
305
|
+
request.query as { exp?: unknown; sig?: unknown },
|
|
306
|
+
);
|
|
307
|
+
if (!resolved.row.public && !user && !signed) {
|
|
308
|
+
await reply.code(404).send({ error: "not_found" });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await reply.type(resolved.row.content_type).send(resolved.body);
|
|
313
|
+
}
|
|
314
|
+
|
|
60
315
|
/**
|
|
61
316
|
* A single-project HTTP host. Unlike the hosted project host there is no `Host` lookup
|
|
62
317
|
* and no static SPA serving (local Vite does that). Fastify stays `logger: false`;
|
|
63
|
-
* project/auth/docs requests are recorded on the process-local
|
|
318
|
+
* project/auth/docs requests are recorded on the process-local dashboard after reply.
|
|
64
319
|
* Routes, validation, and OpenAPI all come from `@robodev-ai/runtime`.
|
|
65
320
|
*/
|
|
66
321
|
export async function startDevServer(
|
|
@@ -74,6 +329,10 @@ export async function startDevServer(
|
|
|
74
329
|
credentials: true,
|
|
75
330
|
methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
76
331
|
});
|
|
332
|
+
await app.register(websocket);
|
|
333
|
+
app.get("/sockets/*", { websocket: true }, (socket, request) => {
|
|
334
|
+
void handleLocalSocket(socket, request, options, getRuntime);
|
|
335
|
+
});
|
|
77
336
|
|
|
78
337
|
app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
|
|
79
338
|
if (!body) {
|
|
@@ -103,13 +362,16 @@ export async function startDevServer(
|
|
|
103
362
|
);
|
|
104
363
|
|
|
105
364
|
// `rawBody` handlers and multipart parsing need the untouched bytes.
|
|
106
|
-
app.addHook("preParsing", async (
|
|
365
|
+
app.addHook("preParsing", async (request, _reply, payload) => {
|
|
366
|
+
if (String(request.headers.upgrade ?? "").toLowerCase() === "websocket") {
|
|
367
|
+
return payload;
|
|
368
|
+
}
|
|
107
369
|
const chunks: Buffer[] = [];
|
|
108
370
|
for await (const chunk of payload) {
|
|
109
371
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
110
372
|
}
|
|
111
373
|
const raw = Buffer.concat(chunks);
|
|
112
|
-
(
|
|
374
|
+
(request as FastifyRequest & { rawBody?: Buffer }).rawBody = raw;
|
|
113
375
|
return Readable.from(raw);
|
|
114
376
|
});
|
|
115
377
|
|
|
@@ -120,8 +382,18 @@ export async function startDevServer(
|
|
|
120
382
|
const method = request.method.toLowerCase();
|
|
121
383
|
|
|
122
384
|
if (isInspectPath(pathname)) {
|
|
123
|
-
if (pathname === "/_robodev/logs
|
|
124
|
-
return reply.
|
|
385
|
+
if (pathname === "/_robodev/logs") {
|
|
386
|
+
return reply.redirect("/_robodev");
|
|
387
|
+
}
|
|
388
|
+
if (pathname === "/_robodev/state.json" || pathname === "/_robodev/logs.json") {
|
|
389
|
+
return reply.send(
|
|
390
|
+
await buildDashboardState({
|
|
391
|
+
inspect: options.inspect,
|
|
392
|
+
runtime: getRuntime(),
|
|
393
|
+
googleClientId: options.googleClientId,
|
|
394
|
+
googleClientSecret: options.googleClientSecret,
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
125
397
|
}
|
|
126
398
|
return reply.type("text/html; charset=utf-8").send(options.inspect.html());
|
|
127
399
|
}
|
|
@@ -132,15 +404,20 @@ export async function startDevServer(
|
|
|
132
404
|
} finally {
|
|
133
405
|
const status = reply.statusCode;
|
|
134
406
|
const durationMs = Date.now() - started;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
407
|
+
if (!isInspectPath(pathname)) {
|
|
408
|
+
options.inspect.record({
|
|
409
|
+
kind: "request",
|
|
410
|
+
method: request.method,
|
|
411
|
+
path: pathname,
|
|
412
|
+
status,
|
|
413
|
+
durationMs,
|
|
414
|
+
});
|
|
415
|
+
// Print here (not via `log`) so request lines stay off compile/reload logs.
|
|
416
|
+
console.log(`${request.method} ${pathname} ${status} ${durationMs}ms`);
|
|
417
|
+
if (shouldCountRequestHistogram(pathname, getRuntime(), request.method)) {
|
|
418
|
+
options.inspect.incrementRequestHistogram(status);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
144
421
|
}
|
|
145
422
|
return reply;
|
|
146
423
|
|
|
@@ -214,6 +491,11 @@ export async function startDevServer(
|
|
|
214
491
|
return;
|
|
215
492
|
}
|
|
216
493
|
|
|
494
|
+
if (request.method.toUpperCase() === "GET" && isReservedStoragePath(pathname)) {
|
|
495
|
+
await serveLocalStorage(request, reply, options, getRuntime, pathname);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
217
499
|
if (!matchProjectRoute(pathname, method, runtime.routes)) {
|
|
218
500
|
await reply.code(404).send({
|
|
219
501
|
error: "not_found",
|
|
@@ -238,13 +520,8 @@ export async function startDevServer(
|
|
|
238
520
|
{ projectId: options.projectId, jwtSecret: options.jwtSecret },
|
|
239
521
|
authorization,
|
|
240
522
|
),
|
|
241
|
-
clients: () =>
|
|
242
|
-
|
|
243
|
-
env: options.env,
|
|
244
|
-
log: options.log,
|
|
245
|
-
enqueue: options.enqueue,
|
|
246
|
-
inspect: options.inspect,
|
|
247
|
-
}),
|
|
523
|
+
clients: () => routeClients(options, getRuntime),
|
|
524
|
+
mapError: (error) => codedErrorBody(error),
|
|
248
525
|
});
|
|
249
526
|
|
|
250
527
|
if (outcome.kind === "error") {
|
package/src/dev.ts
CHANGED
|
@@ -9,9 +9,13 @@ import {
|
|
|
9
9
|
assertJobCrons,
|
|
10
10
|
buildPlan,
|
|
11
11
|
compileApiTree,
|
|
12
|
+
closeProjectSockets,
|
|
12
13
|
createLocalJobsEngine,
|
|
13
14
|
ensureAuthSchema,
|
|
14
15
|
ensureJobsSchema,
|
|
16
|
+
ensureStorageSchema,
|
|
17
|
+
setProjectSocketNames,
|
|
18
|
+
SOCKET_CLOSE_RESTART,
|
|
15
19
|
isEsbuildFailure,
|
|
16
20
|
loadModules,
|
|
17
21
|
mapEsbuildErrors,
|
|
@@ -154,6 +158,8 @@ export async function main(args: string[]): Promise<void> {
|
|
|
154
158
|
const postgres: DevPostgres = parsed.databaseUrl
|
|
155
159
|
? externalDevPostgres(parsed.databaseUrl)
|
|
156
160
|
: await startDevPostgres({ dataDir: join(devDir, "pg"), port: parsed.dbPort, quiet: true });
|
|
161
|
+
const storageDir = join(devDir, "storage");
|
|
162
|
+
await mkdir(storageDir, { recursive: true });
|
|
157
163
|
|
|
158
164
|
let current: DevRuntime | null = null;
|
|
159
165
|
let lastSchemaSource = "";
|
|
@@ -172,7 +178,30 @@ export async function main(args: string[]): Promise<void> {
|
|
|
172
178
|
getGeneration: () => current,
|
|
173
179
|
log,
|
|
174
180
|
clients: () =>
|
|
175
|
-
devRouteClients({
|
|
181
|
+
devRouteClients({
|
|
182
|
+
env,
|
|
183
|
+
log,
|
|
184
|
+
inspect,
|
|
185
|
+
enqueue: (input) => engine.enqueue(input),
|
|
186
|
+
query: (sql, values) => {
|
|
187
|
+
if (!current) throw new Error("dev runtime not ready");
|
|
188
|
+
return current.pool.query(sql, values);
|
|
189
|
+
},
|
|
190
|
+
objectsDir: storageDir,
|
|
191
|
+
publicBaseUrl: `http://localhost:${parsed.port}`,
|
|
192
|
+
projectId: DEV_PROJECT_ID,
|
|
193
|
+
jwtSecret: DEV_JWT_SECRET,
|
|
194
|
+
}),
|
|
195
|
+
onJobFinished: (event) => {
|
|
196
|
+
inspect.record({
|
|
197
|
+
kind: "job_run",
|
|
198
|
+
name: event.name,
|
|
199
|
+
status: event.outcome,
|
|
200
|
+
payload: event.payload,
|
|
201
|
+
error: event.error ?? undefined,
|
|
202
|
+
});
|
|
203
|
+
inspect.incrementJobHistogram(event.outcome);
|
|
204
|
+
},
|
|
176
205
|
});
|
|
177
206
|
|
|
178
207
|
async function compileAndActivate(input: { syncSchema: boolean }): Promise<void> {
|
|
@@ -222,15 +251,22 @@ export async function main(args: string[]): Promise<void> {
|
|
|
222
251
|
await pool.query("SELECT 1");
|
|
223
252
|
await ensureAuthSchema(pool);
|
|
224
253
|
await ensureJobsSchema(pool);
|
|
254
|
+
await ensureStorageSchema(pool);
|
|
225
255
|
|
|
256
|
+
closeProjectSockets(DEV_PROJECT_ID, SOCKET_CLOSE_RESTART, "deployed");
|
|
226
257
|
const previous = current;
|
|
227
258
|
current = {
|
|
228
259
|
db,
|
|
229
260
|
pool,
|
|
230
261
|
routes: loaded.routes,
|
|
231
262
|
jobs: loaded.jobs,
|
|
263
|
+
sockets: loaded.sockets,
|
|
232
264
|
openapi: buildDevOpenApi(loaded.database.name, loaded.routes),
|
|
233
265
|
};
|
|
266
|
+
setProjectSocketNames(
|
|
267
|
+
DEV_PROJECT_ID,
|
|
268
|
+
loaded.sockets.map((socket) => socket.name),
|
|
269
|
+
);
|
|
234
270
|
await engine.reconcile(loaded.jobs);
|
|
235
271
|
|
|
236
272
|
for (const route of loaded.routes) {
|
|
@@ -243,10 +279,8 @@ export async function main(args: string[]): Promise<void> {
|
|
|
243
279
|
log(` job ${job.name}`);
|
|
244
280
|
}
|
|
245
281
|
}
|
|
246
|
-
|
|
247
|
-
log(
|
|
248
|
-
` ${loaded.sockets.length} socket(s) compiled but not served — sockets need hosted Starbase.`,
|
|
249
|
-
);
|
|
282
|
+
for (const socket of loaded.sockets) {
|
|
283
|
+
log(` WS ${socket.path}`);
|
|
250
284
|
}
|
|
251
285
|
if (previous) {
|
|
252
286
|
await previous.pool.end().catch(() => undefined);
|
|
@@ -275,6 +309,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
275
309
|
brokerBaseUrl: starbaseUrl(),
|
|
276
310
|
googleClientId: env.GOOGLE_CLIENT_ID,
|
|
277
311
|
googleClientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
312
|
+
storageDir,
|
|
278
313
|
},
|
|
279
314
|
() => current,
|
|
280
315
|
);
|
|
@@ -295,9 +330,10 @@ export async function main(args: string[]): Promise<void> {
|
|
|
295
330
|
`Postgres ${postgres.host}:${postgres.port}/${postgres.database} (--database-url)`,
|
|
296
331
|
);
|
|
297
332
|
}
|
|
333
|
+
console.log(`Storage ${relative(project.root, storageDir) || storageDir}`);
|
|
298
334
|
console.log(`API ${apiUrl}`);
|
|
299
335
|
console.log(`Swagger ${apiUrl}/_robodev/docs`);
|
|
300
|
-
console.log(`
|
|
336
|
+
console.log(`Dashboard ${apiUrl}/_robodev`);
|
|
301
337
|
console.log(
|
|
302
338
|
`Project ${relative(process.cwd(), project.backendDir) || "."} (${project.layout})`,
|
|
303
339
|
);
|
|
@@ -352,6 +388,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
352
388
|
async function shutdown(code: number): Promise<void> {
|
|
353
389
|
if (closing) return;
|
|
354
390
|
closing = true;
|
|
391
|
+
closeProjectSockets(DEV_PROJECT_ID);
|
|
355
392
|
await engine.stop();
|
|
356
393
|
for (const watcher of watchers) watcher.close();
|
|
357
394
|
fe?.kill("SIGTERM");
|