@warpmetrics/warp 0.0.22 → 0.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpmetrics/warp",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "Measure your agents, not your LLM calls.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -26,6 +26,17 @@ const queue = {
26
26
 
27
27
  let flushTimeout = null;
28
28
 
29
+ // Backoff state for 429 retry
30
+ let backoff = {
31
+ active: false,
32
+ delay: 0, // current delay in ms
33
+ retries: 0,
34
+ };
35
+
36
+ const BACKOFF_BASE = 2000; // 2s initial backoff
37
+ const BACKOFF_MAX = 60000; // 60s cap
38
+ const BACKOFF_JITTER = 0.3; // ±30% jitter
39
+
29
40
  // ---------------------------------------------------------------------------
30
41
  // Config
31
42
  // ---------------------------------------------------------------------------
@@ -46,6 +57,25 @@ export function clearQueue() {
46
57
  queue.links.length = 0;
47
58
  queue.outcomes.length = 0;
48
59
  queue.acts.length = 0;
60
+ resetBackoff();
61
+ }
62
+
63
+ /** Reset backoff state. Exported for testing. */
64
+ export function resetBackoff() {
65
+ backoff = { active: false, delay: 0, retries: 0 };
66
+ }
67
+
68
+ /** Get current backoff state. Exported for testing. */
69
+ export function getBackoff() {
70
+ return { ...backoff };
71
+ }
72
+
73
+ /** Compute next backoff delay with jitter. */
74
+ function nextBackoffDelay(retryAfterMs) {
75
+ if (retryAfterMs) return retryAfterMs;
76
+ const base = Math.min(BACKOFF_BASE * Math.pow(2, backoff.retries), BACKOFF_MAX);
77
+ const jitter = base * BACKOFF_JITTER * (Math.random() * 2 - 1);
78
+ return Math.round(base + jitter);
49
79
  }
50
80
 
51
81
  // ---------------------------------------------------------------------------
@@ -57,6 +87,9 @@ function enqueue(type, event) {
57
87
 
58
88
  queue[type].push(event);
59
89
 
90
+ // During backoff, don't schedule additional flushes — the backoff timer handles it
91
+ if (backoff.active) return;
92
+
60
93
  const total = queue.runs.length + queue.groups.length + queue.calls.length
61
94
  + queue.links.length + queue.outcomes.length + queue.acts.length;
62
95
 
@@ -67,6 +100,19 @@ function enqueue(type, event) {
67
100
  }
68
101
  }
69
102
 
103
+ // ---------------------------------------------------------------------------
104
+ // Re-queue helper
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function requeue(batch) {
108
+ queue.runs.unshift(...batch.runs);
109
+ queue.groups.unshift(...batch.groups);
110
+ queue.calls.unshift(...batch.calls);
111
+ queue.links.unshift(...batch.links);
112
+ queue.outcomes.unshift(...batch.outcomes);
113
+ queue.acts.unshift(...batch.acts);
114
+ }
115
+
70
116
  // ---------------------------------------------------------------------------
71
117
  // Flush
72
118
  // ---------------------------------------------------------------------------
@@ -125,11 +171,41 @@ export async function flush() {
125
171
  body,
126
172
  });
127
173
 
174
+ if (res.status === 429) {
175
+ // Parse Retry-After header (seconds) if present
176
+ const retryAfterHeader = res.headers?.get?.('Retry-After');
177
+ const retryAfterMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1000 : 0;
178
+ const delay = nextBackoffDelay(retryAfterMs || 0);
179
+
180
+ backoff.active = true;
181
+ backoff.retries++;
182
+ backoff.delay = delay;
183
+
184
+ if (config.debug) {
185
+ console.warn(`[warpmetrics] Rate limited (429). Backing off ${delay}ms (retry #${backoff.retries})`);
186
+ }
187
+
188
+ // Re-queue events
189
+ requeue(batch);
190
+
191
+ // Schedule retry after backoff delay
192
+ flushTimeout = setTimeout(flush, delay);
193
+ return;
194
+ }
195
+
128
196
  if (!res.ok) {
129
197
  const body = await res.text().catch(() => '');
130
198
  throw new Error(`HTTP ${res.status}: ${body}`);
131
199
  }
132
200
 
201
+ // Success — reset backoff state
202
+ if (backoff.active) {
203
+ if (config.debug) {
204
+ console.log(`[warpmetrics] Backoff cleared after ${backoff.retries} retries`);
205
+ }
206
+ resetBackoff();
207
+ }
208
+
133
209
  if (config.debug) {
134
210
  const result = await res.json();
135
211
  const d = result.data || result;
@@ -139,13 +215,7 @@ export async function flush() {
139
215
  if (config.debug) {
140
216
  console.error('[warpmetrics] Flush failed:', err.message);
141
217
  }
142
- // Re-queue so nothing is lost.
143
- queue.runs.unshift(...batch.runs);
144
- queue.groups.unshift(...batch.groups);
145
- queue.calls.unshift(...batch.calls);
146
- queue.links.unshift(...batch.links);
147
- queue.outcomes.unshift(...batch.outcomes);
148
- queue.acts.unshift(...batch.acts);
218
+ requeue(batch);
149
219
  throw err;
150
220
  }
151
221
  }