@rivetkit/engine-test-runner 0.0.0-pr.4600.8dee603

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.
@@ -0,0 +1,17 @@
1
+
2
+ > @rivetkit/engine-test-runner@2.2.1 build /home/runner/work/rivet/rivet/engine/sdks/typescript/test-runner
3
+ > tsup src/index.ts
4
+
5
+ CLI Building entry: src/index.ts
6
+ CLI Using tsconfig: tsconfig.json
7
+ CLI tsup v8.5.1
8
+ CLI Using tsup config: /home/runner/work/rivet/rivet/engine/sdks/typescript/test-runner/tsup.config.ts
9
+ CLI Target: node16
10
+ CLI Cleaning output folder
11
+ ESM Build start
12
+ ESM dist/index.js 10.71 KB
13
+ ESM dist/index.js.map 20.96 KB
14
+ ESM ⚡️ Build success in 9ms
15
+ DTS Build start
16
+ DTS ⚡️ Build success in 741ms
17
+ DTS dist/index.d.ts 175.00 B
@@ -0,0 +1,6 @@
1
+ import * as hono_types from 'hono/types';
2
+ import { Hono } from 'hono';
3
+
4
+ declare const app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
5
+
6
+ export { app as default };
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.8dee603",
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.8dee603",
13
+ "@rivetkit/engine-runner-protocol": "0.0.0-pr.4600.8dee603",
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
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "paths": {
5
+ "@/*": ["./src/*"]
6
+ },
7
+ "lib": ["ESNext", "DOM"]
8
+ },
9
+ "include": ["src/**/*", "tests/**/*", "benches/**/*"],
10
+ "exclude": ["node_modules"]
11
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "tsup";
2
+ import defaultConfig from "../../../../tsup.base.ts";
3
+
4
+ export default defineConfig({
5
+ ...defaultConfig,
6
+ format: "esm",
7
+ });
package/turbo.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "extends": ["//"]
4
+ }
@@ -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
+ });