decap-cms-backend-github 3.7.0 → 3.8.1
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 +8 -0
- package/dist/decap-cms-backend-github.js +73 -73
- package/dist/decap-cms-backend-github.js.LICENSE.txt +19 -0
- package/dist/decap-cms-backend-github.js.map +1 -1
- package/dist/esm/API.js +414 -0
- package/dist/esm/AuthenticationPage.js +28 -19
- package/dist/esm/implementation.js +176 -7
- package/package.json +17 -15
- package/src/API.ts +485 -0
- package/src/AuthenticationPage.js +4 -4
- package/src/__tests__/implementation.spec.js +487 -0
- package/src/implementation.tsx +203 -3
- package/src/polling.ts +472 -0
- package/LICENSE +0 -22
- package/dist/esm/rateLimitLink.js +0 -24
package/src/polling.ts
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Notes Polling System
|
|
3
|
+
*
|
|
4
|
+
* ETag-based polling manager that efficiently checks for changes in GitHub Issues
|
|
5
|
+
* Used for a real-time feel of notes updates without excessive API calls leveraging conditional requests (304) that don't count on Github rate limits.
|
|
6
|
+
*
|
|
7
|
+
* @module polling
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Note, IssueState, CommentData, IssueChange } from 'decap-cms-lib-util';
|
|
11
|
+
|
|
12
|
+
interface WatchedIssue {
|
|
13
|
+
issueNumber: number;
|
|
14
|
+
collection: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
etag: string | null;
|
|
17
|
+
lastState: IssueState | null;
|
|
18
|
+
onUpdate?: (notes: Note[], changes: IssueChange[]) => void;
|
|
19
|
+
onChange?: (change: IssueChange) => void;
|
|
20
|
+
retryCount?: number;
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GitHubNotesAPI {
|
|
25
|
+
getIssueState(issueNumber: number): Promise<IssueState>;
|
|
26
|
+
getIssueWithETag(
|
|
27
|
+
issueNumber: number,
|
|
28
|
+
etag: string | null,
|
|
29
|
+
): Promise<
|
|
30
|
+
| { status: 304; data?: never; etag?: never }
|
|
31
|
+
| { status: 200; data: IssueState; etag: string | null }
|
|
32
|
+
>;
|
|
33
|
+
parseCommentToNote(comment: CommentData): Note;
|
|
34
|
+
findEntryIssue(collection: string, slug: string): Promise<{ number: number } | null>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Redux action types
|
|
38
|
+
export const NOTES_POLLING_START = 'NOTES_POLLING_START';
|
|
39
|
+
export const NOTES_POLLING_STOP = 'NOTES_POLLING_STOP';
|
|
40
|
+
export const NOTES_POLLING_UPDATE = 'NOTES_POLLING_UPDATE';
|
|
41
|
+
export const NOTES_CHANGE_DETECTED = 'NOTES_CHANGE_DETECTED';
|
|
42
|
+
|
|
43
|
+
export class ETagPollingManager {
|
|
44
|
+
private currentWatch: WatchedIssue | null = null;
|
|
45
|
+
private currentIssueKey: string | null = null;
|
|
46
|
+
private pollingInterval = 15000;
|
|
47
|
+
private intervalId: NodeJS.Timeout | null = null;
|
|
48
|
+
private isDocumentVisible = true;
|
|
49
|
+
private api: GitHubNotesAPI;
|
|
50
|
+
private isPolling = false;
|
|
51
|
+
private pendingRetryTimeout: NodeJS.Timeout | null = null;
|
|
52
|
+
|
|
53
|
+
constructor(api: GitHubNotesAPI, pollingInterval = 15000) {
|
|
54
|
+
this.api = api;
|
|
55
|
+
this.pollingInterval = pollingInterval;
|
|
56
|
+
this.setupVisibilityListener();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Setup Page Visibility API listener
|
|
61
|
+
* Pauses polling when tab is hidden
|
|
62
|
+
*/
|
|
63
|
+
private setupVisibilityListener() {
|
|
64
|
+
if (typeof document !== 'undefined') {
|
|
65
|
+
document.addEventListener('visibilitychange', () => {
|
|
66
|
+
this.isDocumentVisible = !document.hidden;
|
|
67
|
+
|
|
68
|
+
if (this.isDocumentVisible) {
|
|
69
|
+
this.startPolling();
|
|
70
|
+
this.checkAllIssuesNow();
|
|
71
|
+
} else {
|
|
72
|
+
this.stopPolling();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start watching an issue for changes
|
|
80
|
+
* This will automatically stop watching any previously watched issue
|
|
81
|
+
*
|
|
82
|
+
* @param issueNumber - GitHub issue number
|
|
83
|
+
* @param collection - Collection name
|
|
84
|
+
* @param slug - Entry slug
|
|
85
|
+
* @returns Function to stop watching
|
|
86
|
+
*/
|
|
87
|
+
async watchIssue(
|
|
88
|
+
issueNumber: number,
|
|
89
|
+
collection: string,
|
|
90
|
+
slug: string,
|
|
91
|
+
callbacks: {
|
|
92
|
+
onUpdate?: (notes: Note[], changes: IssueChange[]) => void;
|
|
93
|
+
onChange?: (change: IssueChange) => void;
|
|
94
|
+
},
|
|
95
|
+
initialState: IssueState | null = null,
|
|
96
|
+
): Promise<() => void> {
|
|
97
|
+
const issueKey = this.getIssueKey(collection, slug);
|
|
98
|
+
|
|
99
|
+
// STOP ANY EXISTING WATCH FIRST
|
|
100
|
+
if (this.currentWatch) {
|
|
101
|
+
this.stopCurrentWatch();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get initial state if not provided
|
|
105
|
+
if (!initialState) {
|
|
106
|
+
try {
|
|
107
|
+
initialState = await this.api.getIssueState(issueNumber);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('[DecapNotes Polling] Failed to get initial state:', error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.currentWatch = {
|
|
114
|
+
issueNumber,
|
|
115
|
+
collection,
|
|
116
|
+
slug,
|
|
117
|
+
etag: null,
|
|
118
|
+
lastState: initialState,
|
|
119
|
+
onUpdate: callbacks.onUpdate,
|
|
120
|
+
onChange: callbacks.onChange,
|
|
121
|
+
retryCount: 0,
|
|
122
|
+
maxRetries: 5,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
this.currentIssueKey = issueKey;
|
|
126
|
+
|
|
127
|
+
// Start polling if not already running
|
|
128
|
+
if (!this.intervalId && this.isDocumentVisible) {
|
|
129
|
+
this.startPolling();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Do an immediate check
|
|
133
|
+
this.checkCurrentIssue();
|
|
134
|
+
|
|
135
|
+
// Return unwatch function
|
|
136
|
+
return () => this.stopCurrentWatch();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Watch issue with retry logic for newly created issues
|
|
141
|
+
*/
|
|
142
|
+
async watchIssueWithRetry(
|
|
143
|
+
collection: string,
|
|
144
|
+
slug: string,
|
|
145
|
+
callbacks: {
|
|
146
|
+
onUpdate?: (notes: Note[], changes: IssueChange[]) => void;
|
|
147
|
+
onChange?: (change: IssueChange) => void;
|
|
148
|
+
},
|
|
149
|
+
maxRetries = 5,
|
|
150
|
+
retryDelay = 2000,
|
|
151
|
+
): Promise<() => void> {
|
|
152
|
+
const issueKey = this.getIssueKey(collection, slug);
|
|
153
|
+
|
|
154
|
+
// STOP ANY EXISTING WATCH FIRST
|
|
155
|
+
if (this.currentWatch) {
|
|
156
|
+
this.stopCurrentWatch();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const attemptWatch = async (attempt: number): Promise<() => void> => {
|
|
160
|
+
try {
|
|
161
|
+
const issue = await this.api.findEntryIssue(collection, slug);
|
|
162
|
+
|
|
163
|
+
if (issue) {
|
|
164
|
+
return await this.watchIssue(issue.number, collection, slug, callbacks);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (attempt < maxRetries) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
this.pendingRetryTimeout = setTimeout(async () => {
|
|
170
|
+
this.pendingRetryTimeout = null;
|
|
171
|
+
try {
|
|
172
|
+
const unwatchFn = await attemptWatch(attempt + 1);
|
|
173
|
+
resolve(unwatchFn);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
reject(error);
|
|
176
|
+
}
|
|
177
|
+
}, retryDelay);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(
|
|
182
|
+
`[DecapNotes Polling] No issue found for ${issueKey} after ${maxRetries} attempts. This is expected if there are no notes for this entry yet.`,
|
|
183
|
+
);
|
|
184
|
+
// Return a no-op unwatch function
|
|
185
|
+
return () => {
|
|
186
|
+
/* no-op */
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(`[DecapNotes Polling] Error finding issue for ${issueKey}:`, error);
|
|
190
|
+
|
|
191
|
+
if (attempt < maxRetries) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
this.pendingRetryTimeout = setTimeout(async () => {
|
|
194
|
+
this.pendingRetryTimeout = null;
|
|
195
|
+
try {
|
|
196
|
+
const unwatchFn = await attemptWatch(attempt + 1);
|
|
197
|
+
resolve(unwatchFn);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
reject(err);
|
|
200
|
+
}
|
|
201
|
+
}, retryDelay);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return attemptWatch(1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Stop watching the current issue - complete cleanup
|
|
214
|
+
*/
|
|
215
|
+
private stopCurrentWatch() {
|
|
216
|
+
if (!this.currentWatch) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Clear any pending retry timeout
|
|
221
|
+
if (this.pendingRetryTimeout) {
|
|
222
|
+
clearTimeout(this.pendingRetryTimeout);
|
|
223
|
+
this.pendingRetryTimeout = null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Clear current watch
|
|
227
|
+
this.currentWatch = null;
|
|
228
|
+
this.currentIssueKey = null;
|
|
229
|
+
|
|
230
|
+
// Stop polling since there's nothing to watch
|
|
231
|
+
this.stopPolling();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Start the polling loop
|
|
236
|
+
*/
|
|
237
|
+
private startPolling() {
|
|
238
|
+
if (this.intervalId || !this.isDocumentVisible || !this.currentWatch) return;
|
|
239
|
+
|
|
240
|
+
console.log(
|
|
241
|
+
`[DecapNotes Polling] Starting polling loop (${this.pollingInterval}ms interval) for ${this.currentIssueKey}`,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
this.intervalId = setInterval(() => {
|
|
245
|
+
this.pollAllIssues();
|
|
246
|
+
}, this.pollingInterval);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Stop the polling loop
|
|
251
|
+
*/
|
|
252
|
+
private stopPolling() {
|
|
253
|
+
if (this.intervalId) {
|
|
254
|
+
console.log('[DecapNotes Polling] Stopping polling loop');
|
|
255
|
+
clearInterval(this.intervalId);
|
|
256
|
+
this.intervalId = null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Poll current watched issue
|
|
262
|
+
*/
|
|
263
|
+
private async pollAllIssues() {
|
|
264
|
+
await this.checkCurrentIssue();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check current issue for changes using ETag
|
|
269
|
+
*/
|
|
270
|
+
private async checkCurrentIssue() {
|
|
271
|
+
if (!this.currentWatch) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.isPolling) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.isPolling = true;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const watch = this.currentWatch;
|
|
283
|
+
|
|
284
|
+
const response = await this.api.getIssueWithETag(watch.issueNumber, watch.etag);
|
|
285
|
+
|
|
286
|
+
if (response.status === 304) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (response.status === 200) {
|
|
291
|
+
const newState: IssueState = response.data;
|
|
292
|
+
const newETag = response.etag;
|
|
293
|
+
|
|
294
|
+
// Update ETag
|
|
295
|
+
watch.etag = newETag || null;
|
|
296
|
+
|
|
297
|
+
// Detect specific changes
|
|
298
|
+
const changes = this.detectChanges(watch.lastState, newState);
|
|
299
|
+
|
|
300
|
+
if (changes.length > 0) {
|
|
301
|
+
// Convert comments to notes
|
|
302
|
+
const newNotes = newState.comments.map(comment => ({
|
|
303
|
+
...this.api.parseCommentToNote(comment),
|
|
304
|
+
issueUrl: newState.html_url,
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
if (watch.onUpdate) {
|
|
308
|
+
watch.onUpdate(newNotes, changes);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (watch.onChange) {
|
|
312
|
+
changes.forEach(change => {
|
|
313
|
+
watch.onChange!(change);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Update stored state
|
|
319
|
+
watch.lastState = newState;
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error && typeof error === 'object' && 'status' in error && error.status !== 304) {
|
|
323
|
+
console.error(`[DecapNotes Polling] Error checking ${this.currentIssueKey}:`, error);
|
|
324
|
+
}
|
|
325
|
+
} finally {
|
|
326
|
+
this.isPolling = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Immediately check current issue
|
|
332
|
+
*/
|
|
333
|
+
private async checkAllIssuesNow() {
|
|
334
|
+
await this.checkCurrentIssue();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Manually trigger a check - only works if this is the current entry
|
|
339
|
+
*/
|
|
340
|
+
async checkIssueNow(collection: string, slug: string) {
|
|
341
|
+
const issueKey = this.getIssueKey(collection, slug);
|
|
342
|
+
|
|
343
|
+
if (this.currentIssueKey !== issueKey) {
|
|
344
|
+
console.warn(
|
|
345
|
+
`[DecapNotes Polling] Cannot check ${issueKey} - currently watching ${this.currentIssueKey}`,
|
|
346
|
+
);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await this.checkCurrentIssue();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Detect what changed between two states
|
|
355
|
+
*/
|
|
356
|
+
private detectChanges(previous: IssueState | null, current: IssueState): IssueChange[] {
|
|
357
|
+
if (!previous) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const changes: IssueChange[] = [];
|
|
362
|
+
|
|
363
|
+
// New comments
|
|
364
|
+
const newComments = current.comments.filter(
|
|
365
|
+
comment => !previous.comments.some(prev => prev.id === comment.id),
|
|
366
|
+
);
|
|
367
|
+
newComments.forEach(comment => {
|
|
368
|
+
changes.push({
|
|
369
|
+
type: 'comment_added',
|
|
370
|
+
data: comment,
|
|
371
|
+
timestamp: comment.created_at,
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Updated comments
|
|
376
|
+
current.comments.forEach(comment => {
|
|
377
|
+
const prevComment = previous.comments.find(prev => prev.id === comment.id);
|
|
378
|
+
if (prevComment && prevComment.updated_at !== comment.updated_at) {
|
|
379
|
+
changes.push({
|
|
380
|
+
type: 'comment_updated',
|
|
381
|
+
data: comment,
|
|
382
|
+
previousData: prevComment,
|
|
383
|
+
timestamp: comment.updated_at,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Deleted comments
|
|
389
|
+
const deletedComments = previous.comments.filter(
|
|
390
|
+
prevComment => !current.comments.some(comment => comment.id === prevComment.id),
|
|
391
|
+
);
|
|
392
|
+
deletedComments.forEach(comment => {
|
|
393
|
+
changes.push({
|
|
394
|
+
type: 'comment_deleted',
|
|
395
|
+
data: comment,
|
|
396
|
+
timestamp: new Date().toISOString(),
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Issue state changed
|
|
401
|
+
if (previous.state !== current.state) {
|
|
402
|
+
changes.push({
|
|
403
|
+
type: 'issue_state_changed',
|
|
404
|
+
data: { from: previous.state, to: current.state },
|
|
405
|
+
timestamp: current.updated_at,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Labels changed
|
|
410
|
+
if (this.hasLabelsChanged(previous.labels, current.labels)) {
|
|
411
|
+
changes.push({
|
|
412
|
+
type: 'issue_labels_changed',
|
|
413
|
+
data: { from: previous.labels, to: current.labels },
|
|
414
|
+
timestamp: current.updated_at,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return changes;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Check if labels changed
|
|
423
|
+
*/
|
|
424
|
+
private hasLabelsChanged(
|
|
425
|
+
previous: Array<{ name: string }>,
|
|
426
|
+
current: Array<{ name: string }>,
|
|
427
|
+
): boolean {
|
|
428
|
+
if (previous.length !== current.length) return true;
|
|
429
|
+
const prevNames = previous.map(l => l.name).sort();
|
|
430
|
+
const currNames = current.map(l => l.name).sort();
|
|
431
|
+
return prevNames.join(',') !== currNames.join(',');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get issue key for storage
|
|
436
|
+
*/
|
|
437
|
+
private getIssueKey(collection: string, slug: string): string {
|
|
438
|
+
return `${collection}/${slug}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get polling status
|
|
443
|
+
*/
|
|
444
|
+
getStatus() {
|
|
445
|
+
return {
|
|
446
|
+
isPolling: this.intervalId !== null,
|
|
447
|
+
currentWatch: this.currentIssueKey,
|
|
448
|
+
watchedCount: this.currentWatch ? 1 : 0,
|
|
449
|
+
pollingInterval: this.pollingInterval,
|
|
450
|
+
isDocumentVisible: this.isDocumentVisible,
|
|
451
|
+
hasPendingRetry: this.pendingRetryTimeout !== null,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Clean up - stop all polling
|
|
457
|
+
*/
|
|
458
|
+
destroy() {
|
|
459
|
+
console.log('[DecapNotes Polling] Destroying polling manager');
|
|
460
|
+
|
|
461
|
+
// Clear pending retry
|
|
462
|
+
if (this.pendingRetryTimeout) {
|
|
463
|
+
clearTimeout(this.pendingRetryTimeout);
|
|
464
|
+
this.pendingRetryTimeout = null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Stop current watch
|
|
468
|
+
this.stopCurrentWatch();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export default ETagPollingManager;
|
package/LICENSE
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2016 Netlify <decap@p-m.si>
|
|
2
|
-
|
|
3
|
-
MIT License
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
-
a copy of this software and associated documentation files (the
|
|
7
|
-
"Software"), to deal in the Software without restriction, including
|
|
8
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
-
the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
|
14
|
-
included in all copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rate limit info type and callback
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Extracts rate limit information from response headers
|
|
7
|
-
*/
|
|
8
|
-
export function extractRateLimitInfo(headers) {
|
|
9
|
-
const used = headers.get('x-ratelimit-used');
|
|
10
|
-
const limit = headers.get('x-ratelimit-limit');
|
|
11
|
-
const remaining = headers.get('x-ratelimit-remaining');
|
|
12
|
-
const reset = headers.get('x-ratelimit-reset');
|
|
13
|
-
const resource = headers.get('x-ratelimit-resource');
|
|
14
|
-
if (!used || !limit || !remaining || !reset || !resource) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
return {
|
|
18
|
-
used: parseInt(used, 10),
|
|
19
|
-
limit: parseInt(limit, 10),
|
|
20
|
-
remaining: parseInt(remaining, 10),
|
|
21
|
-
reset: parseInt(reset, 10),
|
|
22
|
-
resource
|
|
23
|
-
};
|
|
24
|
-
}
|