spooder 3.2.6 → 3.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,7 +37,7 @@ Both the runner and the API are configured in the same way by providing a `spood
37
37
  ```json
38
38
  {
39
39
  "spooder": {
40
- "autoRestart": 5000,
40
+ "auto_restart": 5000,
41
41
  "run": "bun run index.ts",
42
42
  "update": [
43
43
  "git pull",
@@ -81,12 +81,12 @@ While `spooder` uses a `bun run` command by default, it is possible to use any c
81
81
 
82
82
  ## Auto Restart
83
83
 
84
- In the event that the server exits (regardless of exit code), `spooder` can automatically restart it after a short delay. To enable this feature specify the restart delay in milliseconds as `autoRestart` in the configuration.
84
+ In the event that the server exits (regardless of exit code), `spooder` can automatically restart it after a short delay. To enable this feature specify the restart delay in milliseconds as `auto_restart` in the configuration.
85
85
 
86
86
  ```json
87
87
  {
88
88
  "spooder": {
89
- "autoRestart": 5000
89
+ "auto_restart": 5000
90
90
  }
91
91
  }
92
92
  ```
@@ -192,12 +192,12 @@ It is recommended that you harden your server code against unexpected exceptions
192
192
 
193
193
  In the event that the server does encounter an unexpected exception which causes it to exit with a non-zero exit code, `spooder` will automatically raise an issue on GitHub using the canary feature, if configured.
194
194
 
195
- Since this issue has been caught externally, `spooder` has no context of the exception which was raised. Instead, the canary report will contain the output from `stderr`.
195
+ Since this issue has been caught externally, `spooder` has no context of the exception which was raised. Instead, the canary report will contain the output from both `stdout` and `stderr`.
196
196
 
197
197
  ```json
198
198
  {
199
- "exitCode": 1,
200
- "stderr": [
199
+ "proc_exit_code": 1,
200
+ "console_output": [
201
201
  "[2.48ms] \".env.local\"",
202
202
  "Test output",
203
203
  "Test output",
@@ -216,10 +216,26 @@ Since this issue has been caught externally, `spooder` has no context of the exc
216
216
  }
217
217
  ```
218
218
 
219
+ The `proc_exit_code` property contains the exit code that the server exited with.
220
+
221
+ The `console_output` will contain the last `64` lines of output from `stdout` and `stderr` combined. This can be configured by setting the `spooder.canary.crash_console_history` property.
222
+
223
+ ```json
224
+ {
225
+ "spooder": {
226
+ "canary": {
227
+ "crash_console_history": 128
228
+ }
229
+ }
230
+ }
231
+ ```
232
+
219
233
  This information is subject to sanitization, as described in the `Sanitization` section, however you should be aware that stack traces may contain sensitive information.
220
234
 
221
235
  Additionally, Bun includes a relevant code snippet from the source file where the exception was raised. This is intended to help you identify the source of the problem.
222
236
 
237
+ Setting `spooder.canary.crash_console_history` to `0` will omit the `console_output` property from the report entirely, which may make it harder to diagnose the problem but will ensure that no sensitive information is leaked.
238
+
223
239
  ## Sanitization
224
240
 
225
241
  All reports sent via the canary feature are sanitized to prevent sensitive information from being leaked. This includes:
@@ -303,13 +319,17 @@ In addition to the information provided by the developer, `spooder` also include
303
319
  "uv": "1.44.2",
304
320
  "napi": "8",
305
321
  "modules": "108"
322
+ },
323
+ "bun": {
324
+ "version": "0.6.4",
325
+ "rev": "f02561530fda1ee9396f51c8bc99b38716e38296"
306
326
  }
307
327
  }
308
328
  ```
309
329
 
310
330
  # API
311
331
 
312
- `spooder` exposes a build-block style API for developing servers. The API is designed to be minimal to leave control in the hands of the developer and not add overhead for features you may not need.
332
+ `spooder` exposes a building-block style API for developing servers. The API is designed to be minimal to leave control in the hands of the developer and not add overhead for features you may not need.
313
333
 
314
334
  ```ts
315
335
  import { ... } from 'spooder';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "3.2.6",
4
+ "version": "3.2.8",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/api.ts",
package/src/api.ts CHANGED
@@ -14,12 +14,19 @@ export class ErrorWithMetadata extends Error {
14
14
  async resolve_metadata(): Promise<object> {
15
15
  const metadata = Object.assign({}, this.metadata);
16
16
  for (const [key, value] of Object.entries(metadata)) {
17
+ let resolved_value = value;
18
+
17
19
  if (value instanceof Promise)
18
- metadata[key] = await value;
20
+ resolved_value = await value;
19
21
  else if (typeof value === 'function')
20
- metadata[key] = value();
22
+ resolved_value = value();
21
23
  else if (value instanceof ReadableStream)
22
- metadata[key] = await new Response(value).text();
24
+ resolved_value = await Bun.readableStreamToText(value);
25
+
26
+ if (typeof resolved_value === 'string' && resolved_value.includes('\n'))
27
+ resolved_value = resolved_value.split(/\r?\n/);
28
+
29
+ metadata[key] = resolved_value;
23
30
  }
24
31
 
25
32
  return metadata;
package/src/cli.ts CHANGED
@@ -36,35 +36,56 @@ async function start_server() {
36
36
  }
37
37
  }
38
38
 
39
+ const crash_console_history = config.canary.crash_console_history;
40
+ const include_crash_history = crash_console_history > 0;
41
+
42
+ const std_mode = include_crash_history ? 'pipe' : 'inherit';
39
43
  const proc = Bun.spawn(parse_command_line(config.run), {
40
44
  cwd: process.cwd(),
41
- stdout: 'inherit',
42
- stderr: 'pipe'
45
+ stdout: std_mode,
46
+ stderr: std_mode
43
47
  });
44
48
 
45
- await proc.exited;
49
+ const stream_history = new Array<string>();
50
+ if (include_crash_history) {
51
+ const text_decoder = new TextDecoder();
46
52
 
47
- const proc_exit_code = proc.exitCode;
48
- log('server exited with code %s', proc_exit_code);
53
+ function capture_stream(stream: ReadableStream, output: NodeJS.WritableStream) {
54
+ const reader = stream.getReader();
49
55
 
50
- if (proc_exit_code !== 0) {
51
- if (proc.stderr !== undefined) {
52
- const res = new Response(proc.stderr as ReadableStream);
53
-
54
- res.text().then(async stderr => {
55
- await dispatch_report('crash: server exited unexpectedly', [{
56
- proc_exit_code,
57
- stderr: strip_color_codes(stderr).split(/\r?\n/)
58
- }]);
56
+ reader.read().then(function read_chunk(chunk: ReadableStreamDefaultReadResult<Uint8Array>) {
57
+ if (chunk.done)
58
+ return;
59
+
60
+ const chunk_str = text_decoder.decode(chunk.value);
61
+ stream_history.push(...chunk_str.split(/\r?\n/));
62
+
63
+ if (stream_history.length > crash_console_history)
64
+ stream_history.splice(0, stream_history.length - crash_console_history);
65
+
66
+ output.write(chunk.value);
67
+ reader.read().then(read_chunk);
59
68
  });
60
- } else {
61
- dispatch_report('crash: service exited unexpectedly', [{
62
- proc_exit_code
63
- }]);
64
69
  }
70
+
71
+ capture_stream(proc.stdout as ReadableStream, process.stdout);
72
+ capture_stream(proc.stderr as ReadableStream, process.stderr);
73
+ }
74
+
75
+ await proc.exited;
76
+
77
+ const proc_exit_code = proc.exitCode;
78
+ log('server exited with code %s', proc_exit_code);
79
+
80
+ if (proc_exit_code !== 0) {
81
+ const console_output = include_crash_history ? strip_color_codes(stream_history.join('\n')) : undefined;
82
+ dispatch_report('crash: server exited unexpectedly', [{
83
+ proc_exit_code, console_output
84
+ }]);
65
85
  }
86
+
66
87
 
67
- const auto_restart_ms = config.autoRestart;
88
+ const auto_restart_ms = config.auto_restart;
68
89
  if (auto_restart_ms > -1) {
69
90
  log('restarting server in %dms', auto_restart_ms);
70
91
  setTimeout(start_server, auto_restart_ms);
package/src/config.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  declare const internal_config: {
2
2
  run: string;
3
- autoRestart: number;
3
+ auto_restart: number;
4
4
  update: never[];
5
5
  canary: {
6
6
  account: string;
7
7
  repository: string;
8
8
  labels: never[];
9
+ crash_console_history: number;
9
10
  throttle: number;
10
11
  sanitize: boolean;
11
12
  };
package/src/config.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import path from 'node:path';
2
- import { warn } from './utils';
2
+ import { log } from './utils';
3
3
 
4
4
  const internal_config = {
5
5
  run: 'bun run index.ts',
6
- autoRestart: -1,
6
+ auto_restart: -1,
7
7
  update: [],
8
8
  canary: {
9
9
  account: '',
10
10
  repository: '',
11
11
  labels: [],
12
+ crash_console_history: 64,
12
13
  throttle: 86400,
13
14
  sanitize: true
14
15
  }
@@ -27,7 +28,7 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
27
28
  const actual_type = typeof value;
28
29
 
29
30
  if (actual_type !== expected_type) {
30
- warn('ignoring invalid configuration value `%s` (expected %s, got %s)', key_name, expected_type, actual_type);
31
+ log('ignoring invalid configuration value `%s` (expected %s, got %s)', key_name, expected_type, actual_type);
31
32
  continue;
32
33
  }
33
34
 
@@ -37,14 +38,14 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
37
38
 
38
39
  if (is_default_array) {
39
40
  if (!is_actual_array) {
40
- warn('ignoring invalid configuration value `%s` (expected array)', key_name);
41
+ log('ignoring invalid configuration value `%s` (expected array)', key_name);
41
42
  continue;
42
43
  }
43
44
 
44
45
  source[key as keyof Config] = value as Config[keyof Config];
45
46
  } else {
46
47
  if (is_actual_array) {
47
- warn('ignoring invalid configuration value `%s` (expected object)', key_name);
48
+ log('ignoring invalid configuration value `%s` (expected object)', key_name);
48
49
  continue;
49
50
  }
50
51
 
@@ -54,7 +55,7 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
54
55
  source[key as keyof Config] = value as Config[keyof Config];
55
56
  }
56
57
  } else {
57
- warn('ignoring unknown configuration key `%s`', key_name);
58
+ log('ignoring unknown configuration key `%s`', key_name);
58
59
  }
59
60
  }
60
61
  }
@@ -65,13 +66,13 @@ export async function get_config(): Promise<Config> {
65
66
  const json = await config_file.json();
66
67
 
67
68
  if (json.spooder === null || typeof json.spooder !== 'object') {
68
- warn('failed to parse spooder configuration in package.json, using defaults');
69
+ log('failed to parse spooder configuration in package.json, using defaults');
69
70
  return internal_config;
70
71
  }
71
72
 
72
73
  validate_config_option(internal_config, json.spooder, 'spooder');
73
74
  } catch (e) {
74
- warn('failed to read package.json, using configuration defaults');
75
+ log('failed to read package.json, using configuration defaults');
75
76
  }
76
77
 
77
78
  return internal_config;
package/src/dispatch.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { App } from '@octokit/app';
2
2
  import { get_config } from './config';
3
- import { warn, log } from './utils';
3
+ import { log } from './utils';
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import os from 'node:os';
@@ -93,9 +93,9 @@ async function check_cache_table(key: string, repository: string, expiry: number
93
93
  }
94
94
  }
95
95
  } catch (e) {
96
- warn('Failed to read canary cache file ' + cache_file_path);
97
- warn('Error: ' + (e as Error).message);
98
- warn('You should resolve this issue to prevent spamming GitHub with canary reports.');
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.');
99
99
  }
100
100
 
101
101
  if (cache_table.has(key_hash)) {
@@ -142,6 +142,10 @@ function generate_diagnostics(): object {
142
142
  'platform': os.platform(),
143
143
  'uptime': os.uptime(),
144
144
  'versions': process.versions,
145
+ 'bun': {
146
+ 'version': Bun.version,
147
+ 'rev': Bun.revision,
148
+ }
145
149
  }
146
150
  }
147
151
 
@@ -158,7 +162,7 @@ export async function dispatch_report(report_title: string, report_body: Array<u
158
162
 
159
163
  const is_cached = await check_cache_table(report_title, canary_repostiory, config.canary.throttle);
160
164
  if (is_cached) {
161
- warn('Throttled canary report: ' + report_title);
165
+ log('Throttled canary report: ' + report_title);
162
166
  return;
163
167
  }
164
168
 
@@ -218,6 +222,6 @@ export async function dispatch_report(report_title: string, report_body: Array<u
218
222
  }
219
223
  }
220
224
  } catch (e) {
221
- warn('Failed to dispatch canary report: ' + (e as Error)?.message ?? 'unspecified error');
225
+ log('Failed to dispatch canary report: ' + (e as Error)?.message ?? 'unspecified error');
222
226
  }
223
227
  }
package/src/utils.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  /** Logs a message to stdout with the prefix `[spooder] ` */
2
2
  export declare function log(message: string, ...args: unknown[]): void;
3
- /** Logs a message to stderr with the prefix `[spooder] ` */
4
- export declare function warn(message: string, ...args: unknown[]): void;
5
3
  /** Strips ANSI color codes from a string */
6
4
  export declare function strip_color_codes(str: string): string;
7
5
  /** Converts a command line string into an array of arguments */
package/src/utils.ts CHANGED
@@ -3,11 +3,6 @@ export function log(message: string, ...args: unknown[]): void {
3
3
  console.log('[spooder] ' + message, ...args);
4
4
  }
5
5
 
6
- /** Logs a message to stderr with the prefix `[spooder] ` */
7
- export function warn(message: string, ...args: unknown[]): void {
8
- console.error('[spooder] ' + message, ...args);
9
- }
10
-
11
6
  /** Strips ANSI color codes from a string */
12
7
  export function strip_color_codes(str: string): string {
13
8
  return str.replace(/\x1b\[[0-9;]*m/g, '');