@fink-andreas/pi-linear-tools 0.5.1 → 0.7.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/CHANGELOG.md +38 -0
- package/README.md +44 -16
- package/extensions/pi-linear-tools.js +210 -27
- package/package.json +2 -2
- package/settings.json.example +6 -2
- package/src/cli.js +101 -8
- package/src/handlers.js +405 -18
- package/src/linear-client.js +120 -14
- package/src/linear.js +319 -26
- package/src/settings.js +22 -0
package/src/linear-client.js
CHANGED
|
@@ -11,7 +11,11 @@ import { debug, warn, info } from './logger.js';
|
|
|
11
11
|
/** @type {Function|null} Test-only client factory override */
|
|
12
12
|
let _testClientFactory = null;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const DEFAULT_REQUEST_LIMIT = 5000;
|
|
15
|
+
const LOW_RATE_LIMIT_THRESHOLD = 0.10;
|
|
16
|
+
const RATE_LIMIT_WARN_MIN_MS = 30000;
|
|
17
|
+
|
|
18
|
+
/** @type {Map<string, {limit: number, remaining: number, resetAt: number, lastWarnAt?: number}>} Per-client request rate limit tracking */
|
|
15
19
|
const rateLimitTracker = new Map();
|
|
16
20
|
|
|
17
21
|
/** @type {Map<string, {total: number, success: number, failed: number, rateLimited: number, windowStart: number, lastSummaryAt: number}>} */
|
|
@@ -32,6 +36,33 @@ function getTrackerKeyFromClient(client) {
|
|
|
32
36
|
return client?.__piLinearTrackerKey || client?.apiKey || 'default';
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
function parseHeaderNumber(value) {
|
|
40
|
+
if (value === null || value === undefined) return null;
|
|
41
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
42
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getTrackerLimit(tracker) {
|
|
46
|
+
return tracker?.limit || tracker?.total || DEFAULT_REQUEST_LIMIT;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getUsedRequests(tracker) {
|
|
50
|
+
const limit = getTrackerLimit(tracker);
|
|
51
|
+
const remaining = tracker?.remaining;
|
|
52
|
+
if (!Number.isFinite(remaining)) return 0;
|
|
53
|
+
return Math.max(0, limit - remaining);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getUsagePercent(tracker) {
|
|
57
|
+
const limit = getTrackerLimit(tracker);
|
|
58
|
+
if (!limit) return null;
|
|
59
|
+
return Math.round((getUsedRequests(tracker) / limit) * 100);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getLowRequestThreshold(tracker) {
|
|
63
|
+
return Math.max(1, Math.floor(getTrackerLimit(tracker) * LOW_RATE_LIMIT_THRESHOLD));
|
|
64
|
+
}
|
|
65
|
+
|
|
35
66
|
function getRequestMetric(trackerKey) {
|
|
36
67
|
let metric = requestMetrics.get(trackerKey);
|
|
37
68
|
if (!metric) {
|
|
@@ -60,7 +91,8 @@ function maybeLogRequestSummary(trackerKey) {
|
|
|
60
91
|
if (!shouldLogByCount && !shouldLogByTime) return;
|
|
61
92
|
|
|
62
93
|
metric.lastSummaryAt = now;
|
|
63
|
-
const used =
|
|
94
|
+
const used = getUsedRequests(tracker);
|
|
95
|
+
const limit = getTrackerLimit(tracker);
|
|
64
96
|
|
|
65
97
|
info('[pi-linear-tools] Linear API usage summary', {
|
|
66
98
|
trackerKey,
|
|
@@ -68,6 +100,7 @@ function maybeLogRequestSummary(trackerKey) {
|
|
|
68
100
|
requestsSuccess: metric.success,
|
|
69
101
|
requestsFailed: metric.failed,
|
|
70
102
|
requestsRateLimited: metric.rateLimited,
|
|
103
|
+
requestsLimit: limit,
|
|
71
104
|
requestsRemaining: tracker.remaining,
|
|
72
105
|
requestsUsed: used,
|
|
73
106
|
resetAt: tracker.resetAt,
|
|
@@ -125,19 +158,54 @@ export function markRateLimited(resetAt) {
|
|
|
125
158
|
* Extract rate limit info from SDK client response
|
|
126
159
|
* The Linear SDK stores response metadata on the client after requests
|
|
127
160
|
* @param {LinearClient} client - Linear SDK client
|
|
128
|
-
* @returns {{remaining: number|null, resetAt: number|null, resetTime: string|null}}
|
|
161
|
+
* @returns {{limit: number|null, remaining: number|null, resetAt: number|null, resetTime: string|null}}
|
|
129
162
|
*/
|
|
130
163
|
export function getClientRateLimit(client) {
|
|
131
164
|
const trackerData = rateLimitTracker.get(getTrackerKeyFromClient(client));
|
|
132
165
|
if (trackerData) {
|
|
133
166
|
return {
|
|
167
|
+
limit: getTrackerLimit(trackerData),
|
|
134
168
|
remaining: trackerData.remaining,
|
|
135
169
|
resetAt: trackerData.resetAt,
|
|
136
170
|
resetTime: trackerData.resetAt ? new Date(trackerData.resetAt).toLocaleTimeString() : null,
|
|
137
171
|
};
|
|
138
172
|
}
|
|
139
173
|
|
|
140
|
-
return { remaining: null, resetAt: null, resetTime: null };
|
|
174
|
+
return { limit: null, remaining: null, resetAt: null, resetTime: null };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get detailed rate limit info including usage percentage
|
|
179
|
+
* @param {LinearClient} client - Linear SDK client
|
|
180
|
+
* @returns {{remaining: number|null, resetAt: number|null, resetTime: string|null, used: number, usagePercent: number|null, total: number}}
|
|
181
|
+
*/
|
|
182
|
+
export function getClientRateLimitInfo(client) {
|
|
183
|
+
const trackerData = rateLimitTracker.get(getTrackerKeyFromClient(client));
|
|
184
|
+
const total = getTrackerLimit(trackerData);
|
|
185
|
+
|
|
186
|
+
if (trackerData && trackerData.remaining !== undefined) {
|
|
187
|
+
const remaining = trackerData.remaining;
|
|
188
|
+
const used = getUsedRequests(trackerData);
|
|
189
|
+
const usagePercent = getUsagePercent(trackerData);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
remaining,
|
|
193
|
+
resetAt: trackerData.resetAt,
|
|
194
|
+
resetTime: trackerData.resetAt ? new Date(trackerData.resetAt).toLocaleTimeString() : null,
|
|
195
|
+
used,
|
|
196
|
+
usagePercent,
|
|
197
|
+
total,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
remaining: null,
|
|
203
|
+
resetAt: null,
|
|
204
|
+
resetTime: null,
|
|
205
|
+
used: 0,
|
|
206
|
+
usagePercent: null,
|
|
207
|
+
total,
|
|
208
|
+
};
|
|
141
209
|
}
|
|
142
210
|
|
|
143
211
|
/**
|
|
@@ -172,11 +240,12 @@ export function getClientRequestMetrics(client) {
|
|
|
172
240
|
* @returns {boolean} True if warning was issued
|
|
173
241
|
*/
|
|
174
242
|
export function checkAndWarnRateLimit(client) {
|
|
175
|
-
const
|
|
243
|
+
const rateLimitInfo = getClientRateLimitInfo(client);
|
|
244
|
+
const { remaining, resetTime, usagePercent } = rateLimitInfo;
|
|
176
245
|
|
|
177
|
-
if (remaining !== null && remaining <=
|
|
178
|
-
const usagePercent = Math.round(((5000 - remaining) / 5000) * 100);
|
|
246
|
+
if (remaining !== null && remaining <= getLowRequestThreshold(rateLimitInfo)) {
|
|
179
247
|
warn(`Linear API rate limit running low: ${remaining} requests remaining (~${usagePercent}% used). Resets at ${resetTime}`, {
|
|
248
|
+
limit: rateLimitInfo.total,
|
|
180
249
|
remaining,
|
|
181
250
|
resetTime,
|
|
182
251
|
usagePercent,
|
|
@@ -233,7 +302,12 @@ export function createLinearClient(auth) {
|
|
|
233
302
|
const trackerKey = getTrackerKey(apiKey);
|
|
234
303
|
client.__piLinearTrackerKey = trackerKey;
|
|
235
304
|
if (!rateLimitTracker.has(trackerKey)) {
|
|
236
|
-
rateLimitTracker.set(trackerKey, {
|
|
305
|
+
rateLimitTracker.set(trackerKey, {
|
|
306
|
+
limit: DEFAULT_REQUEST_LIMIT,
|
|
307
|
+
remaining: DEFAULT_REQUEST_LIMIT,
|
|
308
|
+
resetAt: Date.now() + 3600000,
|
|
309
|
+
lastWarnAt: 0,
|
|
310
|
+
});
|
|
237
311
|
}
|
|
238
312
|
getRequestMetric(trackerKey);
|
|
239
313
|
|
|
@@ -254,22 +328,34 @@ export function createLinearClient(auth) {
|
|
|
254
328
|
metric.success += 1;
|
|
255
329
|
|
|
256
330
|
if (response.headers) {
|
|
257
|
-
const
|
|
258
|
-
const
|
|
331
|
+
const limit = parseHeaderNumber(response.headers.get('X-RateLimit-Requests-Limit'));
|
|
332
|
+
const remaining = parseHeaderNumber(response.headers.get('X-RateLimit-Requests-Remaining'));
|
|
333
|
+
const resetAt = parseHeaderNumber(response.headers.get('X-RateLimit-Requests-Reset'));
|
|
259
334
|
|
|
260
335
|
const tracker = rateLimitTracker.get(trackerKey);
|
|
336
|
+
if (tracker && limit !== null) {
|
|
337
|
+
tracker.limit = limit;
|
|
338
|
+
}
|
|
261
339
|
if (tracker && remaining !== null) {
|
|
262
|
-
tracker.remaining =
|
|
340
|
+
tracker.remaining = remaining;
|
|
263
341
|
}
|
|
264
342
|
if (tracker && resetAt !== null) {
|
|
265
|
-
tracker.resetAt =
|
|
343
|
+
tracker.resetAt = resetAt;
|
|
266
344
|
}
|
|
267
345
|
}
|
|
268
346
|
|
|
269
347
|
const tracker = rateLimitTracker.get(trackerKey);
|
|
270
|
-
|
|
271
|
-
|
|
348
|
+
const now = Date.now();
|
|
349
|
+
if (
|
|
350
|
+
tracker &&
|
|
351
|
+
tracker.remaining <= getLowRequestThreshold(tracker) &&
|
|
352
|
+
tracker.remaining > 0 &&
|
|
353
|
+
now - (tracker.lastWarnAt || 0) >= RATE_LIMIT_WARN_MIN_MS
|
|
354
|
+
) {
|
|
355
|
+
tracker.lastWarnAt = now;
|
|
356
|
+
const usagePercent = getUsagePercent(tracker);
|
|
272
357
|
warn(`Linear API rate limit running low: ${tracker.remaining} requests remaining (~${usagePercent}% used). Resets at ${new Date(tracker.resetAt).toLocaleTimeString()}`, {
|
|
358
|
+
limit: getTrackerLimit(tracker),
|
|
273
359
|
remaining: tracker.remaining,
|
|
274
360
|
resetTime: new Date(tracker.resetAt).toLocaleTimeString(),
|
|
275
361
|
usagePercent,
|
|
@@ -288,9 +374,12 @@ export function createLinearClient(auth) {
|
|
|
288
374
|
metric.rateLimited += 1;
|
|
289
375
|
const resetAt = Number(error?.requestsResetAt) || Date.now() + 3600000;
|
|
290
376
|
const remaining = Number.isFinite(error?.requestsRemaining) ? error.requestsRemaining : 0;
|
|
377
|
+
const previousTracker = rateLimitTracker.get(trackerKey);
|
|
291
378
|
rateLimitTracker.set(trackerKey, {
|
|
379
|
+
limit: getTrackerLimit(previousTracker),
|
|
292
380
|
remaining,
|
|
293
381
|
resetAt,
|
|
382
|
+
lastWarnAt: previousTracker?.lastWarnAt || 0,
|
|
294
383
|
});
|
|
295
384
|
markRateLimited(resetAt);
|
|
296
385
|
}
|
|
@@ -317,3 +406,20 @@ export function setTestClientFactory(factory) {
|
|
|
317
406
|
export function resetTestClientFactory() {
|
|
318
407
|
_testClientFactory = null;
|
|
319
408
|
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Set rate limit tracker state for testing (TEST ONLY)
|
|
412
|
+
* @param {string} apiKey - API key used to create the client
|
|
413
|
+
* @param {{remaining: number, resetAt: number}} state - Tracker state
|
|
414
|
+
*/
|
|
415
|
+
export function setTestRateLimitTracker(apiKey, state) {
|
|
416
|
+
const trackerKey = getTrackerKey(apiKey);
|
|
417
|
+
rateLimitTracker.set(trackerKey, state);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Clear rate limit tracker for testing (TEST ONLY)
|
|
422
|
+
*/
|
|
423
|
+
export function clearTestRateLimitTracker() {
|
|
424
|
+
rateLimitTracker.clear();
|
|
425
|
+
}
|