@f5-sales-demo/pi-utils 19.51.2

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.
@@ -0,0 +1,136 @@
1
+ // 16-bit hex lookup table (65536 entries) for fast conversion
2
+ const HEX4 = Array.from({ length: 65536 }, (_, i) => i.toString(16).padStart(4, "0"));
3
+
4
+ function randu32() {
5
+ return crypto.getRandomValues(new Uint32Array(1))[0];
6
+ }
7
+
8
+ const EPOCH = 1420070400000;
9
+ const MAX_SEQ = 0x3fffff;
10
+
11
+ // Snowflake as a hex string (16 chars, zero-padded).
12
+ //
13
+ // Since this is not distributed (no machine ID needed), we use an extended
14
+ // 22-bit sequence instead of the standard 10-bit machine ID + 12-bit sequence.
15
+ //
16
+ type Snowflake = string & { readonly __brand: unique symbol };
17
+
18
+ namespace Snowflake {
19
+ // Hex string validation pattern (16 lowercase hex chars).
20
+ //
21
+ export const PATTERN = /^[0-9a-f]{16}$/;
22
+
23
+ // Epoch timestamp.
24
+ //
25
+ export const EPOCH_TIMESTAMP = EPOCH;
26
+
27
+ // Maximum sequence number.
28
+ //
29
+ export const MAX_SEQUENCE = MAX_SEQ;
30
+
31
+ // Parses a hex string or bigint to bigint.
32
+ //
33
+ function toBigInt(value: Snowflake): bigint {
34
+ const hi = Number.parseInt(value.substring(0, 8), 16);
35
+ const lo = Number.parseInt(value.substring(8, 16), 16);
36
+ return (BigInt(hi) << 32n) | BigInt(lo);
37
+ }
38
+
39
+ // Formats a sequence and timestamp into a snowflake hex string.
40
+ //
41
+ export function formatParts(dt: number, seq: number): Snowflake {
42
+ // Split dt into hi/lo to avoid exceeding Number.MAX_SAFE_INTEGER.
43
+ // dt is ~39 bits; dt<<22 would be ~61 bits, so we split at bit 10:
44
+ // lo32 = (dtLo << 22) | seq (10+22 = 32 bits, no overlap)
45
+ // hi32 = dtHi (~29 bits)
46
+ const dtLo = dt % 1024;
47
+ const hi = (dt - dtLo) / 1024; // dt >>> 10
48
+ const lo = ((dtLo << 22) | seq) >>> 0;
49
+ const hi1 = (hi >>> 16) & 0xffff;
50
+ const hi2 = hi & 0xffff;
51
+ const lo1 = (lo >>> 16) & 0xffff;
52
+ const lo2 = lo & 0xffff;
53
+ return `${HEX4[hi1]}${HEX4[hi2]}${HEX4[lo1]}${HEX4[lo2]}` as Snowflake;
54
+ }
55
+
56
+ // Snowflake generator type.
57
+ //
58
+ export class Source {
59
+ #seq = 0;
60
+ constructor(sequence: number = randu32() & MAX_SEQ) {
61
+ this.#seq = sequence & MAX_SEQ;
62
+ }
63
+
64
+ // Sequence number.
65
+ //
66
+ get sequence() {
67
+ return this.#seq & MAX_SEQ;
68
+ }
69
+ set sequence(v: number) {
70
+ this.#seq = v & MAX_SEQ;
71
+ }
72
+ reset() {
73
+ this.#seq = 0;
74
+ }
75
+
76
+ // Generates the next value as a hex string.
77
+ //
78
+ generate(timestamp: number): Snowflake {
79
+ const seq = (this.#seq + 1) & MAX_SEQ;
80
+ const dt = timestamp - EPOCH;
81
+ this.#seq = seq;
82
+ return formatParts(dt, seq);
83
+ }
84
+ }
85
+
86
+ // Gets the next snowflake given the timestamp.
87
+ //
88
+ const defaultSource = new Source();
89
+ export function next(timestamp = Date.now()): Snowflake {
90
+ return defaultSource.generate(timestamp);
91
+ }
92
+
93
+ // Validates a snowflake hex string.
94
+ //
95
+ export function valid(value: string): value is Snowflake {
96
+ return value.length === 16 && PATTERN.test(value);
97
+ }
98
+
99
+ // Returns the upper/lower boundaries for the given timestamp.
100
+ //
101
+ export function lowerbound(timelike: Date | number | Snowflake): Snowflake {
102
+ switch (typeof timelike) {
103
+ case "object": // Date
104
+ return formatParts(timelike.getTime() - EPOCH, 0);
105
+ case "number":
106
+ return formatParts(timelike - EPOCH, 0);
107
+ case "string": // Snowflake hex string
108
+ return timelike;
109
+ }
110
+ }
111
+ export function upperbound(timelike: Date | number | Snowflake): Snowflake {
112
+ switch (typeof timelike) {
113
+ case "object": // Date
114
+ return formatParts(timelike.getTime() - EPOCH, MAX_SEQ);
115
+ case "number":
116
+ return formatParts(timelike - EPOCH, MAX_SEQ);
117
+ case "string": // Snowflake hex string
118
+ return timelike;
119
+ }
120
+ }
121
+
122
+ // Returns the individual bits given the snowflake.
123
+ //
124
+ export function getSequence(value: Snowflake) {
125
+ return Number.parseInt(value.substring(8, 16), 16) & MAX_SEQ;
126
+ }
127
+ export function getTimestamp(value: Snowflake) {
128
+ const n = toBigInt(value) >> 22n;
129
+ return Number(n + BigInt(EPOCH));
130
+ }
131
+ export function getDate(value: Snowflake) {
132
+ return new Date(getTimestamp(value));
133
+ }
134
+ }
135
+
136
+ export { Snowflake };
package/src/stream.ts ADDED
@@ -0,0 +1,394 @@
1
+ import { createAbortableStream } from "./abortable";
2
+
3
+ /**
4
+ * Default ceiling for fully buffering a stream into memory (50 MiB). Reading an
5
+ * unbounded subprocess/HTTP stream into one buffer can exhaust the heap and crash
6
+ * the process with `RangeError: Out of memory`; this bounds that.
7
+ */
8
+ export const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
9
+
10
+ /** Thrown when a capped stream read exceeds its byte limit. */
11
+ export class OutputTooLargeError extends Error {
12
+ constructor(
13
+ readonly maxBytes: number,
14
+ readonly source?: string,
15
+ ) {
16
+ super(`Output exceeded ${maxBytes} bytes${source ? ` (${source})` : ""}`);
17
+ this.name = "OutputTooLargeError";
18
+ }
19
+ }
20
+
21
+ export interface ReadStreamCappedOptions {
22
+ /** Maximum bytes to buffer before throwing OutputTooLargeError (default 50 MiB). */
23
+ maxBytes?: number;
24
+ /** Short label (e.g. the command/URL) included in the error and any log. */
25
+ source?: string;
26
+ /** Abort signal; aborting cancels the reader and rejects. */
27
+ signal?: AbortSignal;
28
+ }
29
+
30
+ /**
31
+ * Read a ReadableStream fully into a single Uint8Array, enforcing a hard byte
32
+ * cap. On exceeding the cap the reader is cancelled and `OutputTooLargeError` is
33
+ * thrown — the size is checked BEFORE retaining each chunk, so memory never
34
+ * exceeds the cap (unlike `new Response(stream).text()`, which buffers unbounded).
35
+ */
36
+ export async function readStreamCapped(
37
+ stream: ReadableStream<Uint8Array>,
38
+ options: ReadStreamCappedOptions = {},
39
+ ): Promise<Uint8Array> {
40
+ const { maxBytes = DEFAULT_MAX_OUTPUT_BYTES, source, signal } = options;
41
+ const reader = stream.getReader();
42
+ const chunks: Uint8Array[] = [];
43
+ let total = 0;
44
+ try {
45
+ while (true) {
46
+ if (signal?.aborted) {
47
+ await reader.cancel();
48
+ throw signal.reason instanceof Error ? signal.reason : new Error("aborted");
49
+ }
50
+ const { done, value } = await reader.read();
51
+ if (done) break;
52
+ if (!value || value.byteLength === 0) continue;
53
+ total += value.byteLength;
54
+ if (total > maxBytes) {
55
+ await reader.cancel();
56
+ throw new OutputTooLargeError(maxBytes, source);
57
+ }
58
+ chunks.push(value);
59
+ }
60
+ } finally {
61
+ reader.releaseLock();
62
+ }
63
+
64
+ const out = new Uint8Array(total);
65
+ let offset = 0;
66
+ for (const chunk of chunks) {
67
+ out.set(chunk, offset);
68
+ offset += chunk.byteLength;
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /** Like {@link readStreamCapped} but decodes the result as UTF-8 text. */
74
+ export async function readStreamCappedText(
75
+ stream: ReadableStream<Uint8Array>,
76
+ options: ReadStreamCappedOptions = {},
77
+ ): Promise<string> {
78
+ return new TextDecoder().decode(await readStreamCapped(stream, options));
79
+ }
80
+
81
+ const LF = 0x0a;
82
+ type JsonlChunkResult = {
83
+ values: unknown[];
84
+ error: unknown;
85
+ read: number;
86
+ done: boolean;
87
+ };
88
+
89
+ function parseJsonlChunkCompat(input: Uint8Array, beg?: number, end?: number): JsonlChunkResult;
90
+ function parseJsonlChunkCompat(input: string): JsonlChunkResult;
91
+ function parseJsonlChunkCompat(input: Uint8Array | string, beg?: number, end?: number): JsonlChunkResult {
92
+ if (typeof input === "string") {
93
+ const { values, error, read, done } = Bun.JSONL.parseChunk(input);
94
+ return { values, error, read, done };
95
+ }
96
+ const start = beg ?? 0;
97
+ const stop = end ?? input.length;
98
+ const { values, error, read, done } = Bun.JSONL.parseChunk(input, start, stop);
99
+ return { values, error, read, done };
100
+ }
101
+
102
+ export async function* readLines(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<Uint8Array> {
103
+ const buffer = new ConcatSink();
104
+ const source = createAbortableStream(stream, signal);
105
+ try {
106
+ for await (const chunk of source) {
107
+ for (const line of buffer.appendAndFlushLines(chunk)) {
108
+ yield line;
109
+ }
110
+ }
111
+ if (!buffer.isEmpty) {
112
+ const tail = buffer.flush();
113
+ if (tail) {
114
+ buffer.clear();
115
+ yield tail;
116
+ }
117
+ }
118
+ } catch (err) {
119
+ // Abort errors are expected — just stop the generator.
120
+ if (signal?.aborted) return;
121
+ throw err;
122
+ }
123
+ }
124
+
125
+ export async function* readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<T> {
126
+ const buffer = new ConcatSink();
127
+ const source = createAbortableStream(stream, signal);
128
+ try {
129
+ for await (const chunk of source) {
130
+ yield* buffer.pullJSONL<T>(chunk, 0, chunk.length);
131
+ }
132
+ if (!buffer.isEmpty) {
133
+ const tail = buffer.flush();
134
+ if (tail) {
135
+ buffer.clear();
136
+ const { values, error, done } = parseJsonlChunkCompat(tail, 0, tail.length);
137
+ if (values.length > 0) {
138
+ yield* values as T[];
139
+ }
140
+ if (error) throw error;
141
+ if (!done) {
142
+ throw new Error("JSONL stream ended unexpectedly");
143
+ }
144
+ }
145
+ }
146
+ } catch (err) {
147
+ // Abort errors are expected — just stop the generator.
148
+ if (signal?.aborted) return;
149
+ throw err;
150
+ }
151
+ }
152
+
153
+ // =============================================================================
154
+ // SSE (Server-Sent Events)
155
+ // =============================================================================
156
+
157
+ /** Byte lookup table: 1 = whitespace, 0 = not. */
158
+ const WS = new Uint8Array(256);
159
+ WS[0x09] = 1; // tab
160
+ WS[0x0a] = 1; // LF
161
+ WS[0x0d] = 1; // CR
162
+ WS[0x20] = 1; // space
163
+
164
+ const createPattern = (prefix: string) => {
165
+ const pre = Buffer.from(prefix, "utf-8");
166
+ return {
167
+ strip(buf: Uint8Array): number | null {
168
+ const n = pre.length;
169
+ if (buf.length < n) return null;
170
+ if (pre.equals(buf.subarray(0, n))) {
171
+ return n;
172
+ }
173
+ return null;
174
+ },
175
+ };
176
+ };
177
+
178
+ const PAT_DATA = createPattern("data:");
179
+
180
+ const PAT_DONE = createPattern("[DONE]");
181
+
182
+ class ConcatSink {
183
+ #space?: Buffer;
184
+ #length = 0;
185
+
186
+ #ensureCapacity(size: number): Buffer {
187
+ const space = this.#space;
188
+ if (space && space.length >= size) return space;
189
+ const nextSize = space ? Math.max(size, space.length * 2) : size;
190
+ const next = Buffer.allocUnsafe(nextSize);
191
+ if (space && this.#length > 0) {
192
+ space.copy(next, 0, 0, this.#length);
193
+ }
194
+ this.#space = next;
195
+ return next;
196
+ }
197
+
198
+ append(chunk: Uint8Array) {
199
+ const n = chunk.length;
200
+ if (!n) return;
201
+ const offset = this.#length;
202
+ const space = this.#ensureCapacity(offset + n);
203
+ space.set(chunk, offset);
204
+ this.#length += n;
205
+ }
206
+
207
+ reset(chunk: Uint8Array) {
208
+ const n = chunk.length;
209
+ if (!n) {
210
+ this.#length = 0;
211
+ return;
212
+ }
213
+ const space = this.#ensureCapacity(n);
214
+ space.set(chunk, 0);
215
+ this.#length = n;
216
+ }
217
+
218
+ get isEmpty(): boolean {
219
+ return this.#length === 0;
220
+ }
221
+
222
+ flush(): Uint8Array | undefined {
223
+ if (!this.#length) return undefined;
224
+ return this.#space!.subarray(0, this.#length);
225
+ }
226
+
227
+ clear() {
228
+ this.#length = 0;
229
+ }
230
+
231
+ *appendAndFlushLines(chunk: Uint8Array) {
232
+ let pos = 0;
233
+ while (pos < chunk.length) {
234
+ const nl = chunk.indexOf(LF, pos);
235
+ if (nl === -1) {
236
+ this.append(chunk.subarray(pos));
237
+ return;
238
+ }
239
+ const suffix = chunk.subarray(pos, nl);
240
+ pos = nl + 1;
241
+ if (this.isEmpty) {
242
+ yield suffix;
243
+ } else {
244
+ this.append(suffix);
245
+ const payload = this.flush();
246
+ if (payload) {
247
+ yield payload;
248
+ this.clear();
249
+ }
250
+ }
251
+ }
252
+ }
253
+ *pullJSONL<T>(chunk: Uint8Array, beg: number, end: number) {
254
+ if (this.isEmpty) {
255
+ const { values, error, read, done } = parseJsonlChunkCompat(chunk, beg, end);
256
+ if (values.length > 0) {
257
+ yield* values as T[];
258
+ }
259
+ if (error) throw error;
260
+ if (done) return;
261
+ this.reset(chunk.subarray(read, end));
262
+ return;
263
+ }
264
+
265
+ const offset = this.#length;
266
+ const n = end - beg;
267
+ const total = offset + n;
268
+ const space = this.#ensureCapacity(total);
269
+ space.set(chunk.subarray(beg, end), offset);
270
+ this.#length = total;
271
+
272
+ const { values, error, read, done } = parseJsonlChunkCompat(space.subarray(0, total), 0, total);
273
+ if (values.length > 0) {
274
+ yield* values as T[];
275
+ }
276
+ if (error) throw error;
277
+ if (done) {
278
+ this.#length = 0;
279
+ return;
280
+ }
281
+ const rem = total - read;
282
+ if (rem < total) {
283
+ space.copyWithin(0, read, total);
284
+ }
285
+ this.#length = rem;
286
+ }
287
+ }
288
+
289
+ const kDoneError = new Error("SSE stream done");
290
+
291
+ /**
292
+ * Stream parsed JSON objects from SSE `data:` lines.
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * for await (const obj of readSseJson(response.body!)) {
297
+ * console.log(obj);
298
+ * }
299
+ * ```
300
+ */
301
+ export async function* readSseJson<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<T> {
302
+ const lineBuffer = new ConcatSink();
303
+ const jsonBuffer = new ConcatSink();
304
+
305
+ // pipeThrough with { signal } makes the stream abort-aware: the pipe
306
+ // cancels the source and errors the output when the signal fires,
307
+ // so for-await-of exits cleanly without manual reader/listener management.
308
+ stream = createAbortableStream(stream, signal);
309
+ try {
310
+ const processLine = function* (line: Uint8Array) {
311
+ // Strip trailing spaces including \r.
312
+ let end = line.length;
313
+ while (end && WS[line[end - 1]]) {
314
+ --end;
315
+ }
316
+ if (!end) return; // blank line
317
+
318
+ const trimmed = end === line.length ? line : line.subarray(0, end);
319
+
320
+ // Check "data:" prefix and optional space afterwards.
321
+ let beg = PAT_DATA.strip(trimmed);
322
+ if (beg === null) return;
323
+ while (beg < end && WS[trimmed[beg]]) {
324
+ ++beg;
325
+ }
326
+ if (beg >= end) return;
327
+
328
+ // Fast-path: the OpenAI-style done marker isn't JSON.
329
+ const donePrefix = PAT_DONE.strip(trimmed.subarray(beg, end));
330
+ if (donePrefix !== null && donePrefix === end - beg) {
331
+ throw kDoneError;
332
+ }
333
+
334
+ yield* jsonBuffer.pullJSONL<T>(trimmed, beg, end);
335
+ };
336
+ for await (const chunk of stream) {
337
+ for (const line of lineBuffer.appendAndFlushLines(chunk)) {
338
+ yield* processLine(line);
339
+ }
340
+ }
341
+ if (!lineBuffer.isEmpty) {
342
+ const tail = lineBuffer.flush();
343
+ if (tail) {
344
+ lineBuffer.clear();
345
+ yield* processLine(tail);
346
+ }
347
+ }
348
+ } catch (err) {
349
+ if (err === kDoneError) return;
350
+ // Abort errors are expected — just stop the generator.
351
+ if (signal?.aborted) return;
352
+ throw err;
353
+ }
354
+ if (!jsonBuffer.isEmpty) {
355
+ throw new Error("SSE stream ended unexpectedly");
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Parse a complete JSONL string, skipping malformed lines instead of throwing.
361
+ *
362
+ * Uses `Bun.JSONL.parseChunk` internally. On parse errors, the malformed
363
+ * region is skipped up to the next newline and parsing continues.
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * const entries = parseJsonlLenient<MyType>(fileContents);
368
+ * ```
369
+ */
370
+ export function parseJsonlLenient<T>(buffer: string): T[] {
371
+ let entries: T[] | undefined;
372
+
373
+ while (buffer.length > 0) {
374
+ const { values, error, read, done } = parseJsonlChunkCompat(buffer);
375
+ if (values.length > 0) {
376
+ const ext = values as T[];
377
+ if (!entries) {
378
+ entries = ext;
379
+ } else {
380
+ entries.push(...ext);
381
+ }
382
+ }
383
+ if (error) {
384
+ const nextNewline = buffer.indexOf("\n", read);
385
+ if (nextNewline === -1) break;
386
+ buffer = buffer.substring(nextNewline + 1);
387
+ continue;
388
+ }
389
+ if (read === 0) break;
390
+ buffer = buffer.substring(read);
391
+ if (done) break;
392
+ }
393
+ return entries ?? [];
394
+ }