trickle-observe 0.2.83 → 0.2.85

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/trace-var.js CHANGED
@@ -59,6 +59,9 @@ let varsFilePath = '';
59
59
  let debugMode = false;
60
60
  /** Cache: "file:line:varName" → { fingerprint, timestamp } for value-aware dedup */
61
61
  const varCache = new Map();
62
+ /** Per-line sample count to avoid loop variable spam */
63
+ const sampleCount = new Map();
64
+ const MAX_SAMPLES_PER_LINE = 5;
62
65
  /** Batch buffer for writing — avoids one fs.appendFileSync per variable */
63
66
  let varBuffer = [];
64
67
  let flushTimer = null;
@@ -105,8 +108,12 @@ function traceVar(value, varName, line, moduleName, filePath) {
105
108
  // Create a stable hash for dedup
106
109
  const dummyArgs = { kind: 'tuple', elements: [] };
107
110
  const typeHash = (0, type_hash_1.hashType)(dummyArgs, type);
108
- // Value-aware dedup: re-send if value changed or 10s elapsed
111
+ // Per-line sample count limit: stop after N samples to avoid loop spam
109
112
  const cacheKey = `${filePath}:${line}:${varName}`;
113
+ const cnt = sampleCount.get(cacheKey) || 0;
114
+ if (cnt >= MAX_SAMPLES_PER_LINE)
115
+ return;
116
+ // Value-aware dedup: re-send if value changed or 10s elapsed
110
117
  const t = typeof value;
111
118
  const fp = (t === 'string' || t === 'number' || t === 'boolean' || value === null || value === undefined)
112
119
  ? String(value).substring(0, 60)
@@ -116,6 +123,7 @@ function traceVar(value, varName, line, moduleName, filePath) {
116
123
  if (prev && prev.fp === fp && (now - prev.ts) < 10000)
117
124
  return;
118
125
  varCache.set(cacheKey, { fp, ts: now });
126
+ sampleCount.set(cacheKey, cnt + 1);
119
127
  const sample = sanitizeVarSample(value);
120
128
  const observation = {
121
129
  kind: 'variable',
@@ -47,6 +47,13 @@ export declare function tricklePlugin(options?: TricklePluginOptions): {
47
47
  * Returns -1 if not found.
48
48
  */
49
49
  export declare function findFunctionBodyBrace(source: string, afterOpenParen: number): number;
50
+ /**
51
+ * Find class body ranges in source code. Handles both:
52
+ * class Foo { ... }
53
+ * var Foo = class { ... }
54
+ * Returns an array of [start, end] positions (inclusive of braces).
55
+ */
56
+ export declare function findClassBodyRanges(source: string): Array<[number, number]>;
50
57
  /**
51
58
  * Find variable reassignments (not declarations) and return insertions for tracing.
52
59
  * Handles: x = newValue; x += 1; x ||= fallback; etc.
@@ -24,6 +24,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.tricklePlugin = tricklePlugin;
26
26
  exports.findFunctionBodyBrace = findFunctionBodyBrace;
27
+ exports.findClassBodyRanges = findClassBodyRanges;
27
28
  exports.findReassignments = findReassignments;
28
29
  exports.findCatchVars = findCatchVars;
29
30
  exports.findForLoopVars = findForLoopVars;
@@ -504,6 +505,73 @@ function extractDestructuredNames(pattern) {
504
505
  }
505
506
  return names;
506
507
  }
508
+ /**
509
+ * Find class body ranges in source code. Handles both:
510
+ * class Foo { ... }
511
+ * var Foo = class { ... }
512
+ * Returns an array of [start, end] positions (inclusive of braces).
513
+ */
514
+ function findClassBodyRanges(source) {
515
+ const ranges = [];
516
+ // Match both class declarations and class expressions
517
+ const classRegex = /\bclass\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*(?:extends\s+[a-zA-Z_$.[\]]+\s*)?\{/g;
518
+ let m;
519
+ while ((m = classRegex.exec(source)) !== null) {
520
+ const openBrace = source.indexOf('{', m.index + 5); // skip past 'class'
521
+ if (openBrace === -1)
522
+ continue;
523
+ // Find matching close brace
524
+ let depth = 1;
525
+ let pos = openBrace + 1;
526
+ while (pos < source.length && depth > 0) {
527
+ const ch = source[pos];
528
+ if (ch === '{')
529
+ depth++;
530
+ else if (ch === '}') {
531
+ depth--;
532
+ if (depth === 0)
533
+ break;
534
+ }
535
+ else if (ch === '"' || ch === "'" || ch === '`') {
536
+ const q = ch;
537
+ pos++;
538
+ while (pos < source.length) {
539
+ if (source[pos] === '\\')
540
+ pos++;
541
+ else if (source[pos] === q)
542
+ break;
543
+ else if (q === '`' && source[pos] === '$' && source[pos + 1] === '{') {
544
+ pos += 2;
545
+ let td = 1;
546
+ while (pos < source.length && td > 0) {
547
+ if (source[pos] === '{')
548
+ td++;
549
+ else if (source[pos] === '}')
550
+ td--;
551
+ pos++;
552
+ }
553
+ continue;
554
+ }
555
+ pos++;
556
+ }
557
+ }
558
+ else if (ch === '/' && source[pos + 1] === '/') {
559
+ while (pos < source.length && source[pos] !== '\n')
560
+ pos++;
561
+ }
562
+ else if (ch === '/' && source[pos + 1] === '*') {
563
+ pos += 2;
564
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
565
+ pos++;
566
+ pos++;
567
+ }
568
+ pos++;
569
+ }
570
+ if (depth === 0)
571
+ ranges.push([openBrace, pos]);
572
+ }
573
+ return ranges;
574
+ }
507
575
  /**
508
576
  * Find variable reassignments (not declarations) and return insertions for tracing.
509
577
  * Handles: x = newValue; x += 1; x ||= fallback; etc.
@@ -513,6 +581,8 @@ function extractDestructuredNames(pattern) {
513
581
  */
514
582
  function findReassignments(source) {
515
583
  const results = [];
584
+ // Pre-compute class body ranges to skip class field declarations
585
+ const classRanges = findClassBodyRanges(source);
516
586
  // Match: <identifier> <assignOp>= <value> at the start of a line
517
587
  // Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
518
588
  // Plain: = (but not ==, ===, =>, !=)
@@ -548,6 +618,10 @@ function findReassignments(source) {
548
618
  const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
549
619
  if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
550
620
  continue;
621
+ // Skip class field declarations (e.g., `tasks = []` inside a class body)
622
+ // Inserting trace calls inside class bodies causes SyntaxError
623
+ if (classRanges.some(([start, end]) => match.index > start && match.index < end))
624
+ continue;
551
625
  // Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
552
626
  // or if it's a label (label: ...)
553
627
  const beforeOnLine = source.slice(lineStart, match.index).trim();
@@ -1530,7 +1604,7 @@ ingestUrl) {
1530
1604
  }
1531
1605
  // Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
1532
1606
  if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
1533
- prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
1607
+ prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` const _sampleCount = new Map();`, ` const _MAX_SAMPLES = 5;`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const cnt = _sampleCount.get(ck) || 0;`, ` if (cnt >= _MAX_SAMPLES) return;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` _sampleCount.set(ck, cnt + 1);`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
1534
1608
  }
1535
1609
  // Add React component render tracker if needed
1536
1610
  if (bodyInsertions.length > 0 || conciseBodyInsertions.length > 0) {
@@ -492,6 +492,73 @@ function extractDestructuredNames(pattern) {
492
492
  }
493
493
  return names;
494
494
  }
495
+ /**
496
+ * Find class body ranges in source code. Handles both:
497
+ * class Foo { ... }
498
+ * var Foo = class { ... }
499
+ * Returns an array of [start, end] positions (inclusive of braces).
500
+ */
501
+ export function findClassBodyRanges(source) {
502
+ const ranges = [];
503
+ // Match both class declarations and class expressions
504
+ const classRegex = /\bclass\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*(?:extends\s+[a-zA-Z_$.[\]]+\s*)?\{/g;
505
+ let m;
506
+ while ((m = classRegex.exec(source)) !== null) {
507
+ const openBrace = source.indexOf('{', m.index + 5); // skip past 'class'
508
+ if (openBrace === -1)
509
+ continue;
510
+ // Find matching close brace
511
+ let depth = 1;
512
+ let pos = openBrace + 1;
513
+ while (pos < source.length && depth > 0) {
514
+ const ch = source[pos];
515
+ if (ch === '{')
516
+ depth++;
517
+ else if (ch === '}') {
518
+ depth--;
519
+ if (depth === 0)
520
+ break;
521
+ }
522
+ else if (ch === '"' || ch === "'" || ch === '`') {
523
+ const q = ch;
524
+ pos++;
525
+ while (pos < source.length) {
526
+ if (source[pos] === '\\')
527
+ pos++;
528
+ else if (source[pos] === q)
529
+ break;
530
+ else if (q === '`' && source[pos] === '$' && source[pos + 1] === '{') {
531
+ pos += 2;
532
+ let td = 1;
533
+ while (pos < source.length && td > 0) {
534
+ if (source[pos] === '{')
535
+ td++;
536
+ else if (source[pos] === '}')
537
+ td--;
538
+ pos++;
539
+ }
540
+ continue;
541
+ }
542
+ pos++;
543
+ }
544
+ }
545
+ else if (ch === '/' && source[pos + 1] === '/') {
546
+ while (pos < source.length && source[pos] !== '\n')
547
+ pos++;
548
+ }
549
+ else if (ch === '/' && source[pos + 1] === '*') {
550
+ pos += 2;
551
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/'))
552
+ pos++;
553
+ pos++;
554
+ }
555
+ pos++;
556
+ }
557
+ if (depth === 0)
558
+ ranges.push([openBrace, pos]);
559
+ }
560
+ return ranges;
561
+ }
495
562
  /**
496
563
  * Find variable reassignments (not declarations) and return insertions for tracing.
497
564
  * Handles: x = newValue; x += 1; x ||= fallback; etc.
@@ -501,6 +568,8 @@ function extractDestructuredNames(pattern) {
501
568
  */
502
569
  export function findReassignments(source) {
503
570
  const results = [];
571
+ // Pre-compute class body ranges to skip class field declarations
572
+ const classRanges = findClassBodyRanges(source);
504
573
  // Match: <identifier> <assignOp>= <value> at the start of a line
505
574
  // Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
506
575
  // Plain: = (but not ==, ===, =>, !=)
@@ -536,6 +605,10 @@ export function findReassignments(source) {
536
605
  const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
537
606
  if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart()))
538
607
  continue;
608
+ // Skip class field declarations (e.g., `tasks = []` inside a class body)
609
+ // Inserting trace calls inside class bodies causes SyntaxError
610
+ if (classRanges.some(([start, end]) => match.index > start && match.index < end))
611
+ continue;
539
612
  // Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
540
613
  // or if it's a label (label: ...)
541
614
  const beforeOnLine = source.slice(lineStart, match.index).trim();
@@ -1518,7 +1591,7 @@ ingestUrl) {
1518
1591
  }
1519
1592
  // Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
1520
1593
  if (varInsertions.length > 0 || destructInsertions.length > 0 || reassignInsertions.length > 0 || forLoopInsertions.length > 0 || catchInsertions.length > 0 || funcParamInsertions.length > 0 || jsxExprInsertions.length > 0) {
1521
- prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
1594
+ prefixLines.push(`if (!globalThis.__trickle_var_tracer) {`, ` const _cache = new Map();`, ` const _sampleCount = new Map();`, ` const _MAX_SAMPLES = 5;`, ` function _inferType(v, d) {`, ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`, ` if (v === null) return { kind: 'primitive', name: 'null' };`, ` if (v === undefined) return { kind: 'primitive', name: 'undefined' };`, ` const t = typeof v;`, ` if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint' || t === 'symbol') return { kind: 'primitive', name: t };`, ` if (t === 'function') return { kind: 'function' };`, ` if (Array.isArray(v)) { return v.length === 0 ? { kind: 'array', element: { kind: 'primitive', name: 'unknown' } } : { kind: 'array', element: _inferType(v[0], d-1) }; }`, ` if (t === 'object') {`, ` if (v instanceof Date) return { kind: 'object', properties: { __date: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof RegExp) return { kind: 'object', properties: { __regexp: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Error) return { kind: 'object', properties: { __error: { kind: 'primitive', name: 'string' } } };`, ` if (v instanceof Promise) return { kind: 'promise', resolved: { kind: 'primitive', name: 'unknown' } };`, ` const props = {}; const keys = Object.keys(v).slice(0, 20);`, ` for (const k of keys) { try { props[k] = _inferType(v[k], d-1); } catch(e) { props[k] = { kind: 'primitive', name: 'unknown' }; } }`, ` return { kind: 'object', properties: props };`, ` }`, ` return { kind: 'primitive', name: 'unknown' };`, ` }`, ` function _sanitize(v, d) {`, ` if (d <= 0) return '[truncated]'; if (v === null || v === undefined) return v; const t = typeof v;`, ` if (t === 'string') return v.length > 100 ? v.substring(0, 100) + '...' : v;`, ` if (t === 'number' || t === 'boolean') return v; if (t === 'bigint') return String(v);`, ` if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';`, ` if (Array.isArray(v)) return v.slice(0, 3).map(i => _sanitize(i, d-1));`, ` if (t === 'object') { if (v instanceof Date) return v.toISOString(); if (v instanceof RegExp) return String(v); if (v instanceof Error) return { error: v.message }; if (v instanceof Promise) return '[Promise]';`, ` const r = {}; const keys = Object.keys(v).slice(0, 10); for (const k of keys) { try { r[k] = _sanitize(v[k], d-1); } catch(e) { r[k] = '[unreadable]'; } } return r; }`, ` return String(v);`, ` }`, ` globalThis.__trickle_var_tracer = function(v, n, l, mod, file) {`, ` try {`, ` const type = _inferType(v, 3);`, ` const th = JSON.stringify(type).substring(0, 32);`, ` const sample = _sanitize(v, 2);`, ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`, ` const ck = file + ':' + l + ':' + n;`, ` const cnt = _sampleCount.get(ck) || 0;`, ` if (cnt >= _MAX_SAMPLES) return;`, ` const prev = _cache.get(ck);`, ` const now = Date.now();`, ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`, ` _cache.set(ck, { sv: sv, ts: now });`, ` _sampleCount.set(ck, cnt + 1);`, ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`, ` } catch(e) {}`, ` };`, `}`, `function __trickle_tv(v, n, l) { try { globalThis.__trickle_var_tracer(v, n, l, ${JSON.stringify(moduleName)}, ${JSON.stringify(filename)}); } catch(e) {} }`);
1522
1595
  }
1523
1596
  // Add React component render tracker if needed
1524
1597
  if (bodyInsertions.length > 0 || conciseBodyInsertions.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.83",
3
+ "version": "0.2.85",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/trace-var.ts CHANGED
@@ -26,6 +26,9 @@ let debugMode = false;
26
26
 
27
27
  /** Cache: "file:line:varName" → { fingerprint, timestamp } for value-aware dedup */
28
28
  const varCache = new Map<string, { fp: string; ts: number }>();
29
+ /** Per-line sample count to avoid loop variable spam */
30
+ const sampleCount = new Map<string, number>();
31
+ const MAX_SAMPLES_PER_LINE = 5;
29
32
 
30
33
  /** Batch buffer for writing — avoids one fs.appendFileSync per variable */
31
34
  let varBuffer: string[] = [];
@@ -92,8 +95,12 @@ export function traceVar(
92
95
  const dummyArgs: TypeNode = { kind: 'tuple', elements: [] };
93
96
  const typeHash = hashType(dummyArgs, type);
94
97
 
95
- // Value-aware dedup: re-send if value changed or 10s elapsed
98
+ // Per-line sample count limit: stop after N samples to avoid loop spam
96
99
  const cacheKey = `${filePath}:${line}:${varName}`;
100
+ const cnt = sampleCount.get(cacheKey) || 0;
101
+ if (cnt >= MAX_SAMPLES_PER_LINE) return;
102
+
103
+ // Value-aware dedup: re-send if value changed or 10s elapsed
97
104
  const t = typeof value;
98
105
  const fp = (t === 'string' || t === 'number' || t === 'boolean' || value === null || value === undefined)
99
106
  ? String(value).substring(0, 60)
@@ -102,6 +109,7 @@ export function traceVar(
102
109
  const prev = varCache.get(cacheKey);
103
110
  if (prev && prev.fp === fp && (now - prev.ts) < 10000) return;
104
111
  varCache.set(cacheKey, { fp, ts: now });
112
+ sampleCount.set(cacheKey, cnt + 1);
105
113
 
106
114
  const sample = sanitizeVarSample(value);
107
115
 
@@ -472,6 +472,57 @@ function extractDestructuredNames(pattern: string): string[] {
472
472
  return names;
473
473
  }
474
474
 
475
+ /**
476
+ * Find class body ranges in source code. Handles both:
477
+ * class Foo { ... }
478
+ * var Foo = class { ... }
479
+ * Returns an array of [start, end] positions (inclusive of braces).
480
+ */
481
+ export function findClassBodyRanges(source: string): Array<[number, number]> {
482
+ const ranges: Array<[number, number]> = [];
483
+ // Match both class declarations and class expressions
484
+ const classRegex = /\bclass\s*(?:[a-zA-Z_$][a-zA-Z0-9_$]*)?\s*(?:extends\s+[a-zA-Z_$.[\]]+\s*)?\{/g;
485
+ let m;
486
+ while ((m = classRegex.exec(source)) !== null) {
487
+ const openBrace = source.indexOf('{', m.index + 5); // skip past 'class'
488
+ if (openBrace === -1) continue;
489
+ // Find matching close brace
490
+ let depth = 1;
491
+ let pos = openBrace + 1;
492
+ while (pos < source.length && depth > 0) {
493
+ const ch = source[pos];
494
+ if (ch === '{') depth++;
495
+ else if (ch === '}') { depth--; if (depth === 0) break; }
496
+ else if (ch === '"' || ch === "'" || ch === '`') {
497
+ const q = ch; pos++;
498
+ while (pos < source.length) {
499
+ if (source[pos] === '\\') pos++;
500
+ else if (source[pos] === q) break;
501
+ else if (q === '`' && source[pos] === '$' && source[pos + 1] === '{') {
502
+ pos += 2; let td = 1;
503
+ while (pos < source.length && td > 0) {
504
+ if (source[pos] === '{') td++;
505
+ else if (source[pos] === '}') td--;
506
+ pos++;
507
+ }
508
+ continue;
509
+ }
510
+ pos++;
511
+ }
512
+ } else if (ch === '/' && source[pos + 1] === '/') {
513
+ while (pos < source.length && source[pos] !== '\n') pos++;
514
+ } else if (ch === '/' && source[pos + 1] === '*') {
515
+ pos += 2;
516
+ while (pos < source.length - 1 && !(source[pos] === '*' && source[pos + 1] === '/')) pos++;
517
+ pos++;
518
+ }
519
+ pos++;
520
+ }
521
+ if (depth === 0) ranges.push([openBrace, pos]);
522
+ }
523
+ return ranges;
524
+ }
525
+
475
526
  /**
476
527
  * Find variable reassignments (not declarations) and return insertions for tracing.
477
528
  * Handles: x = newValue; x += 1; x ||= fallback; etc.
@@ -481,6 +532,8 @@ function extractDestructuredNames(pattern: string): string[] {
481
532
  */
482
533
  export function findReassignments(source: string): Array<{ lineEnd: number; varName: string; lineNo: number }> {
483
534
  const results: Array<{ lineEnd: number; varName: string; lineNo: number }> = [];
535
+ // Pre-compute class body ranges to skip class field declarations
536
+ const classRanges = findClassBodyRanges(source);
484
537
 
485
538
  // Match: <identifier> <assignOp>= <value> at the start of a line
486
539
  // Compound operators: +=, -=, *=, /=, %=, **=, &&=, ||=, ??=, <<=, >>=, >>>=, &=, |=, ^=
@@ -513,6 +566,10 @@ export function findReassignments(source: string): Array<{ lineEnd: number; varN
513
566
  const linePrefix = source.slice(lineStart, match.index + match[1].length).trim();
514
567
  if (/^(export\s+)?(const|let|var)\s/.test(source.slice(lineStart).trimStart())) continue;
515
568
 
569
+ // Skip class field declarations (e.g., `tasks = []` inside a class body)
570
+ // Inserting trace calls inside class bodies causes SyntaxError
571
+ if (classRanges.some(([start, end]) => match!.index > start && match!.index < end)) continue;
572
+
516
573
  // Skip if this looks like a property in an object literal (preceded by a key: pattern on same line)
517
574
  // or if it's a label (label: ...)
518
575
  const beforeOnLine = source.slice(lineStart, match.index).trim();
@@ -1567,6 +1624,8 @@ export function transformEsmSource(
1567
1624
  prefixLines.push(
1568
1625
  `if (!globalThis.__trickle_var_tracer) {`,
1569
1626
  ` const _cache = new Map();`,
1627
+ ` const _sampleCount = new Map();`,
1628
+ ` const _MAX_SAMPLES = 5;`,
1570
1629
  ` function _inferType(v, d) {`,
1571
1630
  ` if (d <= 0) return { kind: 'primitive', name: 'unknown' };`,
1572
1631
  ` if (v === null) return { kind: 'primitive', name: 'null' };`,
@@ -1603,10 +1662,13 @@ export function transformEsmSource(
1603
1662
  ` const sample = _sanitize(v, 2);`,
1604
1663
  ` const sv = typeof v === 'object' && v !== null ? JSON.stringify(sample).substring(0, 60) : String(v).substring(0, 60);`,
1605
1664
  ` const ck = file + ':' + l + ':' + n;`,
1665
+ ` const cnt = _sampleCount.get(ck) || 0;`,
1666
+ ` if (cnt >= _MAX_SAMPLES) return;`,
1606
1667
  ` const prev = _cache.get(ck);`,
1607
1668
  ` const now = Date.now();`,
1608
1669
  ` if (prev && prev.sv === sv && now - prev.ts < 5000) return;`,
1609
1670
  ` _cache.set(ck, { sv: sv, ts: now });`,
1671
+ ` _sampleCount.set(ck, cnt + 1);`,
1610
1672
  ` __trickle_send(JSON.stringify({ kind: 'variable', varName: n, line: l, module: mod, file: file, type: type, typeHash: th, sample: sample }));`,
1611
1673
  ` } catch(e) {}`,
1612
1674
  ` };`,