@rivetkit/engine-test-runner 0.0.0-pr.4600.32b0fc8
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/.turbo/turbo-build.log +17 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/package.json +27 -0
- package/src/index.ts +310 -0
- package/src/log.ts +180 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +7 -0
- package/turbo.json +4 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
> @rivetkit/engine-test-runner@2.3.0-rc.1 build /home/runner/work/rivet/rivet/engine/sdks/typescript/test-runner
|
|
3
|
+
> tsup src/index.ts
|
|
4
|
+
|
|
5
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.1
|
|
8
|
+
[34mCLI[39m Using tsup config: /home/runner/work/rivet/rivet/engine/sdks/typescript/test-runner/tsup.config.ts
|
|
9
|
+
[34mCLI[39m Target: node16
|
|
10
|
+
[34mCLI[39m Cleaning output folder
|
|
11
|
+
[34mESM[39m Build start
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m10.71 KB[39m
|
|
13
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m20.96 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 7ms
|
|
15
|
+
[34mDTS[39m Build start
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 744ms
|
|
17
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m175.00 B[39m
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { Runner } from "@rivetkit/engine-runner";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { streamSSE } from "hono/streaming";
|
|
6
|
+
|
|
7
|
+
// src/log.ts
|
|
8
|
+
import { inspect } from "util";
|
|
9
|
+
import {
|
|
10
|
+
pino,
|
|
11
|
+
stdTimeFunctions
|
|
12
|
+
} from "pino";
|
|
13
|
+
var baseLogger;
|
|
14
|
+
var loggerCache = /* @__PURE__ */ new Map();
|
|
15
|
+
function getPinoLevel() {
|
|
16
|
+
return (process.env["LOG_LEVEL"] || "warn").toString().toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
function getIncludeTarget() {
|
|
19
|
+
return process.env["LOG_TARGET"] === "1";
|
|
20
|
+
}
|
|
21
|
+
function customWrite(level, o) {
|
|
22
|
+
const entries = {};
|
|
23
|
+
if (process.env["LOG_TIMESTAMP"] === "1" && o.time) {
|
|
24
|
+
const date = typeof o.time === "number" ? new Date(o.time) : /* @__PURE__ */ new Date();
|
|
25
|
+
entries.ts = date;
|
|
26
|
+
}
|
|
27
|
+
entries.level = level.toUpperCase();
|
|
28
|
+
if (o.target) {
|
|
29
|
+
entries.target = o.target;
|
|
30
|
+
}
|
|
31
|
+
if (o.msg) {
|
|
32
|
+
entries.msg = o.msg;
|
|
33
|
+
}
|
|
34
|
+
for (const [key, value] of Object.entries(o)) {
|
|
35
|
+
if (key !== "time" && key !== "level" && key !== "target" && key !== "msg" && key !== "pid" && key !== "hostname") {
|
|
36
|
+
entries[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const output = inspect(entries, {
|
|
40
|
+
compact: true,
|
|
41
|
+
breakLength: Infinity,
|
|
42
|
+
colors: true
|
|
43
|
+
});
|
|
44
|
+
console.log(output);
|
|
45
|
+
}
|
|
46
|
+
async function configureDefaultLogger() {
|
|
47
|
+
baseLogger = pino({
|
|
48
|
+
level: getPinoLevel(),
|
|
49
|
+
messageKey: "msg",
|
|
50
|
+
// Do not include pid/hostname in output
|
|
51
|
+
base: {},
|
|
52
|
+
// Keep a string level in the output
|
|
53
|
+
formatters: {
|
|
54
|
+
level(_label, number) {
|
|
55
|
+
return { level: number };
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
timestamp: process.env["LOG_TIMESTAMP"] === "1" ? stdTimeFunctions.epochTime : false,
|
|
59
|
+
browser: {
|
|
60
|
+
write: {
|
|
61
|
+
fatal: customWrite.bind(null, "fatal"),
|
|
62
|
+
error: customWrite.bind(null, "error"),
|
|
63
|
+
warn: customWrite.bind(null, "warn"),
|
|
64
|
+
info: customWrite.bind(null, "info"),
|
|
65
|
+
debug: customWrite.bind(null, "debug"),
|
|
66
|
+
trace: customWrite.bind(null, "trace")
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
hooks: {
|
|
70
|
+
logMethod(inputArgs, _method, level) {
|
|
71
|
+
const levelMap = {
|
|
72
|
+
10: "trace",
|
|
73
|
+
20: "debug",
|
|
74
|
+
30: "info",
|
|
75
|
+
40: "warn",
|
|
76
|
+
50: "error",
|
|
77
|
+
60: "fatal"
|
|
78
|
+
};
|
|
79
|
+
const levelName = levelMap[level] || "info";
|
|
80
|
+
const time = process.env["LOG_TIMESTAMP"] === "1" ? Date.now() : void 0;
|
|
81
|
+
if (inputArgs.length >= 2) {
|
|
82
|
+
const [objOrMsg, msg] = inputArgs;
|
|
83
|
+
if (typeof objOrMsg === "object" && objOrMsg !== null) {
|
|
84
|
+
customWrite(levelName, { ...objOrMsg, msg, time });
|
|
85
|
+
} else {
|
|
86
|
+
customWrite(levelName, { msg: String(objOrMsg), time });
|
|
87
|
+
}
|
|
88
|
+
} else if (inputArgs.length === 1) {
|
|
89
|
+
const [objOrMsg] = inputArgs;
|
|
90
|
+
if (typeof objOrMsg === "object" && objOrMsg !== null) {
|
|
91
|
+
customWrite(levelName, { ...objOrMsg, time });
|
|
92
|
+
} else {
|
|
93
|
+
customWrite(levelName, { msg: String(objOrMsg), time });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
loggerCache.clear();
|
|
100
|
+
}
|
|
101
|
+
function getBaseLogger() {
|
|
102
|
+
if (!baseLogger) {
|
|
103
|
+
configureDefaultLogger();
|
|
104
|
+
}
|
|
105
|
+
return baseLogger;
|
|
106
|
+
}
|
|
107
|
+
function getLogger(name = "default") {
|
|
108
|
+
const cached = loggerCache.get(name);
|
|
109
|
+
if (cached) {
|
|
110
|
+
return cached;
|
|
111
|
+
}
|
|
112
|
+
const base = getBaseLogger();
|
|
113
|
+
const child = getIncludeTarget() ? base.child({ target: name }) : base;
|
|
114
|
+
loggerCache.set(name, child);
|
|
115
|
+
return child;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/index.ts
|
|
119
|
+
var INTERNAL_SERVER_PORT = process.env.INTERNAL_SERVER_PORT ? Number(process.env.INTERNAL_SERVER_PORT) : 5051;
|
|
120
|
+
var RIVET_NAMESPACE = process.env.RIVET_NAMESPACE ?? "default";
|
|
121
|
+
var RIVET_RUNNER_NAME = process.env.RIVET_RUNNER_NAME ?? "test-runner";
|
|
122
|
+
var RIVET_RUNNER_VERSION = process.env.RIVET_RUNNER_VERSION ? Number(process.env.RIVET_RUNNER_VERSION) : 1;
|
|
123
|
+
var RIVET_RUNNER_TOTAL_SLOTS = parseInt(process.env.RIVET_RUNNER_TOTAL_SLOTS ?? "1");
|
|
124
|
+
var RIVET_ENDPOINT = process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420";
|
|
125
|
+
var RIVET_TOKEN = process.env.RIVET_TOKEN ?? "dev";
|
|
126
|
+
var AUTOSTART_SERVER = (process.env.AUTOSTART_SERVER ?? "1") == "1";
|
|
127
|
+
var AUTOSTART_RUNNER = (process.env.AUTOSTART_RUNNER ?? "0") == "1";
|
|
128
|
+
var AUTOCONFIGURE_SERVERLESS = (process.env.AUTOCONFIGURE_SERVERLESS ?? "1") == "1";
|
|
129
|
+
var runnerStarted = Promise.withResolvers();
|
|
130
|
+
var runnerStopped = Promise.withResolvers();
|
|
131
|
+
var runner = null;
|
|
132
|
+
var websocketLastMsgIndexes = /* @__PURE__ */ new Map();
|
|
133
|
+
var app = new Hono();
|
|
134
|
+
function loggerMiddleware(logger) {
|
|
135
|
+
return async (c, next) => {
|
|
136
|
+
const method = c.req.method;
|
|
137
|
+
const path = c.req.path;
|
|
138
|
+
const startTime = Date.now();
|
|
139
|
+
await next();
|
|
140
|
+
const duration = Date.now() - startTime;
|
|
141
|
+
logger.debug({
|
|
142
|
+
msg: "http request",
|
|
143
|
+
method,
|
|
144
|
+
path,
|
|
145
|
+
status: c.res.status,
|
|
146
|
+
dt: `${duration}ms`,
|
|
147
|
+
reqSize: c.req.header("content-length"),
|
|
148
|
+
resSize: c.res.headers.get("content-length"),
|
|
149
|
+
userAgent: c.req.header("user-agent")
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
app.use("*", loggerMiddleware(getLogger()));
|
|
154
|
+
app.get("/wait-ready", async (c) => {
|
|
155
|
+
const runner2 = await runnerStarted.promise;
|
|
156
|
+
return c.json(runner2.runnerId);
|
|
157
|
+
});
|
|
158
|
+
app.get("/has-actor", async (c) => {
|
|
159
|
+
const actorIdQuery = c.req.query("actor");
|
|
160
|
+
const generationQuery = c.req.query("generation");
|
|
161
|
+
const generation = generationQuery ? Number(generationQuery) : void 0;
|
|
162
|
+
if (!actorIdQuery || !(runner == null ? void 0 : runner.hasActor(actorIdQuery, generation))) {
|
|
163
|
+
return c.text("", 404);
|
|
164
|
+
}
|
|
165
|
+
return c.text("ok");
|
|
166
|
+
});
|
|
167
|
+
app.get("/health", (c) => {
|
|
168
|
+
return c.text("ok");
|
|
169
|
+
});
|
|
170
|
+
app.get("/shutdown", async (c) => {
|
|
171
|
+
await (runner == null ? void 0 : runner.shutdown(true));
|
|
172
|
+
return c.text("ok");
|
|
173
|
+
});
|
|
174
|
+
app.get("/api/rivet/start", async (c) => {
|
|
175
|
+
return streamSSE(c, async (stream) => {
|
|
176
|
+
const runnerStarted2 = Promise.withResolvers();
|
|
177
|
+
const runnerStopped2 = Promise.withResolvers();
|
|
178
|
+
const runner2 = await startRunner(runnerStarted2, runnerStopped2);
|
|
179
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
180
|
+
getLogger().debug("SSE aborted, shutting down runner");
|
|
181
|
+
runner2.shutdown(true);
|
|
182
|
+
});
|
|
183
|
+
await runnerStarted2.promise;
|
|
184
|
+
stream.writeSSE({ data: runner2.getServerlessInitPacket() });
|
|
185
|
+
await runnerStopped2.promise;
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
app.get("/api/rivet/metadata", async (c) => {
|
|
189
|
+
return c.json({
|
|
190
|
+
// Not actually rivetkit
|
|
191
|
+
runtime: "rivetkit",
|
|
192
|
+
version: "1"
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
if (AUTOSTART_SERVER) {
|
|
196
|
+
serve({
|
|
197
|
+
fetch: app.fetch,
|
|
198
|
+
port: INTERNAL_SERVER_PORT
|
|
199
|
+
});
|
|
200
|
+
getLogger().info(
|
|
201
|
+
`Internal HTTP server listening on port ${INTERNAL_SERVER_PORT}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (AUTOSTART_RUNNER) {
|
|
205
|
+
runner = await startRunner(runnerStarted, runnerStopped);
|
|
206
|
+
} else if (AUTOCONFIGURE_SERVERLESS) {
|
|
207
|
+
await autoConfigureServerless();
|
|
208
|
+
}
|
|
209
|
+
process.on("SIGTERM", async () => {
|
|
210
|
+
getLogger().debug("received SIGTERM, force exiting in 3s");
|
|
211
|
+
await new Promise((res) => setTimeout(res, 3e3));
|
|
212
|
+
process.exit(0);
|
|
213
|
+
});
|
|
214
|
+
process.on("SIGINT", async () => {
|
|
215
|
+
getLogger().debug("received SIGTERM, force exiting in 3s");
|
|
216
|
+
await new Promise((res) => setTimeout(res, 3e3));
|
|
217
|
+
process.exit(0);
|
|
218
|
+
});
|
|
219
|
+
async function autoConfigureServerless() {
|
|
220
|
+
getLogger().info("Configuring serverless");
|
|
221
|
+
const res = await fetch(
|
|
222
|
+
`${RIVET_ENDPOINT}/runner-configs/${RIVET_RUNNER_NAME}?namespace=${RIVET_NAMESPACE}`,
|
|
223
|
+
{
|
|
224
|
+
method: "PUT",
|
|
225
|
+
headers: {
|
|
226
|
+
Authorization: `Bearer ${RIVET_TOKEN}`,
|
|
227
|
+
"Content-Type": "application/json"
|
|
228
|
+
},
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
datacenters: {
|
|
231
|
+
default: {
|
|
232
|
+
serverless: {
|
|
233
|
+
url: `http://localhost:${INTERNAL_SERVER_PORT}/api/rivet`,
|
|
234
|
+
max_runners: 1e4,
|
|
235
|
+
slots_per_runner: 1,
|
|
236
|
+
request_lifespan: 300
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`request failed: ${res.statusText} (${res.status}):
|
|
246
|
+
${await res.text()}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function startRunner(runnerStarted2, runnerStopped2) {
|
|
251
|
+
getLogger().info("Starting runner");
|
|
252
|
+
let runner2;
|
|
253
|
+
const config = {
|
|
254
|
+
logger: getLogger(),
|
|
255
|
+
version: RIVET_RUNNER_VERSION,
|
|
256
|
+
endpoint: RIVET_ENDPOINT,
|
|
257
|
+
token: RIVET_TOKEN,
|
|
258
|
+
namespace: RIVET_NAMESPACE,
|
|
259
|
+
runnerName: RIVET_RUNNER_NAME,
|
|
260
|
+
totalSlots: RIVET_RUNNER_TOTAL_SLOTS,
|
|
261
|
+
prepopulateActorNames: {},
|
|
262
|
+
onConnected: () => {
|
|
263
|
+
runnerStarted2.resolve(runner2);
|
|
264
|
+
},
|
|
265
|
+
onDisconnected: () => {
|
|
266
|
+
},
|
|
267
|
+
onShutdown: () => {
|
|
268
|
+
runnerStopped2.resolve(runner2);
|
|
269
|
+
},
|
|
270
|
+
fetch: async (runner3, actorId, _gatewayId, _requestId, request) => {
|
|
271
|
+
getLogger().info(
|
|
272
|
+
`Fetch called for actor ${actorId}, URL: ${request.url}`
|
|
273
|
+
);
|
|
274
|
+
const url = new URL(request.url);
|
|
275
|
+
if (url.pathname === "/ping") {
|
|
276
|
+
const responseData = {
|
|
277
|
+
actorId,
|
|
278
|
+
status: "ok",
|
|
279
|
+
timestamp: Date.now()
|
|
280
|
+
};
|
|
281
|
+
return new Response(JSON.stringify(responseData), {
|
|
282
|
+
status: 200,
|
|
283
|
+
headers: { "Content-Type": "application/json" }
|
|
284
|
+
});
|
|
285
|
+
} else if (url.pathname === "/sleep") {
|
|
286
|
+
runner3.sleepActor(actorId);
|
|
287
|
+
return new Response("ok", {
|
|
288
|
+
status: 200,
|
|
289
|
+
headers: { "Content-Type": "application/json" }
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return new Response("ok", { status: 200 });
|
|
293
|
+
},
|
|
294
|
+
onActorStart: async (_actorId, _generation, _config) => {
|
|
295
|
+
getLogger().info(
|
|
296
|
+
`Actor ${_actorId} started (generation ${_generation})`
|
|
297
|
+
);
|
|
298
|
+
},
|
|
299
|
+
onActorStop: async (_actorId, _generation) => {
|
|
300
|
+
getLogger().info(
|
|
301
|
+
`Actor ${_actorId} stopped (generation ${_generation})`
|
|
302
|
+
);
|
|
303
|
+
},
|
|
304
|
+
websocket: async (runner3, actorId, ws, _gatewayId, _requestId, _request) => {
|
|
305
|
+
getLogger().info(`WebSocket connected for actor ${actorId}`);
|
|
306
|
+
ws.addEventListener("message", (event) => {
|
|
307
|
+
const data = event.data;
|
|
308
|
+
getLogger().info({
|
|
309
|
+
msg: `WebSocket message from actor ${actorId}`,
|
|
310
|
+
data,
|
|
311
|
+
index: event.rivetMessageIndex
|
|
312
|
+
});
|
|
313
|
+
ws.send(`Echo: ${data}`);
|
|
314
|
+
const websocketId = Buffer.from(
|
|
315
|
+
event.rivetRequestId
|
|
316
|
+
).toString("base64");
|
|
317
|
+
websocketLastMsgIndexes.set(
|
|
318
|
+
websocketId,
|
|
319
|
+
event.rivetMessageIndex
|
|
320
|
+
);
|
|
321
|
+
runner3.sendHibernatableWebSocketMessageAck(
|
|
322
|
+
event.rivetGatewayId,
|
|
323
|
+
event.rivetRequestId,
|
|
324
|
+
event.rivetMessageIndex
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
ws.addEventListener("close", () => {
|
|
328
|
+
getLogger().info(`WebSocket closed for actor ${actorId}`);
|
|
329
|
+
});
|
|
330
|
+
ws.addEventListener("error", (error) => {
|
|
331
|
+
getLogger().error({
|
|
332
|
+
msg: `WebSocket error for actor ${actorId}:`,
|
|
333
|
+
error
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
hibernatableWebSocket: {
|
|
338
|
+
canHibernate() {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
runner2 = new Runner(config);
|
|
344
|
+
await runner2.start();
|
|
345
|
+
getLogger().info("Waiting runner start...");
|
|
346
|
+
await runnerStarted2.promise;
|
|
347
|
+
getLogger().info("Runner started");
|
|
348
|
+
return runner2;
|
|
349
|
+
}
|
|
350
|
+
var index_default = app;
|
|
351
|
+
export {
|
|
352
|
+
index_default as default
|
|
353
|
+
};
|
|
354
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/log.ts"],"sourcesContent":["import { serve } from \"@hono/node-server\";\nimport type { ActorConfig, RunnerConfig } from \"@rivetkit/engine-runner\";\nimport { Runner } from \"@rivetkit/engine-runner\";\nimport { Hono, type Context as HonoContext, type Next } from \"hono\";\nimport { streamSSE } from \"hono/streaming\";\nimport type { Logger } from \"pino\";\nimport type WebSocket from \"ws\";\nimport { getLogger } from \"./log\";\n\nconst INTERNAL_SERVER_PORT = process.env.INTERNAL_SERVER_PORT\n\t? Number(process.env.INTERNAL_SERVER_PORT)\n\t: 5051;\nconst RIVET_NAMESPACE = process.env.RIVET_NAMESPACE ?? \"default\";\nconst RIVET_RUNNER_NAME = process.env.RIVET_RUNNER_NAME ?? \"test-runner\";\nconst RIVET_RUNNER_VERSION = process.env.RIVET_RUNNER_VERSION\n\t? Number(process.env.RIVET_RUNNER_VERSION)\n\t: 1;\nconst RIVET_RUNNER_TOTAL_SLOTS = parseInt(process.env.RIVET_RUNNER_TOTAL_SLOTS ?? \"1\");\nconst RIVET_ENDPOINT = process.env.RIVET_ENDPOINT ?? \"http://127.0.0.1:6420\";\nconst RIVET_TOKEN = process.env.RIVET_TOKEN ?? \"dev\";\nconst AUTOSTART_SERVER = (process.env.AUTOSTART_SERVER ?? \"1\") == \"1\";\nconst AUTOSTART_RUNNER = (process.env.AUTOSTART_RUNNER ?? \"0\") == \"1\";\nconst AUTOCONFIGURE_SERVERLESS = (process.env.AUTOCONFIGURE_SERVERLESS ?? \"1\") == \"1\";\n\nconst runnerStarted = Promise.withResolvers<Runner>();\nconst runnerStopped = Promise.withResolvers<Runner>();\nlet runner: Runner | null = null;\nconst websocketLastMsgIndexes: Map<string, number> = new Map();\n\n// Create internal server\nconst app = new Hono();\n\nfunction loggerMiddleware(logger: Logger) {\n\treturn async (c: HonoContext, next: Next) => {\n\t\tconst method = c.req.method;\n\t\tconst path = c.req.path;\n\t\tconst startTime = Date.now();\n\n\t\tawait next();\n\n\t\tconst duration = Date.now() - startTime;\n\t\tlogger.debug({\n\t\t\tmsg: \"http request\",\n\t\t\tmethod,\n\t\t\tpath,\n\t\t\tstatus: c.res.status,\n\t\t\tdt: `${duration}ms`,\n\t\t\treqSize: c.req.header(\"content-length\"),\n\t\t\tresSize: c.res.headers.get(\"content-length\"),\n\t\t\tuserAgent: c.req.header(\"user-agent\"),\n\t\t});\n\t};\n}\napp.use(\"*\", loggerMiddleware(getLogger()));\n\napp.get(\"/wait-ready\", async (c) => {\n\tconst runner = await runnerStarted.promise;\n\treturn c.json(runner.runnerId);\n});\n\napp.get(\"/has-actor\", async (c) => {\n\tconst actorIdQuery = c.req.query(\"actor\");\n\tconst generationQuery = c.req.query(\"generation\");\n\tconst generation = generationQuery ? Number(generationQuery) : undefined;\n\n\tif (!actorIdQuery || !runner?.hasActor(actorIdQuery, generation)) {\n\t\treturn c.text(\"\", 404);\n\t}\n\treturn c.text(\"ok\");\n});\n\napp.get(\"/health\", (c) => {\n\treturn c.text(\"ok\");\n});\n\napp.get(\"/shutdown\", async (c) => {\n\tawait runner?.shutdown(true);\n\treturn c.text(\"ok\");\n});\n\napp.get(\"/api/rivet/start\", async (c) => {\n\treturn streamSSE(c, async (stream) => {\n\t\tconst runnerStarted = Promise.withResolvers<Runner>();\n\t\tconst runnerStopped = Promise.withResolvers<Runner>();\n\t\tconst runner = await startRunner(runnerStarted, runnerStopped);\n\n\t\tc.req.raw.signal.addEventListener(\"abort\", () => {\n\t\t\tgetLogger().debug(\"SSE aborted, shutting down runner\");\n\t\t\trunner!.shutdown(true);\n\t\t});\n\n\t\tawait runnerStarted.promise;\n\n\t\tstream.writeSSE({ data: runner.getServerlessInitPacket()! });\n\n\t\tawait runnerStopped.promise;\n\t});\n});\n\napp.get(\"/api/rivet/metadata\", async (c) => {\n\treturn c.json({\n\t\t// Not actually rivetkit\n\t\truntime: \"rivetkit\",\n\t\tversion: \"1\",\n\t});\n});\n\nif (AUTOSTART_SERVER) {\n\tserve({\n\t\tfetch: app.fetch,\n\t\tport: INTERNAL_SERVER_PORT,\n\t});\n\tgetLogger().info(\n\t\t`Internal HTTP server listening on port ${INTERNAL_SERVER_PORT}`,\n\t);\n}\n\nif (AUTOSTART_RUNNER) {\n\trunner = await startRunner(runnerStarted, runnerStopped);\n} else if (AUTOCONFIGURE_SERVERLESS) {\n\tawait autoConfigureServerless();\n}\n\nprocess.on(\"SIGTERM\", async () => {\n\tgetLogger().debug(\"received SIGTERM, force exiting in 3s\");\n\n\tawait new Promise(res => setTimeout(res, 3000));\n\n\tprocess.exit(0);\n});\nprocess.on(\"SIGINT\", async () => {\n\tgetLogger().debug(\"received SIGTERM, force exiting in 3s\");\n\n\tawait new Promise(res => setTimeout(res, 3000));\n\n\tprocess.exit(0);\n});\n\nasync function autoConfigureServerless() {\n\tgetLogger().info(\"Configuring serverless\");\n\n\tconst res = await fetch(\n\t\t`${RIVET_ENDPOINT}/runner-configs/${RIVET_RUNNER_NAME}?namespace=${RIVET_NAMESPACE}`,\n\t\t{\n\t\t\tmethod: \"PUT\",\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${RIVET_TOKEN}`,\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tdatacenters: {\n\t\t\t\t\tdefault: {\n\t\t\t\t\t\tserverless: {\n\t\t\t\t\t\t\turl: `http://localhost:${INTERNAL_SERVER_PORT}/api/rivet`,\n\t\t\t\t\t\t\tmax_runners: 10000,\n\t\t\t\t\t\t\tslots_per_runner: 1,\n\t\t\t\t\t\t\trequest_lifespan: 300,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t},\n\t);\n\n\tif (!res.ok) {\n\t\tthrow new Error(\n\t\t\t`request failed: ${res.statusText} (${res.status}):\\n${await res.text()}`,\n\t\t);\n\t}\n}\n\nasync function startRunner(\n\trunnerStarted: PromiseWithResolvers<Runner>,\n\trunnerStopped: PromiseWithResolvers<Runner>,\n): Promise<Runner> {\n\tgetLogger().info(\"Starting runner\");\n\tlet runner: Runner;\n\tconst config: RunnerConfig = {\n\t\tlogger: getLogger(),\n\t\tversion: RIVET_RUNNER_VERSION,\n\t\tendpoint: RIVET_ENDPOINT,\n\t\ttoken: RIVET_TOKEN,\n\t\tnamespace: RIVET_NAMESPACE,\n\t\trunnerName: RIVET_RUNNER_NAME,\n\t\ttotalSlots: RIVET_RUNNER_TOTAL_SLOTS,\n\t\tprepopulateActorNames: {},\n\t\tonConnected: () => {\n\t\t\trunnerStarted.resolve(runner);\n\t\t},\n\t\tonDisconnected: () => { },\n\t\tonShutdown: () => {\n\t\t\trunnerStopped.resolve(runner);\n\t\t},\n\t\tfetch: async (\n\t\t\trunner: Runner,\n\t\t\tactorId: string,\n\t\t\t_gatewayId: ArrayBuffer,\n\t\t\t_requestId: ArrayBuffer,\n\t\t\trequest: Request,\n\t\t) => {\n\t\t\tgetLogger().info(\n\t\t\t\t`Fetch called for actor ${actorId}, URL: ${request.url}`,\n\t\t\t);\n\t\t\tconst url = new URL(request.url);\n\t\t\tif (url.pathname === \"/ping\") {\n\t\t\t\t// Return the actor ID in response\n\t\t\t\tconst responseData = {\n\t\t\t\t\tactorId,\n\t\t\t\t\tstatus: \"ok\",\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t};\n\n\t\t\t\treturn new Response(JSON.stringify(responseData), {\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t});\n\t\t\t} else if (url.pathname === \"/sleep\") {\n\t\t\t\trunner.sleepActor(actorId);\n\n\t\t\t\treturn new Response(\"ok\", {\n\t\t\t\t\tstatus: 200,\n\t\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn new Response(\"ok\", { status: 200 });\n\t\t},\n\t\tonActorStart: async (\n\t\t\t_actorId: string,\n\t\t\t_generation: number,\n\t\t\t_config: ActorConfig,\n\t\t) => {\n\t\t\tgetLogger().info(\n\t\t\t\t`Actor ${_actorId} started (generation ${_generation})`,\n\t\t\t);\n\t\t},\n\t\tonActorStop: async (_actorId: string, _generation: number) => {\n\t\t\tgetLogger().info(\n\t\t\t\t`Actor ${_actorId} stopped (generation ${_generation})`,\n\t\t\t);\n\t\t},\n\t\twebsocket: async (\n\t\t\trunner: Runner,\n\t\t\tactorId: string,\n\t\t\tws: WebSocket,\n\t\t\t_gatewayId: ArrayBuffer,\n\t\t\t_requestId: ArrayBuffer,\n\t\t\t_request: Request,\n\t\t) => {\n\t\t\tgetLogger().info(`WebSocket connected for actor ${actorId}`);\n\n\t\t\t// Echo server - send back any messages received\n\t\t\tws.addEventListener(\"message\", (event) => {\n\t\t\t\tconst data = event.data;\n\t\t\t\tgetLogger().info({\n\t\t\t\t\tmsg: `WebSocket message from actor ${actorId}`,\n\t\t\t\t\tdata,\n\t\t\t\t\tindex: (event as any).rivetMessageIndex,\n\t\t\t\t});\n\n\t\t\t\tws.send(`Echo: ${data}`);\n\n\t\t\t\t// Ack\n\t\t\t\tconst websocketId = Buffer.from(\n\t\t\t\t\t(event as any).rivetRequestId,\n\t\t\t\t).toString(\"base64\");\n\t\t\t\twebsocketLastMsgIndexes.set(\n\t\t\t\t\twebsocketId,\n\t\t\t\t\t(event as any).rivetMessageIndex,\n\t\t\t\t);\n\t\t\t\trunner.sendHibernatableWebSocketMessageAck(\n\t\t\t\t\t(event as any).rivetGatewayId,\n\t\t\t\t\t(event as any).rivetRequestId,\n\t\t\t\t\t(event as any).rivetMessageIndex,\n\t\t\t\t);\n\t\t\t});\n\n\t\t\tws.addEventListener(\"close\", () => {\n\t\t\t\tgetLogger().info(`WebSocket closed for actor ${actorId}`);\n\t\t\t});\n\n\t\t\tws.addEventListener(\"error\", (error) => {\n\t\t\t\tgetLogger().error({\n\t\t\t\t\tmsg: `WebSocket error for actor ${actorId}:`,\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t});\n\t\t},\n\t\thibernatableWebSocket: {\n\t\t\tcanHibernate() {\n\t\t\t\treturn true;\n\t\t\t},\n\t\t},\n\t};\n\n\trunner = new Runner(config);\n\n\t// Start runner\n\tawait runner.start();\n\n\t// Wait for runner to be ready\n\tgetLogger().info(\"Waiting runner start...\");\n\tawait runnerStarted.promise;\n\n\tgetLogger().info(\"Runner started\");\n\n\treturn runner;\n}\n\nexport default app;\n","import { inspect } from \"node:util\";\nimport {\n\ttype Level,\n\ttype LevelWithSilent,\n\ttype Logger,\n\tpino,\n\tstdTimeFunctions,\n} from \"pino\";\n\nexport type { Logger } from \"pino\";\n\nlet baseLogger: Logger | undefined;\n\n/** Cache of child loggers by logger name. */\nconst loggerCache = new Map<string, Logger>();\n\nexport function getPinoLevel(): LevelWithSilent {\n\t// Priority: env > default\n\treturn (process.env[\"LOG_LEVEL\"] || \"warn\")\n\t\t.toString()\n\t\t.toLowerCase() as LevelWithSilent;\n}\n\nexport function getIncludeTarget(): boolean {\n\treturn process.env[\"LOG_TARGET\"] === \"1\";\n}\n\n/**\n * Configure a custom base logger.\n */\nexport function configureBaseLogger(logger: Logger): void {\n\tbaseLogger = logger;\n\tloggerCache.clear();\n}\n\n// TODO: This can be simplified in logfmt.ts\nfunction customWrite(level: string, o: any) {\n\tconst entries: any = {};\n\n\t// Add timestamp if enabled\n\tif (process.env[\"LOG_TIMESTAMP\"] === \"1\" && o.time) {\n\t\tconst date = typeof o.time === \"number\" ? new Date(o.time) : new Date();\n\t\tentries.ts = date;\n\t}\n\n\t// Add level\n\tentries.level = level.toUpperCase();\n\n\t// Add target if present\n\tif (o.target) {\n\t\tentries.target = o.target;\n\t}\n\n\t// Add message\n\tif (o.msg) {\n\t\tentries.msg = o.msg;\n\t}\n\n\t// Add other properties\n\tfor (const [key, value] of Object.entries(o)) {\n\t\tif (\n\t\t\tkey !== \"time\" &&\n\t\t\tkey !== \"level\" &&\n\t\t\tkey !== \"target\" &&\n\t\t\tkey !== \"msg\" &&\n\t\t\tkey !== \"pid\" &&\n\t\t\tkey !== \"hostname\"\n\t\t) {\n\t\t\tentries[key] = value;\n\t\t}\n\t}\n\n\tconst output = inspect(entries, {\n\t\tcompact: true,\n\t\tbreakLength: Infinity,\n\t\tcolors: true,\n\t});\n\tconsole.log(output);\n}\n\n/**\n * Configure the default logger with optional log level.\n */\nexport async function configureDefaultLogger(): Promise<void> {\n\tbaseLogger = pino({\n\t\tlevel: getPinoLevel(),\n\t\tmessageKey: \"msg\",\n\t\t// Do not include pid/hostname in output\n\t\tbase: {},\n\t\t// Keep a string level in the output\n\t\tformatters: {\n\t\t\tlevel(_label: string, number: number) {\n\t\t\t\treturn { level: number };\n\t\t\t},\n\t\t},\n\t\ttimestamp:\n\t\t\tprocess.env[\"LOG_TIMESTAMP\"] === \"1\"\n\t\t\t\t? stdTimeFunctions.epochTime\n\t\t\t\t: false,\n\t\tbrowser: {\n\t\t\twrite: {\n\t\t\t\tfatal: customWrite.bind(null, \"fatal\"),\n\t\t\t\terror: customWrite.bind(null, \"error\"),\n\t\t\t\twarn: customWrite.bind(null, \"warn\"),\n\t\t\t\tinfo: customWrite.bind(null, \"info\"),\n\t\t\t\tdebug: customWrite.bind(null, \"debug\"),\n\t\t\t\ttrace: customWrite.bind(null, \"trace\"),\n\t\t\t},\n\t\t},\n\t\thooks: {\n\t\t\tlogMethod(inputArgs, _method, level) {\n\t\t\t\t// TODO: This is a hack to not implement our own transport target. We can get better perf if we have our own transport target.\n\n\t\t\t\tconst levelMap: Record<number, string> = {\n\t\t\t\t\t10: \"trace\",\n\t\t\t\t\t20: \"debug\",\n\t\t\t\t\t30: \"info\",\n\t\t\t\t\t40: \"warn\",\n\t\t\t\t\t50: \"error\",\n\t\t\t\t\t60: \"fatal\",\n\t\t\t\t};\n\t\t\t\tconst levelName = levelMap[level] || \"info\";\n\t\t\t\tconst time =\n\t\t\t\t\tprocess.env[\"LOG_TIMESTAMP\"] === \"1\"\n\t\t\t\t\t\t? Date.now()\n\t\t\t\t\t\t: undefined;\n\t\t\t\t// TODO: This can be simplified in logfmt.ts\n\t\t\t\tif (inputArgs.length >= 2) {\n\t\t\t\t\tconst [objOrMsg, msg] = inputArgs;\n\t\t\t\t\tif (typeof objOrMsg === \"object\" && objOrMsg !== null) {\n\t\t\t\t\t\tcustomWrite(levelName, { ...objOrMsg, msg, time });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcustomWrite(levelName, { msg: String(objOrMsg), time });\n\t\t\t\t\t}\n\t\t\t\t} else if (inputArgs.length === 1) {\n\t\t\t\t\tconst [objOrMsg] = inputArgs;\n\t\t\t\t\tif (typeof objOrMsg === \"object\" && objOrMsg !== null) {\n\t\t\t\t\t\tcustomWrite(levelName, { ...objOrMsg, time });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcustomWrite(levelName, { msg: String(objOrMsg), time });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t});\n\n\tloggerCache.clear();\n}\n\n/**\n * Get or initialize the base logger.\n */\nexport function getBaseLogger(): Logger {\n\tif (!baseLogger) {\n\t\tconfigureDefaultLogger();\n\t}\n\treturn baseLogger!;\n}\n\n/**\n * Returns a child logger with `target` bound for the given name.\n */\nexport function getLogger(name = \"default\"): Logger {\n\t// Check cache first\n\tconst cached = loggerCache.get(name);\n\tif (cached) {\n\t\treturn cached;\n\t}\n\n\t// Create\n\tconst base = getBaseLogger();\n\n\t// Add target to log if enabled\n\tconst child = getIncludeTarget() ? base.child({ target: name }) : base;\n\n\t// Cache the logger\n\tloggerCache.set(name, child);\n\n\treturn child;\n}\n"],"mappings":"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","names":["runner","runnerStarted","runnerStopped"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rivetkit/engine-test-runner",
|
|
3
|
+
"version": "0.0.0-pr.4600.32b0fc8",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "tsx src/index.ts",
|
|
7
|
+
"build": "tsup src/index.ts",
|
|
8
|
+
"check-types": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@hono/node-server": "^1.19.1",
|
|
12
|
+
"@rivetkit/engine-runner": "0.0.0-pr.4600.32b0fc8",
|
|
13
|
+
"@rivetkit/engine-runner-protocol": "0.0.0-pr.4600.32b0fc8",
|
|
14
|
+
"hono": "^4.7.0",
|
|
15
|
+
"pino": "^9.9.5",
|
|
16
|
+
"ws": "^8.18.3"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.18.1",
|
|
20
|
+
"@types/ws": "^8.18.1",
|
|
21
|
+
"tinybench": "^5.0.1",
|
|
22
|
+
"tsup": "^8.5.0",
|
|
23
|
+
"tsx": "^4.20.5",
|
|
24
|
+
"typescript": "^5.9.2",
|
|
25
|
+
"vitest": "^1.6.1"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import type { ActorConfig, RunnerConfig } from "@rivetkit/engine-runner";
|
|
3
|
+
import { Runner } from "@rivetkit/engine-runner";
|
|
4
|
+
import { Hono, type Context as HonoContext, type Next } from "hono";
|
|
5
|
+
import { streamSSE } from "hono/streaming";
|
|
6
|
+
import type { Logger } from "pino";
|
|
7
|
+
import type WebSocket from "ws";
|
|
8
|
+
import { getLogger } from "./log";
|
|
9
|
+
|
|
10
|
+
const INTERNAL_SERVER_PORT = process.env.INTERNAL_SERVER_PORT
|
|
11
|
+
? Number(process.env.INTERNAL_SERVER_PORT)
|
|
12
|
+
: 5051;
|
|
13
|
+
const RIVET_NAMESPACE = process.env.RIVET_NAMESPACE ?? "default";
|
|
14
|
+
const RIVET_RUNNER_NAME = process.env.RIVET_RUNNER_NAME ?? "test-runner";
|
|
15
|
+
const RIVET_RUNNER_VERSION = process.env.RIVET_RUNNER_VERSION
|
|
16
|
+
? Number(process.env.RIVET_RUNNER_VERSION)
|
|
17
|
+
: 1;
|
|
18
|
+
const RIVET_RUNNER_TOTAL_SLOTS = parseInt(process.env.RIVET_RUNNER_TOTAL_SLOTS ?? "1");
|
|
19
|
+
const RIVET_ENDPOINT = process.env.RIVET_ENDPOINT ?? "http://127.0.0.1:6420";
|
|
20
|
+
const RIVET_TOKEN = process.env.RIVET_TOKEN ?? "dev";
|
|
21
|
+
const AUTOSTART_SERVER = (process.env.AUTOSTART_SERVER ?? "1") == "1";
|
|
22
|
+
const AUTOSTART_RUNNER = (process.env.AUTOSTART_RUNNER ?? "0") == "1";
|
|
23
|
+
const AUTOCONFIGURE_SERVERLESS = (process.env.AUTOCONFIGURE_SERVERLESS ?? "1") == "1";
|
|
24
|
+
|
|
25
|
+
const runnerStarted = Promise.withResolvers<Runner>();
|
|
26
|
+
const runnerStopped = Promise.withResolvers<Runner>();
|
|
27
|
+
let runner: Runner | null = null;
|
|
28
|
+
const websocketLastMsgIndexes: Map<string, number> = new Map();
|
|
29
|
+
|
|
30
|
+
// Create internal server
|
|
31
|
+
const app = new Hono();
|
|
32
|
+
|
|
33
|
+
function loggerMiddleware(logger: Logger) {
|
|
34
|
+
return async (c: HonoContext, next: Next) => {
|
|
35
|
+
const method = c.req.method;
|
|
36
|
+
const path = c.req.path;
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
|
|
39
|
+
await next();
|
|
40
|
+
|
|
41
|
+
const duration = Date.now() - startTime;
|
|
42
|
+
logger.debug({
|
|
43
|
+
msg: "http request",
|
|
44
|
+
method,
|
|
45
|
+
path,
|
|
46
|
+
status: c.res.status,
|
|
47
|
+
dt: `${duration}ms`,
|
|
48
|
+
reqSize: c.req.header("content-length"),
|
|
49
|
+
resSize: c.res.headers.get("content-length"),
|
|
50
|
+
userAgent: c.req.header("user-agent"),
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
app.use("*", loggerMiddleware(getLogger()));
|
|
55
|
+
|
|
56
|
+
app.get("/wait-ready", async (c) => {
|
|
57
|
+
const runner = await runnerStarted.promise;
|
|
58
|
+
return c.json(runner.runnerId);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
app.get("/has-actor", async (c) => {
|
|
62
|
+
const actorIdQuery = c.req.query("actor");
|
|
63
|
+
const generationQuery = c.req.query("generation");
|
|
64
|
+
const generation = generationQuery ? Number(generationQuery) : undefined;
|
|
65
|
+
|
|
66
|
+
if (!actorIdQuery || !runner?.hasActor(actorIdQuery, generation)) {
|
|
67
|
+
return c.text("", 404);
|
|
68
|
+
}
|
|
69
|
+
return c.text("ok");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
app.get("/health", (c) => {
|
|
73
|
+
return c.text("ok");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
app.get("/shutdown", async (c) => {
|
|
77
|
+
await runner?.shutdown(true);
|
|
78
|
+
return c.text("ok");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
app.get("/api/rivet/start", async (c) => {
|
|
82
|
+
return streamSSE(c, async (stream) => {
|
|
83
|
+
const runnerStarted = Promise.withResolvers<Runner>();
|
|
84
|
+
const runnerStopped = Promise.withResolvers<Runner>();
|
|
85
|
+
const runner = await startRunner(runnerStarted, runnerStopped);
|
|
86
|
+
|
|
87
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
88
|
+
getLogger().debug("SSE aborted, shutting down runner");
|
|
89
|
+
runner!.shutdown(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await runnerStarted.promise;
|
|
93
|
+
|
|
94
|
+
stream.writeSSE({ data: runner.getServerlessInitPacket()! });
|
|
95
|
+
|
|
96
|
+
await runnerStopped.promise;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
app.get("/api/rivet/metadata", async (c) => {
|
|
101
|
+
return c.json({
|
|
102
|
+
// Not actually rivetkit
|
|
103
|
+
runtime: "rivetkit",
|
|
104
|
+
version: "1",
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (AUTOSTART_SERVER) {
|
|
109
|
+
serve({
|
|
110
|
+
fetch: app.fetch,
|
|
111
|
+
port: INTERNAL_SERVER_PORT,
|
|
112
|
+
});
|
|
113
|
+
getLogger().info(
|
|
114
|
+
`Internal HTTP server listening on port ${INTERNAL_SERVER_PORT}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (AUTOSTART_RUNNER) {
|
|
119
|
+
runner = await startRunner(runnerStarted, runnerStopped);
|
|
120
|
+
} else if (AUTOCONFIGURE_SERVERLESS) {
|
|
121
|
+
await autoConfigureServerless();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
process.on("SIGTERM", async () => {
|
|
125
|
+
getLogger().debug("received SIGTERM, force exiting in 3s");
|
|
126
|
+
|
|
127
|
+
await new Promise(res => setTimeout(res, 3000));
|
|
128
|
+
|
|
129
|
+
process.exit(0);
|
|
130
|
+
});
|
|
131
|
+
process.on("SIGINT", async () => {
|
|
132
|
+
getLogger().debug("received SIGTERM, force exiting in 3s");
|
|
133
|
+
|
|
134
|
+
await new Promise(res => setTimeout(res, 3000));
|
|
135
|
+
|
|
136
|
+
process.exit(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
async function autoConfigureServerless() {
|
|
140
|
+
getLogger().info("Configuring serverless");
|
|
141
|
+
|
|
142
|
+
const res = await fetch(
|
|
143
|
+
`${RIVET_ENDPOINT}/runner-configs/${RIVET_RUNNER_NAME}?namespace=${RIVET_NAMESPACE}`,
|
|
144
|
+
{
|
|
145
|
+
method: "PUT",
|
|
146
|
+
headers: {
|
|
147
|
+
Authorization: `Bearer ${RIVET_TOKEN}`,
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
datacenters: {
|
|
152
|
+
default: {
|
|
153
|
+
serverless: {
|
|
154
|
+
url: `http://localhost:${INTERNAL_SERVER_PORT}/api/rivet`,
|
|
155
|
+
max_runners: 10000,
|
|
156
|
+
slots_per_runner: 1,
|
|
157
|
+
request_lifespan: 300,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`request failed: ${res.statusText} (${res.status}):\n${await res.text()}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function startRunner(
|
|
173
|
+
runnerStarted: PromiseWithResolvers<Runner>,
|
|
174
|
+
runnerStopped: PromiseWithResolvers<Runner>,
|
|
175
|
+
): Promise<Runner> {
|
|
176
|
+
getLogger().info("Starting runner");
|
|
177
|
+
let runner: Runner;
|
|
178
|
+
const config: RunnerConfig = {
|
|
179
|
+
logger: getLogger(),
|
|
180
|
+
version: RIVET_RUNNER_VERSION,
|
|
181
|
+
endpoint: RIVET_ENDPOINT,
|
|
182
|
+
token: RIVET_TOKEN,
|
|
183
|
+
namespace: RIVET_NAMESPACE,
|
|
184
|
+
runnerName: RIVET_RUNNER_NAME,
|
|
185
|
+
totalSlots: RIVET_RUNNER_TOTAL_SLOTS,
|
|
186
|
+
prepopulateActorNames: {},
|
|
187
|
+
onConnected: () => {
|
|
188
|
+
runnerStarted.resolve(runner);
|
|
189
|
+
},
|
|
190
|
+
onDisconnected: () => { },
|
|
191
|
+
onShutdown: () => {
|
|
192
|
+
runnerStopped.resolve(runner);
|
|
193
|
+
},
|
|
194
|
+
fetch: async (
|
|
195
|
+
runner: Runner,
|
|
196
|
+
actorId: string,
|
|
197
|
+
_gatewayId: ArrayBuffer,
|
|
198
|
+
_requestId: ArrayBuffer,
|
|
199
|
+
request: Request,
|
|
200
|
+
) => {
|
|
201
|
+
getLogger().info(
|
|
202
|
+
`Fetch called for actor ${actorId}, URL: ${request.url}`,
|
|
203
|
+
);
|
|
204
|
+
const url = new URL(request.url);
|
|
205
|
+
if (url.pathname === "/ping") {
|
|
206
|
+
// Return the actor ID in response
|
|
207
|
+
const responseData = {
|
|
208
|
+
actorId,
|
|
209
|
+
status: "ok",
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return new Response(JSON.stringify(responseData), {
|
|
214
|
+
status: 200,
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
});
|
|
217
|
+
} else if (url.pathname === "/sleep") {
|
|
218
|
+
runner.sleepActor(actorId);
|
|
219
|
+
|
|
220
|
+
return new Response("ok", {
|
|
221
|
+
status: 200,
|
|
222
|
+
headers: { "Content-Type": "application/json" },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return new Response("ok", { status: 200 });
|
|
227
|
+
},
|
|
228
|
+
onActorStart: async (
|
|
229
|
+
_actorId: string,
|
|
230
|
+
_generation: number,
|
|
231
|
+
_config: ActorConfig,
|
|
232
|
+
) => {
|
|
233
|
+
getLogger().info(
|
|
234
|
+
`Actor ${_actorId} started (generation ${_generation})`,
|
|
235
|
+
);
|
|
236
|
+
},
|
|
237
|
+
onActorStop: async (_actorId: string, _generation: number) => {
|
|
238
|
+
getLogger().info(
|
|
239
|
+
`Actor ${_actorId} stopped (generation ${_generation})`,
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
websocket: async (
|
|
243
|
+
runner: Runner,
|
|
244
|
+
actorId: string,
|
|
245
|
+
ws: WebSocket,
|
|
246
|
+
_gatewayId: ArrayBuffer,
|
|
247
|
+
_requestId: ArrayBuffer,
|
|
248
|
+
_request: Request,
|
|
249
|
+
) => {
|
|
250
|
+
getLogger().info(`WebSocket connected for actor ${actorId}`);
|
|
251
|
+
|
|
252
|
+
// Echo server - send back any messages received
|
|
253
|
+
ws.addEventListener("message", (event) => {
|
|
254
|
+
const data = event.data;
|
|
255
|
+
getLogger().info({
|
|
256
|
+
msg: `WebSocket message from actor ${actorId}`,
|
|
257
|
+
data,
|
|
258
|
+
index: (event as any).rivetMessageIndex,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
ws.send(`Echo: ${data}`);
|
|
262
|
+
|
|
263
|
+
// Ack
|
|
264
|
+
const websocketId = Buffer.from(
|
|
265
|
+
(event as any).rivetRequestId,
|
|
266
|
+
).toString("base64");
|
|
267
|
+
websocketLastMsgIndexes.set(
|
|
268
|
+
websocketId,
|
|
269
|
+
(event as any).rivetMessageIndex,
|
|
270
|
+
);
|
|
271
|
+
runner.sendHibernatableWebSocketMessageAck(
|
|
272
|
+
(event as any).rivetGatewayId,
|
|
273
|
+
(event as any).rivetRequestId,
|
|
274
|
+
(event as any).rivetMessageIndex,
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
ws.addEventListener("close", () => {
|
|
279
|
+
getLogger().info(`WebSocket closed for actor ${actorId}`);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
ws.addEventListener("error", (error) => {
|
|
283
|
+
getLogger().error({
|
|
284
|
+
msg: `WebSocket error for actor ${actorId}:`,
|
|
285
|
+
error,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
hibernatableWebSocket: {
|
|
290
|
+
canHibernate() {
|
|
291
|
+
return true;
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
runner = new Runner(config);
|
|
297
|
+
|
|
298
|
+
// Start runner
|
|
299
|
+
await runner.start();
|
|
300
|
+
|
|
301
|
+
// Wait for runner to be ready
|
|
302
|
+
getLogger().info("Waiting runner start...");
|
|
303
|
+
await runnerStarted.promise;
|
|
304
|
+
|
|
305
|
+
getLogger().info("Runner started");
|
|
306
|
+
|
|
307
|
+
return runner;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default app;
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { inspect } from "node:util";
|
|
2
|
+
import {
|
|
3
|
+
type Level,
|
|
4
|
+
type LevelWithSilent,
|
|
5
|
+
type Logger,
|
|
6
|
+
pino,
|
|
7
|
+
stdTimeFunctions,
|
|
8
|
+
} from "pino";
|
|
9
|
+
|
|
10
|
+
export type { Logger } from "pino";
|
|
11
|
+
|
|
12
|
+
let baseLogger: Logger | undefined;
|
|
13
|
+
|
|
14
|
+
/** Cache of child loggers by logger name. */
|
|
15
|
+
const loggerCache = new Map<string, Logger>();
|
|
16
|
+
|
|
17
|
+
export function getPinoLevel(): LevelWithSilent {
|
|
18
|
+
// Priority: env > default
|
|
19
|
+
return (process.env["LOG_LEVEL"] || "warn")
|
|
20
|
+
.toString()
|
|
21
|
+
.toLowerCase() as LevelWithSilent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getIncludeTarget(): boolean {
|
|
25
|
+
return process.env["LOG_TARGET"] === "1";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configure a custom base logger.
|
|
30
|
+
*/
|
|
31
|
+
export function configureBaseLogger(logger: Logger): void {
|
|
32
|
+
baseLogger = logger;
|
|
33
|
+
loggerCache.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// TODO: This can be simplified in logfmt.ts
|
|
37
|
+
function customWrite(level: string, o: any) {
|
|
38
|
+
const entries: any = {};
|
|
39
|
+
|
|
40
|
+
// Add timestamp if enabled
|
|
41
|
+
if (process.env["LOG_TIMESTAMP"] === "1" && o.time) {
|
|
42
|
+
const date = typeof o.time === "number" ? new Date(o.time) : new Date();
|
|
43
|
+
entries.ts = date;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add level
|
|
47
|
+
entries.level = level.toUpperCase();
|
|
48
|
+
|
|
49
|
+
// Add target if present
|
|
50
|
+
if (o.target) {
|
|
51
|
+
entries.target = o.target;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Add message
|
|
55
|
+
if (o.msg) {
|
|
56
|
+
entries.msg = o.msg;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add other properties
|
|
60
|
+
for (const [key, value] of Object.entries(o)) {
|
|
61
|
+
if (
|
|
62
|
+
key !== "time" &&
|
|
63
|
+
key !== "level" &&
|
|
64
|
+
key !== "target" &&
|
|
65
|
+
key !== "msg" &&
|
|
66
|
+
key !== "pid" &&
|
|
67
|
+
key !== "hostname"
|
|
68
|
+
) {
|
|
69
|
+
entries[key] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const output = inspect(entries, {
|
|
74
|
+
compact: true,
|
|
75
|
+
breakLength: Infinity,
|
|
76
|
+
colors: true,
|
|
77
|
+
});
|
|
78
|
+
console.log(output);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Configure the default logger with optional log level.
|
|
83
|
+
*/
|
|
84
|
+
export async function configureDefaultLogger(): Promise<void> {
|
|
85
|
+
baseLogger = pino({
|
|
86
|
+
level: getPinoLevel(),
|
|
87
|
+
messageKey: "msg",
|
|
88
|
+
// Do not include pid/hostname in output
|
|
89
|
+
base: {},
|
|
90
|
+
// Keep a string level in the output
|
|
91
|
+
formatters: {
|
|
92
|
+
level(_label: string, number: number) {
|
|
93
|
+
return { level: number };
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
timestamp:
|
|
97
|
+
process.env["LOG_TIMESTAMP"] === "1"
|
|
98
|
+
? stdTimeFunctions.epochTime
|
|
99
|
+
: false,
|
|
100
|
+
browser: {
|
|
101
|
+
write: {
|
|
102
|
+
fatal: customWrite.bind(null, "fatal"),
|
|
103
|
+
error: customWrite.bind(null, "error"),
|
|
104
|
+
warn: customWrite.bind(null, "warn"),
|
|
105
|
+
info: customWrite.bind(null, "info"),
|
|
106
|
+
debug: customWrite.bind(null, "debug"),
|
|
107
|
+
trace: customWrite.bind(null, "trace"),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
hooks: {
|
|
111
|
+
logMethod(inputArgs, _method, level) {
|
|
112
|
+
// TODO: This is a hack to not implement our own transport target. We can get better perf if we have our own transport target.
|
|
113
|
+
|
|
114
|
+
const levelMap: Record<number, string> = {
|
|
115
|
+
10: "trace",
|
|
116
|
+
20: "debug",
|
|
117
|
+
30: "info",
|
|
118
|
+
40: "warn",
|
|
119
|
+
50: "error",
|
|
120
|
+
60: "fatal",
|
|
121
|
+
};
|
|
122
|
+
const levelName = levelMap[level] || "info";
|
|
123
|
+
const time =
|
|
124
|
+
process.env["LOG_TIMESTAMP"] === "1"
|
|
125
|
+
? Date.now()
|
|
126
|
+
: undefined;
|
|
127
|
+
// TODO: This can be simplified in logfmt.ts
|
|
128
|
+
if (inputArgs.length >= 2) {
|
|
129
|
+
const [objOrMsg, msg] = inputArgs;
|
|
130
|
+
if (typeof objOrMsg === "object" && objOrMsg !== null) {
|
|
131
|
+
customWrite(levelName, { ...objOrMsg, msg, time });
|
|
132
|
+
} else {
|
|
133
|
+
customWrite(levelName, { msg: String(objOrMsg), time });
|
|
134
|
+
}
|
|
135
|
+
} else if (inputArgs.length === 1) {
|
|
136
|
+
const [objOrMsg] = inputArgs;
|
|
137
|
+
if (typeof objOrMsg === "object" && objOrMsg !== null) {
|
|
138
|
+
customWrite(levelName, { ...objOrMsg, time });
|
|
139
|
+
} else {
|
|
140
|
+
customWrite(levelName, { msg: String(objOrMsg), time });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
loggerCache.clear();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get or initialize the base logger.
|
|
152
|
+
*/
|
|
153
|
+
export function getBaseLogger(): Logger {
|
|
154
|
+
if (!baseLogger) {
|
|
155
|
+
configureDefaultLogger();
|
|
156
|
+
}
|
|
157
|
+
return baseLogger!;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Returns a child logger with `target` bound for the given name.
|
|
162
|
+
*/
|
|
163
|
+
export function getLogger(name = "default"): Logger {
|
|
164
|
+
// Check cache first
|
|
165
|
+
const cached = loggerCache.get(name);
|
|
166
|
+
if (cached) {
|
|
167
|
+
return cached;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Create
|
|
171
|
+
const base = getBaseLogger();
|
|
172
|
+
|
|
173
|
+
// Add target to log if enabled
|
|
174
|
+
const child = getIncludeTarget() ? base.child({ target: name }) : base;
|
|
175
|
+
|
|
176
|
+
// Cache the logger
|
|
177
|
+
loggerCache.set(name, child);
|
|
178
|
+
|
|
179
|
+
return child;
|
|
180
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
package/turbo.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
import defaultConfig from "../../../../vitest.base.ts";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
...defaultConfig,
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
"@": resolve(__dirname, "./src"),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
test: {
|
|
13
|
+
...defaultConfig.test,
|
|
14
|
+
include: ["tests/**/*.test.ts"],
|
|
15
|
+
},
|
|
16
|
+
});
|