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,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTracker = createTracker;
4
+ const type_inference_1 = require("./type-inference");
5
+ /**
6
+ * Creates a deep Proxy around a value to track property accesses.
7
+ * Returns the proxy and a function to retrieve all accessed paths with their inferred types.
8
+ *
9
+ * The proxy is designed to be fully transparent:
10
+ * - Array.isArray() returns true for proxied arrays
11
+ * - JSON.stringify works correctly
12
+ * - typeof, ===, iteration, spread, Object.keys all work identically
13
+ * - Symbol.toPrimitive, Symbol.iterator, Symbol.toStringTag are forwarded
14
+ */
15
+ function createTracker(value) {
16
+ const accessedPaths = new Map();
17
+ const proxyCache = new WeakMap();
18
+ function wrap(val, path) {
19
+ // Only proxy objects and arrays (not primitives or functions at top level)
20
+ if (val === null || val === undefined)
21
+ return val;
22
+ if (typeof val !== 'object' && typeof val !== 'function')
23
+ return val;
24
+ const obj = val;
25
+ // Check cache to prevent double-wrapping
26
+ if (proxyCache.has(obj)) {
27
+ return proxyCache.get(obj);
28
+ }
29
+ // For arrays, the proxy target must be an array so Array.isArray() works
30
+ const target = Array.isArray(obj) ? obj : obj;
31
+ const proxy = new Proxy(target, {
32
+ get(target, prop, receiver) {
33
+ // Forward well-known symbols transparently
34
+ if (typeof prop === 'symbol') {
35
+ // Forward Symbol.toPrimitive, Symbol.iterator, Symbol.toStringTag, Symbol.hasInstance
36
+ const raw = Reflect.get(target, prop, target);
37
+ // For iterator, bind to target so iteration works on original
38
+ if (prop === Symbol.iterator && typeof raw === 'function') {
39
+ return raw.bind(target);
40
+ }
41
+ return raw;
42
+ }
43
+ const raw = Reflect.get(target, prop, target);
44
+ // Don't track internal/meta properties
45
+ if (prop === 'constructor' || prop === 'prototype' || prop === '__proto__') {
46
+ return raw;
47
+ }
48
+ // toJSON: return original value's toJSON or the target itself for JSON.stringify
49
+ if (prop === 'toJSON') {
50
+ if (typeof raw === 'function') {
51
+ return raw.bind(target);
52
+ }
53
+ return raw;
54
+ }
55
+ // For arrays, forward array methods transparently
56
+ if (Array.isArray(target)) {
57
+ if (prop === 'length') {
58
+ // Record that length was accessed
59
+ const fullPath = path ? `${path}.length` : 'length';
60
+ accessedPaths.set(fullPath, { kind: 'primitive', name: 'number' });
61
+ return raw;
62
+ }
63
+ // Array index access
64
+ if (isArrayIndex(prop)) {
65
+ const fullPath = path ? `${path}[${prop}]` : `[${prop}]`;
66
+ const val = raw;
67
+ if (val !== undefined) {
68
+ accessedPaths.set(fullPath, (0, type_inference_1.inferType)(val, 3));
69
+ }
70
+ // Recursively wrap object elements
71
+ if (val !== null && val !== undefined && typeof val === 'object') {
72
+ return wrap(val, fullPath);
73
+ }
74
+ return val;
75
+ }
76
+ // Array methods that take callbacks - wrap to track callback args
77
+ if (typeof raw === 'function') {
78
+ if (isCallbackMethod(prop)) {
79
+ return createTrackedArrayMethod(target, prop, path, raw, accessedPaths, wrap);
80
+ }
81
+ // Other array methods: bind to original
82
+ return raw.bind(target);
83
+ }
84
+ }
85
+ // Record the access path and type
86
+ const fullPath = path ? `${path}.${prop}` : prop;
87
+ if (raw !== undefined) {
88
+ accessedPaths.set(fullPath, (0, type_inference_1.inferType)(raw, 3));
89
+ }
90
+ // Recursively wrap sub-objects
91
+ if (raw !== null && raw !== undefined && typeof raw === 'object') {
92
+ return wrap(raw, fullPath);
93
+ }
94
+ // Bind functions to original target
95
+ if (typeof raw === 'function') {
96
+ return raw.bind(target);
97
+ }
98
+ return raw;
99
+ },
100
+ set(target, prop, value, receiver) {
101
+ return Reflect.set(target, prop, value, target);
102
+ },
103
+ has(target, prop) {
104
+ return Reflect.has(target, prop);
105
+ },
106
+ ownKeys(target) {
107
+ return Reflect.ownKeys(target);
108
+ },
109
+ getOwnPropertyDescriptor(target, prop) {
110
+ return Reflect.getOwnPropertyDescriptor(target, prop);
111
+ },
112
+ getPrototypeOf(target) {
113
+ return Reflect.getPrototypeOf(target);
114
+ },
115
+ isExtensible(target) {
116
+ return Reflect.isExtensible(target);
117
+ },
118
+ preventExtensions(target) {
119
+ return Reflect.preventExtensions(target);
120
+ },
121
+ defineProperty(target, prop, descriptor) {
122
+ return Reflect.defineProperty(target, prop, descriptor);
123
+ },
124
+ deleteProperty(target, prop) {
125
+ return Reflect.deleteProperty(target, prop);
126
+ },
127
+ });
128
+ proxyCache.set(obj, proxy);
129
+ return proxy;
130
+ }
131
+ const proxy = wrap(value, '');
132
+ return {
133
+ proxy,
134
+ getAccessedPaths: () => new Map(accessedPaths),
135
+ };
136
+ }
137
+ function isArrayIndex(prop) {
138
+ const num = Number(prop);
139
+ return Number.isInteger(num) && num >= 0 && String(num) === prop;
140
+ }
141
+ const CALLBACK_METHODS = new Set([
142
+ 'map', 'filter', 'forEach', 'find', 'findIndex', 'some', 'every',
143
+ 'reduce', 'reduceRight', 'flatMap', 'sort',
144
+ ]);
145
+ function isCallbackMethod(prop) {
146
+ return CALLBACK_METHODS.has(prop);
147
+ }
148
+ /**
149
+ * Creates a tracked version of an array method that takes a callback.
150
+ * Wraps callback arguments (the element) in proxies so property accesses within
151
+ * the callback are also tracked.
152
+ */
153
+ function createTrackedArrayMethod(target, method, basePath, rawFn, accessedPaths, wrap) {
154
+ return function (...args) {
155
+ if (args.length > 0 && typeof args[0] === 'function') {
156
+ const originalCb = args[0];
157
+ args[0] = function (element, index, array) {
158
+ const elementPath = basePath ? `${basePath}[${index}]` : `[${index}]`;
159
+ // Wrap the element so accesses inside the callback are tracked
160
+ let wrappedElement = element;
161
+ if (element !== null && element !== undefined && typeof element === 'object') {
162
+ wrappedElement = wrap(element, elementPath);
163
+ }
164
+ else if (element !== undefined) {
165
+ accessedPaths.set(elementPath, (0, type_inference_1.inferType)(element, 3));
166
+ }
167
+ return originalCb.call(this, wrappedElement, index, array);
168
+ };
169
+ }
170
+ return rawFn.apply(target, args);
171
+ };
172
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Zero-code auto-instrumentation for Node.js applications.
3
+ *
4
+ * Usage — just add a flag to your start command:
5
+ *
6
+ * node -r trickle/register app.js
7
+ *
8
+ * Or with environment variables:
9
+ *
10
+ * TRICKLE_BACKEND_URL=http://localhost:4888 node -r trickle/register app.js
11
+ *
12
+ * This module patches Node's module loader to intercept `require('express')`
13
+ * and automatically instrument any Express app created — no code changes needed.
14
+ *
15
+ * Supported environment variables:
16
+ * TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
17
+ * TRICKLE_ENABLED — Set to "0" or "false" to disable (default: enabled)
18
+ * TRICKLE_DEBUG — Set to "1" or "true" for debug logging
19
+ * TRICKLE_ENV — Override detected environment name
20
+ */
21
+ export {};
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * Zero-code auto-instrumentation for Node.js applications.
4
+ *
5
+ * Usage — just add a flag to your start command:
6
+ *
7
+ * node -r trickle/register app.js
8
+ *
9
+ * Or with environment variables:
10
+ *
11
+ * TRICKLE_BACKEND_URL=http://localhost:4888 node -r trickle/register app.js
12
+ *
13
+ * This module patches Node's module loader to intercept `require('express')`
14
+ * and automatically instrument any Express app created — no code changes needed.
15
+ *
16
+ * Supported environment variables:
17
+ * TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
18
+ * TRICKLE_ENABLED — Set to "0" or "false" to disable (default: enabled)
19
+ * TRICKLE_DEBUG — Set to "1" or "true" for debug logging
20
+ * TRICKLE_ENV — Override detected environment name
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const module_1 = __importDefault(require("module"));
27
+ const transport_1 = require("./transport");
28
+ const express_1 = require("./express");
29
+ const env_detect_1 = require("./env-detect");
30
+ const M = module_1.default;
31
+ const originalLoad = M._load;
32
+ const patched = new Set();
33
+ // Read config from environment
34
+ const backendUrl = process.env.TRICKLE_BACKEND_URL || 'http://localhost:4888';
35
+ const enabled = process.env.TRICKLE_ENABLED !== '0' && process.env.TRICKLE_ENABLED !== 'false';
36
+ const debug = process.env.TRICKLE_DEBUG === '1' || process.env.TRICKLE_DEBUG === 'true';
37
+ const envOverride = process.env.TRICKLE_ENV || undefined;
38
+ if (enabled) {
39
+ // Configure the transport
40
+ (0, transport_1.configure)({
41
+ backendUrl,
42
+ batchIntervalMs: 2000,
43
+ debug,
44
+ enabled: true,
45
+ environment: envOverride || (0, env_detect_1.detectEnvironment)(),
46
+ });
47
+ if (debug) {
48
+ console.log(`[trickle] Auto-instrumentation enabled (backend: ${backendUrl})`);
49
+ }
50
+ // Patch Module._load to intercept framework requires
51
+ M._load = function hookedLoad(request, parent, isMain) {
52
+ const exports = originalLoad.apply(this, arguments);
53
+ // Intercept require('express')
54
+ if (request === 'express' && !patched.has('express')) {
55
+ patched.add('express');
56
+ return patchExpress(exports, request, parent);
57
+ }
58
+ return exports;
59
+ };
60
+ }
61
+ else if (debug) {
62
+ console.log('[trickle] Auto-instrumentation disabled (TRICKLE_ENABLED=false)');
63
+ }
64
+ /**
65
+ * Wrap the Express factory function so every app created is auto-instrumented.
66
+ * Preserves all static properties (express.json, express.static, etc.).
67
+ */
68
+ function patchExpress(originalExpress, request, parent) {
69
+ function wrappedExpress(...args) {
70
+ const app = originalExpress.apply(this, args);
71
+ try {
72
+ (0, express_1.instrumentExpress)(app, {
73
+ environment: envOverride || (0, env_detect_1.detectEnvironment)(),
74
+ });
75
+ if (debug) {
76
+ console.log('[trickle] Auto-instrumented Express app');
77
+ }
78
+ }
79
+ catch (err) {
80
+ if (debug) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ console.warn(`[trickle] Failed to auto-instrument Express: ${msg}`);
83
+ }
84
+ }
85
+ return app;
86
+ }
87
+ // Copy all static properties (express.json, express.static, express.Router, etc.)
88
+ for (const key of Object.keys(originalExpress)) {
89
+ wrappedExpress[key] = originalExpress[key];
90
+ }
91
+ // Preserve prototype chain
92
+ Object.setPrototypeOf(wrappedExpress, Object.getPrototypeOf(originalExpress));
93
+ // Update require cache so subsequent require('express') returns the patched version
94
+ try {
95
+ const resolvedPath = M._resolveFilename(request, parent);
96
+ if (require.cache[resolvedPath]) {
97
+ require.cache[resolvedPath].exports = wrappedExpress;
98
+ }
99
+ }
100
+ catch {
101
+ // Cache update failed — first require still returns patched, but subsequent
102
+ // requires from other modules may get the original. This is rare.
103
+ }
104
+ return wrappedExpress;
105
+ }
@@ -0,0 +1,22 @@
1
+ import { IngestPayload, GlobalOpts } from './types';
2
+ /**
3
+ * Configure the transport layer with global options.
4
+ */
5
+ export declare function configure(opts: GlobalOpts): void;
6
+ /**
7
+ * Enqueue a payload for batched sending.
8
+ */
9
+ export declare function enqueue(payload: IngestPayload): void;
10
+ /**
11
+ * Flush all queued payloads to the backend.
12
+ * Returns a promise that resolves when the flush completes.
13
+ */
14
+ export declare function flush(): Promise<void>;
15
+ /**
16
+ * Get the current queue length (for testing/debugging).
17
+ */
18
+ export declare function getQueueLength(): number;
19
+ /**
20
+ * Reset the transport state (for testing).
21
+ */
22
+ export declare function reset(): void;
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.configure = configure;
37
+ exports.enqueue = enqueue;
38
+ exports.flush = flush;
39
+ exports.getQueueLength = getQueueLength;
40
+ exports.reset = reset;
41
+ const fs = __importStar(require("fs"));
42
+ const pathMod = __importStar(require("path"));
43
+ const MAX_RETRIES = 3;
44
+ const INITIAL_RETRY_DELAY_MS = 1000;
45
+ const DEFAULT_BATCH_INTERVAL_MS = 2000;
46
+ const DEFAULT_MAX_BATCH_SIZE = 50;
47
+ let backendUrl = 'http://localhost:4888';
48
+ let batchIntervalMs = DEFAULT_BATCH_INTERVAL_MS;
49
+ let maxBatchSize = DEFAULT_MAX_BATCH_SIZE;
50
+ let enabled = true;
51
+ let debug = false;
52
+ let localMode = process.env.TRICKLE_LOCAL === '1';
53
+ let localFilePath = '';
54
+ let queue = [];
55
+ let flushTimer = null;
56
+ let isFlushing = false;
57
+ /**
58
+ * Configure the transport layer with global options.
59
+ */
60
+ function configure(opts) {
61
+ backendUrl = opts.backendUrl || backendUrl;
62
+ batchIntervalMs = opts.batchIntervalMs || DEFAULT_BATCH_INTERVAL_MS;
63
+ maxBatchSize = opts.maxBatchSize || DEFAULT_MAX_BATCH_SIZE;
64
+ enabled = opts.enabled !== false;
65
+ debug = opts.debug === true;
66
+ // Check for local/file-based mode
67
+ if (process.env.TRICKLE_LOCAL === '1') {
68
+ localMode = true;
69
+ const dir = process.env.TRICKLE_LOCAL_DIR || pathMod.join(process.cwd(), '.trickle');
70
+ try {
71
+ fs.mkdirSync(dir, { recursive: true });
72
+ }
73
+ catch { }
74
+ localFilePath = pathMod.join(dir, 'observations.jsonl');
75
+ if (debug) {
76
+ console.log(`[trickle] Local mode: writing to ${localFilePath}`);
77
+ }
78
+ return; // no timer needed for file mode
79
+ }
80
+ // Restart the flush timer with new interval
81
+ stopTimer();
82
+ startTimer();
83
+ }
84
+ /**
85
+ * Enqueue a payload for batched sending.
86
+ */
87
+ function enqueue(payload) {
88
+ if (!enabled)
89
+ return;
90
+ // Local file mode: append directly to JSONL file
91
+ if (localMode && localFilePath) {
92
+ try {
93
+ fs.appendFileSync(localFilePath, JSON.stringify(payload) + '\n');
94
+ }
95
+ catch {
96
+ // Never crash user's app
97
+ }
98
+ return;
99
+ }
100
+ queue.push(payload);
101
+ // Flush immediately if batch is full
102
+ if (queue.length >= maxBatchSize) {
103
+ flush().catch(silentError);
104
+ }
105
+ // Ensure timer is running
106
+ if (!flushTimer) {
107
+ startTimer();
108
+ }
109
+ }
110
+ /**
111
+ * Flush all queued payloads to the backend.
112
+ * Returns a promise that resolves when the flush completes.
113
+ */
114
+ async function flush() {
115
+ if (queue.length === 0)
116
+ return;
117
+ if (isFlushing)
118
+ return;
119
+ isFlushing = true;
120
+ const batch = queue.splice(0);
121
+ try {
122
+ await sendBatch(batch);
123
+ }
124
+ catch {
125
+ // Batch is already dropped after max retries — nothing more to do
126
+ if (debug) {
127
+ console.warn('[trickle] Failed to flush batch, data dropped');
128
+ }
129
+ }
130
+ finally {
131
+ isFlushing = false;
132
+ }
133
+ }
134
+ /**
135
+ * Send a batch with exponential backoff retry.
136
+ */
137
+ async function sendBatch(batch) {
138
+ let delay = INITIAL_RETRY_DELAY_MS;
139
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
140
+ try {
141
+ const response = await fetch(`${backendUrl}/api/ingest/batch`, {
142
+ method: 'POST',
143
+ headers: { 'Content-Type': 'application/json' },
144
+ body: JSON.stringify({ payloads: batch }),
145
+ signal: AbortSignal.timeout(10000), // 10 second timeout
146
+ });
147
+ if (response.ok) {
148
+ if (debug) {
149
+ console.log(`[trickle] Sent batch of ${batch.length} events`);
150
+ }
151
+ return;
152
+ }
153
+ // Server error — retry
154
+ if (response.status >= 500 && attempt < MAX_RETRIES) {
155
+ await sleep(delay);
156
+ delay *= 2;
157
+ continue;
158
+ }
159
+ // Client error (4xx) or final attempt — drop
160
+ if (debug) {
161
+ console.warn(`[trickle] Backend returned ${response.status}, dropping batch`);
162
+ }
163
+ return;
164
+ }
165
+ catch (err) {
166
+ if (attempt < MAX_RETRIES) {
167
+ await sleep(delay);
168
+ delay *= 2;
169
+ continue;
170
+ }
171
+ // Final attempt failed
172
+ if (debug) {
173
+ const msg = err instanceof Error ? err.message : String(err);
174
+ console.warn(`[trickle] Could not reach backend after ${MAX_RETRIES + 1} attempts: ${msg}`);
175
+ }
176
+ return;
177
+ }
178
+ }
179
+ }
180
+ function startTimer() {
181
+ if (flushTimer)
182
+ return;
183
+ flushTimer = setInterval(() => {
184
+ flush().catch(silentError);
185
+ }, batchIntervalMs);
186
+ // Don't keep the process alive just for trickle
187
+ if (flushTimer && typeof flushTimer === 'object' && 'unref' in flushTimer) {
188
+ flushTimer.unref();
189
+ }
190
+ }
191
+ function stopTimer() {
192
+ if (flushTimer) {
193
+ clearInterval(flushTimer);
194
+ flushTimer = null;
195
+ }
196
+ }
197
+ function sleep(ms) {
198
+ return new Promise(resolve => setTimeout(resolve, ms));
199
+ }
200
+ function silentError() {
201
+ // Intentionally empty — never crash user's app
202
+ }
203
+ // Register process exit handler to flush remaining events
204
+ if (typeof process !== 'undefined' && process.on) {
205
+ process.on('beforeExit', () => {
206
+ flush().catch(silentError);
207
+ });
208
+ // SIGTERM / SIGINT: attempt a sync-ish flush
209
+ const exitHandler = () => {
210
+ flush().catch(silentError);
211
+ };
212
+ process.on('SIGTERM', exitHandler);
213
+ process.on('SIGINT', exitHandler);
214
+ }
215
+ /**
216
+ * Get the current queue length (for testing/debugging).
217
+ */
218
+ function getQueueLength() {
219
+ return queue.length;
220
+ }
221
+ /**
222
+ * Reset the transport state (for testing).
223
+ */
224
+ function reset() {
225
+ queue = [];
226
+ stopTimer();
227
+ isFlushing = false;
228
+ }
@@ -0,0 +1,5 @@
1
+ import { TypeNode } from './types';
2
+ /**
3
+ * Hash the combined args + return type into a deterministic 16-hex-char string.
4
+ */
5
+ export declare function hashType(argsType: TypeNode, returnType: TypeNode): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hashType = hashType;
4
+ const crypto_1 = require("crypto");
5
+ /**
6
+ * Canonicalize a TypeNode for deterministic hashing.
7
+ * - Object properties are sorted alphabetically by key at all levels.
8
+ * - Union members are sorted by their canonical string representation.
9
+ */
10
+ function canonicalize(node) {
11
+ switch (node.kind) {
12
+ case 'primitive':
13
+ return { kind: 'primitive', name: node.name };
14
+ case 'array':
15
+ return { kind: 'array', element: canonicalize(node.element) };
16
+ case 'object': {
17
+ const sortedKeys = Object.keys(node.properties).sort();
18
+ const properties = {};
19
+ for (const key of sortedKeys) {
20
+ properties[key] = canonicalize(node.properties[key]);
21
+ }
22
+ return { kind: 'object', properties };
23
+ }
24
+ case 'union': {
25
+ const members = node.members
26
+ .map(m => canonicalize(m))
27
+ .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
28
+ return { kind: 'union', members };
29
+ }
30
+ case 'function': {
31
+ return {
32
+ kind: 'function',
33
+ params: node.params.map(canonicalize),
34
+ returnType: canonicalize(node.returnType),
35
+ };
36
+ }
37
+ case 'promise':
38
+ return { kind: 'promise', resolved: canonicalize(node.resolved) };
39
+ case 'map':
40
+ return { kind: 'map', key: canonicalize(node.key), value: canonicalize(node.value) };
41
+ case 'set':
42
+ return { kind: 'set', element: canonicalize(node.element) };
43
+ case 'tuple':
44
+ return { kind: 'tuple', elements: node.elements.map(canonicalize) };
45
+ case 'unknown':
46
+ return { kind: 'unknown' };
47
+ default:
48
+ return { kind: 'unknown' };
49
+ }
50
+ }
51
+ /**
52
+ * Hash the combined args + return type into a deterministic 16-hex-char string.
53
+ */
54
+ function hashType(argsType, returnType) {
55
+ const canonical = JSON.stringify({
56
+ args: canonicalize(argsType),
57
+ ret: canonicalize(returnType),
58
+ });
59
+ return (0, crypto_1.createHash)('sha256').update(canonical).digest('hex').substring(0, 16);
60
+ }
@@ -0,0 +1,14 @@
1
+ import { TypeNode } from './types';
2
+ /**
3
+ * Infer the TypeNode representation of a runtime JavaScript value.
4
+ * Uses a WeakSet to detect circular references.
5
+ * Samples at most the first 20 elements of arrays for performance.
6
+ */
7
+ export declare function inferType(value: unknown, maxDepth?: number): TypeNode;
8
+ /**
9
+ * Unify an array of TypeNodes into a single TypeNode.
10
+ * If all types are structurally identical, returns that type.
11
+ * Otherwise, returns a union of the distinct types.
12
+ */
13
+ declare function unifyTypes(types: TypeNode[]): TypeNode;
14
+ export { unifyTypes };