trickle-observe 0.1.0

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.
Files changed (57) hide show
  1. package/auto-env.js +13 -0
  2. package/auto-esm.mjs +128 -0
  3. package/auto.js +3 -0
  4. package/dist/auto-codegen.d.ts +29 -0
  5. package/dist/auto-codegen.js +999 -0
  6. package/dist/auto-register.d.ts +16 -0
  7. package/dist/auto-register.js +99 -0
  8. package/dist/cache.d.ts +27 -0
  9. package/dist/cache.js +52 -0
  10. package/dist/env-detect.d.ts +5 -0
  11. package/dist/env-detect.js +35 -0
  12. package/dist/express.d.ts +44 -0
  13. package/dist/express.js +342 -0
  14. package/dist/fetch-observer.d.ts +24 -0
  15. package/dist/fetch-observer.js +217 -0
  16. package/dist/index.d.ts +64 -0
  17. package/dist/index.js +172 -0
  18. package/dist/observe-register.d.ts +29 -0
  19. package/dist/observe-register.js +455 -0
  20. package/dist/observe.d.ts +44 -0
  21. package/dist/observe.js +109 -0
  22. package/dist/proxy-tracker.d.ts +15 -0
  23. package/dist/proxy-tracker.js +172 -0
  24. package/dist/register.d.ts +21 -0
  25. package/dist/register.js +105 -0
  26. package/dist/transport.d.ts +22 -0
  27. package/dist/transport.js +228 -0
  28. package/dist/type-hash.d.ts +5 -0
  29. package/dist/type-hash.js +60 -0
  30. package/dist/type-inference.d.ts +14 -0
  31. package/dist/type-inference.js +259 -0
  32. package/dist/types.d.ts +78 -0
  33. package/dist/types.js +2 -0
  34. package/dist/wrap.d.ts +10 -0
  35. package/dist/wrap.js +247 -0
  36. package/observe-esm-hooks.mjs +367 -0
  37. package/observe-esm.mjs +40 -0
  38. package/observe.js +2 -0
  39. package/package.json +26 -0
  40. package/register.js +2 -0
  41. package/src/auto-codegen.ts +1058 -0
  42. package/src/auto-register.ts +102 -0
  43. package/src/cache.ts +53 -0
  44. package/src/env-detect.ts +22 -0
  45. package/src/express.ts +386 -0
  46. package/src/fetch-observer.ts +226 -0
  47. package/src/index.ts +199 -0
  48. package/src/observe-register.ts +453 -0
  49. package/src/observe.ts +127 -0
  50. package/src/proxy-tracker.ts +208 -0
  51. package/src/register.ts +110 -0
  52. package/src/transport.ts +207 -0
  53. package/src/type-hash.ts +71 -0
  54. package/src/type-inference.ts +285 -0
  55. package/src/types.ts +61 -0
  56. package/src/wrap.ts +289 -0
  57. package/tsconfig.json +8 -0
@@ -0,0 +1,285 @@
1
+ import { TypeNode } from './types';
2
+
3
+ const MAX_ARRAY_SAMPLE = 20;
4
+ const DEFAULT_MAX_DEPTH = 5;
5
+
6
+ /**
7
+ * Infer the TypeNode representation of a runtime JavaScript value.
8
+ * Uses a WeakSet to detect circular references.
9
+ * Samples at most the first 20 elements of arrays for performance.
10
+ */
11
+ export function inferType(value: unknown, maxDepth: number = DEFAULT_MAX_DEPTH): TypeNode {
12
+ const seen = new WeakSet();
13
+ return infer(value, maxDepth, seen);
14
+ }
15
+
16
+ function infer(value: unknown, depth: number, seen: WeakSet<object>): TypeNode {
17
+ // Null
18
+ if (value === null) {
19
+ return { kind: 'primitive', name: 'null' };
20
+ }
21
+
22
+ // Undefined
23
+ if (value === undefined) {
24
+ return { kind: 'primitive', name: 'undefined' };
25
+ }
26
+
27
+ const t = typeof value;
28
+
29
+ // Primitives
30
+ if (t === 'string') return { kind: 'primitive', name: 'string' };
31
+ if (t === 'number') return { kind: 'primitive', name: 'number' };
32
+ if (t === 'boolean') return { kind: 'primitive', name: 'boolean' };
33
+ if (t === 'bigint') return { kind: 'primitive', name: 'bigint' };
34
+ if (t === 'symbol') return { kind: 'primitive', name: 'symbol' };
35
+
36
+ // Functions
37
+ if (t === 'function') {
38
+ return {
39
+ kind: 'function',
40
+ params: new Array((value as Function).length).fill({ kind: 'unknown' } as TypeNode),
41
+ returnType: { kind: 'unknown' },
42
+ };
43
+ }
44
+
45
+ // Object types — check depth and circular refs
46
+ if (t === 'object') {
47
+ const obj = value as object;
48
+
49
+ // Circular reference detection
50
+ if (seen.has(obj)) {
51
+ return { kind: 'unknown' };
52
+ }
53
+
54
+ if (depth <= 0) {
55
+ return { kind: 'unknown' };
56
+ }
57
+
58
+ seen.add(obj);
59
+
60
+ try {
61
+ return inferObject(obj, depth, seen);
62
+ } finally {
63
+ // Don't remove from seen — keeps circular detection intact for the full traversal
64
+ }
65
+ }
66
+
67
+ return { kind: 'unknown' };
68
+ }
69
+
70
+ function inferObject(obj: object, depth: number, seen: WeakSet<object>): TypeNode {
71
+ // Promise
72
+ if (obj instanceof Promise) {
73
+ return { kind: 'promise', resolved: { kind: 'unknown' } };
74
+ }
75
+
76
+ // Map
77
+ if (obj instanceof Map) {
78
+ let keyType: TypeNode = { kind: 'unknown' };
79
+ let valType: TypeNode = { kind: 'unknown' };
80
+ let count = 0;
81
+ const keyTypes: TypeNode[] = [];
82
+ const valTypes: TypeNode[] = [];
83
+
84
+ for (const [k, v] of obj) {
85
+ if (count >= MAX_ARRAY_SAMPLE) break;
86
+ keyTypes.push(infer(k, depth - 1, seen));
87
+ valTypes.push(infer(v, depth - 1, seen));
88
+ count++;
89
+ }
90
+
91
+ keyType = unifyTypes(keyTypes);
92
+ valType = unifyTypes(valTypes);
93
+
94
+ return { kind: 'map', key: keyType, value: valType };
95
+ }
96
+
97
+ // Set
98
+ if (obj instanceof Set) {
99
+ const elementTypes: TypeNode[] = [];
100
+ let count = 0;
101
+ for (const item of obj) {
102
+ if (count >= MAX_ARRAY_SAMPLE) break;
103
+ elementTypes.push(infer(item, depth - 1, seen));
104
+ count++;
105
+ }
106
+ return { kind: 'set', element: unifyTypes(elementTypes) };
107
+ }
108
+
109
+ // Array (including TypedArrays)
110
+ if (Array.isArray(obj)) {
111
+ return inferArray(obj, depth, seen);
112
+ }
113
+
114
+ // TypedArrays (not real arrays)
115
+ if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) {
116
+ return {
117
+ kind: 'object',
118
+ properties: {
119
+ __typedArray: { kind: 'primitive', name: 'string' },
120
+ length: { kind: 'primitive', name: 'number' },
121
+ },
122
+ };
123
+ }
124
+
125
+ // Buffer (Node.js)
126
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(obj)) {
127
+ return {
128
+ kind: 'object',
129
+ properties: {
130
+ __buffer: { kind: 'primitive', name: 'string' },
131
+ length: { kind: 'primitive', name: 'number' },
132
+ },
133
+ };
134
+ }
135
+
136
+ // Date
137
+ if (obj instanceof Date) {
138
+ return {
139
+ kind: 'object',
140
+ properties: {
141
+ __date: { kind: 'primitive', name: 'string' },
142
+ },
143
+ };
144
+ }
145
+
146
+ // RegExp
147
+ if (obj instanceof RegExp) {
148
+ return {
149
+ kind: 'object',
150
+ properties: {
151
+ __regexp: { kind: 'primitive', name: 'string' },
152
+ source: { kind: 'primitive', name: 'string' },
153
+ flags: { kind: 'primitive', name: 'string' },
154
+ },
155
+ };
156
+ }
157
+
158
+ // Error
159
+ if (obj instanceof Error) {
160
+ return {
161
+ kind: 'object',
162
+ properties: {
163
+ __error: { kind: 'primitive', name: 'string' },
164
+ name: { kind: 'primitive', name: 'string' },
165
+ message: { kind: 'primitive', name: 'string' },
166
+ stack: { kind: 'primitive', name: 'string' },
167
+ },
168
+ };
169
+ }
170
+
171
+ // Plain objects
172
+ return inferPlainObject(obj, depth, seen);
173
+ }
174
+
175
+ function inferArray(arr: unknown[], depth: number, seen: WeakSet<object>): TypeNode {
176
+ if (arr.length === 0) {
177
+ return { kind: 'array', element: { kind: 'unknown' } };
178
+ }
179
+
180
+ const sampleSize = Math.min(arr.length, MAX_ARRAY_SAMPLE);
181
+ const elementTypes: TypeNode[] = [];
182
+
183
+ for (let i = 0; i < sampleSize; i++) {
184
+ elementTypes.push(infer(arr[i], depth - 1, seen));
185
+ }
186
+
187
+ return { kind: 'array', element: unifyTypes(elementTypes) };
188
+ }
189
+
190
+ function inferPlainObject(obj: object, depth: number, seen: WeakSet<object>): TypeNode {
191
+ const properties: Record<string, TypeNode> = {};
192
+ const keys = Object.keys(obj);
193
+
194
+ for (const key of keys) {
195
+ try {
196
+ properties[key] = infer((obj as Record<string, unknown>)[key], depth - 1, seen);
197
+ } catch {
198
+ properties[key] = { kind: 'unknown' };
199
+ }
200
+ }
201
+
202
+ return { kind: 'object', properties };
203
+ }
204
+
205
+ /**
206
+ * Unify an array of TypeNodes into a single TypeNode.
207
+ * If all types are structurally identical, returns that type.
208
+ * Otherwise, returns a union of the distinct types.
209
+ */
210
+ function unifyTypes(types: TypeNode[]): TypeNode {
211
+ if (types.length === 0) return { kind: 'unknown' };
212
+ if (types.length === 1) return types[0];
213
+
214
+ // Deduplicate by serialization
215
+ const uniqueMap = new Map<string, TypeNode>();
216
+ for (const t of types) {
217
+ const key = canonicalStringify(t);
218
+ if (!uniqueMap.has(key)) {
219
+ uniqueMap.set(key, t);
220
+ }
221
+ }
222
+
223
+ const unique = Array.from(uniqueMap.values());
224
+ if (unique.length === 1) return unique[0];
225
+
226
+ // Flatten nested unions
227
+ const members: TypeNode[] = [];
228
+ for (const u of unique) {
229
+ if (u.kind === 'union') {
230
+ members.push(...u.members);
231
+ } else {
232
+ members.push(u);
233
+ }
234
+ }
235
+
236
+ // Deduplicate again after flattening
237
+ const finalMap = new Map<string, TypeNode>();
238
+ for (const m of members) {
239
+ const key = canonicalStringify(m);
240
+ if (!finalMap.has(key)) {
241
+ finalMap.set(key, m);
242
+ }
243
+ }
244
+
245
+ const finalMembers = Array.from(finalMap.values());
246
+ if (finalMembers.length === 1) return finalMembers[0];
247
+
248
+ return { kind: 'union', members: finalMembers };
249
+ }
250
+
251
+ function canonicalStringify(node: TypeNode): string {
252
+ if (node.kind === 'object') {
253
+ const sorted = Object.keys(node.properties).sort();
254
+ const entries = sorted.map(k => `${JSON.stringify(k)}:${canonicalStringify(node.properties[k])}`);
255
+ return `{object:{${entries.join(',')}}}`;
256
+ }
257
+ if (node.kind === 'union') {
258
+ const sorted = node.members.map(canonicalStringify).sort();
259
+ return `{union:[${sorted.join(',')}]}`;
260
+ }
261
+ if (node.kind === 'array') {
262
+ return `{array:${canonicalStringify(node.element)}}`;
263
+ }
264
+ if (node.kind === 'primitive') {
265
+ return `{prim:${node.name}}`;
266
+ }
267
+ if (node.kind === 'function') {
268
+ return `{fn:[${node.params.map(canonicalStringify).join(',')}]->${canonicalStringify(node.returnType)}}`;
269
+ }
270
+ if (node.kind === 'promise') {
271
+ return `{promise:${canonicalStringify(node.resolved)}}`;
272
+ }
273
+ if (node.kind === 'map') {
274
+ return `{map:${canonicalStringify(node.key)},${canonicalStringify(node.value)}}`;
275
+ }
276
+ if (node.kind === 'set') {
277
+ return `{set:${canonicalStringify(node.element)}}`;
278
+ }
279
+ if (node.kind === 'tuple') {
280
+ return `{tuple:[${node.elements.map(canonicalStringify).join(',')}]}`;
281
+ }
282
+ return '{unknown}';
283
+ }
284
+
285
+ export { unifyTypes };
package/src/types.ts ADDED
@@ -0,0 +1,61 @@
1
+ export type TypeNode =
2
+ | { kind: "primitive"; name: "string" | "number" | "boolean" | "null" | "undefined" | "bigint" | "symbol" }
3
+ | { kind: "array"; element: TypeNode }
4
+ | { kind: "object"; properties: Record<string, TypeNode> }
5
+ | { kind: "union"; members: TypeNode[] }
6
+ | { kind: "function"; params: TypeNode[]; returnType: TypeNode }
7
+ | { kind: "promise"; resolved: TypeNode }
8
+ | { kind: "map"; key: TypeNode; value: TypeNode }
9
+ | { kind: "set"; element: TypeNode }
10
+ | { kind: "tuple"; elements: TypeNode[] }
11
+ | { kind: "unknown" };
12
+
13
+ export interface IngestPayload {
14
+ functionName: string;
15
+ module: string;
16
+ language: "js" | "python";
17
+ environment: string;
18
+ typeHash: string;
19
+ argsType: TypeNode;
20
+ returnType: TypeNode;
21
+ isAsync?: boolean;
22
+ paramNames?: string[];
23
+ sampleInput?: unknown;
24
+ sampleOutput?: unknown;
25
+ error?: {
26
+ type: string;
27
+ message: string;
28
+ stackTrace?: string;
29
+ argsSnapshot?: unknown;
30
+ };
31
+ }
32
+
33
+ export interface GlobalOpts {
34
+ backendUrl: string;
35
+ batchIntervalMs: number;
36
+ enabled: boolean;
37
+ environment: string | undefined;
38
+ maxBatchSize?: number;
39
+ debug?: boolean;
40
+ }
41
+
42
+ export interface TrickleOpts {
43
+ name?: string;
44
+ module?: string;
45
+ trackArgs?: boolean;
46
+ trackReturn?: boolean;
47
+ sampleRate?: number;
48
+ maxDepth?: number;
49
+ }
50
+
51
+ export interface WrapOptions {
52
+ functionName: string;
53
+ module: string;
54
+ trackArgs: boolean;
55
+ trackReturn: boolean;
56
+ sampleRate: number;
57
+ maxDepth: number;
58
+ environment: string;
59
+ enabled: boolean;
60
+ paramNames?: string[];
61
+ }
package/src/wrap.ts ADDED
@@ -0,0 +1,289 @@
1
+ import { TypeNode, IngestPayload, WrapOptions } from './types';
2
+ import { inferType } from './type-inference';
3
+ import { hashType } from './type-hash';
4
+ import { createTracker } from './proxy-tracker';
5
+ import { TypeCache } from './cache';
6
+ import { enqueue } from './transport';
7
+
8
+ const typeCache = new TypeCache();
9
+
10
+ /** Symbol to mark already-wrapped functions, preventing double-wrap. */
11
+ const TRICKLE_WRAPPED = Symbol.for('__trickle_wrapped');
12
+
13
+ /**
14
+ * Wrap a function to capture runtime type information on each call.
15
+ * The wrapper is completely transparent: same name, same length, same behavior.
16
+ * Errors are always re-thrown after capturing type context.
17
+ */
18
+ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: WrapOptions): T {
19
+ if (!opts.enabled) return fn;
20
+
21
+ // Prevent double-wrapping (compile hook + load hook may both see the same function)
22
+ if ((fn as any)[TRICKLE_WRAPPED]) return fn;
23
+
24
+ const functionKey = `${opts.module}::${opts.functionName}`;
25
+
26
+ // Create wrapper with same length using a dynamic approach
27
+ const wrapper = function (this: any, ...args: any[]): any {
28
+ // Sample rate check
29
+ if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
30
+ return fn.apply(this, args);
31
+ }
32
+
33
+ // Set up arg tracking
34
+ const trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }> = [];
35
+ const proxiedArgs = args.map((arg, i) => {
36
+ if (arg !== null && arg !== undefined && typeof arg === 'object') {
37
+ const tracker = createTracker(arg);
38
+ trackers.push(tracker);
39
+ return tracker.proxy;
40
+ }
41
+ return arg;
42
+ });
43
+
44
+ let result: any;
45
+ let threwError = false;
46
+ let caughtError: unknown;
47
+
48
+ try {
49
+ result = fn.apply(this, proxiedArgs);
50
+ } catch (err) {
51
+ threwError = true;
52
+ caughtError = err;
53
+
54
+ // Capture error context
55
+ try {
56
+ captureErrorPayload(functionKey, opts, args, trackers, err);
57
+ } catch {
58
+ // Never let our instrumentation interfere
59
+ }
60
+
61
+ // CRITICAL: always re-throw
62
+ throw err;
63
+ }
64
+
65
+ // Handle async functions (Promise return)
66
+ if (result !== null && result !== undefined && typeof result === 'object' && typeof result.then === 'function') {
67
+ return result.then(
68
+ (resolved: unknown) => {
69
+ try {
70
+ capturePayload(functionKey, opts, args, trackers, resolved, true);
71
+ } catch {
72
+ // Never let our instrumentation interfere
73
+ }
74
+ return resolved;
75
+ },
76
+ (err: unknown) => {
77
+ try {
78
+ captureErrorPayload(functionKey, opts, args, trackers, err);
79
+ } catch {
80
+ // Never let our instrumentation interfere
81
+ }
82
+ // Re-throw the original rejection
83
+ throw err;
84
+ },
85
+ );
86
+ }
87
+
88
+ // Synchronous return
89
+ try {
90
+ capturePayload(functionKey, opts, args, trackers, result);
91
+ } catch {
92
+ // Never let our instrumentation interfere
93
+ }
94
+
95
+ return result;
96
+ };
97
+
98
+ // Preserve function name and length
99
+ Object.defineProperty(wrapper, 'name', { value: fn.name || opts.functionName, configurable: true });
100
+ Object.defineProperty(wrapper, 'length', { value: fn.length, configurable: true });
101
+
102
+ // Mark as wrapped to prevent double-wrapping
103
+ (wrapper as any)[TRICKLE_WRAPPED] = true;
104
+
105
+ return wrapper as unknown as T;
106
+ }
107
+
108
+ /**
109
+ * Capture and enqueue a successful invocation's type data.
110
+ */
111
+ function capturePayload(
112
+ functionKey: string,
113
+ opts: WrapOptions,
114
+ originalArgs: unknown[],
115
+ trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }>,
116
+ returnValue: unknown,
117
+ isAsync: boolean = false,
118
+ ): void {
119
+ // Build args type as a tuple
120
+ const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
121
+ const returnType = inferType(returnValue, opts.maxDepth);
122
+
123
+ const hash = hashType(argsType, returnType);
124
+
125
+ // Check cache
126
+ if (!typeCache.shouldSend(functionKey, hash)) {
127
+ return;
128
+ }
129
+
130
+ typeCache.markSent(functionKey, hash);
131
+
132
+ const payload: IngestPayload = {
133
+ functionName: opts.functionName,
134
+ module: opts.module,
135
+ language: 'js',
136
+ environment: opts.environment,
137
+ typeHash: hash,
138
+ argsType,
139
+ returnType,
140
+ sampleInput: sanitizeSample(originalArgs),
141
+ sampleOutput: sanitizeSample(returnValue),
142
+ };
143
+
144
+ if (isAsync) {
145
+ payload.isAsync = true;
146
+ }
147
+
148
+ if (opts.paramNames && opts.paramNames.length > 0) {
149
+ payload.paramNames = opts.paramNames;
150
+ }
151
+
152
+ enqueue(payload);
153
+ }
154
+
155
+ /**
156
+ * Capture type context for a failed invocation.
157
+ */
158
+ function captureErrorPayload(
159
+ functionKey: string,
160
+ opts: WrapOptions,
161
+ originalArgs: unknown[],
162
+ trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }>,
163
+ error: unknown,
164
+ ): void {
165
+ const argsType = buildArgsType(originalArgs, trackers, opts.maxDepth);
166
+ const returnType: TypeNode = { kind: 'unknown' };
167
+ const hash = hashType(argsType, returnType);
168
+
169
+ // Always send error payloads (don't cache-skip them)
170
+
171
+ const errorInfo = extractErrorInfo(error);
172
+
173
+ // Collect accessed paths from trackers as "variable" context
174
+ const accessedPaths: Record<string, TypeNode> = {};
175
+ for (const tracker of trackers) {
176
+ const paths = tracker.getAccessedPaths();
177
+ for (const [path, type] of paths) {
178
+ accessedPaths[path] = type;
179
+ }
180
+ }
181
+
182
+ const payload: IngestPayload = {
183
+ functionName: opts.functionName,
184
+ module: opts.module,
185
+ language: 'js',
186
+ environment: opts.environment,
187
+ typeHash: hash,
188
+ argsType,
189
+ returnType,
190
+ sampleInput: sanitizeSample(originalArgs),
191
+ error: {
192
+ type: errorInfo.type,
193
+ message: errorInfo.message,
194
+ stackTrace: errorInfo.stack,
195
+ argsSnapshot: sanitizeSample(originalArgs),
196
+ },
197
+ };
198
+
199
+ enqueue(payload);
200
+ }
201
+
202
+ /**
203
+ * Build a tuple TypeNode for the args array.
204
+ * Uses tracker data for objects/arrays that were proxied (captures the "shape" actually used),
205
+ * and falls back to full inference for primitives and untracked args.
206
+ */
207
+ function buildArgsType(
208
+ originalArgs: unknown[],
209
+ trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }>,
210
+ maxDepth: number,
211
+ ): TypeNode {
212
+ const elements: TypeNode[] = originalArgs.map(arg => inferType(arg, maxDepth));
213
+
214
+ if (elements.length === 0) {
215
+ return { kind: 'tuple', elements: [] };
216
+ }
217
+
218
+ return { kind: 'tuple', elements };
219
+ }
220
+
221
+ /**
222
+ * Extract error information safely.
223
+ */
224
+ function extractErrorInfo(err: unknown): { type: string; message: string; stack?: string } {
225
+ if (err instanceof Error) {
226
+ return {
227
+ type: err.constructor.name || 'Error',
228
+ message: err.message,
229
+ stack: err.stack,
230
+ };
231
+ }
232
+
233
+ if (typeof err === 'string') {
234
+ return { type: 'String', message: err };
235
+ }
236
+
237
+ return {
238
+ type: typeof err,
239
+ message: String(err),
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Sanitize a sample value for safe serialization.
245
+ * Truncates large strings, limits array lengths, strips functions.
246
+ */
247
+ function sanitizeSample(value: unknown, depth: number = 3): unknown {
248
+ if (depth <= 0) return '[truncated]';
249
+ if (value === null || value === undefined) return value;
250
+
251
+ const t = typeof value;
252
+ if (t === 'string') {
253
+ const s = value as string;
254
+ return s.length > 200 ? s.substring(0, 200) + '...' : s;
255
+ }
256
+ if (t === 'number' || t === 'boolean') return value;
257
+ if (t === 'bigint') return String(value);
258
+ if (t === 'symbol') return String(value);
259
+ if (t === 'function') return `[Function: ${(value as Function).name || 'anonymous'}]`;
260
+
261
+ if (Array.isArray(value)) {
262
+ const sample = value.slice(0, 5);
263
+ return sample.map(item => sanitizeSample(item, depth - 1));
264
+ }
265
+
266
+ if (t === 'object') {
267
+ if (value instanceof Date) return value.toISOString();
268
+ if (value instanceof RegExp) return String(value);
269
+ if (value instanceof Error) return { error: value.message };
270
+ if (value instanceof Map) return `[Map: ${value.size} entries]`;
271
+ if (value instanceof Set) return `[Set: ${value.size} items]`;
272
+
273
+ const obj = value as Record<string, unknown>;
274
+ const result: Record<string, unknown> = {};
275
+ const keys = Object.keys(obj).slice(0, 20);
276
+ for (const key of keys) {
277
+ try {
278
+ result[key] = sanitizeSample(obj[key], depth - 1);
279
+ } catch {
280
+ result[key] = '[unreadable]';
281
+ }
282
+ }
283
+ return result;
284
+ }
285
+
286
+ return String(value);
287
+ }
288
+
289
+ export { typeCache };
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }