spooder 4.2.10 → 4.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,9 +17,6 @@ It consists of two components, the `CLI` and the `API`.
17
17
  - The `CLI` is responsible for keeping the server process running, applying updates in response to source control changes, and automatically raising issues on GitHub via the canary feature.
18
18
  - The `API` provides a minimal building-block style API for developing servers, with a focus on simplicity and performance.
19
19
 
20
- > [!WARNING]
21
- > `spooder` is stable but still in active development. Backwards compatibility between versions is not guaranteed and breaking changes may be introduced. Consider pinning a specific version in your `package.json`.
22
-
23
20
  # CLI
24
21
 
25
22
  The `CLI` component of `spooder` is a global command-line tool for running server processes.
@@ -49,6 +46,7 @@ The `CLI` component of `spooder` is a global command-line tool for running serve
49
46
  - [`server.handle(status_code: number, handler: RequestHandler)`](#api-routing-server-handle)
50
47
  - [`server.default(handler: DefaultHandler)`](#api-routing-server-default)
51
48
  - [`server.error(handler: ErrorHandler)`](#api-routing-server-error)
49
+ - [`server.on_slow_request(callback: SlowRequestCallback, threshold: number)`](#api-routing-server-on-slow-request)
52
50
  - [API > Routing > Directory Serving](#api-routing-directory-serving)
53
51
  - [`server.dir(path: string, dir: string, handler?: DirHandler, method: HTTP_METHODS)`](#api-routing-server-dir)
54
52
  - [API > Routing > Server-Sent Events](#api-routing-server-sent-events)
@@ -69,6 +67,9 @@ The `CLI` component of `spooder` is a global command-line tool for running serve
69
67
  - [API > State Management](#api-state-management)
70
68
  - [`set_cookie(res: Response, name: string, value: string, options?: CookieOptions)`](#api-state-management-set-cookie)
71
69
  - [`get_cookies(source: Request | Response): Record<string, string>`](#api-state-management-get-cookies)
70
+ - [API > Database Schema](#api-database-schema)
71
+ - [`db_update_schema_sqlite(db: Database, schema: string): Promise<void>`](#api-database-schema-db-update-schema-sqlite)
72
+ - [`db_init_schema_sqlite(db_path: string, schema: string): Promise<Database>`](#api-database-schema-db-init-schema-sqlite)
72
73
 
73
74
  # Installation
74
75
 
@@ -663,6 +664,27 @@ server.error((err, req, url) => {
663
664
  });
664
665
  ```
665
666
 
667
+ <a id="api-routing-server-on-slow-request"></a>
668
+ ### 🔧 `server.on_slow_request(callback: SlowRequestCallback, threshold: number)`
669
+
670
+ `server.on_slow_request` can be used to register a callback for requests that take an undesirable amount of time to process.
671
+
672
+ By default requests that take longer than `1000ms` to process will trigger the callback, but this can be adjusted by providing a custom threshold.
673
+
674
+ > [!IMPORTANT]
675
+ > If your canary reports to a public repository, be cautious about directly including the `req` object in the callback. This can lead to sensitive information being leaked.
676
+
677
+ ```ts
678
+ server.on_slow_request(async (req, time) => {
679
+ // avoid `time` in the title to avoid canary spam
680
+ // see caution() API for information
681
+ await caution('Slow request warning', { req, time });
682
+ }, 500);
683
+ ```
684
+
685
+ > [!NOTE]
686
+ > The callback is not awaited internally, so you can use `async/await` freely without blocking the server/request.
687
+
666
688
  <a id="api-routing-directory-serving"></a>
667
689
  ## API > Routing > Directory Serving
668
690
 
@@ -1217,6 +1239,110 @@ const cookies = get_cookies(req, true);
1217
1239
  { my_test_cookie: 'my cookie value' }
1218
1240
  ```
1219
1241
 
1242
+ <a id="api-database-schema"></a>
1243
+ ## API > Database Schema
1244
+
1245
+ `spooder` provides a straightforward API to manage database schema in revisions through source control.
1246
+
1247
+ > [!NOTE]
1248
+ > Currently, only SQLite is supported. This may be expanded once Bun supports more database drivers.
1249
+
1250
+ <a id="api-database-schema-db-update-schema-sqlite"></a>
1251
+ ### 🔧 `db_update_schema_sqlite(db: Database, schema: string): Promise<void>`
1252
+
1253
+ `db_update_schema_sqlite` takes a [`Database`](https://bun.sh/docs/api/sqlite) instance and a schema directory.
1254
+
1255
+ The schema directory is expected to contain an SQL file for each table in the database with the file name matching the name of the table.
1256
+
1257
+ > [!NOTE]
1258
+ > Files without the `.sql` extension (case-insensitive) will be ignored.
1259
+
1260
+ ```
1261
+ - database.sqlite
1262
+ - schema/
1263
+ - users.sql
1264
+ - posts.sql
1265
+ - comments.sql
1266
+ ```
1267
+
1268
+ ```ts
1269
+ import { db_update_schema_sqlite } from 'spooder';
1270
+ import { Database } from 'bun:sqlite';
1271
+
1272
+ const db = new Database('./database.sqlite');
1273
+ await db_update_schema_sqlite(db, './schema');
1274
+ ```
1275
+
1276
+ Each of the SQL files should contain all of the revisions for the table, with the first revision being table creation and subsequent revisions being table modifications.
1277
+
1278
+ ```sql
1279
+ -- [1] Table creation.
1280
+ CREATE TABLE users (
1281
+ id INTEGER PRIMARY KEY,
1282
+ username TEXT NOT NULL,
1283
+ password TEXT NOT NULL
1284
+ );
1285
+
1286
+ -- [2] Add email column.
1287
+ ALTER TABLE users ADD COLUMN email TEXT;
1288
+
1289
+ -- [3] Cleanup invalid usernames.
1290
+ DELETE FROM users WHERE username = 'admin';
1291
+ DELETE FROM users WHERE username = 'root';
1292
+ ```
1293
+
1294
+ Each revision should be clearly marked with a comment containing the revision number in square brackets. Anything proceeding the revision number is treated as a comment and ignored.
1295
+
1296
+ >[!NOTE]
1297
+ > The exact revision header syntax is `^--\s*\[(\d+)\]`.
1298
+
1299
+ Everything following a revision header is considered part of that revision until the next revision header or the end of the file, allowing for multiple SQL statements to be included in a single revision.
1300
+
1301
+ When calling `db_update_schema_sqlite`, unapplied revisions will be applied in ascending order (regardless of order within the file) until the schema is up-to-date. Schema revisions are tracked in a table called `db_schema` which is created automatically if it does not exist with the following schema.
1302
+
1303
+ ```sql
1304
+ CREATE TABLE db_schema (
1305
+ db_schema_table_name TEXT PRIMARY KEY,
1306
+ db_schema_version INTEGER
1307
+ );
1308
+ ```
1309
+
1310
+ >[!IMPORTANT]
1311
+ > The entire process is transactional. If an error occurs during the application of **any** revision for **any** table, the entire process will be rolled back and the database will be left in the state it was before the update was attempted.
1312
+
1313
+ >[!IMPORTANT]
1314
+ > `db_update_schema_sqlite` will throw an error if the revisions cannot be parsed or applied for any reason. It is important you catch and handle appropriately.
1315
+
1316
+ ```ts
1317
+ try {
1318
+ const db = new Database('./database.sqlite');
1319
+ await db_update_schema_sqlite(db, './schema');
1320
+ } catch (e) {
1321
+ // panic (crash) or gracefully continue, etc.
1322
+ await panic(e);
1323
+ }
1324
+ ```
1325
+
1326
+ <a id="api-database-schema-db-init-schema-sqlite"></a>
1327
+ ### 🔧 `db_init_schema_sqlite(db_path: string, schema: string): Promise<Database>`
1328
+
1329
+ `db_init_schema_sqlite` exists as a convenience function to create a new database and apply the schema in one step.
1330
+
1331
+ ```ts
1332
+ import { db_init_schema_sqlite } from 'spooder';
1333
+ const db = await db_init_schema_sqlite('./database.sqlite', './schema');
1334
+ ```
1335
+
1336
+ The above is equivalent to the following.
1337
+
1338
+ ```ts
1339
+ import { db_update_schema_sqlite } from 'spooder';
1340
+ import { Database } from 'bun:sqlite';
1341
+
1342
+ const db = new Database('./database.sqlite', { create: true });
1343
+ await db_update_schema_sqlite(db, './schema');
1344
+ ```
1345
+
1220
1346
  ## Legal
1221
1347
  This software is provided as-is with no warranty or guarantee. The authors of this project are not responsible or liable for any problems caused by using this software or any part thereof. Use of this software does not entitle you to any support or assistance from the authors of this project.
1222
1348
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "4.2.10",
4
+ "version": "4.2.11",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/api.ts",
package/src/api.ts CHANGED
@@ -5,6 +5,7 @@ import fs from 'node:fs/promises';
5
5
  import { log } from './utils';
6
6
  import crypto from 'crypto';
7
7
  import { Blob } from 'node:buffer';
8
+ import { Database } from 'bun:sqlite';
8
9
 
9
10
  export const HTTP_STATUS_CODE = http.STATUS_CODES;
10
11
 
@@ -76,9 +77,9 @@ async function handle_error(prefix: string, err_message_or_obj: string | object,
76
77
  }
77
78
 
78
79
  if (process.env.SPOODER_ENV === 'dev') {
79
- log('[dev] dispatch_report %s', prefix + error_message);
80
- log('[dev] without --dev, this would raise a canary report');
81
- log('[dev] %o', final_err);
80
+ log('[{dev}] dispatch_report %s', prefix + error_message);
81
+ log('[{dev}] without {--dev}, this would raise a canary report');
82
+ log('[{dev}] %o', final_err);
82
83
  } else {
83
84
  await dispatch_report(prefix + error_message, final_err);
84
85
  }
@@ -196,6 +197,97 @@ export async function generate_hash_subs(length = 7, prefix = 'hash='): Promise<
196
197
  return hash_map;
197
198
  }
198
199
 
200
+ type Row_DBSchema = { db_schema_table_name: string, db_schema_version: number };
201
+
202
+ export async function db_update_schema_sqlite(db: Database, schema_dir: string) {
203
+ log('[{db}] updating database schema for {%s}', db.filename);
204
+
205
+ const schema_versions = new Map();
206
+
207
+ try {
208
+ const query = db.query('SELECT db_schema_table_name, db_schema_version FROM db_schema');
209
+ for (const row of query.all() as Array<Row_DBSchema>)
210
+ schema_versions.set(row.db_schema_table_name, row.db_schema_version);
211
+ } catch (e) {
212
+ log('[{db}] creating {db_schema} table');
213
+ db.run('CREATE TABLE db_schema (db_schema_table_name TEXT PRIMARY KEY, db_schema_version INTEGER)');
214
+ }
215
+
216
+ db.transaction(async () => {
217
+ const update_schema_query = db.prepare(`
218
+ INSERT INTO db_schema (db_schema_version, db_schema_table_name) VALUES (?1, ?2)
219
+ ON CONFLICT(db_schema_table_name) DO UPDATE SET db_schema_version = EXCLUDED.db_schema_version
220
+ `);
221
+
222
+ const schema_files = await fs.readdir(schema_dir);
223
+ for (const schema_file of schema_files) {
224
+ const schema_file_lower = schema_file.toLowerCase();
225
+ if (!schema_file_lower.endsWith('.sql'))
226
+ continue;
227
+
228
+ log('[{db}] parsing schema file {%s}', schema_file_lower);
229
+
230
+ const schema_name = path.basename(schema_file_lower, '.sql');
231
+ const schema_path = path.join(schema_dir, schema_file);
232
+ const schema = await fs.readFile(schema_path, 'utf8');
233
+
234
+ const revisions = new Map();
235
+ let current_rev_id = 0;
236
+ let current_rev = '';
237
+
238
+ for (const line of schema.split(/\r?\n/)) {
239
+ const rev_start = line.match(/^--\s*\[(\d+)\]/);
240
+ if (rev_start !== null) {
241
+ // New chunk definition detected, store the current chunk and start a new one.
242
+ if (current_rev_id > 0) {
243
+ revisions.set(current_rev_id, current_rev);
244
+ current_rev = '';
245
+ }
246
+
247
+ const rev_number = parseInt(rev_start[1]);
248
+ if (isNaN(rev_number) || rev_number < 1)
249
+ throw new Error(rev_number + ' is not a valid revision number in ' + schema_file_lower);
250
+ current_rev_id = rev_number;
251
+ } else {
252
+ // Append to existing revision.
253
+ current_rev += line + '\n';
254
+ }
255
+ }
256
+
257
+ // There may be something left in current_chunk once we reach end of the file.
258
+ if (current_rev_id > 0)
259
+ revisions.set(current_rev_id, current_rev);
260
+
261
+ if (revisions.size === 0) {
262
+ log('[{db}] {%s} contains no valid revisions', schema_file);
263
+ continue;
264
+ }
265
+
266
+ const current_schema_version = schema_versions.get(schema_name) ?? 0;
267
+ const chunk_keys = Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b);
268
+
269
+ let newest_schema_version = current_schema_version;
270
+ for (const rev_id of chunk_keys) {
271
+ const revision = revisions.get(rev_id);
272
+ log('[{db}] applying revision {%d} to {%s}', rev_id, schema_name);
273
+ db.transaction(() => db.run(revision))();
274
+ newest_schema_version = rev_id;
275
+ }
276
+
277
+ if (newest_schema_version > current_schema_version) {
278
+ log('[{db}] updated table {%s} to revision {%d}', schema_name, newest_schema_version);
279
+ update_schema_query.run(newest_schema_version, schema_name);
280
+ }
281
+ }
282
+ })();
283
+ }
284
+
285
+ export async function db_init_schema_sqlite(db_path: string, schema_dir: string): Promise<Database> {
286
+ const db = new Database(db_path, { create: true });
287
+ await db_update_schema_sqlite(db, schema_dir);
288
+ return db;
289
+ }
290
+
199
291
  type CookieOptions = {
200
292
  same_site?: 'Strict' | 'Lax' | 'None',
201
293
  secure?: boolean,
@@ -251,7 +343,6 @@ export function get_cookies(source: Request | Response, decode: boolean = false)
251
343
  export function apply_range(file: BunFile, request: Request): BunFile {
252
344
  const range_header = request.headers.get('range');
253
345
  if (range_header !== null) {
254
- console.log(range_header);
255
346
  const regex = /bytes=(\d*)-(\d*)/;
256
347
  const match = range_header.match(regex);
257
348
 
@@ -348,13 +439,21 @@ function format_query_parameters(search_params: URLSearchParams): string {
348
439
  for (let [key, value] of search_params)
349
440
  result_parts.push(`${key}: ${value}`);
350
441
 
351
- return '{ ' + result_parts.join(', ') + ' }';
442
+ return '\x1b[90m( ' + result_parts.join(', ') + ' )\x1b[0m';
352
443
  }
353
444
 
354
- function print_request_info(req: Request, res: Response, url: URL, request_start: number): Response {
355
- const request_time = Date.now() - request_start;
445
+ function print_request_info(req: Request, res: Response, url: URL, request_time: number): Response {
356
446
  const search_params = url.search.length > 0 ? format_query_parameters(url.searchParams) : '';
357
- console.log(`[${res.status}] ${req.method} ${url.pathname} ${search_params} [${request_time}ms]`);
447
+
448
+ // format status code based on range (2xx is green, 4xx is yellow, 5xx is red), use ansi colors.
449
+ const status_fmt = res.status < 300 ? '\x1b[32m' : res.status < 500 ? '\x1b[33m' : '\x1b[31m';
450
+ const status_code = status_fmt + res.status + '\x1b[0m';
451
+
452
+ // format request time based on range (0-100ms is green, 100-500ms is yellow, 500ms+ is red), use ansi colors.
453
+ const time_fmt = request_time < 100 ? '\x1b[32m' : request_time < 500 ? '\x1b[33m' : '\x1b[31m';
454
+ const request_time_str = time_fmt + request_time + 'ms\x1b[0m';
455
+
456
+ log('[%s] {%s} %s %s [{%s}]', status_code, req.method, url.pathname, search_params, request_time_str);
358
457
  return res;
359
458
  }
360
459
 
@@ -478,6 +577,11 @@ export function serve(port: number) {
478
577
  }
479
578
  }
480
579
 
580
+ type SlowRequestCallback = (req: Request, request_time: number) => void;
581
+
582
+ let slow_request_callback: SlowRequestCallback | null = null;
583
+ let slow_request_threshold: number = 1000;
584
+
481
585
  const server = Bun.serve({
482
586
  port,
483
587
  development: false,
@@ -487,11 +591,16 @@ export function serve(port: number) {
487
591
  const request_start = Date.now();
488
592
 
489
593
  const response = await generate_response(req, url);
490
- return print_request_info(req, response, url, request_start);
594
+ const request_time = Date.now() - request_start;
595
+
596
+ if (slow_request_callback !== null && request_time > slow_request_threshold)
597
+ slow_request_callback(req, request_time);
598
+
599
+ return print_request_info(req, response, url, request_time);
491
600
  }
492
601
  });
493
602
 
494
- log('server started on port ' + port);
603
+ log('server started on port {%d}', port);
495
604
 
496
605
  return {
497
606
  /** Register a handler for a specific route. */
@@ -527,6 +636,12 @@ export function serve(port: number) {
527
636
  }, 'POST']);
528
637
  },
529
638
 
639
+ /** Register a callback for slow requests. */
640
+ on_slow_request: (callback: SlowRequestCallback, threshold = 1000): void => {
641
+ slow_request_callback = callback;
642
+ slow_request_threshold = threshold;
643
+ },
644
+
530
645
  /** Register a default handler for all status codes. */
531
646
  default: (handler: DefaultHandler): void => {
532
647
  default_handler = handler;
package/src/cli.ts CHANGED
@@ -10,7 +10,7 @@ async function start_server() {
10
10
  const is_dev_mode = argv.includes('--dev');
11
11
 
12
12
  if (is_dev_mode)
13
- log('[dev] spooder has been started in dev mode');
13
+ log('[{dev}] spooder has been started in {dev mode}');
14
14
 
15
15
  const config = await get_config();
16
16
 
@@ -19,12 +19,12 @@ async function start_server() {
19
19
  const n_update_commands = update_commands.length;
20
20
 
21
21
  if (n_update_commands > 0) {
22
- log('running %d update commands', n_update_commands);
22
+ log('running {%d} update commands', n_update_commands);
23
23
 
24
24
  for (let i = 0; i < n_update_commands; i++) {
25
25
  const config_update_command = update_commands[i];
26
26
 
27
- log('[%d] %s', i, config_update_command);
27
+ log('[{%d}] %s', i, config_update_command);
28
28
 
29
29
  const update_proc = Bun.spawn(parse_command_line(config_update_command), {
30
30
  cwd: process.cwd(),
@@ -34,7 +34,7 @@ async function start_server() {
34
34
 
35
35
  await update_proc.exited;
36
36
 
37
- log('[%d] exited with code %d', i, update_proc.exitCode);
37
+ log('[{%d}] exited with code {%d}', i, update_proc.exitCode);
38
38
 
39
39
  if (update_proc.exitCode !== 0) {
40
40
  log('aborting update due to non-zero exit code from [%d]', i);
@@ -43,7 +43,7 @@ async function start_server() {
43
43
  }
44
44
  }
45
45
  } else {
46
- log('[dev] skipping update commands in dev mode');
46
+ log('[{dev}] skipping update commands in {dev mode}');
47
47
  }
48
48
 
49
49
  const crash_console_history = config.canary.crash_console_history;
@@ -86,15 +86,15 @@ async function start_server() {
86
86
  await proc.exited;
87
87
 
88
88
  const proc_exit_code = proc.exitCode;
89
- log('server exited with code %s', proc_exit_code);
89
+ log('server exited with code {%s}', proc_exit_code);
90
90
 
91
91
  if (proc_exit_code !== 0) {
92
92
  const console_output = include_crash_history ? strip_color_codes(stream_history.join('\n')) : undefined;
93
93
 
94
94
  if (is_dev_mode) {
95
- log('[dev] crash: server exited unexpectedly (exit code %d)', proc_exit_code);
96
- log('[dev] without --dev, this would raise a canary report');
97
- log('[dev] console output:\n%s', console_output);
95
+ log('[{dev}] crash: server exited unexpectedly (exit code {%d})', proc_exit_code);
96
+ log('[{dev}] without {--dev}, this would raise a canary report');
97
+ log('[{dev}] console output:\n%s', console_output);
98
98
  } else {
99
99
  dispatch_report('crash: server exited unexpectedly', [{
100
100
  proc_exit_code, console_output
@@ -105,10 +105,10 @@ async function start_server() {
105
105
  const auto_restart_ms = config.auto_restart;
106
106
  if (auto_restart_ms > -1) {
107
107
  if (is_dev_mode) {
108
- log('[dev] auto-restart is disabled in dev mode');
108
+ log('[{dev}] auto-restart is {disabled} in {dev mode}');
109
109
  process.exit(proc_exit_code ?? 0);
110
110
  } else {
111
- log('restarting server in %dms', auto_restart_ms);
111
+ log('restarting server in {%dms}', auto_restart_ms);
112
112
  setTimeout(start_server, auto_restart_ms);
113
113
  }
114
114
  }
package/src/config.ts CHANGED
@@ -30,7 +30,7 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
30
30
  const actual_type = typeof value;
31
31
 
32
32
  if (actual_type !== expected_type) {
33
- log('ignoring invalid configuration value `%s` (expected %s, got %s)', key_name, expected_type, actual_type);
33
+ log('ignoring invalid configuration value {%s} (expected {%s}, got {%s})', key_name, expected_type, actual_type);
34
34
  continue;
35
35
  }
36
36
 
@@ -40,7 +40,7 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
40
40
 
41
41
  if (is_default_array) {
42
42
  if (!is_actual_array) {
43
- log('ignoring invalid configuration value `%s` (expected array)', key_name);
43
+ log('ignoring invalid configuration value {%s} (expected array)', key_name);
44
44
  continue;
45
45
  }
46
46
 
@@ -57,7 +57,7 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
57
57
  source[key as keyof Config] = value as Config[keyof Config];
58
58
  }
59
59
  } else {
60
- log('ignoring unknown configuration key `%s`', key_name);
60
+ log('ignoring unknown configuration key {%s}', key_name);
61
61
  }
62
62
  }
63
63
  }
@@ -68,13 +68,13 @@ async function load_config(): Promise<Config> {
68
68
  const json = await config_file.json();
69
69
 
70
70
  if (json.spooder === null || typeof json.spooder !== 'object') {
71
- log('failed to parse spooder configuration in package.json, using defaults');
71
+ log('failed to parse spooder configuration in {package.json}, using defaults');
72
72
  return internal_config;
73
73
  }
74
74
 
75
75
  validate_config_option(internal_config, json.spooder, 'spooder');
76
76
  } catch (e) {
77
- log('failed to read package.json, using configuration defaults');
77
+ log('failed to read {package.json}, using configuration defaults');
78
78
  }
79
79
 
80
80
  return internal_config;
package/src/dispatch.ts CHANGED
@@ -93,9 +93,9 @@ async function check_cache_table(key: string, repository: string, expiry: number
93
93
  }
94
94
  }
95
95
  } catch (e) {
96
- log('failed to read canary cache file ' + cache_file_path);
97
- log('error: ' + (e as Error).message);
98
- log('you should resolve this issue to prevent spamming GitHub with canary reports');
96
+ log('[{canary}] failed to read canary cache file {%s}', cache_file_path);
97
+ log('[{canary}] error: ' + (e as Error).message);
98
+ log('[{canary}] resolve this issue to prevent spamming GitHub with canary reports');
99
99
  }
100
100
 
101
101
  if (cache_table.has(key_hash)) {
@@ -159,13 +159,13 @@ export async function dispatch_report(report_title: string, report_body: Array<u
159
159
  const canary_repostiory = config.canary.repository;
160
160
 
161
161
  if (canary_account.length === 0|| canary_repostiory.length === 0) {
162
- log('[canary] report dispatch failed; no account/repository configured');
162
+ log('[{canary}] report dispatch failed; no account/repository configured');
163
163
  return;
164
164
  }
165
165
 
166
166
  const is_cached = await check_cache_table(report_title, canary_repostiory, config.canary.throttle);
167
167
  if (is_cached) {
168
- log('[canary] throttled canary report: ' + report_title);
168
+ log('[{canary}] throttled canary report: {%s}', report_title);
169
169
  return;
170
170
  }
171
171
 
@@ -211,6 +211,6 @@ export async function dispatch_report(report_title: string, report_body: Array<u
211
211
  issue_labels: config.canary.labels
212
212
  });
213
213
  } catch (e) {
214
- log('[canary error] ' + (e as Error)?.message ?? 'unspecified error');
214
+ log('[{canary error}] ' + (e as Error)?.message ?? 'unspecified error');
215
215
  }
216
216
  }
package/src/github.ts CHANGED
@@ -117,5 +117,5 @@ export async function create_github_issue(issue: Issue): Promise<void> {
117
117
  check_response_is_ok(issue_res, 'cannot create GitHub issue');
118
118
 
119
119
  const json_issue = await issue_res.json() as IssueResponse;
120
- log('raised canary issue #%d in %s: %s', json_issue.number, repository.full_name, json_issue.url);
120
+ log('[{canary}] raised issue {#%d} in {%s}: %s', json_issue.number, repository.full_name, json_issue.url);
121
121
  }
package/src/utils.ts CHANGED
@@ -1,6 +1,13 @@
1
+ import { format } from 'node:util';
2
+
1
3
  /** Logs a message to stdout with the prefix `[spooder] ` */
2
4
  export function log(message: string, ...args: unknown[]): void {
3
- console.log('[spooder] ' + message, ...args);
5
+ let formatted_message = format('[{spooder}] ' + message, ...args);
6
+
7
+ // Replace all {...} with text wrapped in ANSI color code 6.
8
+ formatted_message = formatted_message.replace(/\{([^}]+)\}/g, '\x1b[38;5;6m$1\x1b[0m');
9
+
10
+ process.stdout.write(formatted_message + '\n');
4
11
  }
5
12
 
6
13
  /** Strips ANSI color codes from a string */
package/src/api.d.ts DELETED
@@ -1,80 +0,0 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
- import fs from 'node:fs/promises';
4
- import { Blob } from 'node:buffer';
5
- export declare const HTTP_STATUS_CODE: {
6
- [errorCode: number]: string | undefined;
7
- [errorCode: string]: string | undefined;
8
- };
9
- type HTTP_METHOD = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE';
10
- type HTTP_METHODS = HTTP_METHOD | HTTP_METHOD[];
11
- export declare class ErrorWithMetadata extends Error {
12
- metadata: Record<string, unknown>;
13
- constructor(message: string, metadata: Record<string, unknown>);
14
- resolve_metadata(): Promise<object>;
15
- }
16
- export declare function panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>;
17
- export declare function caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>;
18
- type CallableFunction = (...args: any[]) => any;
19
- type Callable = Promise<any> | CallableFunction;
20
- export declare function safe(target_fn: Callable): Promise<void>;
21
- type ReplacerFn = (key: string) => string | Array<string>;
22
- type Replacements = Record<string, string | Array<string>> | ReplacerFn;
23
- export declare function parse_template(template: string, replacements: Replacements, drop_missing?: boolean): string;
24
- export declare function generate_hash_subs(length?: number, prefix?: string): Promise<Record<string, string>>;
25
- type CookieOptions = {
26
- same_site?: 'Strict' | 'Lax' | 'None';
27
- secure?: boolean;
28
- http_only?: boolean;
29
- path?: string;
30
- expires?: number;
31
- encode?: boolean;
32
- };
33
- export declare function set_cookie(res: Response, name: string, value: string, options?: CookieOptions): void;
34
- export declare function get_cookies(source: Request | Response, decode?: boolean): Record<string, string>;
35
- export declare function apply_range(file: BunFile, request: Request): BunFile;
36
- type Resolvable<T> = T | Promise<T>;
37
- type PromiseType<T extends Promise<any>> = T extends Promise<infer U> ? U : never;
38
- type JsonPrimitive = string | number | boolean | null;
39
- type JsonArray = JsonSerializable[];
40
- interface JsonObject {
41
- [key: string]: JsonSerializable;
42
- }
43
- interface ToJson {
44
- toJSON(): any;
45
- }
46
- type JsonSerializable = JsonPrimitive | JsonObject | JsonArray | ToJson;
47
- type HandlerReturnType = Resolvable<string | number | BunFile | Response | JsonSerializable | Blob>;
48
- type RequestHandler = (req: Request, url: URL) => HandlerReturnType;
49
- type WebhookHandler = (payload: JsonSerializable) => HandlerReturnType;
50
- type ErrorHandler = (err: Error, req: Request, url: URL) => Resolvable<Response>;
51
- type DefaultHandler = (req: Request, status_code: number) => HandlerReturnType;
52
- type StatusCodeHandler = (req: Request) => HandlerReturnType;
53
- type ServerSentEventClient = {
54
- message: (message: string) => void;
55
- event: (event_name: string, message: string) => void;
56
- close: () => void;
57
- closed: Promise<void>;
58
- };
59
- type ServerSentEventHandler = (req: Request, url: URL, client: ServerSentEventClient) => void;
60
- type BunFile = ReturnType<typeof Bun.file>;
61
- type DirStat = PromiseType<ReturnType<typeof fs.stat>>;
62
- type DirHandler = (file_path: string, file: BunFile, stat: DirStat, request: Request, url: URL) => HandlerReturnType;
63
- export declare function serve(port: number): {
64
- /** Register a handler for a specific route. */
65
- route: (path: string, handler: RequestHandler, method?: HTTP_METHODS) => void;
66
- /** Serve a directory for a specific route. */
67
- dir: (path: string, dir: string, handler?: DirHandler, method?: HTTP_METHODS) => void;
68
- webhook: (secret: string, path: string, handler: WebhookHandler) => void;
69
- /** Register a default handler for all status codes. */
70
- default: (handler: DefaultHandler) => void;
71
- /** Register a handler for a specific status code. */
72
- handle: (status_code: number, handler: StatusCodeHandler) => void;
73
- /** Register a handler for uncaught errors. */
74
- error: (handler: ErrorHandler) => void;
75
- /** Stops the server. */
76
- stop: (immediate?: boolean) => void;
77
- /** Register a handler for server-sent events. */
78
- sse: (path: string, handler: ServerSentEventHandler) => void;
79
- };
80
- export {};
package/src/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bun
2
- export {};
package/src/config.d.ts DELETED
@@ -1,16 +0,0 @@
1
- declare const internal_config: {
2
- run: string;
3
- auto_restart: number;
4
- update: never[];
5
- canary: {
6
- account: string;
7
- repository: string;
8
- labels: never[];
9
- crash_console_history: number;
10
- throttle: number;
11
- sanitize: boolean;
12
- };
13
- };
14
- type Config = typeof internal_config;
15
- export declare function get_config(): Promise<Config>;
16
- export {};
package/src/dispatch.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function dispatch_report(report_title: string, report_body: Array<unknown>): Promise<void>;
package/src/github.d.ts DELETED
@@ -1,11 +0,0 @@
1
- type Issue = {
2
- app_id: number;
3
- private_key: string;
4
- login_name: string;
5
- repository_name: string;
6
- issue_title: string;
7
- issue_body: string;
8
- issue_labels?: Array<string>;
9
- };
10
- export declare function create_github_issue(issue: Issue): Promise<void>;
11
- export {};
package/src/utils.d.ts DELETED
@@ -1,6 +0,0 @@
1
- /** Logs a message to stdout with the prefix `[spooder] ` */
2
- export declare function log(message: string, ...args: unknown[]): void;
3
- /** Strips ANSI color codes from a string */
4
- export declare function strip_color_codes(str: string): string;
5
- /** Converts a command line string into an array of arguments */
6
- export declare function parse_command_line(command: string): string[];