@hue-run/sdk 0.18.2 → 0.19.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/CLI.md +16 -12
- package/EVALUATIONS.md +1 -1
- package/README.md +110 -68
- package/dist/blobs.d.ts +116 -36
- package/dist/blobs.js +359 -162
- package/dist/cli/langfuse.d.ts +21 -5
- package/dist/cli/langfuse.js +123 -16
- package/dist/client.d.ts +11 -7
- package/dist/client.js +35 -26
- package/dist/inline-files.d.ts +21 -4
- package/dist/inline-files.js +100 -14
- package/dist/large-media.d.ts +15 -2
- package/dist/large-media.js +27 -16
- package/dist/transport.d.ts +38 -7
- package/dist/transport.js +240 -63
- package/dist/types.d.ts +5 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/large-media.d.ts
CHANGED
|
@@ -24,10 +24,18 @@ export declare const WINDOW_UNITS: number;
|
|
|
24
24
|
* reused for the next one, so a reader is done with each slice before it asks for the next. */
|
|
25
25
|
export interface ByteSource {
|
|
26
26
|
readonly size: number;
|
|
27
|
+
/** The strings the source keeps alive while it is held, each whole; absent for one read from
|
|
28
|
+
* elsewhere. */
|
|
29
|
+
readonly held?: readonly HeldText[];
|
|
27
30
|
chunks(): Iterable<Uint8Array> | AsyncIterable<Uint8Array>;
|
|
28
31
|
/** Set when the latest read found the source no longer holds the bytes it was recorded with. */
|
|
29
32
|
changed?: boolean;
|
|
30
33
|
}
|
|
34
|
+
/** A string sources keep alive, as its two bytes a code unit: a slice read in place keeps the
|
|
35
|
+
* whole string, so the sources that read one share its {@link HeldText}, charged once. */
|
|
36
|
+
export interface HeldText {
|
|
37
|
+
readonly bytes: number;
|
|
38
|
+
}
|
|
31
39
|
/** Whether a value is a {@link ByteSource}. */
|
|
32
40
|
export declare function isByteSource(value: unknown): value is ByteSource;
|
|
33
41
|
/** How the bytes of a long string are read from its text. */
|
|
@@ -42,6 +50,8 @@ export interface LongText {
|
|
|
42
50
|
kind: LongKind;
|
|
43
51
|
/** A `data:` URL's header, between `data:` and its comma. */
|
|
44
52
|
header?: string;
|
|
53
|
+
/** `text` whole, shared by every long string read from it. */
|
|
54
|
+
held: HeldText;
|
|
45
55
|
}
|
|
46
56
|
/** A media value read in place, with the long strings its stand-ins name. Its `source` is the
|
|
47
57
|
* skeleton: placing its files edits the skeleton, and {@link placedLargeText} writes the long
|
|
@@ -57,7 +67,8 @@ export interface LargeMediaValue extends MediaValue {
|
|
|
57
67
|
* An attribute's recorded text read for files as `readMediaText` reads it, its long base64 and
|
|
58
68
|
* base64 `data:` URL strings read in place (see {@link skeletonOf}). Undefined for text whose JSON
|
|
59
69
|
* around those strings is over the 8 Mi bound, or that reading finds nothing in. Throws for JSON
|
|
60
|
-
* that does not parse
|
|
70
|
+
* that does not parse, except text read by JSON convention that is a `data:` URL, which is read
|
|
71
|
+
* as the text itself, as `readMediaText` reads it.
|
|
61
72
|
*/
|
|
62
73
|
export declare function readLargeMediaText(key: string, text: string, longUnits?: number): Promise<LargeMediaValue | undefined>;
|
|
63
74
|
/**
|
|
@@ -67,8 +78,10 @@ export declare function readLargeMediaText(key: string, text: string, longUnits?
|
|
|
67
78
|
* remainder is never held in two copies.
|
|
68
79
|
*/
|
|
69
80
|
export declare class JoinedText {
|
|
81
|
+
private readonly held;
|
|
70
82
|
private readonly pieces;
|
|
71
|
-
|
|
83
|
+
/** `held`: the strings the pieces are cut from, which the text keeps alive whole. */
|
|
84
|
+
constructor(pieces: [string, number, number][], held: readonly HeldText[]);
|
|
72
85
|
/** The text a window at a time, never between the halves of a surrogate pair. */
|
|
73
86
|
windows(): Generator<string>;
|
|
74
87
|
/** The first `units` code units. */
|
package/dist/large-media.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { MAX_INLINE_FILE_TEXT, mediaIn, placedMediaText, readerParses, readingOf, } from "./inline-files.js";
|
|
2
|
+
import { headerMediaType, MAX_INLINE_FILE_TEXT, mediaIn, placedMediaText, readerParses, readingOf, } from "./inline-files.js";
|
|
3
3
|
/**
|
|
4
4
|
* Inline media in a value too long for the media inspection bounds (8 Mi UTF-16 code units for one
|
|
5
5
|
* attribute, 16 Mi for a record), read without parsing the files out of it. A span's own attribute
|
|
@@ -63,7 +63,7 @@ async function strictBase64(text, start, end) {
|
|
|
63
63
|
* is parsed with the rest of the value (a percent-escaped `data:` URL included); a string literal
|
|
64
64
|
* whose header holds a control character, which no JSON does, is left for the parse to refuse.
|
|
65
65
|
*/
|
|
66
|
-
async function classify(text, start, end, raw = false) {
|
|
66
|
+
async function classify(text, start, end, held, raw = false) {
|
|
67
67
|
if (text.startsWith("data:", start)) {
|
|
68
68
|
// The comma is looked for within the string and the header's bound only.
|
|
69
69
|
const found = text
|
|
@@ -77,11 +77,11 @@ async function classify(text, start, end, raw = false) {
|
|
|
77
77
|
return undefined;
|
|
78
78
|
if (!(await strictBase64(text, comma + 1, end)))
|
|
79
79
|
return undefined;
|
|
80
|
-
return { text, start, end, data: comma + 1, kind: "dataBase64", header };
|
|
80
|
+
return { text, start, end, data: comma + 1, kind: "dataBase64", header, held };
|
|
81
81
|
}
|
|
82
82
|
if (!(await strictBase64(text, start, end)))
|
|
83
83
|
return undefined;
|
|
84
|
-
return { text, start, end, data: start, kind: "base64" };
|
|
84
|
+
return { text, start, end, data: start, kind: "base64", held };
|
|
85
85
|
}
|
|
86
86
|
/** The stand-in for a long string in the skeleton: its kind kept (base64 and its first 16
|
|
87
87
|
* characters, which name an audio container; a `data:` URL's header), longer than any label, and
|
|
@@ -139,7 +139,7 @@ async function find(text, character, from, to) {
|
|
|
139
139
|
* unterminated (text that is no JSON) ends the scan; the skeleton then holds the rest as recorded
|
|
140
140
|
* and fails to parse. The scan reads at most a window of the text in one synchronous step.
|
|
141
141
|
*/
|
|
142
|
-
async function skeletonOf(text, longUnits) {
|
|
142
|
+
async function skeletonOf(text, longUnits, held) {
|
|
143
143
|
const nonce = randomBytes(16).toString("hex");
|
|
144
144
|
const long = new Map();
|
|
145
145
|
const replaced = [];
|
|
@@ -193,7 +193,7 @@ async function skeletonOf(text, longUnits) {
|
|
|
193
193
|
// Only a value: a key stays as it is.
|
|
194
194
|
const found = escaped || close - open - 1 <= longUnits || text.charCodeAt(space(text, at)) === 0x3a
|
|
195
195
|
? undefined
|
|
196
|
-
: await classify(text, open + 1, close);
|
|
196
|
+
: await classify(text, open + 1, close, held);
|
|
197
197
|
if (found) {
|
|
198
198
|
const stub = standIn(found, `${nonce}${long.size.toString(16).padStart(8, "0")}`);
|
|
199
199
|
long.set(stub, found);
|
|
@@ -218,15 +218,17 @@ async function skeletonOf(text, longUnits) {
|
|
|
218
218
|
* An attribute's recorded text read for files as `readMediaText` reads it, its long base64 and
|
|
219
219
|
* base64 `data:` URL strings read in place (see {@link skeletonOf}). Undefined for text whose JSON
|
|
220
220
|
* around those strings is over the 8 Mi bound, or that reading finds nothing in. Throws for JSON
|
|
221
|
-
* that does not parse
|
|
221
|
+
* that does not parse, except text read by JSON convention that is a `data:` URL, which is read
|
|
222
|
+
* as the text itself, as `readMediaText` reads it.
|
|
222
223
|
*/
|
|
223
224
|
export async function readLargeMediaText(key, text, longUnits = LONG_STRING_UNITS) {
|
|
224
225
|
const reading = readingOf(key);
|
|
226
|
+
const held = { bytes: text.length * 2 };
|
|
225
227
|
/** Text that is no JSON is the file itself when it is one long base64 `data:` URL; other text
|
|
226
228
|
* is read as `readMediaText` reads it, within the 8 Mi bound. */
|
|
227
229
|
const whole = async () => {
|
|
228
230
|
const found = text.length > longUnits && text.startsWith("data:")
|
|
229
|
-
? await classify(text, 0, text.length, true)
|
|
231
|
+
? await classify(text, 0, text.length, held, true)
|
|
230
232
|
: undefined;
|
|
231
233
|
if (!found)
|
|
232
234
|
return text.length > MAX_INLINE_FILE_TEXT
|
|
@@ -237,7 +239,7 @@ export async function readLargeMediaText(key, text, longUnits = LONG_STRING_UNIT
|
|
|
237
239
|
return { ...mediaIn(stub), long: new Map([[stub, found]]), nonce, order: [stub] };
|
|
238
240
|
};
|
|
239
241
|
const json = async (check) => {
|
|
240
|
-
const skeleton = await skeletonOf(text, longUnits);
|
|
242
|
+
const skeleton = await skeletonOf(text, longUnits, held);
|
|
241
243
|
if (!skeleton)
|
|
242
244
|
return undefined;
|
|
243
245
|
const parsed = JSON.parse(skeleton.text);
|
|
@@ -254,7 +256,9 @@ export async function readLargeMediaText(key, text, longUnits = LONG_STRING_UNIT
|
|
|
254
256
|
};
|
|
255
257
|
switch (reading) {
|
|
256
258
|
case "json":
|
|
257
|
-
|
|
259
|
+
// No JSON text starts with `data:`: such text is the text itself, as `readMediaText` reads
|
|
260
|
+
// text that does not parse, and a whole `data:` URL is the file.
|
|
261
|
+
return text.startsWith("data:") ? whole() : json();
|
|
258
262
|
case "metadata":
|
|
259
263
|
return /^\s*[[{]/.test(text) ? json() : whole();
|
|
260
264
|
case "parts":
|
|
@@ -279,8 +283,11 @@ export async function readLargeMediaText(key, text, longUnits = LONG_STRING_UNIT
|
|
|
279
283
|
* remainder is never held in two copies.
|
|
280
284
|
*/
|
|
281
285
|
export class JoinedText {
|
|
286
|
+
held;
|
|
282
287
|
pieces;
|
|
283
|
-
|
|
288
|
+
/** `held`: the strings the pieces are cut from, which the text keeps alive whole. */
|
|
289
|
+
constructor(pieces, held) {
|
|
290
|
+
this.held = held;
|
|
284
291
|
this.pieces = pieces.filter(([, start, end]) => end > start);
|
|
285
292
|
}
|
|
286
293
|
/** The text a window at a time, never between the halves of a surrogate pair. */
|
|
@@ -329,6 +336,7 @@ export class JoinedText {
|
|
|
329
336
|
const windows = () => this.windows();
|
|
330
337
|
return {
|
|
331
338
|
size,
|
|
339
|
+
held: this.held,
|
|
332
340
|
*chunks() {
|
|
333
341
|
const buffer = Buffer.allocUnsafe(WINDOW_UNITS * 3);
|
|
334
342
|
for (const window of windows())
|
|
@@ -350,6 +358,7 @@ export async function placedLargeText(media) {
|
|
|
350
358
|
// One forward scan for the stand-ins' shared nonce finds each one left, in order, by the index
|
|
351
359
|
// its tag carries: linear in the text however many there are, a window at a time.
|
|
352
360
|
const pieces = [];
|
|
361
|
+
const held = new Set([{ bytes: edited.length * 2 }]);
|
|
353
362
|
let from = 0;
|
|
354
363
|
let turn = 0;
|
|
355
364
|
for (let at = edited.indexOf(media.nonce); at !== -1; at = edited.indexOf(media.nonce, at + 1)) {
|
|
@@ -365,21 +374,23 @@ export async function placedLargeText(media) {
|
|
|
365
374
|
continue;
|
|
366
375
|
const long = media.long.get(stub);
|
|
367
376
|
pieces.push([edited, from, start], [long.text, long.start, long.end]);
|
|
377
|
+
held.add(long.held);
|
|
368
378
|
from = start + stub.length;
|
|
369
379
|
at = from - 1;
|
|
370
380
|
}
|
|
371
381
|
if (!pieces.length)
|
|
372
382
|
return edited;
|
|
373
383
|
pieces.push([edited, from, edited.length]);
|
|
374
|
-
return new JoinedText(pieces);
|
|
384
|
+
return new JoinedText(pieces, [...held]);
|
|
375
385
|
}
|
|
376
386
|
/** Base64 from `start` to `end` of `text` (strict, padded to a multiple of four), decoded a window
|
|
377
|
-
* at a time into one buffer each read reuses. */
|
|
378
|
-
function base64Source(text, start, end) {
|
|
387
|
+
* at a time into one buffer each read reuses. `held` is `text` whole. */
|
|
388
|
+
function base64Source(text, start, end, held) {
|
|
379
389
|
const length = end - start;
|
|
380
390
|
const pad = (text.charCodeAt(end - 1) === 0x3d ? 1 : 0) + (text.charCodeAt(end - 2) === 0x3d ? 1 : 0);
|
|
381
391
|
return {
|
|
382
392
|
size: (length / 4) * 3 - pad,
|
|
393
|
+
held: [held],
|
|
383
394
|
*chunks() {
|
|
384
395
|
const buffer = Buffer.allocUnsafe((WINDOW_UNITS / 4) * 3);
|
|
385
396
|
for (let at = start; at < end; at += WINDOW_UNITS) {
|
|
@@ -392,7 +403,7 @@ function base64Source(text, start, end) {
|
|
|
392
403
|
/** The bytes of a file whose content is a long string, as `mediaBytes` reads them: a field its
|
|
393
404
|
* convention declares base64 decoded, base64 text decoded (`bareBase64`), a base64 `data:` URL. */
|
|
394
405
|
export function longFileBytes(file, long) {
|
|
395
|
-
const source = base64Source(long.text, long.data, long.end);
|
|
406
|
+
const source = base64Source(long.text, long.data, long.end, long.held);
|
|
396
407
|
if (long.kind === "base64")
|
|
397
408
|
return {
|
|
398
409
|
size: source.size,
|
|
@@ -400,7 +411,7 @@ export function longFileBytes(file, long) {
|
|
|
400
411
|
...(file.base64 ? {} : { bareBase64: true }),
|
|
401
412
|
source,
|
|
402
413
|
};
|
|
403
|
-
const mediaType = long.header
|
|
414
|
+
const mediaType = headerMediaType(long.header);
|
|
404
415
|
return {
|
|
405
416
|
size: source.size,
|
|
406
417
|
decoded: true,
|
package/dist/transport.d.ts
CHANGED
|
@@ -11,9 +11,11 @@ export type RecordValue = ReadableSpan | ReadableLogRecord;
|
|
|
11
11
|
* them, reported like any refused request.
|
|
12
12
|
*/
|
|
13
13
|
export declare const MAX_RATE_LIMIT_HOLD_MILLIS = 60000;
|
|
14
|
-
/** How long one export may take with requests of `timeoutMillis` each: its
|
|
14
|
+
/** How long one export may take with requests of `timeoutMillis` each: hashing its values over
|
|
15
15
|
* the inline limit, its requests, and a hold for a rate-limited receiver on top. Processors and
|
|
16
|
-
* providers wait this long for a drain.
|
|
16
|
+
* providers wait this long for a drain. A drain also waits for the uploads its exports queued,
|
|
17
|
+
* which run beside the requests and end within six timeouts and a minute of being queued, so they
|
|
18
|
+
* fit within the same budget. */
|
|
17
19
|
export declare function exportBudgetMillis(timeoutMillis: number): number;
|
|
18
20
|
/**
|
|
19
21
|
* Rejection of {@link HueClient.flush} and {@link HueClient.shutdown}: telemetry was not fully
|
|
@@ -67,7 +69,11 @@ export declare class HueTransport {
|
|
|
67
69
|
private logExporter;
|
|
68
70
|
private closed;
|
|
69
71
|
private shutdownPromise?;
|
|
70
|
-
|
|
72
|
+
/** The latest flush's send of its records; the next flush's send starts after it. */
|
|
73
|
+
private sendPromise?;
|
|
74
|
+
/** Set while a flush sends its records: the processors leave the uploads to that flush, which
|
|
75
|
+
* waits for them after its send, so no flush's send waits behind another's uploads. */
|
|
76
|
+
private sendingRecords;
|
|
71
77
|
private live;
|
|
72
78
|
private liveTimer?;
|
|
73
79
|
private liveSpans;
|
|
@@ -89,6 +95,10 @@ export declare class HueTransport {
|
|
|
89
95
|
/** Bytes of values over the inline limit queued spans hold whole for upload, and each span's. */
|
|
90
96
|
private heldBytes;
|
|
91
97
|
private held;
|
|
98
|
+
/** Bytes of each record's charge (its own held values and its staged files) that its queued
|
|
99
|
+
* uploads took over before the record was finished: charged to the uploads alone until they
|
|
100
|
+
* settle or the record is finished. */
|
|
101
|
+
private heldByUploads;
|
|
92
102
|
private mediaMemo;
|
|
93
103
|
private media;
|
|
94
104
|
private logParts;
|
|
@@ -98,6 +108,11 @@ export declare class HueTransport {
|
|
|
98
108
|
private mediaFinalizer;
|
|
99
109
|
private uploaded;
|
|
100
110
|
private uploadFallbacks;
|
|
111
|
+
/** Each export's queued uploads, until they all settle and are reported. */
|
|
112
|
+
private uploadReports;
|
|
113
|
+
/** Settles once every upload report registered so far has: each drain awaits this one promise,
|
|
114
|
+
* so a drain costs the same however many uploads are pending. */
|
|
115
|
+
private uploadsSettled;
|
|
101
116
|
constructor(options: HueOptions);
|
|
102
117
|
/**
|
|
103
118
|
* Advisory records (placeholders) are admitted only while the queue is under a quarter of its
|
|
@@ -113,6 +128,15 @@ export declare class HueTransport {
|
|
|
113
128
|
/** Holds the staged attachments of a log record being admitted with the queued record, within
|
|
114
129
|
* the held-value budget; those the budget refuses are counted as not uploaded. */
|
|
115
130
|
private admitLogParts;
|
|
131
|
+
/** What the held-value budget has room for: values queued records hold and bytes unsettled
|
|
132
|
+
* uploads hold share it. */
|
|
133
|
+
private heldRoom;
|
|
134
|
+
/** An upload `record`'s export queued, as it is queued: flushes and the processors wait for it
|
|
135
|
+
* from now on, and the `shared` bytes it holds of the record's own are charged to the upload
|
|
136
|
+
* alone, so the held-value budget never counts them twice while the export goes on. A charge
|
|
137
|
+
* `released` before the record is finished is handed back to the record, which still holds
|
|
138
|
+
* them. */
|
|
139
|
+
private uploadQueued;
|
|
116
140
|
/** Registers a span whose staged files are released if it is never exported. */
|
|
117
141
|
private ownMedia;
|
|
118
142
|
private mediaIdentity;
|
|
@@ -136,15 +160,22 @@ export declare class HueTransport {
|
|
|
136
160
|
private endedSpans;
|
|
137
161
|
private countEnded;
|
|
138
162
|
/**
|
|
139
|
-
* Waits for the processors' and exporters' in-flight work
|
|
163
|
+
* Waits for the processors' and exporters' in-flight work, then for the uploads their exports
|
|
164
|
+
* queued; drain the providers first. Only the sends are serialized: a flush sends its records
|
|
165
|
+
* once an earlier flush sent its own, never after that flush's uploads.
|
|
140
166
|
*
|
|
141
167
|
* @throws HueExportError when a new non-warning issue was recorded since the previous observation.
|
|
142
168
|
*/
|
|
143
169
|
flush(): Promise<ExportReport>;
|
|
144
|
-
|
|
170
|
+
/** Sends what the processors and exporters hold, once the previous flush's send ended. */
|
|
171
|
+
private sendRecords;
|
|
172
|
+
/** Waits for the uploads queued so far; each ends within its lifetime, so this is bounded. */
|
|
173
|
+
private drainUploads;
|
|
174
|
+
private flushReport;
|
|
145
175
|
/**
|
|
146
|
-
* Flushes and stops the processors
|
|
147
|
-
* mode call it after shutting down the
|
|
176
|
+
* Flushes and stops the processors, waits for queued uploads, then ends the uploader; records
|
|
177
|
+
* emitted afterwards are dropped and counted. In attach mode call it after shutting down the
|
|
178
|
+
* application's providers.
|
|
148
179
|
*
|
|
149
180
|
* @throws HueExportError when the final flush observed new failures.
|
|
150
181
|
*/
|