@linxiraos/pi-utils 1.1.8 → 1.1.9

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.
Files changed (170) hide show
  1. package/CHANGELOG.md +2 -3
  2. package/README.md +18 -18
  3. package/package.json +57 -63
  4. package/src/async.ts +16 -0
  5. package/src/dirs.ts +15 -10
  6. package/src/incoming-json.ts +856 -0
  7. package/src/index.ts +1 -0
  8. package/src/json-lexer.ts +413 -0
  9. package/src/json-parse.ts +93 -350
  10. package/src/tls-fetch.ts +2 -16
  11. package/THIRD-PARTY-NOTICES.txt +0 -22909
  12. package/dist/types/abortable.d.ts +0 -32
  13. package/dist/types/acp/connection.d.ts +0 -118
  14. package/dist/types/acp/protocol.d.ts +0 -526
  15. package/dist/types/acp/schema.d.ts +0 -41
  16. package/dist/types/acp/stream.d.ts +0 -8
  17. package/dist/types/acp/transport.d.ts +0 -88
  18. package/dist/types/acp.d.ts +0 -6
  19. package/dist/types/ar/arj.d.ts +0 -5
  20. package/dist/types/ar/asar.d.ts +0 -7
  21. package/dist/types/ar/bytes.d.ts +0 -20
  22. package/dist/types/ar/cab.d.ts +0 -5
  23. package/dist/types/ar/checksums.d.ts +0 -10
  24. package/dist/types/ar/codecs/bzip2.d.ts +0 -4
  25. package/dist/types/ar/codecs/gzip.d.ts +0 -6
  26. package/dist/types/ar/codecs/lzma.d.ts +0 -6
  27. package/dist/types/ar/codecs/lzw.d.ts +0 -4
  28. package/dist/types/ar/codecs/lzx.d.ts +0 -7
  29. package/dist/types/ar/codecs/xz.d.ts +0 -4
  30. package/dist/types/ar/codecs/zstd.d.ts +0 -6
  31. package/dist/types/ar/cpio.d.ts +0 -7
  32. package/dist/types/ar/deb.d.ts +0 -5
  33. package/dist/types/ar/entries.d.ts +0 -22
  34. package/dist/types/ar/error.d.ts +0 -8
  35. package/dist/types/ar/index.d.ts +0 -11
  36. package/dist/types/ar/iso.d.ts +0 -5
  37. package/dist/types/ar/limits.d.ts +0 -33
  38. package/dist/types/ar/lzh.d.ts +0 -7
  39. package/dist/types/ar/open.d.ts +0 -45
  40. package/dist/types/ar/paths.d.ts +0 -18
  41. package/dist/types/ar/rar/rar4-decoder.d.ts +0 -6
  42. package/dist/types/ar/rar/rar5-decoder.d.ts +0 -6
  43. package/dist/types/ar/rar.d.ts +0 -5
  44. package/dist/types/ar/reader.d.ts +0 -28
  45. package/dist/types/ar/registry.d.ts +0 -17
  46. package/dist/types/ar/rpm.d.ts +0 -5
  47. package/dist/types/ar/sevenzip/decode.d.ts +0 -33
  48. package/dist/types/ar/sevenzip.d.ts +0 -5
  49. package/dist/types/ar/source.d.ts +0 -54
  50. package/dist/types/ar/tar.d.ts +0 -9
  51. package/dist/types/ar/types.d.ts +0 -99
  52. package/dist/types/ar/unix-ar.d.ts +0 -7
  53. package/dist/types/ar/write.d.ts +0 -10
  54. package/dist/types/ar/zip.d.ts +0 -10
  55. package/dist/types/async.d.ts +0 -22
  56. package/dist/types/binary.d.ts +0 -23
  57. package/dist/types/browsers.d.ts +0 -68
  58. package/dist/types/chalk.d.ts +0 -125
  59. package/dist/types/cli.d.ts +0 -134
  60. package/dist/types/color.d.ts +0 -136
  61. package/dist/types/dates.d.ts +0 -7
  62. package/dist/types/dirs.d.ts +0 -325
  63. package/dist/types/docx/converter.d.ts +0 -46
  64. package/dist/types/docx/xml.d.ts +0 -26
  65. package/dist/types/docx.d.ts +0 -11
  66. package/dist/types/dom/core.d.ts +0 -431
  67. package/dist/types/dom/parser.d.ts +0 -7
  68. package/dist/types/dom/selector.d.ts +0 -5
  69. package/dist/types/dom.d.ts +0 -5
  70. package/dist/types/env.d.ts +0 -133
  71. package/dist/types/fetch-retry.d.ts +0 -98
  72. package/dist/types/file-lock.d.ts +0 -23
  73. package/dist/types/format.d.ts +0 -37
  74. package/dist/types/frontmatter.d.ts +0 -46
  75. package/dist/types/fs-error.d.ts +0 -31
  76. package/dist/types/glob.d.ts +0 -28
  77. package/dist/types/headers.d.ts +0 -34
  78. package/dist/types/index.d.ts +0 -42
  79. package/dist/types/json-parse.d.ts +0 -83
  80. package/dist/types/json.d.ts +0 -20
  81. package/dist/types/logger/rotating-file.d.ts +0 -18
  82. package/dist/types/logger.d.ts +0 -96
  83. package/dist/types/loop-phase.d.ts +0 -10
  84. package/dist/types/lru.d.ts +0 -46
  85. package/dist/types/marked/core.d.ts +0 -445
  86. package/dist/types/marked.d.ts +0 -2
  87. package/dist/types/materialize-string.d.ts +0 -7
  88. package/dist/types/math-delimiters.d.ts +0 -45
  89. package/dist/types/mermaid-ascii.d.ts +0 -11
  90. package/dist/types/mime.d.ts +0 -29
  91. package/dist/types/module-timer.d.ts +0 -1
  92. package/dist/types/path-tree.d.ts +0 -76
  93. package/dist/types/path.d.ts +0 -4
  94. package/dist/types/peek-file.d.ts +0 -29
  95. package/dist/types/postmortem.d.ts +0 -178
  96. package/dist/types/process-name.d.ts +0 -7
  97. package/dist/types/procmgr.d.ts +0 -74
  98. package/dist/types/prompt.d.ts +0 -18
  99. package/dist/types/ptree.d.ts +0 -124
  100. package/dist/types/readability/readability.d.ts +0 -9
  101. package/dist/types/readability/readerable.d.ts +0 -10
  102. package/dist/types/readability/types.d.ts +0 -70
  103. package/dist/types/readability.d.ts +0 -4
  104. package/dist/types/ring.d.ts +0 -93
  105. package/dist/types/runtime-install.d.ts +0 -85
  106. package/dist/types/sanitize-text.d.ts +0 -29
  107. package/dist/types/snowflake.d.ts +0 -25
  108. package/dist/types/sqlite.d.ts +0 -26
  109. package/dist/types/stderr-guard.d.ts +0 -22
  110. package/dist/types/stream.d.ts +0 -75
  111. package/dist/types/tab-spacing.d.ts +0 -24
  112. package/dist/types/temp.d.ts +0 -24
  113. package/dist/types/template.d.ts +0 -62
  114. package/dist/types/timing-buffer.d.ts +0 -22
  115. package/dist/types/tls-fetch.d.ts +0 -37
  116. package/dist/types/turndown/gfm.d.ts +0 -11
  117. package/dist/types/turndown/html.d.ts +0 -5
  118. package/dist/types/turndown/service.d.ts +0 -21
  119. package/dist/types/turndown/types.d.ts +0 -70
  120. package/dist/types/turndown.d.ts +0 -4
  121. package/dist/types/type-guards.d.ts +0 -5
  122. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +0 -41
  123. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +0 -89
  124. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +0 -7
  125. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +0 -12
  126. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +0 -66
  127. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +0 -48
  128. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +0 -43
  129. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +0 -7
  130. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +0 -56
  131. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +0 -65
  132. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +0 -27
  133. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +0 -17
  134. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +0 -7
  135. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +0 -11
  136. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +0 -34
  137. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +0 -11
  138. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +0 -11
  139. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +0 -26
  140. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +0 -31
  141. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +0 -11
  142. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +0 -59
  143. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +0 -17
  144. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +0 -30
  145. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +0 -55
  146. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +0 -206
  147. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +0 -51
  148. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +0 -2
  149. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +0 -6
  150. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +0 -102
  151. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +0 -6
  152. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +0 -76
  153. package/dist/types/vendor/mermaid-ascii/index.d.ts +0 -1
  154. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +0 -9
  155. package/dist/types/vendor/mermaid-ascii/parser.d.ts +0 -7
  156. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +0 -6
  157. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +0 -130
  158. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +0 -23
  159. package/dist/types/vendor/mermaid-ascii/types.d.ts +0 -114
  160. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +0 -25
  161. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +0 -6
  162. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +0 -145
  163. package/dist/types/version.d.ts +0 -18
  164. package/dist/types/vterm/buffer.d.ts +0 -99
  165. package/dist/types/vterm/query-responder.d.ts +0 -24
  166. package/dist/types/vterm/terminal.d.ts +0 -44
  167. package/dist/types/vterm.d.ts +0 -9
  168. package/dist/types/which.d.ts +0 -37
  169. package/dist/types/worker-host.d.ts +0 -51
  170. package/dist/types/xml.d.ts +0 -31
@@ -0,0 +1,856 @@
1
+ /**
2
+ * Typed cursors over a JSON document while its text is still arriving.
3
+ *
4
+ * {@link IncomingDoc.channel} returns a push-side {@link IncomingFeed} and a
5
+ * read-side {@link IncomingDoc}. The producer appends text fragments, then
6
+ * explicitly calls {@link IncomingFeed.finish} or {@link IncomingFeed.abort};
7
+ * a feed that is never closed leaves every pending pull waiting forever.
8
+ * There is one shared append-only buffer; cursors are cheap, immutable
9
+ * path handles over it, and every pull is an ordinary promise that
10
+ * re-scans the buffer whenever the feed changes. There are no snapshots,
11
+ * per-field events, or fan-out channels.
12
+ *
13
+ * A scalar completes at its closing quote/delimiter, and a container
14
+ * completes only when its closing delimiter arrives. Finished-but-truncated
15
+ * input rejects with kind `incomplete`; abandoned input rejects with
16
+ * `aborted`. String chunks contain only decoded text whose meaning is
17
+ * stable, so an escape or Unicode escape may span any number of fragments.
18
+ *
19
+ * Pulling an {@link IncomingObject.key} makes that key required: a missing
20
+ * or mistyped value is a structured {@link IncomingJsonError}. Object members
21
+ * never pulled are skipped without validation. {@link IncomingDoc.whole} is
22
+ * the explicit whole-document pull and runs only after successful input
23
+ * completion.
24
+ *
25
+ * Object cursors bind the first occurrence of a duplicate key, whereas
26
+ * complete-value pulls (`value()`, `collect()`, `whole()`) go through the
27
+ * final parser (`parseJsonWithRepair`), whose objects are last-write-wins.
28
+ *
29
+ * Mid-stream cursors tolerate incomplete tokens but read double-quoted
30
+ * strings with the final parser's strict closing rule: an unescaped inner
31
+ * `"` can never swallow a sibling key or value. A pulled scalar completes
32
+ * only once a value terminator follows it, like numbers, so structural
33
+ * garbage after a value surfaces as `incomplete` rather than a silently
34
+ * misparsed pull. Single-quote recovery (`'it's'`) is shared with the final
35
+ * parser and passes both.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const { feed, doc } = IncomingDoc.channel();
40
+ * const args = doc.root().object();
41
+ * const content = args.key("content").string();
42
+ * feed.push('{"path":"a.ts","content":"hel');
43
+ * await content.nextChunk(); // "hel"
44
+ * feed.push('lo"}');
45
+ * feed.finish();
46
+ * await content.nextChunk(); // "lo"
47
+ * await content.nextChunk(); // undefined
48
+ * await args.key("path").value<string>(); // "a.ts"
49
+ * ```
50
+ */
51
+
52
+ import { Serial } from "./async";
53
+ import {
54
+ COLON,
55
+ COMMA,
56
+ isNumberStart,
57
+ JsonLexer,
58
+ LBRACE,
59
+ LBRACKET,
60
+ QUOTE,
61
+ RBRACE,
62
+ RBRACKET,
63
+ SQUOTE,
64
+ } from "./json-lexer";
65
+ import { parseJsonWithRepair } from "./json-parse";
66
+
67
+ /** Maximum container nesting before a scan reports the value as pending forever. */
68
+ const MAX_DEPTH = 128;
69
+
70
+ /** Location component in a pulled JSON path: an object member name or an array index. */
71
+ export type PullPathSegment = string | number;
72
+
73
+ /** JSON shape observed by a started pull. */
74
+ export type IncomingValueKind = "null" | "boolean" | "number" | "string" | "array" | "object";
75
+
76
+ /** Why a pull could not produce the requested shape. */
77
+ export type PullIssueKind =
78
+ /** The requested member was absent when its container completed. */
79
+ | "missing"
80
+ /** The producer finished before the pulled value's closing token. */
81
+ | "incomplete"
82
+ /** The producer abandoned the input before the pull completed. */
83
+ | "aborted"
84
+ /** A complete pulled value could not be parsed. */
85
+ | "malformed"
86
+ /** A value was present with a different JSON shape (see `found`). */
87
+ | "mismatch";
88
+
89
+ /** Structured failure while awaiting an incoming JSON value. */
90
+ export class IncomingJsonError extends Error {
91
+ /** Full key/index path pulled by the consumer. */
92
+ readonly path: readonly PullPathSegment[];
93
+ /** Shape requested by the typed cursor. */
94
+ readonly expected: string;
95
+ /** Why the pull could not produce that shape. */
96
+ readonly kind: PullIssueKind;
97
+ /** Shape observed in the input when `kind` is `mismatch`. */
98
+ readonly found?: string;
99
+
100
+ constructor(
101
+ path: readonly PullPathSegment[],
102
+ expected: string,
103
+ kind: PullIssueKind,
104
+ options?: { found?: string; cause?: unknown },
105
+ ) {
106
+ const at = path.length === 0 ? "$" : `$${path.map(s => (typeof s === "number" ? `[${s}]` : `.${s}`)).join("")}`;
107
+ const why = kind === "mismatch" ? `found ${options?.found}` : kind;
108
+ super(`invalid JSON pull at ${at}: expected ${expected} (${why})`, { cause: options?.cause });
109
+ this.name = "IncomingJsonError";
110
+ this.path = path;
111
+ this.expected = expected;
112
+ this.kind = kind;
113
+ this.found = options?.found;
114
+ }
115
+ }
116
+
117
+ type Shape =
118
+ | { kind: "null" | "array" | "object" }
119
+ | { kind: "boolean"; value: boolean }
120
+ | { kind: "number"; value: number }
121
+ | { kind: "string"; value: string; stableLen: number };
122
+
123
+ interface Located {
124
+ tag: "located";
125
+ /** Source offset of the value's first char. */
126
+ start: number;
127
+ /** Source offset just past the value's closing token; `undefined` while it is still open. */
128
+ end: number | undefined;
129
+ shape: Shape;
130
+ }
131
+
132
+ type Probe =
133
+ | Located
134
+ /** The selected value has not started, or a token on the way to it is truncated. */
135
+ | { tag: "pending" }
136
+ /** The selected member's container closed without it. */
137
+ | { tag: "missing" }
138
+ /** A container on the path has a different shape than the path segment demands. */
139
+ | { tag: "mismatch"; expected: string; found: string };
140
+
141
+ const PENDING: Probe = { tag: "pending" };
142
+ const MISSING: Probe = { tag: "missing" };
143
+
144
+ /** Readiness predicate a pull waits for once its value has been located. */
145
+ type Ready = (located: Located) => boolean;
146
+
147
+ const STARTED: Ready = () => true;
148
+ const COMPLETE: Ready = located => located.end !== undefined;
149
+
150
+ /**
151
+ * One member reached by a selecting scan: where it starts, and the index of
152
+ * its container value once a scan has descended into it.
153
+ */
154
+ interface MemberSlot {
155
+ /** Source offset of the member's first char (key for objects, value for arrays). */
156
+ offset: number;
157
+ child?: ContainerIndex;
158
+ }
159
+
160
+ /**
161
+ * Member index of one container on a pulled path, so repeated pulls resume
162
+ * where the previous scan stopped instead of re-lexing from the container's
163
+ * opening delimiter. `frontier` is the source offset just after the opening
164
+ * delimiter or the comma following the last fully scanned member — recorded
165
+ * before skipping whitespace, so an unterminated comment at the buffer edge
166
+ * is re-read once its tail arrives. Every member starting before the frontier
167
+ * is recorded in `keys` (first occurrence wins) or `elements`; `tail` is the
168
+ * array element at `elements.length`, which may still be open.
169
+ */
170
+ class ContainerIndex {
171
+ frontier: number;
172
+ readonly keys = new Map<string, MemberSlot>();
173
+ readonly elements: MemberSlot[] = [];
174
+ tail?: MemberSlot;
175
+
176
+ constructor(frontier: number) {
177
+ this.frontier = frontier;
178
+ }
179
+
180
+ /** Advance the frontier to the cursor when a frontier scan moved past it. */
181
+ advance(lex: JsonLexer): void {
182
+ if (lex.pos > this.frontier) this.frontier = lex.pos;
183
+ }
184
+ }
185
+
186
+ /** Append-only buffer, terminal state, and change notification shared by one feed and its cursors. */
187
+ class Shared {
188
+ text = "";
189
+ end: "open" | "finished" | "aborted" = "open";
190
+ readonly #root: MemberSlot = { offset: 0 };
191
+ #changed = Promise.withResolvers<void>();
192
+
193
+ /** Resolves once the buffer or terminal state changes after this call. */
194
+ get changed(): Promise<void> {
195
+ return this.#changed.promise;
196
+ }
197
+
198
+ notify(): void {
199
+ const changed = this.#changed;
200
+ this.#changed = Promise.withResolvers<void>();
201
+ changed.resolve();
202
+ }
203
+
204
+ /** Await input completion; rejects with `aborted` when the feed was abandoned. */
205
+ async finished(): Promise<void> {
206
+ while (this.end === "open") await this.changed;
207
+ if (this.end === "aborted") throw new IncomingJsonError([], "document", "aborted");
208
+ }
209
+
210
+ /**
211
+ * Await the value at `path` until `ready` accepts it. Resolves `undefined`
212
+ * when the value's container completed without it.
213
+ */
214
+ async pull(path: readonly PullPathSegment[], expected: string, ready: Ready): Promise<Located | undefined> {
215
+ for (;;) {
216
+ const probe = this.#locate(path);
217
+ switch (probe.tag) {
218
+ case "located":
219
+ if (ready(probe)) return probe;
220
+ break;
221
+ case "missing":
222
+ return undefined;
223
+ case "mismatch":
224
+ throw new IncomingJsonError(path, probe.expected, "mismatch", { found: probe.found });
225
+ }
226
+ if (this.end === "finished") throw new IncomingJsonError(path, expected, "incomplete");
227
+ if (this.end === "aborted") throw new IncomingJsonError(path, expected, "aborted");
228
+ await this.changed;
229
+ }
230
+ }
231
+
232
+ #locate(path: readonly PullPathSegment[]): Probe {
233
+ const lex = new JsonLexer(this.text, "incoming");
234
+ lex.ws();
235
+ return selectValue(lex, path, 0, this.end === "finished", 0, this.#root);
236
+ }
237
+ }
238
+
239
+ function mismatch(expected: string, found: string): Probe {
240
+ return { tag: "mismatch", expected, found };
241
+ }
242
+
243
+ function located(start: number, end: number | undefined, shape: Shape): Located {
244
+ return { tag: "located", start, end, shape };
245
+ }
246
+
247
+ /**
248
+ * Descend into the value at the cursor following `path[at..]`. `slot` is the
249
+ * member holding this value; its container index is created on first descent
250
+ * and lets later pulls resume the member scan instead of restarting it.
251
+ */
252
+ function selectValue(
253
+ lex: JsonLexer,
254
+ path: readonly PullPathSegment[],
255
+ at: number,
256
+ ended: boolean,
257
+ depth: number,
258
+ slot: MemberSlot,
259
+ ): Probe {
260
+ const c = lex.peek();
261
+ if (Number.isNaN(c)) return PENDING;
262
+ if (at >= path.length) return scanValue(lex, ended, depth);
263
+ const segment = path[at];
264
+ if (c !== (typeof segment === "string" ? LBRACE : LBRACKET)) {
265
+ return mismatch(typeof segment === "string" ? "object" : "array", charName(c));
266
+ }
267
+ if (depth >= MAX_DEPTH) return PENDING;
268
+ // biome-ignore lint/suspicious/noAssignInExpressions: parser state machine relies on the inline memoized slot
269
+ const index = (slot.child ??= new ContainerIndex(lex.pos + 1));
270
+ if (typeof segment === "string") {
271
+ lex.pos = index.keys.get(segment)?.offset ?? index.frontier;
272
+ return selectKey(lex, segment, path, at + 1, ended, depth, index);
273
+ }
274
+ const known = index.elements[segment];
275
+ lex.pos = known?.offset ?? index.frontier;
276
+ return selectIndex(lex, segment, path, at + 1, ended, depth, known ? segment : index.elements.length, index);
277
+ }
278
+
279
+ /**
280
+ * Scan object members from the cursor (positioned at a member start or the
281
+ * closing brace) until `wanted` is found, then descend along `path[at..]`.
282
+ * Binds the first occurrence.
283
+ */
284
+ function selectKey(
285
+ lex: JsonLexer,
286
+ wanted: string,
287
+ path: readonly PullPathSegment[],
288
+ at: number,
289
+ ended: boolean,
290
+ depth: number,
291
+ index: ContainerIndex,
292
+ ): Probe {
293
+ if (depth >= MAX_DEPTH) return PENDING;
294
+ for (;;) {
295
+ index.advance(lex);
296
+ lex.ws();
297
+ const c = lex.peek();
298
+ if (Number.isNaN(c)) return PENDING;
299
+ if (c === RBRACE) {
300
+ lex.pos++;
301
+ return MISSING;
302
+ }
303
+ if (c === COMMA) {
304
+ lex.pos++;
305
+ continue;
306
+ }
307
+ const memberStart = lex.pos;
308
+ let key: string;
309
+ if (c === QUOTE || c === SQUOTE) {
310
+ const progress = lex.string(c);
311
+ if (!progress.complete) return PENDING;
312
+ key = progress.value;
313
+ } else {
314
+ key = lex.unquotedKey();
315
+ if (key.length === 0) return PENDING;
316
+ }
317
+ lex.ws();
318
+ if (lex.peek() !== COLON) return PENDING;
319
+ // The colon fixes the key's spelling; only now is the member stable.
320
+ let slot = index.keys.get(key);
321
+ if (slot === undefined) {
322
+ slot = { offset: memberStart };
323
+ index.keys.set(key, slot);
324
+ }
325
+ lex.pos++;
326
+ lex.ws();
327
+ if (lex.atEnd) return PENDING;
328
+ if (key === wanted) return selectValue(lex, path, at, ended, depth + 1, slot);
329
+ const skipped = scanValue(lex, ended, depth + 1);
330
+ if (skipped.tag !== "located" || skipped.end === undefined) return PENDING;
331
+ lex.ws();
332
+ const d = lex.peek();
333
+ if (d === COMMA) {
334
+ lex.pos++;
335
+ } else if (d === RBRACE) {
336
+ lex.pos++;
337
+ return MISSING;
338
+ } else {
339
+ return PENDING;
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Scan array elements from the cursor (positioned at element `at` or the
346
+ * closing bracket) until `wanted` is reached, then descend along `path[at..]`.
347
+ */
348
+ function selectIndex(
349
+ lex: JsonLexer,
350
+ wanted: number,
351
+ path: readonly PullPathSegment[],
352
+ at: number,
353
+ ended: boolean,
354
+ depth: number,
355
+ element: number,
356
+ index: ContainerIndex,
357
+ ): Probe {
358
+ if (depth >= MAX_DEPTH) return PENDING;
359
+ for (;;) {
360
+ index.advance(lex);
361
+ lex.ws();
362
+ const c = lex.peek();
363
+ if (Number.isNaN(c)) return PENDING;
364
+ if (c === RBRACKET) {
365
+ lex.pos++;
366
+ return MISSING;
367
+ }
368
+ if (c === COMMA) {
369
+ lex.pos++;
370
+ continue;
371
+ }
372
+ // biome-ignore lint/suspicious/noAssignInExpressions: parser state machine relies on the inline memoized slot
373
+ const slot = index.elements[element] ?? (index.tail ??= { offset: lex.pos });
374
+ if (element === wanted) return selectValue(lex, path, at, ended, depth + 1, slot);
375
+ const skipped = scanValue(lex, ended, depth + 1);
376
+ if (skipped.tag !== "located" || skipped.end === undefined) return PENDING;
377
+ if (element === index.elements.length) {
378
+ index.elements.push(slot);
379
+ index.tail = undefined;
380
+ }
381
+ element++;
382
+ lex.ws();
383
+ const d = lex.peek();
384
+ if (d === COMMA) {
385
+ lex.pos++;
386
+ } else if (d === RBRACKET) {
387
+ lex.pos++;
388
+ return MISSING;
389
+ } else {
390
+ return PENDING;
391
+ }
392
+ }
393
+ }
394
+
395
+ /** Locate the value at the cursor and determine whether it is complete. */
396
+ function scanValue(lex: JsonLexer, ended: boolean, depth: number): Probe {
397
+ const start = lex.pos;
398
+ const c = lex.peek();
399
+ if (Number.isNaN(c)) return PENDING;
400
+ if (c === LBRACE) return scanContainer(lex, ended, depth, start, "object");
401
+ if (c === LBRACKET) return scanContainer(lex, ended, depth, start, "array");
402
+ if (c === QUOTE || c === SQUOTE) {
403
+ const progress = lex.string(c);
404
+ const end = lex.pos;
405
+ // Like numbers and keywords, a string is complete only once a value
406
+ // terminator follows (or the input ended): an edge close may still be
407
+ // reopened by later fragments via single-quote recovery.
408
+ const complete = progress.complete && scalarComplete(lex, ended);
409
+ return located(start, complete ? end : undefined, {
410
+ kind: "string",
411
+ value: progress.value,
412
+ stableLen: progress.stableLen,
413
+ });
414
+ }
415
+ if (isNumberStart(c)) {
416
+ const value = lex.number();
417
+ if (value === undefined) return PENDING;
418
+ const end = lex.pos;
419
+ return located(start, scalarComplete(lex, ended) ? end : undefined, { kind: "number", value });
420
+ }
421
+ const keyword = lex.keyword();
422
+ if (keyword !== undefined) {
423
+ const end = lex.pos;
424
+ const complete = scalarComplete(lex, ended);
425
+ return located(
426
+ start,
427
+ complete ? end : undefined,
428
+ keyword === null ? { kind: "null" } : { kind: "boolean", value: keyword },
429
+ );
430
+ }
431
+ const word = lex.bareword();
432
+ if (word === undefined) return PENDING;
433
+ const end = lex.pos;
434
+ return located(start, scalarComplete(lex, ended) ? end : undefined, {
435
+ kind: "string",
436
+ value: word,
437
+ stableLen: word.length,
438
+ });
439
+ }
440
+
441
+ /** A scalar is complete once a value terminator follows it, or the finished input ends. */
442
+ function scalarComplete(lex: JsonLexer, ended: boolean): boolean {
443
+ lex.ws();
444
+ const c = lex.peek();
445
+ return c === COMMA || c === RBRACE || c === RBRACKET || (ended && lex.atEnd);
446
+ }
447
+
448
+ function scanContainer(lex: JsonLexer, ended: boolean, depth: number, start: number, kind: "object" | "array"): Probe {
449
+ if (depth >= MAX_DEPTH) return PENDING;
450
+ const close = kind === "object" ? RBRACE : RBRACKET;
451
+ const open = located(start, undefined, { kind });
452
+ lex.pos++;
453
+ for (;;) {
454
+ lex.ws();
455
+ const c = lex.peek();
456
+ if (Number.isNaN(c)) return open;
457
+ if (c === close) {
458
+ lex.pos++;
459
+ return located(start, lex.pos, { kind });
460
+ }
461
+ if (c === COMMA) {
462
+ lex.pos++;
463
+ continue;
464
+ }
465
+ if (kind === "object") {
466
+ if (c === QUOTE || c === SQUOTE) {
467
+ if (!lex.string(c).complete) return open;
468
+ } else if (lex.unquotedKey().length === 0) {
469
+ return open;
470
+ }
471
+ lex.ws();
472
+ if (lex.peek() !== COLON) return open;
473
+ lex.pos++;
474
+ lex.ws();
475
+ }
476
+ const value = scanValue(lex, ended, depth + 1);
477
+ if (value.tag !== "located" || value.end === undefined) return open;
478
+ lex.ws();
479
+ const d = lex.peek();
480
+ if (d === COMMA) {
481
+ lex.pos++;
482
+ } else if (d === close) {
483
+ lex.pos++;
484
+ return located(start, lex.pos, { kind });
485
+ } else {
486
+ return open;
487
+ }
488
+ }
489
+ }
490
+
491
+ /** Shape name implied by a value's first char, for mismatch reports. */
492
+ function charName(c: number): string {
493
+ if (c === LBRACE) return "object";
494
+ if (c === LBRACKET) return "array";
495
+ if (c === QUOTE || c === SQUOTE) return "string";
496
+ if (isNumberStart(c)) return "number";
497
+ if ((c | 0x20) === 0x74 /* t */ || (c | 0x20) === 0x66 /* f */) return "boolean";
498
+ if ((c | 0x20) === 0x6e /* n */) return "null";
499
+ return "value";
500
+ }
501
+
502
+ /**
503
+ * Push side of an {@link IncomingDoc} channel. Call {@link finish} to mark
504
+ * the document complete or {@link abort} to abandon it; both are idempotent
505
+ * and every pending pull settles on the first one.
506
+ */
507
+ export class IncomingFeed {
508
+ readonly #shared: Shared;
509
+
510
+ constructor(shared: Shared) {
511
+ this.#shared = shared;
512
+ }
513
+
514
+ /** Append one text fragment and wake every pending cursor. Throws once the feed is closed. */
515
+ push(fragment: string): void {
516
+ if (this.#shared.end !== "open") throw new Error("incoming JSON feed is already closed");
517
+ this.#shared.text += fragment;
518
+ this.#shared.notify();
519
+ }
520
+
521
+ /** Mark the input complete. */
522
+ finish(): void {
523
+ this.#close("finished");
524
+ }
525
+
526
+ /** Abandon the input; pending and future pulls reject with kind `aborted`. */
527
+ abort(): void {
528
+ this.#close("aborted");
529
+ }
530
+
531
+ #close(end: "finished" | "aborted"): void {
532
+ if (this.#shared.end !== "open") return;
533
+ this.#shared.end = end;
534
+ this.#shared.notify();
535
+ }
536
+ }
537
+
538
+ /** Read side of one growing JSON document. */
539
+ export class IncomingDoc {
540
+ readonly #shared: Shared;
541
+
542
+ constructor(shared: Shared) {
543
+ this.#shared = shared;
544
+ }
545
+
546
+ /** Create a push feed and its read-side document. */
547
+ static channel(): { feed: IncomingFeed; doc: IncomingDoc } {
548
+ const shared = new Shared();
549
+ return { feed: new IncomingFeed(shared), doc: new IncomingDoc(shared) };
550
+ }
551
+
552
+ /** Text received so far. */
553
+ get text(): string {
554
+ return this.#shared.text;
555
+ }
556
+
557
+ /** Await explicit input completion; rejects with kind `aborted` if the feed was abandoned. */
558
+ finished(): Promise<void> {
559
+ return this.#shared.finished();
560
+ }
561
+
562
+ /**
563
+ * Parse the entire finished document with the final tolerant parser.
564
+ * Waits for {@link IncomingFeed.finish}; aborted input is not decoded and a
565
+ * document that fails the final parse rejects with kind `malformed`.
566
+ */
567
+ async whole<T = unknown>(): Promise<T> {
568
+ await this.#shared.finished();
569
+ try {
570
+ return parseJsonWithRepair<T>(this.#shared.text);
571
+ } catch (cause) {
572
+ throw new IncomingJsonError([], "document", "malformed", { cause });
573
+ }
574
+ }
575
+
576
+ /** Cursor for the root JSON value. */
577
+ root(): IncomingJson {
578
+ return new IncomingJson(this.#shared, []);
579
+ }
580
+ }
581
+
582
+ /** Await the value at `path` until `ready` accepts it; a missing member rejects with kind `missing`. */
583
+ async function pull(
584
+ shared: Shared,
585
+ path: readonly PullPathSegment[],
586
+ expected: string,
587
+ ready: Ready,
588
+ ): Promise<Located> {
589
+ const found = await shared.pull(path, expected, ready);
590
+ if (found === undefined) throw new IncomingJsonError(path, expected, "missing");
591
+ return found;
592
+ }
593
+
594
+ /** Await completion and materialize the value at `path`; containers go through the final tolerant parser. */
595
+ async function materialize<T>(shared: Shared, path: readonly PullPathSegment[], expected: string): Promise<T> {
596
+ const found = await pull(shared, path, expected, COMPLETE);
597
+ const { shape } = found;
598
+ switch (shape.kind) {
599
+ case "null":
600
+ return null as T;
601
+ case "boolean":
602
+ case "number":
603
+ case "string":
604
+ return shape.value as T;
605
+ }
606
+ try {
607
+ return parseJsonWithRepair<T>(shared.text.slice(found.start, found.end));
608
+ } catch (cause) {
609
+ throw new IncomingJsonError(path, expected, "malformed", { cause });
610
+ }
611
+ }
612
+
613
+ function mismatchError(path: readonly PullPathSegment[], expected: string, found: string): IncomingJsonError {
614
+ return new IncomingJsonError(path, expected, "mismatch", { found });
615
+ }
616
+
617
+ /** Cursor for one JSON value in the incoming document, addressed by path. */
618
+ export class IncomingJson {
619
+ readonly #shared: Shared;
620
+ readonly path: readonly PullPathSegment[];
621
+
622
+ constructor(shared: Shared, path: readonly PullPathSegment[]) {
623
+ this.#shared = shared;
624
+ this.path = path;
625
+ }
626
+
627
+ /** Await the value's first token and report its JSON shape. */
628
+ async kind(): Promise<IncomingValueKind> {
629
+ return (await pull(this.#shared, this.path, "value", STARTED)).shape.kind;
630
+ }
631
+
632
+ /** Await and parse the complete value. Containers go through the final tolerant parser. */
633
+ value<T = unknown>(): Promise<T> {
634
+ return materialize<T>(this.#shared, this.path, "value");
635
+ }
636
+
637
+ /** Await a complete number. */
638
+ number(): Promise<number> {
639
+ return this.#scalar("number");
640
+ }
641
+
642
+ /** Await a complete boolean. */
643
+ boolean(): Promise<boolean> {
644
+ return this.#scalar("boolean");
645
+ }
646
+
647
+ /** Await a complete `null`. */
648
+ null(): Promise<null> {
649
+ return this.#scalar("null");
650
+ }
651
+
652
+ /** View this value as an incremental decoded string. */
653
+ string(): IncomingString {
654
+ return new IncomingString(this.#shared, this.path);
655
+ }
656
+
657
+ /** View this value as an array of element cursors. */
658
+ array(): IncomingArray {
659
+ return new IncomingArray(this.#shared, this.path);
660
+ }
661
+
662
+ /** View this value as an object with keyed cursors. */
663
+ object(): IncomingObject {
664
+ return new IncomingObject(this.#shared, this.path);
665
+ }
666
+
667
+ async #scalar<T>(expected: IncomingValueKind): Promise<T> {
668
+ const found = await pull(this.#shared, this.path, expected, COMPLETE);
669
+ if (found.shape.kind !== expected) throw mismatchError(this.path, expected, found.shape.kind);
670
+ return materialize<T>(this.#shared, this.path, expected);
671
+ }
672
+ }
673
+
674
+ /** Decoded-string chunk readiness: stable output past `emitted`, or the closing quote. */
675
+ function chunkReady(emitted: number): Ready {
676
+ return located =>
677
+ located.shape.kind !== "string" || located.shape.stableLen !== emitted || located.end !== undefined;
678
+ }
679
+
680
+ /** Line readiness: a newline in the stable output past `emitted`, or the closing quote. */
681
+ function lineReady(emitted: number): Ready {
682
+ return located =>
683
+ located.shape.kind !== "string" ||
684
+ located.shape.stableLen < emitted ||
685
+ located.end !== undefined ||
686
+ located.shape.value.slice(emitted, located.shape.stableLen).includes("\n");
687
+ }
688
+
689
+ /**
690
+ * Incremental decoded string consumer. Chunks are emitted in order without
691
+ * overlap and are always prefixes of the final decoded string;
692
+ * {@link text} returns the complete string independently of whether chunks
693
+ * were consumed. Async iteration yields chunks. Concurrent `nextChunk` /
694
+ * `nextLine` calls are served in call order, like a stream reader: a call
695
+ * whose result is abandoned still consumes its chunk.
696
+ */
697
+ export class IncomingString implements AsyncIterable<string> {
698
+ readonly #shared: Shared;
699
+ readonly #path: readonly PullPathSegment[];
700
+ readonly #serial = new Serial();
701
+ #emitted = 0;
702
+ #done = false;
703
+
704
+ constructor(shared: Shared, path: readonly PullPathSegment[]) {
705
+ this.#shared = shared;
706
+ this.#path = path;
707
+ }
708
+
709
+ /** Await the next stable decoded chunk, or `undefined` after the closing quote. */
710
+ nextChunk(): Promise<string | undefined> {
711
+ return this.#serial.run(() => this.#nextChunk());
712
+ }
713
+
714
+ /**
715
+ * Await the next complete decoded line, retaining its trailing newline.
716
+ * A final unterminated line is returned once the closing quote arrives.
717
+ */
718
+ nextLine(): Promise<string | undefined> {
719
+ return this.#serial.run(() => this.#nextLine());
720
+ }
721
+
722
+ async #nextChunk(): Promise<string | undefined> {
723
+ if (this.#done) return undefined;
724
+ const found = await pull(this.#shared, this.#path, "string", chunkReady(this.#emitted));
725
+ const { stableLen, value } = this.#string(found);
726
+ if (stableLen > this.#emitted) {
727
+ const chunk = value.slice(this.#emitted, stableLen);
728
+ this.#emitted = stableLen;
729
+ return chunk;
730
+ }
731
+ this.#done = true;
732
+ return undefined;
733
+ }
734
+
735
+ async #nextLine(): Promise<string | undefined> {
736
+ if (this.#done) return undefined;
737
+ const found = await pull(this.#shared, this.#path, "string", lineReady(this.#emitted));
738
+ const { stableLen, value } = this.#string(found);
739
+ const newline = value.indexOf("\n", this.#emitted);
740
+ const upper =
741
+ newline >= 0 && newline < stableLen ? newline + 1 : found.end === undefined ? this.#emitted : stableLen;
742
+ if (upper > this.#emitted) {
743
+ const line = value.slice(this.#emitted, upper);
744
+ this.#emitted = upper;
745
+ return line;
746
+ }
747
+ this.#done = true;
748
+ return undefined;
749
+ }
750
+
751
+ /** Iterate complete decoded lines. */
752
+ async *lines(): AsyncGenerator<string, void, undefined> {
753
+ for (let line = await this.nextLine(); line !== undefined; line = await this.nextLine()) yield line;
754
+ }
755
+
756
+ async *[Symbol.asyncIterator](): AsyncGenerator<string, void, undefined> {
757
+ for (let chunk = await this.nextChunk(); chunk !== undefined; chunk = await this.nextChunk()) yield chunk;
758
+ }
759
+
760
+ /** Await the closing quote and return the complete decoded string. */
761
+ async text(): Promise<string> {
762
+ return this.#string(await pull(this.#shared, this.#path, "string", COMPLETE)).value;
763
+ }
764
+
765
+ #string(found: Located): { value: string; stableLen: number } {
766
+ const { shape } = found;
767
+ if (shape.kind !== "string") throw mismatchError(this.#path, "string", shape.kind);
768
+ if (shape.stableLen < this.#emitted) {
769
+ throw new IncomingJsonError(this.#path, "valid string offset", "malformed");
770
+ }
771
+ return shape;
772
+ }
773
+ }
774
+
775
+ /**
776
+ * Linear cursor over elements of an incoming array. Async iteration yields
777
+ * element cursors. Concurrent `next` calls are served in call order.
778
+ */
779
+ export class IncomingArray implements AsyncIterable<IncomingJson> {
780
+ readonly #shared: Shared;
781
+ readonly #path: readonly PullPathSegment[];
782
+ readonly #serial = new Serial();
783
+ #index = 0;
784
+ #started = false;
785
+
786
+ constructor(shared: Shared, path: readonly PullPathSegment[]) {
787
+ this.#shared = shared;
788
+ this.#path = path;
789
+ }
790
+
791
+ /** Await the start of the next element; `undefined` only after the closing bracket. */
792
+ next(): Promise<IncomingJson | undefined> {
793
+ return this.#serial.run(() => this.#advance());
794
+ }
795
+
796
+ async #advance(): Promise<IncomingJson | undefined> {
797
+ if (!this.#started) {
798
+ const root = await pull(this.#shared, this.#path, "array", STARTED);
799
+ if (root.shape.kind !== "array") throw mismatchError(this.#path, "array", root.shape.kind);
800
+ this.#started = true;
801
+ }
802
+ const path = [...this.#path, this.#index];
803
+ if ((await this.#shared.pull(path, "value", STARTED)) === undefined) return undefined;
804
+ this.#index++;
805
+ return new IncomingJson(this.#shared, path);
806
+ }
807
+
808
+ async *[Symbol.asyncIterator](): AsyncGenerator<IncomingJson, void, undefined> {
809
+ for (let element = await this.next(); element !== undefined; element = await this.next()) yield element;
810
+ }
811
+
812
+ /** Await the closing bracket and collect the fully parsed elements. */
813
+ async collect<T = unknown>(): Promise<T[]> {
814
+ const values = await materialize<unknown>(this.#shared, this.#path, "array");
815
+ if (!Array.isArray(values)) throw mismatchError(this.#path, "array", valueName(values));
816
+ return values as T[];
817
+ }
818
+ }
819
+
820
+ /** Keyed cursor and final collection for an incoming object. */
821
+ export class IncomingObject {
822
+ readonly #shared: Shared;
823
+ readonly #path: readonly PullPathSegment[];
824
+
825
+ constructor(shared: Shared, path: readonly PullPathSegment[]) {
826
+ this.#shared = shared;
827
+ this.#path = path;
828
+ }
829
+
830
+ /**
831
+ * Cursor bound to the first occurrence of `name`. Pulling it makes the
832
+ * key required: a missing member rejects with kind `missing`.
833
+ */
834
+ key(name: string): IncomingJson {
835
+ return new IncomingJson(this.#shared, [...this.#path, name]);
836
+ }
837
+
838
+ /**
839
+ * Await the closing brace and collect the object through the final parser,
840
+ * whose duplicate keys are last-write-wins unlike {@link key}.
841
+ */
842
+ async collect<T = Record<string, unknown>>(): Promise<T> {
843
+ const value = await materialize<unknown>(this.#shared, this.#path, "object");
844
+ const found = valueName(value);
845
+ if (found !== "object") throw mismatchError(this.#path, "object", found);
846
+ return value as T;
847
+ }
848
+ }
849
+
850
+ /** JSON shape name of a materialized value, for mismatch reports. */
851
+ function valueName(value: unknown): IncomingValueKind {
852
+ if (value === null) return "null";
853
+ if (Array.isArray(value)) return "array";
854
+ const type = typeof value;
855
+ return type === "boolean" || type === "number" || type === "string" ? type : "object";
856
+ }