langsmith 0.5.23 → 0.5.25
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/client.cjs +102 -11
- package/dist/client.d.ts +38 -0
- package/dist/client.js +103 -12
- package/dist/evaluation/_runner.cjs +3 -3
- package/dist/evaluation/_runner.js +1 -1
- package/dist/evaluation/evaluate_comparative.cjs +10 -10
- package/dist/evaluation/evaluate_comparative.js +1 -1
- package/dist/evaluation/evaluator.cjs +2 -2
- package/dist/evaluation/evaluator.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run_trees.cjs +8 -7
- package/dist/run_trees.d.ts +7 -0
- package/dist/run_trees.js +7 -6
- package/dist/schemas.d.ts +4 -0
- package/dist/singletons/otel.cjs +3 -2
- package/dist/singletons/otel.js +4 -3
- package/dist/traceable.cjs +1 -2
- package/dist/traceable.js +1 -2
- package/dist/utils/_uuid.cjs +2 -2
- package/dist/utils/_uuid.js +1 -1
- package/dist/utils/env.cjs +33 -0
- package/dist/utils/env.d.ts +9 -0
- package/dist/utils/env.js +32 -0
- package/dist/utils/fast-safe-stringify/index.cjs +10 -35
- package/dist/utils/fast-safe-stringify/index.d.ts +14 -1
- package/dist/utils/fast-safe-stringify/index.js +10 -35
- package/dist/utils/jestlike/index.cjs +5 -5
- package/dist/utils/jestlike/index.js +1 -1
- package/dist/utils/jestlike/vendor/evaluatedBy.cjs +3 -3
- package/dist/utils/jestlike/vendor/evaluatedBy.js +1 -1
- package/dist/utils/serialize_worker.cjs +389 -0
- package/dist/utils/serialize_worker.d.ts +67 -0
- package/dist/utils/serialize_worker.js +383 -0
- package/dist/utils/uuid/src/index.cjs +24 -0
- package/dist/utils/uuid/src/index.d.ts +10 -0
- package/dist/utils/uuid/src/index.js +9 -0
- package/dist/utils/uuid/src/max.cjs +3 -0
- package/dist/utils/uuid/src/max.d.ts +2 -0
- package/dist/utils/uuid/src/max.js +1 -0
- package/dist/utils/uuid/src/nil.cjs +3 -0
- package/dist/utils/uuid/src/nil.d.ts +2 -0
- package/dist/utils/uuid/src/nil.js +1 -0
- package/dist/utils/uuid/src/parse.cjs +23 -0
- package/dist/utils/uuid/src/parse.d.ts +3 -0
- package/dist/utils/uuid/src/parse.js +18 -0
- package/dist/utils/uuid/src/regex.cjs +3 -0
- package/dist/utils/uuid/src/regex.d.ts +2 -0
- package/dist/utils/uuid/src/regex.js +1 -0
- package/dist/utils/uuid/src/rng.cjs +10 -0
- package/dist/utils/uuid/src/rng.d.ts +1 -0
- package/dist/utils/uuid/src/rng.js +7 -0
- package/dist/utils/uuid/src/sha1.cjs +75 -0
- package/dist/utils/uuid/src/sha1.d.ts +2 -0
- package/dist/utils/uuid/src/sha1.js +73 -0
- package/dist/utils/uuid/src/stringify.cjs +55 -0
- package/dist/utils/uuid/src/stringify.d.ts +3 -0
- package/dist/utils/uuid/src/stringify.js +49 -0
- package/dist/utils/uuid/src/types.cjs +2 -0
- package/dist/utils/uuid/src/types.d.ts +22 -0
- package/dist/utils/uuid/src/types.js +1 -0
- package/dist/utils/uuid/src/v35.cjs +52 -0
- package/dist/utils/uuid/src/v35.d.ts +7 -0
- package/dist/utils/uuid/src/v35.js +44 -0
- package/dist/utils/uuid/src/v4.cjs +40 -0
- package/dist/utils/uuid/src/v4.d.ts +4 -0
- package/dist/utils/uuid/src/v4.js +35 -0
- package/dist/utils/uuid/src/v5.cjs +50 -0
- package/dist/utils/uuid/src/v5.d.ts +9 -0
- package/dist/utils/uuid/src/v5.js +9 -0
- package/dist/utils/uuid/src/v7.cjs +88 -0
- package/dist/utils/uuid/src/v7.d.ts +9 -0
- package/dist/utils/uuid/src/v7.js +82 -0
- package/dist/utils/uuid/src/validate.cjs +10 -0
- package/dist/utils/uuid/src/validate.d.ts +2 -0
- package/dist/utils/uuid/src/validate.js +5 -0
- package/dist/utils/uuid/src/version.cjs +13 -0
- package/dist/utils/uuid/src/version.d.ts +2 -0
- package/dist/utils/uuid/src/version.js +8 -0
- package/dist/utils/worker_threads.browser.cjs +16 -0
- package/dist/utils/worker_threads.browser.d.ts +14 -0
- package/dist/utils/worker_threads.browser.js +13 -0
- package/dist/utils/worker_threads.cjs +16 -0
- package/dist/utils/worker_threads.d.ts +13 -0
- package/dist/utils/worker_threads.js +13 -0
- package/dist/uuid.cjs +2 -2
- package/dist/uuid.js +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Off-thread serialization using Node worker_threads.
|
|
4
|
+
*
|
|
5
|
+
* Gated behind LANGSMITH_PERF_OPTIMIZATION=true. Falls back silently to
|
|
6
|
+
* synchronous serialize() when:
|
|
7
|
+
* - worker_threads is unavailable (browsers, Deno, Bun without compat,
|
|
8
|
+
* Cloudflare Workers, Vercel Edge, React Native)
|
|
9
|
+
* - the worker cannot be constructed (bundler/runtime constraints)
|
|
10
|
+
* - DataCloneError is thrown for a payload containing non-cloneable
|
|
11
|
+
* values (functions, class instances with non-cloneable state, etc.)
|
|
12
|
+
* - the worker crashes or throws
|
|
13
|
+
*
|
|
14
|
+
* Protocol:
|
|
15
|
+
* main -> worker: { id, op, payload }
|
|
16
|
+
* op = "serialize" -> worker returns bytes as a transferable ArrayBuffer
|
|
17
|
+
* worker -> main: { id, bytes?: ArrayBuffer, error?: string }
|
|
18
|
+
*
|
|
19
|
+
* The worker source is inlined as a string so the library bundles cleanly
|
|
20
|
+
* under webpack/esbuild/ncc without requiring a separate asset file.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.SerializeWorker = void 0;
|
|
24
|
+
exports.getSharedSerializeWorker = getSharedSerializeWorker;
|
|
25
|
+
exports.hasLargeString = hasLargeString;
|
|
26
|
+
const worker_threads_js_1 = require("./worker_threads.cjs");
|
|
27
|
+
// The worker script: a self-contained mirror of the hot path of
|
|
28
|
+
// src/utils/fast-safe-stringify/index.ts#serialize(). We deliberately
|
|
29
|
+
// don't import the TS module -- the worker runs as a standalone script.
|
|
30
|
+
const WORKER_SOURCE = /* js */ `
|
|
31
|
+
const { parentPort } = require("worker_threads");
|
|
32
|
+
|
|
33
|
+
const CIRCULAR_REPLACE_NODE = { result: "[Circular]" };
|
|
34
|
+
|
|
35
|
+
function serializeWellKnownTypes(val) {
|
|
36
|
+
if (val && typeof val === "object") {
|
|
37
|
+
if (val instanceof Map) return Object.fromEntries(val);
|
|
38
|
+
if (val instanceof Set) return Array.from(val);
|
|
39
|
+
if (val instanceof Date) return val.toISOString();
|
|
40
|
+
if (val instanceof RegExp) return val.toString();
|
|
41
|
+
if (val instanceof Error) return { name: val.name, message: val.message };
|
|
42
|
+
} else if (typeof val === "bigint") {
|
|
43
|
+
return val.toString();
|
|
44
|
+
}
|
|
45
|
+
return val;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultReplacer(_key, val) {
|
|
49
|
+
return serializeWellKnownTypes(val);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Decirculate in-place: replace circular refs with { result: "[Circular]" }
|
|
53
|
+
// then restore after stringify. Mirrors fast-safe-stringify's decirc().
|
|
54
|
+
const restoreStack = [];
|
|
55
|
+
function decirc(val, k, stack, parent) {
|
|
56
|
+
if (typeof val === "object" && val !== null) {
|
|
57
|
+
for (let i = 0; i < stack.length; i++) {
|
|
58
|
+
if (stack[i] === val) {
|
|
59
|
+
const orig = parent[k];
|
|
60
|
+
parent[k] = CIRCULAR_REPLACE_NODE;
|
|
61
|
+
restoreStack.push([parent, k, orig]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
stack.push(val);
|
|
66
|
+
if (Array.isArray(val)) {
|
|
67
|
+
for (let i = 0; i < val.length; i++) decirc(val[i], i, stack, val);
|
|
68
|
+
} else {
|
|
69
|
+
const normalized = serializeWellKnownTypes(val);
|
|
70
|
+
// Only recurse into normalized if it's still an object (arrays/objects),
|
|
71
|
+
// else it was replaced with a primitive (e.g. Date -> string).
|
|
72
|
+
if (normalized === val) {
|
|
73
|
+
const keys = Object.keys(val);
|
|
74
|
+
for (let i = 0; i < keys.length; i++) decirc(val[keys[i]], keys[i], stack, val);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
stack.pop();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function serialize(obj) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.stringify(obj, defaultReplacer);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
if (!String(e && e.message).includes("Converting circular structure to JSON")) {
|
|
86
|
+
return "[Unserializable]";
|
|
87
|
+
}
|
|
88
|
+
decirc(obj, "", [], { "": obj });
|
|
89
|
+
try {
|
|
90
|
+
return JSON.stringify(obj, defaultReplacer);
|
|
91
|
+
} catch (_) {
|
|
92
|
+
return "[unable to serialize, circular reference is too complex to analyze]";
|
|
93
|
+
} finally {
|
|
94
|
+
while (restoreStack.length) {
|
|
95
|
+
const [p, k, v] = restoreStack.pop();
|
|
96
|
+
p[k] = v;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
parentPort.on("message", (msg) => {
|
|
103
|
+
const { id, op, payload } = msg;
|
|
104
|
+
try {
|
|
105
|
+
if (op === "serialize") {
|
|
106
|
+
const str = serialize(payload);
|
|
107
|
+
const buf = Buffer.from(str, "utf8");
|
|
108
|
+
// Slice into its own ArrayBuffer so we can transfer without dragging
|
|
109
|
+
// unrelated bytes from any shared pool buffer.
|
|
110
|
+
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
111
|
+
parentPort.postMessage({ id, bytes: ab, length: buf.byteLength }, [ab]);
|
|
112
|
+
} else if (op === "ping") {
|
|
113
|
+
parentPort.postMessage({ id });
|
|
114
|
+
} else {
|
|
115
|
+
parentPort.postMessage({ id, error: "unknown op: " + op });
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
parentPort.postMessage({ id, error: String((e && e.message) || e) });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
`;
|
|
122
|
+
class SerializeWorker {
|
|
123
|
+
constructor() {
|
|
124
|
+
Object.defineProperty(this, "worker", {
|
|
125
|
+
enumerable: true,
|
|
126
|
+
configurable: true,
|
|
127
|
+
writable: true,
|
|
128
|
+
value: null
|
|
129
|
+
});
|
|
130
|
+
Object.defineProperty(this, "nextId", {
|
|
131
|
+
enumerable: true,
|
|
132
|
+
configurable: true,
|
|
133
|
+
writable: true,
|
|
134
|
+
value: 1
|
|
135
|
+
});
|
|
136
|
+
Object.defineProperty(this, "pending", {
|
|
137
|
+
enumerable: true,
|
|
138
|
+
configurable: true,
|
|
139
|
+
writable: true,
|
|
140
|
+
value: new Map()
|
|
141
|
+
});
|
|
142
|
+
Object.defineProperty(this, "disabled", {
|
|
143
|
+
enumerable: true,
|
|
144
|
+
configurable: true,
|
|
145
|
+
writable: true,
|
|
146
|
+
value: false
|
|
147
|
+
});
|
|
148
|
+
Object.defineProperty(this, "startPromise", {
|
|
149
|
+
enumerable: true,
|
|
150
|
+
configurable: true,
|
|
151
|
+
writable: true,
|
|
152
|
+
value: null
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Try to construct the worker. Returns false if the runtime can't support
|
|
157
|
+
* it -- in that case callers must fall back to synchronous serialization.
|
|
158
|
+
* Kept async so callers don't have to branch on runtime -- the promise
|
|
159
|
+
* resolves synchronously on the microtask queue when the worker module
|
|
160
|
+
* is available, which is the common Node CJS/ESM path.
|
|
161
|
+
*/
|
|
162
|
+
async ensureStarted() {
|
|
163
|
+
if (this.disabled)
|
|
164
|
+
return false;
|
|
165
|
+
if (this.worker !== null)
|
|
166
|
+
return true;
|
|
167
|
+
if (this.startPromise !== null)
|
|
168
|
+
return this.startPromise;
|
|
169
|
+
this.startPromise = this._start();
|
|
170
|
+
try {
|
|
171
|
+
return await this.startPromise;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
this.startPromise = null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async _start() {
|
|
178
|
+
// In browser / edge builds the `worker_threads` module is swapped with
|
|
179
|
+
// a stub that reports unavailability via the package.json `browser`
|
|
180
|
+
// field. Bail out before touching any Node-only surface.
|
|
181
|
+
if (!worker_threads_js_1.WORKER_THREADS_AVAILABLE || worker_threads_js_1.Worker === null) {
|
|
182
|
+
this.disabled = true;
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const worker = new worker_threads_js_1.Worker(WORKER_SOURCE, { eval: true });
|
|
187
|
+
worker.on("message", (msg) => {
|
|
188
|
+
const p = this.pending.get(msg.id);
|
|
189
|
+
if (!p)
|
|
190
|
+
return;
|
|
191
|
+
this.pending.delete(msg.id);
|
|
192
|
+
if (msg.error) {
|
|
193
|
+
p.reject(new Error(msg.error));
|
|
194
|
+
}
|
|
195
|
+
else if (msg.bytes && typeof msg.length === "number") {
|
|
196
|
+
p.resolve(new Uint8Array(msg.bytes, 0, msg.length));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
p.reject(new Error("worker returned malformed message"));
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
worker.on("error", (err) => {
|
|
203
|
+
// Reject all pending and disable; caller will fall back.
|
|
204
|
+
for (const [, p] of this.pending)
|
|
205
|
+
p.reject(err);
|
|
206
|
+
this.pending.clear();
|
|
207
|
+
this.disabled = true;
|
|
208
|
+
this.worker = null;
|
|
209
|
+
});
|
|
210
|
+
worker.on("exit", (code) => {
|
|
211
|
+
// Reject all pending requests regardless of exit code. Even a clean
|
|
212
|
+
// exit (code 0) with in-flight requests means those promises would
|
|
213
|
+
// otherwise hang forever.
|
|
214
|
+
for (const [, p] of this.pending) {
|
|
215
|
+
p.reject(new Error(`worker exited with code ${code}`));
|
|
216
|
+
}
|
|
217
|
+
this.pending.clear();
|
|
218
|
+
this.worker = null;
|
|
219
|
+
});
|
|
220
|
+
// Don't let the worker keep the process alive.
|
|
221
|
+
worker.unref();
|
|
222
|
+
this.worker = worker;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
this.disabled = true;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Serialize a payload off-thread. Rejects with DataCloneError (or similar)
|
|
232
|
+
* if the payload contains non-cloneable values -- callers must catch and
|
|
233
|
+
* fall back to synchronous serialize().
|
|
234
|
+
*
|
|
235
|
+
* Resolves with null if the worker subsystem is unavailable entirely,
|
|
236
|
+
* so the caller can fall back without paying try/catch overhead.
|
|
237
|
+
*/
|
|
238
|
+
async serialize(payload) {
|
|
239
|
+
const ok = await this.ensureStarted();
|
|
240
|
+
if (!ok)
|
|
241
|
+
return null;
|
|
242
|
+
const id = this.nextId++;
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
this.pending.set(id, { resolve, reject });
|
|
245
|
+
try {
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
+
this.worker.postMessage({ id, op: "serialize", payload });
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
// postMessage throws synchronously for DataCloneError, unclonable
|
|
251
|
+
// values, detached buffers, etc.
|
|
252
|
+
this.pending.delete(id);
|
|
253
|
+
reject(e);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async terminate() {
|
|
258
|
+
if (this.worker) {
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
await this.worker.terminate();
|
|
261
|
+
this.worker = null;
|
|
262
|
+
}
|
|
263
|
+
for (const [, p] of this.pending) {
|
|
264
|
+
p.reject(new Error("worker terminated"));
|
|
265
|
+
}
|
|
266
|
+
this.pending.clear();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
exports.SerializeWorker = SerializeWorker;
|
|
270
|
+
let sharedWorker = null;
|
|
271
|
+
/**
|
|
272
|
+
* Process-wide shared worker. One worker serves all Client instances to
|
|
273
|
+
* avoid spawning multiple threads per process.
|
|
274
|
+
*/
|
|
275
|
+
function getSharedSerializeWorker() {
|
|
276
|
+
if (sharedWorker === null)
|
|
277
|
+
sharedWorker = new SerializeWorker();
|
|
278
|
+
return sharedWorker;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Minimum string length (in UTF-16 code units) that justifies the overhead
|
|
282
|
+
* of dispatching serialization to a worker thread.
|
|
283
|
+
*
|
|
284
|
+
* Rationale: V8's postMessage / structuredClone fast-paths large strings
|
|
285
|
+
* across isolates by refcounting their underlying storage rather than
|
|
286
|
+
* copying the bytes. This makes worker offload a big win for payloads
|
|
287
|
+
* dominated by a handful of multi-hundred-KB strings (the classic case is
|
|
288
|
+
* base64-encoded images or audio in LLM messages), but a net loss for
|
|
289
|
+
* payloads whose bulk is structural -- thousands of keys, deep nesting,
|
|
290
|
+
* many small strings -- because every object node must still be walked
|
|
291
|
+
* and cloned.
|
|
292
|
+
*
|
|
293
|
+
* 64KB sits comfortably above typical "chunk of agent state" or "long
|
|
294
|
+
* prompt" values (a few KB) and below typical base64 media payloads
|
|
295
|
+
* (hundreds of KB to several MB).
|
|
296
|
+
*/
|
|
297
|
+
const LARGE_STRING_THRESHOLD = 64 * 1024;
|
|
298
|
+
/**
|
|
299
|
+
* Maximum number of nodes to inspect before giving up and assuming the
|
|
300
|
+
* payload is not worth offloading. Prevents the check itself from becoming
|
|
301
|
+
* expensive on pathologically structural payloads (many thousands of small
|
|
302
|
+
* keys / array elements).
|
|
303
|
+
*
|
|
304
|
+
* When the budget is exhausted without finding a large string we return
|
|
305
|
+
* false (do not offload). This is the conservative choice: such payloads
|
|
306
|
+
* are structural by nature and worker offload empirically regresses them.
|
|
307
|
+
*/
|
|
308
|
+
const NODE_BUDGET = 2048;
|
|
309
|
+
/**
|
|
310
|
+
* Cheap, short-circuiting walk that returns true iff the payload contains
|
|
311
|
+
* at least one string of length >= threshold anywhere in its graph.
|
|
312
|
+
*
|
|
313
|
+
* - Terminates immediately on the first qualifying string.
|
|
314
|
+
* - Caps total nodes visited at `nodeBudget` so cost is bounded for huge
|
|
315
|
+
* structural payloads.
|
|
316
|
+
* - Avoids allocation in the common path: uses an array as a stack and a
|
|
317
|
+
* Set only for cycle detection.
|
|
318
|
+
* - Uses `string.length` (UTF-16 units), not UTF-8 byte length, because
|
|
319
|
+
* that's what V8's string-sharing fast path keys on and because it's
|
|
320
|
+
* an O(1) property access. For ASCII content this is identical to the
|
|
321
|
+
* UTF-8 byte count; for non-ASCII text the two differ by at most 4x,
|
|
322
|
+
* well within the safety margin of the threshold.
|
|
323
|
+
*/
|
|
324
|
+
function hasLargeString(value, threshold = LARGE_STRING_THRESHOLD, nodeBudget = NODE_BUDGET) {
|
|
325
|
+
if (value === null || typeof value !== "object") {
|
|
326
|
+
return typeof value === "string" && value.length >= threshold;
|
|
327
|
+
}
|
|
328
|
+
const stack = [value];
|
|
329
|
+
const seen = new Set();
|
|
330
|
+
let visited = 0;
|
|
331
|
+
while (stack.length > 0) {
|
|
332
|
+
if (visited++ >= nodeBudget)
|
|
333
|
+
return false;
|
|
334
|
+
const cur = stack.pop();
|
|
335
|
+
if (cur === null || cur === undefined)
|
|
336
|
+
continue;
|
|
337
|
+
const t = typeof cur;
|
|
338
|
+
if (t === "string") {
|
|
339
|
+
if (cur.length >= threshold)
|
|
340
|
+
return true;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (t !== "object")
|
|
344
|
+
continue;
|
|
345
|
+
const obj = cur;
|
|
346
|
+
if (seen.has(obj))
|
|
347
|
+
continue;
|
|
348
|
+
seen.add(obj);
|
|
349
|
+
// Skip well-known opaque types -- none of their enumerable own
|
|
350
|
+
// properties produce large strings in practice, and ArrayBuffer views
|
|
351
|
+
// would inflate the node budget if iterated element by element.
|
|
352
|
+
/* eslint-disable no-instanceof/no-instanceof */
|
|
353
|
+
if (obj instanceof Date ||
|
|
354
|
+
obj instanceof RegExp ||
|
|
355
|
+
obj instanceof Error ||
|
|
356
|
+
obj instanceof ArrayBuffer ||
|
|
357
|
+
ArrayBuffer.isView(obj)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (Array.isArray(obj)) {
|
|
361
|
+
// Iterate in reverse so the first element is popped first (stable
|
|
362
|
+
// left-to-right discovery order, harmless but nice for predictable
|
|
363
|
+
// short-circuits in tests).
|
|
364
|
+
for (let i = obj.length - 1; i >= 0; i--)
|
|
365
|
+
stack.push(obj[i]);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (obj instanceof Map) {
|
|
369
|
+
for (const [, v] of obj)
|
|
370
|
+
stack.push(v);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (obj instanceof Set) {
|
|
374
|
+
for (const v of obj)
|
|
375
|
+
stack.push(v);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
/* eslint-enable no-instanceof/no-instanceof */
|
|
379
|
+
// Push keys in reverse so they pop in declared order. Combined with
|
|
380
|
+
// the similar reverse-push for arrays above, this makes discovery
|
|
381
|
+
// order a stable depth-first walk in source order -- which matters
|
|
382
|
+
// for predictable short-circuit behavior under a node budget.
|
|
383
|
+
const keys = Object.keys(obj);
|
|
384
|
+
for (let i = keys.length - 1; i >= 0; i--) {
|
|
385
|
+
stack.push(obj[keys[i]]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Off-thread serialization using Node worker_threads.
|
|
3
|
+
*
|
|
4
|
+
* Gated behind LANGSMITH_PERF_OPTIMIZATION=true. Falls back silently to
|
|
5
|
+
* synchronous serialize() when:
|
|
6
|
+
* - worker_threads is unavailable (browsers, Deno, Bun without compat,
|
|
7
|
+
* Cloudflare Workers, Vercel Edge, React Native)
|
|
8
|
+
* - the worker cannot be constructed (bundler/runtime constraints)
|
|
9
|
+
* - DataCloneError is thrown for a payload containing non-cloneable
|
|
10
|
+
* values (functions, class instances with non-cloneable state, etc.)
|
|
11
|
+
* - the worker crashes or throws
|
|
12
|
+
*
|
|
13
|
+
* Protocol:
|
|
14
|
+
* main -> worker: { id, op, payload }
|
|
15
|
+
* op = "serialize" -> worker returns bytes as a transferable ArrayBuffer
|
|
16
|
+
* worker -> main: { id, bytes?: ArrayBuffer, error?: string }
|
|
17
|
+
*
|
|
18
|
+
* The worker source is inlined as a string so the library bundles cleanly
|
|
19
|
+
* under webpack/esbuild/ncc without requiring a separate asset file.
|
|
20
|
+
*/
|
|
21
|
+
export declare class SerializeWorker {
|
|
22
|
+
private worker;
|
|
23
|
+
private nextId;
|
|
24
|
+
private pending;
|
|
25
|
+
private disabled;
|
|
26
|
+
private startPromise;
|
|
27
|
+
/**
|
|
28
|
+
* Try to construct the worker. Returns false if the runtime can't support
|
|
29
|
+
* it -- in that case callers must fall back to synchronous serialization.
|
|
30
|
+
* Kept async so callers don't have to branch on runtime -- the promise
|
|
31
|
+
* resolves synchronously on the microtask queue when the worker module
|
|
32
|
+
* is available, which is the common Node CJS/ESM path.
|
|
33
|
+
*/
|
|
34
|
+
private ensureStarted;
|
|
35
|
+
private _start;
|
|
36
|
+
/**
|
|
37
|
+
* Serialize a payload off-thread. Rejects with DataCloneError (or similar)
|
|
38
|
+
* if the payload contains non-cloneable values -- callers must catch and
|
|
39
|
+
* fall back to synchronous serialize().
|
|
40
|
+
*
|
|
41
|
+
* Resolves with null if the worker subsystem is unavailable entirely,
|
|
42
|
+
* so the caller can fall back without paying try/catch overhead.
|
|
43
|
+
*/
|
|
44
|
+
serialize(payload: unknown): Promise<Uint8Array<ArrayBuffer> | null>;
|
|
45
|
+
terminate(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Process-wide shared worker. One worker serves all Client instances to
|
|
49
|
+
* avoid spawning multiple threads per process.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getSharedSerializeWorker(): SerializeWorker;
|
|
52
|
+
/**
|
|
53
|
+
* Cheap, short-circuiting walk that returns true iff the payload contains
|
|
54
|
+
* at least one string of length >= threshold anywhere in its graph.
|
|
55
|
+
*
|
|
56
|
+
* - Terminates immediately on the first qualifying string.
|
|
57
|
+
* - Caps total nodes visited at `nodeBudget` so cost is bounded for huge
|
|
58
|
+
* structural payloads.
|
|
59
|
+
* - Avoids allocation in the common path: uses an array as a stack and a
|
|
60
|
+
* Set only for cycle detection.
|
|
61
|
+
* - Uses `string.length` (UTF-16 units), not UTF-8 byte length, because
|
|
62
|
+
* that's what V8's string-sharing fast path keys on and because it's
|
|
63
|
+
* an O(1) property access. For ASCII content this is identical to the
|
|
64
|
+
* UTF-8 byte count; for non-ASCII text the two differ by at most 4x,
|
|
65
|
+
* well within the safety margin of the threshold.
|
|
66
|
+
*/
|
|
67
|
+
export declare function hasLargeString(value: unknown, threshold?: number, nodeBudget?: number): boolean;
|