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.
- package/auto-env.js +13 -0
- package/auto-esm.mjs +128 -0
- package/auto.js +3 -0
- package/dist/auto-codegen.d.ts +29 -0
- package/dist/auto-codegen.js +999 -0
- package/dist/auto-register.d.ts +16 -0
- package/dist/auto-register.js +99 -0
- package/dist/cache.d.ts +27 -0
- package/dist/cache.js +52 -0
- package/dist/env-detect.d.ts +5 -0
- package/dist/env-detect.js +35 -0
- package/dist/express.d.ts +44 -0
- package/dist/express.js +342 -0
- package/dist/fetch-observer.d.ts +24 -0
- package/dist/fetch-observer.js +217 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +172 -0
- package/dist/observe-register.d.ts +29 -0
- package/dist/observe-register.js +455 -0
- package/dist/observe.d.ts +44 -0
- package/dist/observe.js +109 -0
- package/dist/proxy-tracker.d.ts +15 -0
- package/dist/proxy-tracker.js +172 -0
- package/dist/register.d.ts +21 -0
- package/dist/register.js +105 -0
- package/dist/transport.d.ts +22 -0
- package/dist/transport.js +228 -0
- package/dist/type-hash.d.ts +5 -0
- package/dist/type-hash.js +60 -0
- package/dist/type-inference.d.ts +14 -0
- package/dist/type-inference.js +259 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/dist/wrap.d.ts +10 -0
- package/dist/wrap.js +247 -0
- package/observe-esm-hooks.mjs +367 -0
- package/observe-esm.mjs +40 -0
- package/observe.js +2 -0
- package/package.json +26 -0
- package/register.js +2 -0
- package/src/auto-codegen.ts +1058 -0
- package/src/auto-register.ts +102 -0
- package/src/cache.ts +53 -0
- package/src/env-detect.ts +22 -0
- package/src/express.ts +386 -0
- package/src/fetch-observer.ts +226 -0
- package/src/index.ts +199 -0
- package/src/observe-register.ts +453 -0
- package/src/observe.ts +127 -0
- package/src/proxy-tracker.ts +208 -0
- package/src/register.ts +110 -0
- package/src/transport.ts +207 -0
- package/src/type-hash.ts +71 -0
- package/src/type-inference.ts +285 -0
- package/src/types.ts +61 -0
- package/src/wrap.ts +289 -0
- 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 {};
|
package/dist/register.js
ADDED
|
@@ -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,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 };
|