@gajae-code/utils 0.11.5 → 0.11.7
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/dist/types/stream.d.ts +6 -2
- package/package.json +2 -2
- package/src/format.ts +16 -6
- package/src/frontmatter.ts +18 -6
- package/src/stream.ts +27 -3
package/dist/types/stream.d.ts
CHANGED
|
@@ -17,7 +17,11 @@ export declare function readJsonl<T>(stream: ReadableStream<Uint8Array>, signal?
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
export type SseEventObserver = (event: ServerSentEvent) => void;
|
|
20
|
-
export
|
|
20
|
+
export interface SseReadOptions {
|
|
21
|
+
maxEventBytes?: number;
|
|
22
|
+
maxTotalBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function readSseJson<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal, onEvent?: SseEventObserver, options?: SseReadOptions): AsyncGenerator<T>;
|
|
21
25
|
/**
|
|
22
26
|
* A single Server-Sent Event dispatched on a blank-line boundary.
|
|
23
27
|
*
|
|
@@ -53,7 +57,7 @@ export interface ServerSentEvent {
|
|
|
53
57
|
* }
|
|
54
58
|
* ```
|
|
55
59
|
*/
|
|
56
|
-
export declare function readSseEvents(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<ServerSentEvent>;
|
|
60
|
+
export declare function readSseEvents(stream: ReadableStream<Uint8Array>, signal?: AbortSignal, options?: SseReadOptions): AsyncGenerator<ServerSentEvent>;
|
|
57
61
|
/**
|
|
58
62
|
* Parse a complete JSONL string, skipping malformed lines instead of throwing.
|
|
59
63
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/utils",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.7",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@gajae-code/natives": "0.11.
|
|
34
|
+
"@gajae-code/natives": "0.11.7",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/format.ts
CHANGED
|
@@ -9,7 +9,9 @@ const DAY = 24 * HOUR;
|
|
|
9
9
|
*/
|
|
10
10
|
export function formatDuration(ms: number): string {
|
|
11
11
|
if (ms < SEC) return `${ms}ms`;
|
|
12
|
-
|
|
12
|
+
// Truncate below 60.0s instead of rounding up into the next unit (the minute
|
|
13
|
+
// branch below), mirroring roundBelow/formatByteUnit: e.g. 59_999ms -> "59.9s".
|
|
14
|
+
if (ms < MIN) return `${(roundBelow(ms / 100, 600) / 10).toFixed(1)}s`;
|
|
13
15
|
if (ms < HOUR) {
|
|
14
16
|
const mins = Math.floor(ms / MIN);
|
|
15
17
|
const secs = Math.floor((ms % MIN) / SEC);
|
|
@@ -59,13 +61,21 @@ export function formatBytes(bytes: number): string {
|
|
|
59
61
|
if (bytes < 1024) return `${bytes}B`;
|
|
60
62
|
if (bytes < 1024 * 1024) return `${formatByteUnit(bytes, 1024)}KB`;
|
|
61
63
|
if (bytes < 1024 * 1024 * 1024) return `${formatByteUnit(bytes, 1024 * 1024)}MB`;
|
|
62
|
-
|
|
64
|
+
// The GB branch is terminal (no larger unit), so it must not clamp below the
|
|
65
|
+
// next-unit boundary the way KB/MB do; clamping here reports every value >=
|
|
66
|
+
// 1 TiB as "1023.9GB" (e.g. 2 TiB -> "1023.9GB" instead of "2048.0GB").
|
|
67
|
+
return `${formatByteUnit(bytes, 1024 * 1024 * 1024, false)}GB`;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Format bytes to 1 decimal. Intermediate units (KB/MB) clamp just below the
|
|
72
|
+
* next-unit boundary so a value like 1023.95 KB never rounds up to "1024.0KB";
|
|
73
|
+
* the terminal GB unit passes `clampToNextUnit: false` because it has no next
|
|
74
|
+
* unit to protect against.
|
|
75
|
+
*/
|
|
76
|
+
function formatByteUnit(bytes: number, unit: number, clampToNextUnit = true): string {
|
|
77
|
+
const tenths = Math.round((bytes / unit) * 10);
|
|
78
|
+
return ((clampToNextUnit ? Math.min(tenths, 1024 * 10 - 1) : tenths) / 10).toFixed(1);
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
/**
|
package/src/frontmatter.ts
CHANGED
|
@@ -124,18 +124,30 @@ export function parseFrontmatter(
|
|
|
124
124
|
const loc = location ?? source;
|
|
125
125
|
const frontmatter: Record<string, unknown> = { ...fallback };
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
// Normalize away a leading UTF-8 BOM and CRLF line endings before matching so
|
|
128
|
+
// a BOM-prefixed but otherwise valid document is still recognized as
|
|
129
|
+
// frontmatter (BOM-prefixed Markdown is common on Windows-authored files).
|
|
130
|
+
const normalized = normalize ? stripHtmlComments(content.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n")) : content;
|
|
131
|
+
// A frontmatter block opens with a line that is exactly `---` (trailing
|
|
132
|
+
// spaces/tabs allowed). A bare `----` banner or a `--- text` heading is not
|
|
133
|
+
// an opener, so a document without frontmatter keeps its body intact rather
|
|
134
|
+
// than having content silently consumed by the fixed-offset slicing below.
|
|
135
|
+
const open = normalized.match(/^---[ \t]*(?:\n|$)/);
|
|
136
|
+
if (!open) {
|
|
129
137
|
return { frontmatter, body: normalized };
|
|
130
138
|
}
|
|
131
139
|
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
// The block closes at the next line that is exactly `---`. Searching from the
|
|
141
|
+
// end of the opening `---` (offset 3, not the whole opener) keeps an empty
|
|
142
|
+
// block (`---\n---`) matching as empty frontmatter.
|
|
143
|
+
const afterOpen = normalized.slice(3);
|
|
144
|
+
const close = afterOpen.match(/\n---[ \t]*(?:\n|$)/);
|
|
145
|
+
if (!close || close.index === undefined) {
|
|
134
146
|
return { frontmatter, body: normalized };
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
const metadata =
|
|
138
|
-
const body =
|
|
149
|
+
const metadata = afterOpen.slice(open[0].length - 3, close.index);
|
|
150
|
+
const body = afterOpen.slice(close.index + close[0].length).trim();
|
|
139
151
|
|
|
140
152
|
try {
|
|
141
153
|
// Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness
|
package/src/stream.ts
CHANGED
|
@@ -125,16 +125,22 @@ class ConcatSink {
|
|
|
125
125
|
this.#length = 0;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
*appendAndFlushLines(chunk: Uint8Array) {
|
|
128
|
+
*appendAndFlushLines(chunk: Uint8Array, maxLineBytes?: () => number) {
|
|
129
129
|
let pos = 0;
|
|
130
130
|
while (pos < chunk.length) {
|
|
131
131
|
const nl = chunk.indexOf(LF, pos);
|
|
132
132
|
if (nl === -1) {
|
|
133
|
+
if (maxLineBytes && this.#length + chunk.length - pos > maxLineBytes()) {
|
|
134
|
+
throw new Error("SSE event exceeds size limit");
|
|
135
|
+
}
|
|
133
136
|
this.append(chunk.subarray(pos));
|
|
134
137
|
return;
|
|
135
138
|
}
|
|
136
139
|
const suffix = chunk.subarray(pos, nl);
|
|
137
140
|
pos = nl + 1;
|
|
141
|
+
if (maxLineBytes && this.#length + suffix.length + 1 > maxLineBytes()) {
|
|
142
|
+
throw new Error("SSE event exceeds size limit");
|
|
143
|
+
}
|
|
138
144
|
if (this.isEmpty) {
|
|
139
145
|
yield suffix;
|
|
140
146
|
} else {
|
|
@@ -201,6 +207,11 @@ class ConcatSink {
|
|
|
201
207
|
*/
|
|
202
208
|
export type SseEventObserver = (event: ServerSentEvent) => void;
|
|
203
209
|
|
|
210
|
+
export interface SseReadOptions {
|
|
211
|
+
maxEventBytes?: number;
|
|
212
|
+
maxTotalBytes?: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
204
215
|
function notifySseEventObserver(observer: SseEventObserver | undefined, event: ServerSentEvent): void {
|
|
205
216
|
if (!observer) return;
|
|
206
217
|
try {
|
|
@@ -214,8 +225,9 @@ export async function* readSseJson<T>(
|
|
|
214
225
|
stream: ReadableStream<Uint8Array>,
|
|
215
226
|
signal?: AbortSignal,
|
|
216
227
|
onEvent?: SseEventObserver,
|
|
228
|
+
options?: SseReadOptions,
|
|
217
229
|
): AsyncGenerator<T> {
|
|
218
|
-
for await (const sse of readSseEvents(stream, signal)) {
|
|
230
|
+
for await (const sse of readSseEvents(stream, signal, options)) {
|
|
219
231
|
notifySseEventObserver(onEvent, sse);
|
|
220
232
|
const data = sse.data;
|
|
221
233
|
if (data === "" || data === "[DONE]") {
|
|
@@ -336,14 +348,26 @@ function pushSseLine(line: Uint8Array, state: SseEventState): ServerSentEvent |
|
|
|
336
348
|
export async function* readSseEvents(
|
|
337
349
|
stream: ReadableStream<Uint8Array>,
|
|
338
350
|
signal?: AbortSignal,
|
|
351
|
+
options?: SseReadOptions,
|
|
339
352
|
): AsyncGenerator<ServerSentEvent> {
|
|
340
353
|
const lineBuffer = new ConcatSink();
|
|
341
354
|
const state: SseEventState = { event: null, data: null, raw: [] };
|
|
342
355
|
const source = createAbortableStream(stream, signal);
|
|
356
|
+
let eventBytes = 0;
|
|
357
|
+
let totalBytes = 0;
|
|
343
358
|
try {
|
|
344
359
|
for await (const chunk of source) {
|
|
345
|
-
|
|
360
|
+
totalBytes += chunk.length;
|
|
361
|
+
if (options?.maxTotalBytes !== undefined && totalBytes > options.maxTotalBytes) {
|
|
362
|
+
throw new Error("SSE stream exceeds size limit");
|
|
363
|
+
}
|
|
364
|
+
for (const line of lineBuffer.appendAndFlushLines(
|
|
365
|
+
chunk,
|
|
366
|
+
() => (options?.maxEventBytes ?? Infinity) - eventBytes,
|
|
367
|
+
)) {
|
|
368
|
+
eventBytes += line.length + 1;
|
|
346
369
|
const event = pushSseLine(line, state);
|
|
370
|
+
if (line.length === 0 || (line.length === 1 && line[0] === 0x0d)) eventBytes = 0;
|
|
347
371
|
if (event) yield event;
|
|
348
372
|
}
|
|
349
373
|
}
|