decap-cms-backend-github 3.4.0 → 3.6.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 +10 -0
- package/dist/decap-cms-backend-github.js +71 -71
- package/dist/decap-cms-backend-github.js.map +1 -1
- package/dist/esm/API.js +23 -11
- package/dist/esm/implementation.js +13 -6
- package/dist/esm/polling.js +391 -0
- package/dist/esm/rateLimitLink.js +24 -0
- package/package.json +2 -2
- package/src/API.ts +133 -109
- package/src/GraphQLAPI.ts +8 -5
- package/src/__tests__/API.spec.js +15 -3
- package/src/implementation.tsx +16 -9
package/dist/esm/API.js
CHANGED
|
@@ -91,14 +91,18 @@ export default class API {
|
|
|
91
91
|
token: this.token
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
-
return this._userPromise
|
|
94
|
+
return this._userPromise.then(user => ({
|
|
95
|
+
name: user.name || 'Unknown',
|
|
96
|
+
login: user.login,
|
|
97
|
+
email: user.email ?? undefined
|
|
98
|
+
}));
|
|
95
99
|
}
|
|
96
100
|
async hasWriteAccess() {
|
|
97
101
|
try {
|
|
98
102
|
const result = await this.request(this.repoURL);
|
|
99
103
|
// update config repoOwner to avoid case sensitivity issues with GitHub
|
|
100
104
|
this.repoOwner = result.owner.login;
|
|
101
|
-
return result.permissions
|
|
105
|
+
return result.permissions?.push ?? false;
|
|
102
106
|
} catch (error) {
|
|
103
107
|
console.error('Problem fetching repo data from GitHub');
|
|
104
108
|
throw error;
|
|
@@ -402,7 +406,7 @@ export default class API {
|
|
|
402
406
|
const [{
|
|
403
407
|
files
|
|
404
408
|
}, pullRequestAuthor] = await Promise.all([this.getDifferences(this.branch, pullRequest.head.sha), this.getPullRequestAuthor(pullRequest)]);
|
|
405
|
-
const diffs = await Promise.all(files.map(file => this.diffFromFile(file)));
|
|
409
|
+
const diffs = await Promise.all((files || []).map(file => this.diffFromFile(file)));
|
|
406
410
|
const label = pullRequest.labels.find(l => isCMSLabel(l.name, this.cmsLabelPrefix));
|
|
407
411
|
const status = labelToStatus(label.name, this.cmsLabelPrefix);
|
|
408
412
|
const updatedAt = pullRequest.updated_at;
|
|
@@ -450,8 +454,8 @@ export default class API {
|
|
|
450
454
|
commit
|
|
451
455
|
} = result[0];
|
|
452
456
|
return {
|
|
453
|
-
author: commit.author
|
|
454
|
-
updatedOn: commit.author
|
|
457
|
+
author: commit.author?.name || commit.author?.email || '',
|
|
458
|
+
updatedOn: commit.author?.date || ''
|
|
455
459
|
};
|
|
456
460
|
} catch (e) {
|
|
457
461
|
return {
|
|
@@ -502,12 +506,12 @@ export default class API {
|
|
|
502
506
|
});
|
|
503
507
|
return result.tree
|
|
504
508
|
// filter only files and up to the required depth
|
|
505
|
-
.filter(file => file.type === 'blob' && file.path.split('/').length <= depth).map(file => ({
|
|
509
|
+
.filter(file => file.type === 'blob' && file.path && file.path.split('/').length <= depth).map(file => ({
|
|
506
510
|
type: file.type,
|
|
507
511
|
id: file.sha,
|
|
508
512
|
name: basename(file.path),
|
|
509
513
|
path: `${folder}/${file.path}`,
|
|
510
|
-
size: file.size
|
|
514
|
+
size: file.size || 0
|
|
511
515
|
}));
|
|
512
516
|
} catch (err) {
|
|
513
517
|
if (err && err.status === 404) {
|
|
@@ -668,7 +672,7 @@ export default class API {
|
|
|
668
672
|
const resp = await this.request(`${this.originRepoURL}/commits/${sha}/status`);
|
|
669
673
|
return resp.statuses.map(s => ({
|
|
670
674
|
context: s.context,
|
|
671
|
-
target_url: s.target_url,
|
|
675
|
+
target_url: s.target_url || '',
|
|
672
676
|
state: s.state === GitHubCommitStatusState.Success ? PreviewState.Success : PreviewState.Other
|
|
673
677
|
}));
|
|
674
678
|
}
|
|
@@ -744,7 +748,7 @@ export default class API {
|
|
|
744
748
|
return {
|
|
745
749
|
path: diff.filename,
|
|
746
750
|
newFile: diff.status === 'added',
|
|
747
|
-
sha: diff.sha,
|
|
751
|
+
sha: diff.sha || '',
|
|
748
752
|
// media files diffs don't have a patch attribute, except svg files
|
|
749
753
|
// renamed files don't have a patch attribute too
|
|
750
754
|
binary: diff.status !== 'renamed' && !diff.patch || diff.filename.endsWith('.svg')
|
|
@@ -769,7 +773,7 @@ export default class API {
|
|
|
769
773
|
const {
|
|
770
774
|
files: diffFiles
|
|
771
775
|
} = await this.getDifferences(this.branch, await this.getHeadReference(branch));
|
|
772
|
-
const diffs = await Promise.all(diffFiles.map(file => this.diffFromFile(file)));
|
|
776
|
+
const diffs = await Promise.all((diffFiles || []).map(file => this.diffFromFile(file)));
|
|
773
777
|
// mark media files to remove
|
|
774
778
|
const mediaFilesToRemove = [];
|
|
775
779
|
for (const diff of diffs.filter(d => d.binary)) {
|
|
@@ -824,7 +828,15 @@ export default class API {
|
|
|
824
828
|
} = commit.commit;
|
|
825
829
|
|
|
826
830
|
// create a new commit from the updated tree
|
|
827
|
-
const newCommit = await this.createCommit(message, tree.sha, [baseCommit.sha], author
|
|
831
|
+
const newCommit = await this.createCommit(message, tree.sha, [baseCommit.sha], author ? {
|
|
832
|
+
name: author.name || '',
|
|
833
|
+
email: author.email || '',
|
|
834
|
+
date: author.date
|
|
835
|
+
} : undefined, committer ? {
|
|
836
|
+
name: committer.name || '',
|
|
837
|
+
email: committer.email || '',
|
|
838
|
+
date: committer.date
|
|
839
|
+
} : undefined);
|
|
828
840
|
return newCommit;
|
|
829
841
|
} else {
|
|
830
842
|
return commit;
|
|
@@ -124,11 +124,18 @@ export default class GitHub {
|
|
|
124
124
|
token
|
|
125
125
|
}) {
|
|
126
126
|
if (!this._currentUserPromise) {
|
|
127
|
-
this._currentUserPromise =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
this._currentUserPromise = (async () => {
|
|
128
|
+
const res = await fetch(`${this.apiRoot}/user`, {
|
|
129
|
+
headers: {
|
|
130
|
+
Authorization: `${this.tokenKeyword} ${token}`
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const user = await res.json();
|
|
134
|
+
return {
|
|
135
|
+
...user,
|
|
136
|
+
name: user.name || 'Unknown'
|
|
137
|
+
};
|
|
138
|
+
})();
|
|
132
139
|
}
|
|
133
140
|
return this._currentUserPromise;
|
|
134
141
|
}
|
|
@@ -255,7 +262,7 @@ export default class GitHub {
|
|
|
255
262
|
useOpenAuthoring: this.useOpenAuthoring,
|
|
256
263
|
initialWorkflowStatus: this.options.initialWorkflowStatus,
|
|
257
264
|
baseUrl: this.baseUrl,
|
|
258
|
-
getUser: this.currentUser
|
|
265
|
+
getUser: args => this.currentUser(args)
|
|
259
266
|
});
|
|
260
267
|
const user = await this.api.user();
|
|
261
268
|
const isCollab = await this.api.hasWriteAccess().catch(error => {
|
|
@@ -0,0 +1,391 @@
|
|
|
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
|
+
// Redux action types
|
|
11
|
+
export const NOTES_POLLING_START = 'NOTES_POLLING_START';
|
|
12
|
+
export const NOTES_POLLING_STOP = 'NOTES_POLLING_STOP';
|
|
13
|
+
export const NOTES_POLLING_UPDATE = 'NOTES_POLLING_UPDATE';
|
|
14
|
+
export const NOTES_CHANGE_DETECTED = 'NOTES_CHANGE_DETECTED';
|
|
15
|
+
export class ETagPollingManager {
|
|
16
|
+
currentWatch = null;
|
|
17
|
+
currentIssueKey = null;
|
|
18
|
+
pollingInterval = 15000;
|
|
19
|
+
intervalId = null;
|
|
20
|
+
isDocumentVisible = true;
|
|
21
|
+
isPolling = false;
|
|
22
|
+
pendingRetryTimeout = null;
|
|
23
|
+
constructor(api, pollingInterval = 15000) {
|
|
24
|
+
this.api = api;
|
|
25
|
+
this.pollingInterval = pollingInterval;
|
|
26
|
+
this.setupVisibilityListener();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Setup Page Visibility API listener
|
|
31
|
+
* Pauses polling when tab is hidden
|
|
32
|
+
*/
|
|
33
|
+
setupVisibilityListener() {
|
|
34
|
+
if (typeof document !== 'undefined') {
|
|
35
|
+
document.addEventListener('visibilitychange', () => {
|
|
36
|
+
this.isDocumentVisible = !document.hidden;
|
|
37
|
+
if (this.isDocumentVisible) {
|
|
38
|
+
this.startPolling();
|
|
39
|
+
this.checkAllIssuesNow();
|
|
40
|
+
} else {
|
|
41
|
+
this.stopPolling();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Start watching an issue for changes
|
|
49
|
+
* This will automatically stop watching any previously watched issue
|
|
50
|
+
*
|
|
51
|
+
* @param issueNumber - GitHub issue number
|
|
52
|
+
* @param collection - Collection name
|
|
53
|
+
* @param slug - Entry slug
|
|
54
|
+
* @returns Function to stop watching
|
|
55
|
+
*/
|
|
56
|
+
async watchIssue(issueNumber, collection, slug, callbacks, initialState = null) {
|
|
57
|
+
const issueKey = this.getIssueKey(collection, slug);
|
|
58
|
+
|
|
59
|
+
// STOP ANY EXISTING WATCH FIRST
|
|
60
|
+
if (this.currentWatch) {
|
|
61
|
+
this.stopCurrentWatch();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get initial state if not provided
|
|
65
|
+
if (!initialState) {
|
|
66
|
+
try {
|
|
67
|
+
initialState = await this.api.getIssueState(issueNumber);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('[DecapNotes Polling] Failed to get initial state:', error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.currentWatch = {
|
|
73
|
+
issueNumber,
|
|
74
|
+
collection,
|
|
75
|
+
slug,
|
|
76
|
+
etag: null,
|
|
77
|
+
lastState: initialState,
|
|
78
|
+
onUpdate: callbacks.onUpdate,
|
|
79
|
+
onChange: callbacks.onChange,
|
|
80
|
+
retryCount: 0,
|
|
81
|
+
maxRetries: 5
|
|
82
|
+
};
|
|
83
|
+
this.currentIssueKey = issueKey;
|
|
84
|
+
|
|
85
|
+
// Start polling if not already running
|
|
86
|
+
if (!this.intervalId && this.isDocumentVisible) {
|
|
87
|
+
this.startPolling();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Do an immediate check
|
|
91
|
+
this.checkCurrentIssue();
|
|
92
|
+
|
|
93
|
+
// Return unwatch function
|
|
94
|
+
return () => this.stopCurrentWatch();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Watch issue with retry logic for newly created issues
|
|
99
|
+
*/
|
|
100
|
+
async watchIssueWithRetry(collection, slug, callbacks, maxRetries = 5, retryDelay = 2000) {
|
|
101
|
+
const issueKey = this.getIssueKey(collection, slug);
|
|
102
|
+
|
|
103
|
+
// STOP ANY EXISTING WATCH FIRST
|
|
104
|
+
if (this.currentWatch) {
|
|
105
|
+
this.stopCurrentWatch();
|
|
106
|
+
}
|
|
107
|
+
const attemptWatch = async attempt => {
|
|
108
|
+
try {
|
|
109
|
+
const issue = await this.api.findEntryIssue(collection, slug);
|
|
110
|
+
if (issue) {
|
|
111
|
+
return await this.watchIssue(issue.number, collection, slug, callbacks);
|
|
112
|
+
}
|
|
113
|
+
if (attempt < maxRetries) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
this.pendingRetryTimeout = setTimeout(async () => {
|
|
116
|
+
this.pendingRetryTimeout = null;
|
|
117
|
+
try {
|
|
118
|
+
const unwatchFn = await attemptWatch(attempt + 1);
|
|
119
|
+
resolve(unwatchFn);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
reject(error);
|
|
122
|
+
}
|
|
123
|
+
}, retryDelay);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
console.log(`[DecapNotes Polling] No issue found for ${issueKey} after ${maxRetries} attempts. This is expected if there are no notes for this entry yet.`);
|
|
127
|
+
// Return a no-op unwatch function
|
|
128
|
+
return () => {
|
|
129
|
+
/* no-op */
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(`[DecapNotes Polling] Error finding issue for ${issueKey}:`, error);
|
|
133
|
+
if (attempt < maxRetries) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
this.pendingRetryTimeout = setTimeout(async () => {
|
|
136
|
+
this.pendingRetryTimeout = null;
|
|
137
|
+
try {
|
|
138
|
+
const unwatchFn = await attemptWatch(attempt + 1);
|
|
139
|
+
resolve(unwatchFn);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
reject(err);
|
|
142
|
+
}
|
|
143
|
+
}, retryDelay);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return attemptWatch(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Stop watching the current issue - complete cleanup
|
|
154
|
+
*/
|
|
155
|
+
stopCurrentWatch() {
|
|
156
|
+
if (!this.currentWatch) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Clear any pending retry timeout
|
|
161
|
+
if (this.pendingRetryTimeout) {
|
|
162
|
+
clearTimeout(this.pendingRetryTimeout);
|
|
163
|
+
this.pendingRetryTimeout = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Clear current watch
|
|
167
|
+
this.currentWatch = null;
|
|
168
|
+
this.currentIssueKey = null;
|
|
169
|
+
|
|
170
|
+
// Stop polling since there's nothing to watch
|
|
171
|
+
this.stopPolling();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Start the polling loop
|
|
176
|
+
*/
|
|
177
|
+
startPolling() {
|
|
178
|
+
if (this.intervalId || !this.isDocumentVisible || !this.currentWatch) return;
|
|
179
|
+
console.log(`[DecapNotes Polling] Starting polling loop (${this.pollingInterval}ms interval) for ${this.currentIssueKey}`);
|
|
180
|
+
this.intervalId = setInterval(() => {
|
|
181
|
+
this.pollAllIssues();
|
|
182
|
+
}, this.pollingInterval);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Stop the polling loop
|
|
187
|
+
*/
|
|
188
|
+
stopPolling() {
|
|
189
|
+
if (this.intervalId) {
|
|
190
|
+
console.log('[DecapNotes Polling] Stopping polling loop');
|
|
191
|
+
clearInterval(this.intervalId);
|
|
192
|
+
this.intervalId = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Poll current watched issue
|
|
198
|
+
*/
|
|
199
|
+
async pollAllIssues() {
|
|
200
|
+
await this.checkCurrentIssue();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check current issue for changes using ETag
|
|
205
|
+
*/
|
|
206
|
+
async checkCurrentIssue() {
|
|
207
|
+
if (!this.currentWatch) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (this.isPolling) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.isPolling = true;
|
|
214
|
+
try {
|
|
215
|
+
const watch = this.currentWatch;
|
|
216
|
+
const response = await this.api.getIssueWithETag(watch.issueNumber, watch.etag);
|
|
217
|
+
if (response.status === 304) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (response.status === 200) {
|
|
221
|
+
const newState = response.data;
|
|
222
|
+
const newETag = response.etag;
|
|
223
|
+
|
|
224
|
+
// Update ETag
|
|
225
|
+
watch.etag = newETag || null;
|
|
226
|
+
|
|
227
|
+
// Detect specific changes
|
|
228
|
+
const changes = this.detectChanges(watch.lastState, newState);
|
|
229
|
+
if (changes.length > 0) {
|
|
230
|
+
// Convert comments to notes
|
|
231
|
+
const newNotes = newState.comments.map(comment => ({
|
|
232
|
+
...this.api.parseCommentToNote(comment),
|
|
233
|
+
issueUrl: newState.html_url
|
|
234
|
+
}));
|
|
235
|
+
if (watch.onUpdate) {
|
|
236
|
+
watch.onUpdate(newNotes, changes);
|
|
237
|
+
}
|
|
238
|
+
if (watch.onChange) {
|
|
239
|
+
changes.forEach(change => {
|
|
240
|
+
watch.onChange(change);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Update stored state
|
|
246
|
+
watch.lastState = newState;
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error && typeof error === 'object' && 'status' in error && error.status !== 304) {
|
|
250
|
+
console.error(`[DecapNotes Polling] Error checking ${this.currentIssueKey}:`, error);
|
|
251
|
+
}
|
|
252
|
+
} finally {
|
|
253
|
+
this.isPolling = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Immediately check current issue
|
|
259
|
+
*/
|
|
260
|
+
async checkAllIssuesNow() {
|
|
261
|
+
await this.checkCurrentIssue();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Manually trigger a check - only works if this is the current entry
|
|
266
|
+
*/
|
|
267
|
+
async checkIssueNow(collection, slug) {
|
|
268
|
+
const issueKey = this.getIssueKey(collection, slug);
|
|
269
|
+
if (this.currentIssueKey !== issueKey) {
|
|
270
|
+
console.warn(`[DecapNotes Polling] Cannot check ${issueKey} - currently watching ${this.currentIssueKey}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await this.checkCurrentIssue();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Detect what changed between two states
|
|
278
|
+
*/
|
|
279
|
+
detectChanges(previous, current) {
|
|
280
|
+
if (!previous) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
const changes = [];
|
|
284
|
+
|
|
285
|
+
// New comments
|
|
286
|
+
const newComments = current.comments.filter(comment => !previous.comments.some(prev => prev.id === comment.id));
|
|
287
|
+
newComments.forEach(comment => {
|
|
288
|
+
changes.push({
|
|
289
|
+
type: 'comment_added',
|
|
290
|
+
data: comment,
|
|
291
|
+
timestamp: comment.created_at
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Updated comments
|
|
296
|
+
current.comments.forEach(comment => {
|
|
297
|
+
const prevComment = previous.comments.find(prev => prev.id === comment.id);
|
|
298
|
+
if (prevComment && prevComment.updated_at !== comment.updated_at) {
|
|
299
|
+
changes.push({
|
|
300
|
+
type: 'comment_updated',
|
|
301
|
+
data: comment,
|
|
302
|
+
previousData: prevComment,
|
|
303
|
+
timestamp: comment.updated_at
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Deleted comments
|
|
309
|
+
const deletedComments = previous.comments.filter(prevComment => !current.comments.some(comment => comment.id === prevComment.id));
|
|
310
|
+
deletedComments.forEach(comment => {
|
|
311
|
+
changes.push({
|
|
312
|
+
type: 'comment_deleted',
|
|
313
|
+
data: comment,
|
|
314
|
+
timestamp: new Date().toISOString()
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Issue state changed
|
|
319
|
+
if (previous.state !== current.state) {
|
|
320
|
+
changes.push({
|
|
321
|
+
type: 'issue_state_changed',
|
|
322
|
+
data: {
|
|
323
|
+
from: previous.state,
|
|
324
|
+
to: current.state
|
|
325
|
+
},
|
|
326
|
+
timestamp: current.updated_at
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Labels changed
|
|
331
|
+
if (this.hasLabelsChanged(previous.labels, current.labels)) {
|
|
332
|
+
changes.push({
|
|
333
|
+
type: 'issue_labels_changed',
|
|
334
|
+
data: {
|
|
335
|
+
from: previous.labels,
|
|
336
|
+
to: current.labels
|
|
337
|
+
},
|
|
338
|
+
timestamp: current.updated_at
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return changes;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Check if labels changed
|
|
346
|
+
*/
|
|
347
|
+
hasLabelsChanged(previous, current) {
|
|
348
|
+
if (previous.length !== current.length) return true;
|
|
349
|
+
const prevNames = previous.map(l => l.name).sort();
|
|
350
|
+
const currNames = current.map(l => l.name).sort();
|
|
351
|
+
return prevNames.join(',') !== currNames.join(',');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get issue key for storage
|
|
356
|
+
*/
|
|
357
|
+
getIssueKey(collection, slug) {
|
|
358
|
+
return `${collection}/${slug}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Get polling status
|
|
363
|
+
*/
|
|
364
|
+
getStatus() {
|
|
365
|
+
return {
|
|
366
|
+
isPolling: this.intervalId !== null,
|
|
367
|
+
currentWatch: this.currentIssueKey,
|
|
368
|
+
watchedCount: this.currentWatch ? 1 : 0,
|
|
369
|
+
pollingInterval: this.pollingInterval,
|
|
370
|
+
isDocumentVisible: this.isDocumentVisible,
|
|
371
|
+
hasPendingRetry: this.pendingRetryTimeout !== null
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Clean up - stop all polling
|
|
377
|
+
*/
|
|
378
|
+
destroy() {
|
|
379
|
+
console.log('[DecapNotes Polling] Destroying polling manager');
|
|
380
|
+
|
|
381
|
+
// Clear pending retry
|
|
382
|
+
if (this.pendingRetryTimeout) {
|
|
383
|
+
clearTimeout(this.pendingRetryTimeout);
|
|
384
|
+
this.pendingRetryTimeout = null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Stop current watch
|
|
388
|
+
this.stopCurrentWatch();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
export default ETagPollingManager;
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decap-cms-backend-github",
|
|
3
3
|
"description": "GitHub backend for Decap CMS",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.6.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-github",
|
|
7
7
|
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"browser": {
|
|
45
45
|
"path": "path-browserify"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "45c9f5b9a1a12f74321ce4658b71ec88d6365ec1"
|
|
48
48
|
}
|