@jarenjs/core 0.34.2 → 0.43.1

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/src/string.js CHANGED
@@ -276,6 +276,42 @@ export function compareCodePoints(a, b) {
276
276
  return a.codePointAt(i) < b.codePointAt(i) ? -1 : 1;
277
277
  }
278
278
 
279
+ /**
280
+ * The exclusive upper bound of the strings beginning with `prefix`: the
281
+ * smallest string that sorts above every one of them. For a
282
+ * well-formed `value`, `value` begins with `prefix` **iff**
283
+ * `prefix <= value < codePointPrefixSuccessor(prefix)` under
284
+ * {@link compareCodePoints} — which is what lets a caller spell a
285
+ * prefix test as a half-open range, the form an ordered index can seek
286
+ * rather than test row by row.
287
+ *
288
+ * Null when there is no such string, and the caller then has no range
289
+ * to offer: the empty prefix (every string begins with it) and a
290
+ * prefix of nothing but U+10FFFF (nothing sorts above it).
291
+ *
292
+ * The bound never lands in the surrogate range, because it has to be a
293
+ * string the caller can hand on — to a comparator, a database
294
+ * parameter, a serializer. No well-formed string sorts inside that
295
+ * range, so stepping over it leaves the equivalence above intact.
296
+ *
297
+ * @param {string} prefix - The prefix to bound
298
+ * @returns {string | null} The exclusive upper bound, or null when none exists
299
+ */
300
+ export function codePointPrefixSuccessor(prefix) {
301
+ const points = toCodePoints(prefix);
302
+ // the last code point that can still be incremented: U+10FFFF cannot,
303
+ // so it drops off and the carry moves left
304
+ let i = points.length - 1;
305
+ while (i >= 0 && points[i] === 0x10FFFF)
306
+ i--;
307
+ if (i < 0)
308
+ return null;
309
+ const next = points[i] + 1;
310
+ points.length = i + 1;
311
+ points[i] = next >= 0xD800 && next <= 0xDFFF ? 0xE000 : next;
312
+ return fromCodePoints(points);
313
+ }
314
+
279
315
  /** FNV-1a 32-bit offset basis — the seed a fresh hash starts from. */
280
316
  export const FNV1A_OFFSET_BASIS = 0x811c9dc5;
281
317
 
@@ -0,0 +1,197 @@
1
+ //@ts-check
2
+ /**
3
+ * @file An incremental Server-Sent-Events codec (WHATWG HTML §9.2).
4
+ * `createSseEventDecoder` yields complete events `{ event, id, data,
5
+ * retry }` from network chunks fed in any split — mid-line, mid-event,
6
+ * CR, LF or CRLF line endings; `createSseDecoder` is the data-only view
7
+ * of the same machine (the shape every OpenAI-compatible streaming
8
+ * endpoint needs: just the `data:` payloads, in order); and
9
+ * `encodeSseEvent` renders one event back to wire text.
10
+ *
11
+ * Dispatch follows the specification: field lines are `name: value`
12
+ * with one optional leading space in the value, `:` lines are comments,
13
+ * an event is dispatched on the blank line exactly when its data buffer
14
+ * is non-empty (multi-line data joins with `\n`), the last-event-id is
15
+ * a stream attribute that persists across events (an `id` field whose
16
+ * value carries U+0000 is ignored), and `retry` must be ASCII digits.
17
+ * `end()` flushes a final event from a stream that never sent its
18
+ * closing blank line.
19
+ */
20
+
21
+ /**
22
+ * One decoded event. `event` is the `event:` field of the block, `null`
23
+ * when the block had none (the specification's default type "message");
24
+ * `id` is the stream's last-event-id at dispatch, `null` while the
25
+ * stream has not set one; `data` is the joined data payload; `retry` is
26
+ * the reconnection time a `retry:` field set since the previous
27
+ * dispatch, `null` otherwise.
28
+ * @typedef {Object} SseEvent
29
+ * @property {string | null} event
30
+ * @property {string | null} id
31
+ * @property {string} data
32
+ * @property {number | null} retry
33
+ */
34
+
35
+ /**
36
+ * An incremental decoder yielding complete events.
37
+ * @returns {{ feed: (chunk: string) => SseEvent[], end: () => SseEvent[] }}
38
+ * `feed` returns the events completed by this chunk; `end` flushes a
39
+ * final event from a stream that never sent its closing blank line.
40
+ */
41
+ export function createSseEventDecoder() {
42
+ let tail = '';
43
+ /** @type {string[]} */
44
+ let data = [];
45
+ /** @type {string | null} */
46
+ let eventType = null;
47
+ /** @type {string | null} */
48
+ let lastId = null;
49
+ /** @type {number | null} */
50
+ let retry = null;
51
+
52
+ /**
53
+ * @param {string} line - one complete line, no line terminator
54
+ * @param {SseEvent[]} out
55
+ */
56
+ function consumeLine(line, out) {
57
+ if (line === '') {
58
+ // the dispatch rule: no data, no event — but the type buffer resets
59
+ if (data.length > 0) {
60
+ out.push({ event: eventType, id: lastId, data: data.join('\n'), retry });
61
+ data = [];
62
+ retry = null;
63
+ }
64
+ eventType = null;
65
+ return;
66
+ }
67
+ if (line.charCodeAt(0) === 0x3A) return; // a comment
68
+ const colon = line.indexOf(':');
69
+ const name = colon === -1 ? line : line.slice(0, colon);
70
+ const value = colon === -1
71
+ ? ''
72
+ : line.slice(line.charCodeAt(colon + 1) === 0x20 ? colon + 2 : colon + 1);
73
+ switch (name) {
74
+ case 'data':
75
+ data.push(value);
76
+ break;
77
+ case 'event':
78
+ eventType = value;
79
+ break;
80
+ case 'id':
81
+ if (value.indexOf('\0') === -1) lastId = value;
82
+ break;
83
+ case 'retry':
84
+ if (value.length > 0 && /^[0-9]+$/.test(value)) retry = Number.parseInt(value, 10);
85
+ break;
86
+ // every other field name is ignored by the specification
87
+ }
88
+ }
89
+
90
+ return {
91
+ feed(chunk) {
92
+ /** @type {SseEvent[]} */
93
+ const out = [];
94
+ const text = tail + chunk;
95
+ const length = text.length;
96
+ // a trailing CR is held: the next chunk may complete a CRLF
97
+ const limit = length > 0 && text.charCodeAt(length - 1) === 0x0D ? length - 1 : length;
98
+ let start = 0;
99
+ let i = 0;
100
+ while (i < limit) {
101
+ const c = text.charCodeAt(i);
102
+ if (c === 0x0A) {
103
+ consumeLine(text.slice(start, i), out);
104
+ i += 1;
105
+ start = i;
106
+ }
107
+ else if (c === 0x0D) {
108
+ consumeLine(text.slice(start, i), out);
109
+ i += 1;
110
+ if (i < length && text.charCodeAt(i) === 0x0A) i += 1;
111
+ start = i;
112
+ }
113
+ else i += 1;
114
+ }
115
+ tail = text.slice(start);
116
+ return out;
117
+ },
118
+ end() {
119
+ /** @type {SseEvent[]} */
120
+ const out = [];
121
+ if (tail !== '') {
122
+ consumeLine(tail.charCodeAt(tail.length - 1) === 0x0D ? tail.slice(0, -1) : tail, out);
123
+ tail = '';
124
+ }
125
+ if (data.length > 0) {
126
+ out.push({ event: eventType, id: lastId, data: data.join('\n'), retry });
127
+ data = [];
128
+ retry = null;
129
+ }
130
+ eventType = null;
131
+ return out;
132
+ },
133
+ };
134
+ }
135
+
136
+ /**
137
+ * The data-only view of the event decoder: `feed` yields the complete
138
+ * `data:` payloads in order and every other field is ignored — the
139
+ * shape a chat-completion stream consumer needs.
140
+ * @returns {{ feed: (chunk: string) => string[], end: () => string[] }}
141
+ */
142
+ export function createSseDecoder() {
143
+ const inner = createSseEventDecoder();
144
+ /** @param {SseEvent[]} events */
145
+ const dataOf = (events) => events.map((e) => e.data);
146
+ return {
147
+ feed: (chunk) => dataOf(inner.feed(chunk)),
148
+ end: () => dataOf(inner.end()),
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Render one event as wire text: the optional `event:`, `id:` and
154
+ * `retry:` lines, the data split on `\n` into one `data:` line each,
155
+ * and the dispatching blank line. Throws `TypeError` for text the frame
156
+ * cannot carry: a line terminator inside `event` or `id`, U+0000 inside
157
+ * `id`, a bare carriage return inside `data` (a decoder would read it
158
+ * as a line break and corrupt the framing), or a `retry` that is not a
159
+ * non-negative integer.
160
+ * @param {{ event?: string | null, id?: string | null, data: string, retry?: number | null }} fields
161
+ * @returns {string}
162
+ */
163
+ export function encodeSseEvent(fields) {
164
+ if (fields === null || typeof fields !== 'object' || typeof fields.data !== 'string') {
165
+ throw new TypeError('encodeSseEvent: the argument must be { event?, id?, data, retry? } with a string data');
166
+ }
167
+ let out = '';
168
+ const event = fields.event;
169
+ if (event !== undefined && event !== null) {
170
+ if (typeof event !== 'string' || /[\r\n]/.test(event)) {
171
+ throw new TypeError('encodeSseEvent: event must be a single-line string');
172
+ }
173
+ out += 'event: ' + event + '\n';
174
+ }
175
+ const id = fields.id;
176
+ if (id !== undefined && id !== null) {
177
+ if (typeof id !== 'string' || /[\r\n\0]/.test(id)) {
178
+ throw new TypeError('encodeSseEvent: id must be a single-line string without U+0000');
179
+ }
180
+ out += 'id: ' + id + '\n';
181
+ }
182
+ const retry = fields.retry;
183
+ if (retry !== undefined && retry !== null) {
184
+ if (!Number.isInteger(retry) || retry < 0) {
185
+ throw new TypeError('encodeSseEvent: retry must be a non-negative integer');
186
+ }
187
+ out += 'retry: ' + retry + '\n';
188
+ }
189
+ const lines = fields.data.split('\n');
190
+ for (let i = 0; i < lines.length; i++) {
191
+ if (lines[i].indexOf('\r') !== -1) {
192
+ throw new TypeError('encodeSseEvent: data must not contain a bare carriage return — a decoder reads it as a line break');
193
+ }
194
+ out += 'data: ' + lines[i] + '\n';
195
+ }
196
+ return out + '\n';
197
+ }