cursedbelt-server 4.19.1 → 4.20.0

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.
@@ -15,7 +15,7 @@
15
15
  * Two ways to get the events, and a consumer needs neither code nor a new Worker for the first:
16
16
  *
17
17
  * · **`wrangler tail <worker> --format json`** during a bench or a smoke against the live
18
- * Worker — one JSON trace per line, parsed with {@link parseTailLines}.
18
+ * Worker — pretty-printed objects back to back, parsed with {@link parseTailLines}.
19
19
  * · **a Tail Worker** (`tail_consumers` in the producer's wrangler config) whose `tail(events)`
20
20
  * hands the array straight to {@link recordTailTraces}.
21
21
  *
@@ -90,8 +90,14 @@ export interface TailRecordResult {
90
90
  */
91
91
  export declare function recordTailTraces(recorder: CpuRecorder, traces: readonly TailTraceLike[], opts?: RecordTailTracesOpts): TailRecordResult;
92
92
  /**
93
- * `wrangler tail --format json` output → trace events. Tolerates the banner lines wrangler
94
- * prints and blank lines; a line that is not a JSON object is skipped, never guessed at.
93
+ * `wrangler tail --format json` output → trace events.
94
+ *
95
+ * 🔴 That output is PRETTY-PRINTED — one object spread over ~100 lines, objects back to back — not
96
+ * one JSON document per line. Measured 2026-09-23 against the live `collections` Worker: 61 traces
97
+ * in 6,771 lines, and a line-by-line parser (this function's first version) found none of them. So
98
+ * this scans for balanced top-level `{…}` objects, string- and escape-aware, and parses each; it
99
+ * reads JSON-lines output identically. Banner text between objects is skipped, and a truncated
100
+ * object (a tail killed mid-write) is dropped rather than guessed at.
95
101
  */
96
102
  export declare function parseTailLines(text: string): TailTraceLike[];
97
103
  export interface TailTrafficRate {
@@ -15,7 +15,7 @@
15
15
  * Two ways to get the events, and a consumer needs neither code nor a new Worker for the first:
16
16
  *
17
17
  * · **`wrangler tail <worker> --format json`** during a bench or a smoke against the live
18
- * Worker — one JSON trace per line, parsed with {@link parseTailLines}.
18
+ * Worker — pretty-printed objects back to back, parsed with {@link parseTailLines}.
19
19
  * · **a Tail Worker** (`tail_consumers` in the producer's wrangler config) whose `tail(events)`
20
20
  * hands the array straight to {@link recordTailTraces}.
21
21
  *
@@ -144,22 +144,65 @@ export function recordTailTraces(recorder, traces, opts = {}) {
144
144
  return result;
145
145
  }
146
146
  /**
147
- * `wrangler tail --format json` output → trace events. Tolerates the banner lines wrangler
148
- * prints and blank lines; a line that is not a JSON object is skipped, never guessed at.
147
+ * `wrangler tail --format json` output → trace events.
148
+ *
149
+ * 🔴 That output is PRETTY-PRINTED — one object spread over ~100 lines, objects back to back — not
150
+ * one JSON document per line. Measured 2026-09-23 against the live `collections` Worker: 61 traces
151
+ * in 6,771 lines, and a line-by-line parser (this function's first version) found none of them. So
152
+ * this scans for balanced top-level `{…}` objects, string- and escape-aware, and parses each; it
153
+ * reads JSON-lines output identically. Banner text between objects is skipped, and a truncated
154
+ * object (a tail killed mid-write) is dropped rather than guessed at.
149
155
  */
150
156
  export function parseTailLines(text) {
151
157
  const out = [];
152
- for (const line of text.split('\n')) {
153
- const trimmed = line.trim();
154
- if (!trimmed.startsWith('{'))
158
+ let depth = 0;
159
+ let start = -1;
160
+ let inString = false;
161
+ let escaped = false;
162
+ for (let i = 0; i < text.length; i += 1) {
163
+ const ch = text[i];
164
+ if (inString && ch === '\n') {
165
+ // A raw newline cannot occur inside a JSON string: the object this began is truncated.
166
+ inString = false;
167
+ escaped = false;
168
+ depth = 0;
169
+ start = -1;
155
170
  continue;
156
- try {
157
- const parsed = JSON.parse(trimmed);
158
- if (parsed && typeof parsed === 'object')
159
- out.push(parsed);
160
171
  }
161
- catch {
162
- // a partial line from an interrupted tail — not a trace
172
+ if (inString) {
173
+ if (escaped)
174
+ escaped = false;
175
+ else if (ch === '\\')
176
+ escaped = true;
177
+ else if (ch === '"')
178
+ inString = false;
179
+ continue;
180
+ }
181
+ if (ch === '"' && depth > 0) {
182
+ inString = true;
183
+ }
184
+ else if (ch === '{') {
185
+ // wrangler writes every top-level object from column 0 and indents everything nested, so a
186
+ // `{` at the start of a line while one is still open means the open one was cut short.
187
+ if (depth > 0 && (i === 0 || text[i - 1] === '\n'))
188
+ depth = 0;
189
+ if (depth === 0)
190
+ start = i;
191
+ depth += 1;
192
+ }
193
+ else if (ch === '}' && depth > 0) {
194
+ depth -= 1;
195
+ if (depth === 0 && start >= 0) {
196
+ try {
197
+ const parsed = JSON.parse(text.slice(start, i + 1));
198
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
199
+ out.push(parsed);
200
+ }
201
+ catch {
202
+ // not JSON after all (banner text with braces in it) — not a trace
203
+ }
204
+ start = -1;
205
+ }
163
206
  }
164
207
  }
165
208
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt-server",
3
- "version": "4.19.1",
3
+ "version": "4.20.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The app-facing Bun/Hono server tier of the cursedbelt split — storage, sharing, activity, guard, sync. React-free; cursedbelt-core below it.",
@@ -18,13 +18,14 @@
18
18
  "scripts": {
19
19
  "clean": "rm -rf dist",
20
20
  "typecheck": "tsc -p tsconfig.build.json --noEmit",
21
- "test": "NODE_ENV=development bun test src",
21
+ "test": "NODE_ENV=development bun test --timeout 60000 --parallel=6 src",
22
22
  "build": "bun run clean && tsc -p tsconfig.build.json",
23
23
  "paths": "bun run scripts/paths.ts",
24
24
  "surface": "public-surface",
25
- "verify": "bun run paths && bun run typecheck && bun run build && bun run test",
26
- "prepublishOnly": "bun run verify",
27
- "test:pg": "bun run scripts/testPg.ts"
25
+ "verify": "bun run gate",
26
+ "prepublishOnly": "bun run gate --force",
27
+ "test:pg": "bun run scripts/testPg.ts",
28
+ "gate": "bun run scripts/paths.ts --only gate.ts"
28
29
  },
29
30
  "files": [
30
31
  "dist",
@@ -58,6 +59,12 @@
58
59
  "source": "./src/server/analytics/index.ts",
59
60
  "import": "./dist/server/analytics/index.js"
60
61
  },
62
+ "./app-data-home": {
63
+ "types": "./dist/server/paths/appDataHome.d.ts",
64
+ "bun": "./src/server/paths/appDataHome.ts",
65
+ "source": "./src/server/paths/appDataHome.ts",
66
+ "import": "./dist/server/paths/appDataHome.js"
67
+ },
61
68
  "./bench": {
62
69
  "types": "./dist/server/bench/index.d.ts",
63
70
  "bun": "./src/server/bench/index.ts",
@@ -339,5 +346,25 @@
339
346
  "sharp": "^0.35.2",
340
347
  "typescript": "^6.0.3",
341
348
  "zod": "4.4.3"
349
+ },
350
+ "gate": {
351
+ "stages": [
352
+ {
353
+ "script": "paths"
354
+ },
355
+ {
356
+ "script": "typecheck"
357
+ },
358
+ {
359
+ "script": "build"
360
+ },
361
+ {
362
+ "script": "test",
363
+ "needs": [
364
+ "build"
365
+ ],
366
+ "why": "build starts with `rm -rf dist`, and barrelsReachNoOptionalPeer / the publish-shape specs read dist/; racing it they read a directory that is not there yet"
367
+ }
368
+ ]
342
369
  }
343
370
  }
@@ -104,6 +104,16 @@ const REPO = fileURLToPath(new URL('..', import.meta.url));
104
104
  * follows it to the new file rather than going quietly green against the old one.
105
105
  */
106
106
  const LEAVES = [
107
+ {
108
+ subpath: './app-data-home',
109
+ /**
110
+ * Where a PUBLISHED package keeps per-user data: `APP_DATA_HOME`, then XDG, then the OS
111
+ * convention (task 2117). `cursedbelt-cc`'s local host needs this one function, and until
112
+ * this leaf it could reach it only through the root barrel — which made every cc install
113
+ * an `otplib`/`kysely` install. `node:os` and `node:path` only.
114
+ */
115
+ evaluates: 'resolveAppDataHome',
116
+ },
107
117
  {
108
118
  subpath: './storage/types',
109
119
  /** Claim/config shapes + the keyspace helpers. apps/collections wants `FileTokenClaims`. */
@@ -117,6 +117,18 @@ describe('parseTailLines and tailTrafficRate', () => {
117
117
  expect(parseTailLines(text).map((t) => t.cpuTime)).toEqual([1, 2]);
118
118
  });
119
119
 
120
+ it('🔴 reads PRETTY-PRINTED objects back to back — what wrangler actually prints', () => {
121
+ // The shape measured against the live `collections` Worker, 2026-09-23: 61 traces over
122
+ // 6,771 lines. The first version of this parser split on newlines and found none.
123
+ const text = [
124
+ 'Successfully created tail',
125
+ JSON.stringify({ ...trace('GET', '/healthz', 16, 1), logs: [{ message: ['a } brace', 'q "x"'] }] }, null, 4),
126
+ JSON.stringify(trace('GET', '/api/folders', 3, 2), null, 4),
127
+ JSON.stringify(trace('GET', '/cut', 9, 3), null, 4).slice(0, 40),
128
+ ].join('\n');
129
+ expect(parseTailLines(text).map((t) => t.cpuTime)).toEqual([16, 3]);
130
+ });
131
+
120
132
  it('scales a window to requests/day — and refuses to scale one shorter than an hour', () => {
121
133
  const hour = 3_600_000;
122
134
  const two = [trace('GET', '/healthz', 1, 0), trace('GET', '/healthz', 1, 2 * hour), { cpuTime: 5 }];
@@ -15,7 +15,7 @@
15
15
  * Two ways to get the events, and a consumer needs neither code nor a new Worker for the first:
16
16
  *
17
17
  * · **`wrangler tail <worker> --format json`** during a bench or a smoke against the live
18
- * Worker — one JSON trace per line, parsed with {@link parseTailLines}.
18
+ * Worker — pretty-printed objects back to back, parsed with {@link parseTailLines}.
19
19
  * · **a Tail Worker** (`tail_consumers` in the producer's wrangler config) whose `tail(events)`
20
20
  * hands the array straight to {@link recordTailTraces}.
21
21
  *
@@ -192,19 +192,56 @@ export function recordTailTraces(
192
192
  }
193
193
 
194
194
  /**
195
- * `wrangler tail --format json` output → trace events. Tolerates the banner lines wrangler
196
- * prints and blank lines; a line that is not a JSON object is skipped, never guessed at.
195
+ * `wrangler tail --format json` output → trace events.
196
+ *
197
+ * 🔴 That output is PRETTY-PRINTED — one object spread over ~100 lines, objects back to back — not
198
+ * one JSON document per line. Measured 2026-09-23 against the live `collections` Worker: 61 traces
199
+ * in 6,771 lines, and a line-by-line parser (this function's first version) found none of them. So
200
+ * this scans for balanced top-level `{…}` objects, string- and escape-aware, and parses each; it
201
+ * reads JSON-lines output identically. Banner text between objects is skipped, and a truncated
202
+ * object (a tail killed mid-write) is dropped rather than guessed at.
197
203
  */
198
204
  export function parseTailLines(text: string): TailTraceLike[] {
199
205
  const out: TailTraceLike[] = [];
200
- for (const line of text.split('\n')) {
201
- const trimmed = line.trim();
202
- if (!trimmed.startsWith('{')) continue;
203
- try {
204
- const parsed = JSON.parse(trimmed) as unknown;
205
- if (parsed && typeof parsed === 'object') out.push(parsed as TailTraceLike);
206
- } catch {
207
- // a partial line from an interrupted tail — not a trace
206
+ let depth = 0;
207
+ let start = -1;
208
+ let inString = false;
209
+ let escaped = false;
210
+ for (let i = 0; i < text.length; i += 1) {
211
+ const ch = text[i];
212
+ if (inString && ch === '\n') {
213
+ // A raw newline cannot occur inside a JSON string: the object this began is truncated.
214
+ inString = false;
215
+ escaped = false;
216
+ depth = 0;
217
+ start = -1;
218
+ continue;
219
+ }
220
+ if (inString) {
221
+ if (escaped) escaped = false;
222
+ else if (ch === '\\') escaped = true;
223
+ else if (ch === '"') inString = false;
224
+ continue;
225
+ }
226
+ if (ch === '"' && depth > 0) {
227
+ inString = true;
228
+ } else if (ch === '{') {
229
+ // wrangler writes every top-level object from column 0 and indents everything nested, so a
230
+ // `{` at the start of a line while one is still open means the open one was cut short.
231
+ if (depth > 0 && (i === 0 || text[i - 1] === '\n')) depth = 0;
232
+ if (depth === 0) start = i;
233
+ depth += 1;
234
+ } else if (ch === '}' && depth > 0) {
235
+ depth -= 1;
236
+ if (depth === 0 && start >= 0) {
237
+ try {
238
+ const parsed = JSON.parse(text.slice(start, i + 1)) as unknown;
239
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) out.push(parsed as TailTraceLike);
240
+ } catch {
241
+ // not JSON after all (banner text with braces in it) — not a trace
242
+ }
243
+ start = -1;
244
+ }
208
245
  }
209
246
  }
210
247
  return out;