@blamejs/core 0.5.16 → 0.5.17

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/CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.5.x
10
10
 
11
+ - **0.5.16** (2026-04-30) — b.otelExport: OTLP/HTTP exporter for b.observability
11
12
  - **0.5.15** (2026-04-30) — b.archive: ZIP creation
12
13
  - **0.5.14** (2026-04-30) — b.time: timezone-aware datetime arithmetic + formatting
13
14
  - **0.5.13** (2026-04-30) — b.testing.request: supertest-style chainable HTTP test helper
@@ -60,9 +60,9 @@
60
60
  */
61
61
 
62
62
  var fs = require("fs");
63
- var nodeCrypto = require("node:crypto");
64
63
  var os = require("os");
65
64
  var path = require("path");
65
+ var crypto = require("../crypto");
66
66
  var atomicFile = require("../atomic-file");
67
67
  var backupBundle = require("./bundle");
68
68
  var lazyRequire = require("../lazy-require");
@@ -80,7 +80,7 @@ function _isValidBundleId(s) {
80
80
  }
81
81
 
82
82
  function _generateBundleId() {
83
- return atomicFile.pathTimestamp() + "-" + nodeCrypto.randomBytes(4).toString("hex");
83
+ return atomicFile.pathTimestamp() + "-" + crypto.generateToken(4);
84
84
  }
85
85
 
86
86
  function _dirSize(p) {
package/lib/cache.js CHANGED
@@ -94,6 +94,7 @@ var clusterStorage = require("./cluster-storage");
94
94
  var C = require("./constants");
95
95
  var lazyRequire = require("./lazy-require");
96
96
  var requestHelpers = require("./request-helpers");
97
+ var safeAsync = require("./safe-async");
97
98
  var validateOpts = require("./validate-opts");
98
99
  var { CacheError } = require("./framework-error");
99
100
 
@@ -434,12 +435,11 @@ function _memoryBackend(cfg) {
434
435
 
435
436
  function _startSweep(intervalMs) {
436
437
  if (sweepTimer) return;
437
- sweepTimer = setInterval(_sweep, intervalMs);
438
- if (typeof sweepTimer.unref === "function") sweepTimer.unref();
438
+ sweepTimer = safeAsync.repeating(_sweep, intervalMs, { name: "cache-sweep" });
439
439
  }
440
440
 
441
441
  async function close() {
442
- if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
442
+ if (sweepTimer) { sweepTimer.stop(); sweepTimer = null; }
443
443
  entries.clear();
444
444
  tagIndex.clear();
445
445
  totalBytes = 0;
@@ -579,15 +579,11 @@ function _clusterBackend(cfg) {
579
579
  }
580
580
 
581
581
  function _startSweep(intervalMs) {
582
- var t = setInterval(function () {
583
- _sweep().catch(function () { /* sweeper best-effort; next pass picks it up */ });
584
- }, intervalMs);
585
- if (typeof t.unref === "function") t.unref();
586
- cfg._sweepTimer = t;
582
+ cfg._sweepTimer = safeAsync.repeating(_sweep, intervalMs, { name: "cache-sweep-cluster" });
587
583
  }
588
584
 
589
585
  async function close() {
590
- if (cfg._sweepTimer) { clearInterval(cfg._sweepTimer); cfg._sweepTimer = null; }
586
+ if (cfg._sweepTimer) { cfg._sweepTimer.stop(); cfg._sweepTimer = null; }
591
587
  }
592
588
 
593
589
  return {
package/lib/cli.js CHANGED
@@ -35,15 +35,16 @@
35
35
  */
36
36
 
37
37
  var fs = require("node:fs");
38
- var nodeCrypto = require("node:crypto");
39
38
  var os = require("node:os");
40
39
  var path = require("path");
41
40
  var apiSnapshot = require("./api-snapshot");
42
41
  var auditTools = require("./audit-tools");
43
42
  var cliHelpers = require("./cli-helpers");
44
43
  var constants = require("./constants");
44
+ var crypto = require("./crypto");
45
45
  var dev = require("./dev");
46
46
  var migrations = require("./migrations");
47
+ var requestHelpers = require("./request-helpers");
47
48
  var restoreBundle = require("./restore-bundle");
48
49
  var seeders = require("./seeders");
49
50
  var vaultPassphraseOps = require("./vault/passphrase-ops");
@@ -746,7 +747,7 @@ async function _runApiKey(args, ctx) {
746
747
  var scopes = args.flags.scopes;
747
748
  if (!ownerId || ownerId === true) return report.error("--owner-id <id> is required", 2);
748
749
  if (!scopes || scopes === true) return report.error("--scopes <comma-separated> is required", 2);
749
- var scopeList = String(scopes).split(",").map(function (s) { return s.trim(); }).filter(Boolean);
750
+ var scopeList = requestHelpers.parseListHeader(scopes);
750
751
  if (scopeList.length === 0) {
751
752
  return report.error("--scopes must contain at least one non-empty scope", 2);
752
753
  }
@@ -902,7 +903,7 @@ async function _runBackup(args, ctx) {
902
903
 
903
904
  if (sub === "verify") {
904
905
  var stagingDir = path.join(os.tmpdir(),
905
- "blamejs-backup-verify-" + nodeCrypto.randomBytes(8).toString("hex"));
906
+ "blamejs-backup-verify-" + crypto.generateToken(8));
906
907
  try {
907
908
  var r = await restoreBundle.extract({
908
909
  bundleDir: bundleDir,
package/lib/cluster.js CHANGED
@@ -52,6 +52,7 @@
52
52
  */
53
53
  var C = require("./constants");
54
54
  var { boot } = require("./log");
55
+ var safeAsync = require("./safe-async");
55
56
  var safeUrl = require("./safe-url");
56
57
  var { FrameworkError, ClusterError } = require("./framework-error");
57
58
 
@@ -227,8 +228,7 @@ async function init(opts) {
227
228
  }
228
229
 
229
230
  // Start heartbeat
230
- heartbeatTimer = setInterval(_heartbeat, heartbeatMs);
231
- heartbeatTimer.unref();
231
+ heartbeatTimer = safeAsync.repeating(_heartbeat, heartbeatMs, { name: "cluster-heartbeat" });
232
232
  }
233
233
 
234
234
  // Cluster-mode equivalent of db.js's single-node audit.tip-sidecar
@@ -619,7 +619,7 @@ function onTransition(handler) {
619
619
  async function shutdown() {
620
620
  if (!initialized) return;
621
621
  if (heartbeatTimer) {
622
- clearInterval(heartbeatTimer);
622
+ heartbeatTimer.stop();
623
623
  heartbeatTimer = null;
624
624
  }
625
625
  if (lease) {
@@ -649,7 +649,7 @@ async function shutdown() {
649
649
  // ---- test helpers — not part of public contract ----
650
650
 
651
651
  function _resetForTest() {
652
- if (heartbeatTimer) clearInterval(heartbeatTimer);
652
+ if (heartbeatTimer) heartbeatTimer.stop();
653
653
  heartbeatTimer = null;
654
654
  initialized = false;
655
655
  terminated = false;
package/lib/csv.js CHANGED
@@ -1,229 +1,275 @@
1
1
  "use strict";
2
2
  /**
3
- * csv — RFC 4180 parser + serializer.
3
+ * csv — RFC 4180 parser + serializer with operator-friendly defaults.
4
4
  *
5
- * Public API:
5
+ * var rows = b.csv.parse(text);
6
+ * // header row by default — returns array of objects keyed by header
7
+ * // [ { name: "alice", age: "30" }, ... ]
6
8
  *
7
- * b.csv.parse(text, opts?) → { headers, rows } (or { rows } if no header)
8
- * b.csv.stringify(rows, opts?) → string
9
+ * var arrays = b.csv.parse(text, { header: false });
10
+ * // returns array of arrays
9
11
  *
10
- * Opts (parse):
11
- * delimiter: "," default
12
- * header: true default — first row becomes column names; rows
13
- * are objects keyed by header
14
- * maxBytes: 8 MiB default — refuses parse-bombs the same way
15
- * safeJson does
16
- * onBadRow: "throw" default | "skip" — what to do when a row has
17
- * a different column count than the header
12
+ * var out = b.csv.stringify(rows);
13
+ * // RFC 4180 + Excel-formula-injection prevention by default
18
14
  *
19
- * Opts (stringify):
20
- * delimiter: "," default
21
- * header: true default — emit a header row from object keys
22
- * columns: array of explicit column order (default: first row's keys)
23
- * eol: "\r\n" default — RFC 4180 mandates CRLF; some parsers
24
- * want LF. Operators choose.
15
+ * Defaults:
16
+ * parse:
17
+ * header: true first row is column names
18
+ * delimiter: "," single byte; "\t" / ";" supported
19
+ * quote: '"'
20
+ * trim: false cell whitespace preserved
21
+ * maxBytes: 16 MiB
22
+ * maxRows: 1,000,000
23
+ * maxFieldBytes: 1 MiB
24
+ * onBadRow: "throw" "skip" tolerates short/long rows
25
25
  *
26
- * Format support:
27
- * - Quoted fields with embedded commas, quotes ("" → "), newlines.
28
- * - Optional BOM at file start (consumed silently if present).
29
- * - Trailing newline tolerated (RFC 4180 says SHOULD; we tolerate).
26
+ * stringify:
27
+ * header: true (or array of explicit columns)
28
+ * delimiter: ","
29
+ * quote: '"'
30
+ * eol: "\r\n" RFC 4180; "\n" via opt
31
+ * alwaysQuote: false only quote when needed
32
+ * preventFormulaInjection: true prefix '=' / '+' / '-' / '@' / TAB / CR
33
+ * cells with "'" so Excel doesn't
34
+ * execute them when the CSV is opened
35
+ * formulaPrefixChars: ["=","+","-","@","\t","\r"]
30
36
  *
31
- * Throws CsvError (FrameworkError) on shape violations.
37
+ * SECURITY: stringify defends against CSV-injection (Excel/Sheets execute
38
+ * cells starting with "=" / "+" / "-" / "@" / TAB / CR as formulas).
39
+ * Operator-controlled cells stay safe; user-supplied cells get the leading
40
+ * single-quote so the formula engine sees a literal string.
41
+ *
42
+ * Throws CsvError (FrameworkError, permanent) on shape violations.
32
43
  */
33
44
  var C = require("./constants");
34
45
  var { defineClass } = require("./framework-error");
35
46
 
36
47
  var CsvError = defineClass("CsvError", { alwaysPermanent: true });
37
48
 
38
- var DEFAULT_MAX_BYTES = C.BYTES.mib(8);
39
- var DEFAULT_DELIMITER = ",";
40
- var DEFAULT_EOL = "\r\n";
49
+ var DEFAULTS_PARSE = {
50
+ header: true,
51
+ delimiter: ",",
52
+ quote: "\"",
53
+ trim: false,
54
+ maxBytes: C.BYTES.mib(16),
55
+ maxRows: 1000000,
56
+ maxFieldBytes: C.BYTES.mib(1),
57
+ onBadRow: "throw",
58
+ };
59
+
60
+ var DEFAULTS_STRINGIFY = {
61
+ header: true,
62
+ delimiter: ",",
63
+ quote: "\"",
64
+ eol: "\r\n",
65
+ alwaysQuote: false,
66
+ preventFormulaInjection: true,
67
+ formulaPrefixChars: ["=", "+", "-", "@", "\t", "\r"],
68
+ columns: null,
69
+ };
70
+
71
+ function _validateDelim(name, value) {
72
+ if (typeof value !== "string" || value.length !== 1) {
73
+ throw new CsvError("csv/bad-delimiter",
74
+ name + " must be a single character, got " + JSON.stringify(value));
75
+ }
76
+ if (value === "\r" || value === "\n") {
77
+ throw new CsvError("csv/bad-delimiter",
78
+ name + " cannot be CR or LF");
79
+ }
80
+ }
41
81
 
42
82
  // ---- parse ----
43
83
 
44
- function parse(text, opts) {
45
- opts = opts || {};
46
- if (typeof text !== "string" && !Buffer.isBuffer(text)) {
84
+ function parse(input, opts) {
85
+ opts = Object.assign({}, DEFAULTS_PARSE, opts || {});
86
+
87
+ var s;
88
+ if (typeof input === "string") s = input;
89
+ else if (Buffer.isBuffer(input)) s = input.toString("utf8");
90
+ else if (input instanceof Uint8Array) s = Buffer.from(input).toString("utf8");
91
+ else {
47
92
  throw new CsvError("csv/bad-input",
48
- "parse: input must be a string or Buffer, got " + typeof text);
93
+ "parse: input must be string, Buffer, or Uint8Array, got " + typeof input);
49
94
  }
50
- var s = Buffer.isBuffer(text) ? text.toString("utf8") : text;
51
- var maxBytes = opts.maxBytes != null ? opts.maxBytes : DEFAULT_MAX_BYTES;
52
- if (Buffer.byteLength(s, "utf8") > maxBytes) {
95
+ if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
96
+ if (Buffer.byteLength(s, "utf8") > opts.maxBytes) {
53
97
  throw new CsvError("csv/too-large",
54
- "parse: input exceeds maxBytes (" + maxBytes + ")");
55
- }
56
- var delimiter = opts.delimiter || DEFAULT_DELIMITER;
57
- if (typeof delimiter !== "string" || delimiter.length !== 1) {
58
- throw new CsvError("csv/bad-delimiter",
59
- "parse: delimiter must be a single character, got " + JSON.stringify(delimiter));
98
+ "parse: input exceeds maxBytes (" + opts.maxBytes + ")");
60
99
  }
61
- if (delimiter === "\"" || delimiter === "\r" || delimiter === "\n") {
100
+
101
+ _validateDelim("delimiter", opts.delimiter);
102
+ _validateDelim("quote", opts.quote);
103
+ if (opts.delimiter === opts.quote) {
62
104
  throw new CsvError("csv/bad-delimiter",
63
- "parse: delimiter cannot be quote / CR / LF");
105
+ "delimiter and quote must differ");
64
106
  }
65
- var hasHeader = opts.header !== false;
66
- var onBadRow = opts.onBadRow || "throw";
67
- if (onBadRow !== "throw" && onBadRow !== "skip") {
107
+ if (opts.onBadRow !== "throw" && opts.onBadRow !== "skip") {
68
108
  throw new CsvError("csv/bad-opt",
69
- "parse: onBadRow must be 'throw' or 'skip'");
109
+ "onBadRow must be 'throw' or 'skip'");
70
110
  }
71
111
 
72
- // Strip BOM if present.
73
- if (s.charCodeAt(0) === 0xfeff) s = s.slice(1);
74
-
112
+ var len = s.length;
113
+ var pos = 0;
75
114
  var rows = [];
76
- var field = "";
77
115
  var row = [];
78
- var inQuotes = false;
79
- var i = 0;
80
- var len = s.length;
81
- while (i < len) {
82
- var c = s.charAt(i);
83
- if (inQuotes) {
84
- if (c === "\"") {
85
- if (i + 1 < len && s.charAt(i + 1) === "\"") {
86
- field += "\""; // escaped quote
87
- i += 2;
116
+ var field = "";
117
+ var inQuote = false;
118
+
119
+ function pushField() {
120
+ if (Buffer.byteLength(field, "utf8") > opts.maxFieldBytes) {
121
+ throw new CsvError("csv/field-too-large",
122
+ "field exceeds maxFieldBytes at row " + (rows.length + 1));
123
+ }
124
+ row.push(opts.trim ? field.trim() : field);
125
+ field = "";
126
+ }
127
+ function pushRow() {
128
+ pushField();
129
+ rows.push(row);
130
+ if (rows.length > opts.maxRows) {
131
+ throw new CsvError("csv/too-many-rows",
132
+ "row count exceeds maxRows (" + opts.maxRows + ")");
133
+ }
134
+ row = [];
135
+ }
136
+
137
+ while (pos < len) {
138
+ var ch = s.charAt(pos);
139
+ if (inQuote) {
140
+ if (ch === opts.quote) {
141
+ if (pos + 1 < len && s.charAt(pos + 1) === opts.quote) {
142
+ field += opts.quote;
143
+ pos += 2;
88
144
  continue;
89
145
  }
90
- inQuotes = false;
91
- i++;
146
+ inQuote = false;
147
+ pos += 1;
92
148
  continue;
93
149
  }
94
- field += c;
95
- i++;
96
- continue;
97
- }
98
- if (c === "\"") {
99
- // Quote at the start of a field (or anywhere — treat as start of
100
- // quoted run). RFC 4180 says quoted fields begin with a quote;
101
- // fields that contain a quote not at the start are technically
102
- // malformed but we tolerate by appending literally.
103
- if (field.length === 0) {
104
- inQuotes = true;
105
- i++;
106
- continue;
150
+ field += ch;
151
+ pos += 1;
152
+ } else {
153
+ if (ch === opts.delimiter) {
154
+ pushField();
155
+ pos += 1;
156
+ } else if (ch === "\r") {
157
+ pushRow();
158
+ pos += 1;
159
+ if (pos < len && s.charAt(pos) === "\n") pos += 1;
160
+ } else if (ch === "\n") {
161
+ pushRow();
162
+ pos += 1;
163
+ } else if (ch === opts.quote && field === "") {
164
+ inQuote = true;
165
+ pos += 1;
166
+ } else {
167
+ field += ch;
168
+ pos += 1;
107
169
  }
108
- field += "\"";
109
- i++;
110
- continue;
111
- }
112
- if (c === delimiter) {
113
- row.push(field);
114
- field = "";
115
- i++;
116
- continue;
117
- }
118
- if (c === "\r") {
119
- // CRLF or bare CR — treat both as row terminator.
120
- row.push(field);
121
- rows.push(row);
122
- field = "";
123
- row = [];
124
- if (i + 1 < len && s.charAt(i + 1) === "\n") i += 2;
125
- else i++;
126
- continue;
127
- }
128
- if (c === "\n") {
129
- row.push(field);
130
- rows.push(row);
131
- field = "";
132
- row = [];
133
- i++;
134
- continue;
135
170
  }
136
- field += c;
137
- i++;
138
171
  }
139
- // Flush any trailing field/row (no trailing newline case)
140
- if (field.length > 0 || row.length > 0) {
141
- row.push(field);
142
- rows.push(row);
172
+ if (inQuote) {
173
+ throw new CsvError("csv/unterminated-quote",
174
+ "unterminated quoted field");
143
175
  }
176
+ if (field.length > 0 || row.length > 0) pushRow();
144
177
 
145
- if (!hasHeader) {
146
- return { rows: rows };
147
- }
148
- if (rows.length === 0) {
149
- return { headers: [], rows: [] };
150
- }
151
- var headers = rows[0];
178
+ if (!opts.header) return rows;
179
+ if (rows.length === 0) return [];
180
+
181
+ var header = rows[0];
152
182
  var out = [];
153
183
  for (var r = 1; r < rows.length; r++) {
154
- var current = rows[r];
155
- if (current.length === 1 && current[0] === "") continue; // skip blank lines
156
- if (current.length !== headers.length) {
157
- if (onBadRow === "skip") continue;
184
+ var rec = rows[r];
185
+ if (rec.length !== header.length) {
186
+ if (opts.onBadRow === "skip") continue;
158
187
  throw new CsvError("csv/row-length-mismatch",
159
- "parse: row " + r + " has " + current.length + " columns, expected " + headers.length);
188
+ "row " + r + " has " + rec.length + " fields, header has " + header.length);
160
189
  }
161
190
  var obj = {};
162
- for (var k = 0; k < headers.length; k++) obj[headers[k]] = current[k];
191
+ for (var i = 0; i < header.length; i++) obj[header[i]] = rec[i];
163
192
  out.push(obj);
164
193
  }
165
- return { headers: headers, rows: out };
194
+ return out;
166
195
  }
167
196
 
168
197
  // ---- stringify ----
169
198
 
170
199
  function stringify(rows, opts) {
171
- opts = opts || {};
200
+ opts = Object.assign({}, DEFAULTS_STRINGIFY, opts || {});
172
201
  if (!Array.isArray(rows)) {
173
202
  throw new CsvError("csv/bad-input",
174
- "stringify: rows must be an array");
203
+ "stringify: rows must be an array, got " + typeof rows);
204
+ }
205
+ _validateDelim("delimiter", opts.delimiter);
206
+ _validateDelim("quote", opts.quote);
207
+ if (opts.delimiter === opts.quote) {
208
+ throw new CsvError("csv/bad-delimiter",
209
+ "delimiter and quote must differ");
210
+ }
211
+ if (opts.eol !== "\r\n" && opts.eol !== "\n") {
212
+ throw new CsvError("csv/bad-opt",
213
+ "eol must be '\\r\\n' or '\\n'");
175
214
  }
176
- var delimiter = opts.delimiter || DEFAULT_DELIMITER;
177
- var eol = opts.eol || DEFAULT_EOL;
178
- var hasHeader = opts.header !== false;
179
215
  if (rows.length === 0) return "";
180
216
 
181
- var columns;
182
- if (Array.isArray(opts.columns)) {
183
- columns = opts.columns.slice();
184
- } else if (Array.isArray(rows[0])) {
185
- // Array-of-arrays input — no headers to derive; emit as-is.
186
- columns = null;
187
- } else if (rows[0] && typeof rows[0] === "object") {
188
- columns = Object.keys(rows[0]);
217
+ var first = rows[0];
218
+ var asObjects;
219
+ var header;
220
+ if (Array.isArray(first)) {
221
+ asObjects = false;
222
+ header = Array.isArray(opts.columns) ? opts.columns : null;
223
+ } else if (first !== null && typeof first === "object") {
224
+ asObjects = true;
225
+ header = Array.isArray(opts.columns) ? opts.columns : Object.keys(first);
189
226
  } else {
190
227
  throw new CsvError("csv/bad-input",
191
- "stringify: rows[0] must be an object or array");
228
+ "stringify: rows must be arrays or plain objects");
192
229
  }
193
230
 
194
- var lines = [];
195
- if (hasHeader && columns) lines.push(columns.map(_quoteCell).join(delimiter));
231
+ var prefixSet = Object.create(null);
232
+ for (var pi = 0; pi < opts.formulaPrefixChars.length; pi++) {
233
+ prefixSet[opts.formulaPrefixChars[pi]] = true;
234
+ }
196
235
 
197
- for (var r = 0; r < rows.length; r++) {
198
- var row = rows[r];
199
- var cells;
200
- if (Array.isArray(row)) {
201
- cells = row.map(_quoteCell);
202
- } else if (row && typeof row === "object") {
203
- cells = (columns || Object.keys(row)).map(function (col) {
204
- return _quoteCell(row[col]);
205
- });
206
- } else {
207
- throw new CsvError("csv/bad-input",
208
- "stringify: rows[" + r + "] must be an object or array");
236
+ function escapeCell(value) {
237
+ var str = value == null ? "" : String(value);
238
+ if (opts.preventFormulaInjection && str.length > 0 && prefixSet[str.charAt(0)]) {
239
+ str = "'" + str;
240
+ }
241
+ var needsQuote = opts.alwaysQuote ||
242
+ str.indexOf(opts.delimiter) !== -1 ||
243
+ str.indexOf(opts.quote) !== -1 ||
244
+ str.indexOf("\n") !== -1 ||
245
+ str.indexOf("\r") !== -1;
246
+ if (needsQuote) {
247
+ str = opts.quote + str.split(opts.quote).join(opts.quote + opts.quote) + opts.quote;
209
248
  }
210
- lines.push(cells.join(delimiter));
249
+ return str;
211
250
  }
212
- return lines.join(eol) + eol;
213
- }
214
251
 
215
- function _quoteCell(value) {
216
- if (value === null || value === undefined) return "";
217
- var s = typeof value === "string" ? value : String(value);
218
- if (s.indexOf("\"") !== -1 || s.indexOf(",") !== -1 ||
219
- s.indexOf("\r") !== -1 || s.indexOf("\n") !== -1) {
220
- return "\"" + s.replace(/"/g, "\"\"") + "\"";
252
+ var out = [];
253
+ if (opts.header && header) {
254
+ out.push(header.map(escapeCell).join(opts.delimiter));
255
+ }
256
+ for (var i = 0; i < rows.length; i++) {
257
+ var rec = rows[i];
258
+ var cells;
259
+ if (asObjects) {
260
+ cells = header.map(function (k) { return escapeCell(rec[k]); });
261
+ } else {
262
+ cells = rec.map(escapeCell);
263
+ }
264
+ out.push(cells.join(opts.delimiter));
221
265
  }
222
- return s;
266
+ return out.join(opts.eol);
223
267
  }
224
268
 
225
269
  module.exports = {
226
- parse: parse,
227
- stringify: stringify,
228
- CsvError: CsvError,
270
+ parse: parse,
271
+ stringify: stringify,
272
+ CsvError: CsvError,
273
+ DEFAULTS_PARSE: DEFAULTS_PARSE,
274
+ DEFAULTS_STRINGIFY: DEFAULTS_STRINGIFY,
229
275
  };
package/lib/db.js CHANGED
@@ -55,6 +55,7 @@ var cryptoField = require("./crypto-field");
55
55
  var { Query } = require("./db-query");
56
56
  var dbSchema = require("./db-schema");
57
57
  var { boot } = require("./log");
58
+ var safeAsync = require("./safe-async");
58
59
  var safeEnv = require("./parsers/safe-env");
59
60
  var safeJson = require("./safe-json");
60
61
  var vault = require("./vault");
@@ -797,12 +798,11 @@ async function init(opts) {
797
798
 
798
799
  // Start periodic encrypt timer (encrypted mode only)
799
800
  if (atRest === "encrypted") {
800
- encTimer = setInterval(function () {
801
+ encTimer = safeAsync.repeating(function () {
801
802
  try { encryptToDisk(); } catch (e) {
802
803
  log.error("periodic encrypt failed: " + e.message);
803
804
  }
804
- }, C.TIME.minutes(5));
805
- encTimer.unref();
805
+ }, C.TIME.minutes(5), { name: "db-periodic-encrypt" });
806
806
 
807
807
  // Final encrypt on process exit. We don't try to unlink the plaintext
808
808
  // here — the SQLite handle may still be open, and the OS reclaims tmpfs
@@ -914,7 +914,7 @@ function hashFor(table, field, value) {
914
914
  function close() {
915
915
  if (!initialized) return;
916
916
  if (encTimer) {
917
- clearInterval(encTimer);
917
+ encTimer.stop();
918
918
  encTimer = null;
919
919
  }
920
920
  // Best-effort final checkpoint before shutdown so the audit.tip sidecar
@@ -1070,7 +1070,7 @@ async function _runNtpBootCheck(opts) {
1070
1070
 
1071
1071
  // Test helpers — not part of public contract
1072
1072
  function _resetForTest() {
1073
- if (encTimer) { clearInterval(encTimer); encTimer = null; }
1073
+ if (encTimer) { encTimer.stop(); encTimer = null; }
1074
1074
  try { if (database) database.close(); } catch (_e) {}
1075
1075
  database = null;
1076
1076
  dbPath = null;
@@ -53,6 +53,7 @@
53
53
  var retryHelper = require("./retry");
54
54
  var C = require("./constants");
55
55
  var lazyRequire = require("./lazy-require");
56
+ var safeAsync = require("./safe-async");
56
57
  var { ExternalDbError } = require("./framework-error");
57
58
 
58
59
  var audit = lazyRequire(function () { return require("./audit"); });
@@ -78,8 +79,8 @@ class Pool {
78
79
  this.idle = []; // [{ client, lastUsedAt }]
79
80
  this.active = 0; // count of in-use clients
80
81
  this.waiters = []; // queued acquisitions when at max
81
- this._reaper = setInterval(this._reapIdle.bind(this), C.TIME.seconds(10));
82
- this._reaper.unref();
82
+ this._reaper = safeAsync.repeating(this._reapIdle.bind(this),
83
+ C.TIME.seconds(10), { name: "external-db-reaper" });
83
84
  }
84
85
 
85
86
  async acquire() {
@@ -141,7 +142,7 @@ class Pool {
141
142
  }
142
143
 
143
144
  async drain() {
144
- if (this._reaper) { clearInterval(this._reaper); this._reaper = null; }
145
+ if (this._reaper) { this._reaper.stop(); this._reaper = null; }
145
146
  var idleClients = this.idle.map(function (e) { return e.client; });
146
147
  this.idle = [];
147
148
  var self = this;