decap-cms-backend-github 3.8.1 → 3.8.3
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 +62 -62
- package/dist/decap-cms-backend-github.js.map +1 -1
- package/dist/esm/API.js +30 -57
- package/dist/esm/implementation.js +43 -20
- package/package.json +4 -4
- package/src/API.ts +33 -67
- package/src/__tests__/API.spec.js +73 -0
- package/src/__tests__/implementation.spec.js +50 -6
- package/src/implementation.tsx +48 -19
- package/dist/esm/polling.js +0 -391
- package/src/polling.ts +0 -472
package/src/implementation.tsx
CHANGED
|
@@ -19,11 +19,12 @@ import {
|
|
|
19
19
|
contentKeyFromBranch,
|
|
20
20
|
unsentRequest,
|
|
21
21
|
branchFromContentKey,
|
|
22
|
+
NotesPollingManager,
|
|
23
|
+
markOwnNotes,
|
|
22
24
|
} from 'decap-cms-lib-util';
|
|
23
25
|
|
|
24
26
|
import AuthenticationPage from './AuthenticationPage';
|
|
25
27
|
import API, { API_NAME } from './API';
|
|
26
|
-
import { ETagPollingManager } from './polling';
|
|
27
28
|
import GraphQLAPI from './GraphQLAPI';
|
|
28
29
|
|
|
29
30
|
import type { Endpoints } from '@octokit/types';
|
|
@@ -92,7 +93,7 @@ export default class GitHub implements Implementation {
|
|
|
92
93
|
[key: string]: Promise<boolean>;
|
|
93
94
|
};
|
|
94
95
|
_mediaDisplayURLSem?: Semaphore;
|
|
95
|
-
pollingManager?:
|
|
96
|
+
pollingManager?: NotesPollingManager;
|
|
96
97
|
unwatchFunctions: Map<string, () => void> = new Map();
|
|
97
98
|
|
|
98
99
|
constructor(config: Config, options = {}) {
|
|
@@ -386,7 +387,7 @@ export default class GitHub implements Implementation {
|
|
|
386
387
|
// }
|
|
387
388
|
|
|
388
389
|
if (this.api && !this.pollingManager) {
|
|
389
|
-
this.pollingManager = new
|
|
390
|
+
this.pollingManager = new NotesPollingManager(this.api, 15000);
|
|
390
391
|
}
|
|
391
392
|
// Authorized user
|
|
392
393
|
return { ...user, token: state.token as string, useOpenAuthoring: this.useOpenAuthoring };
|
|
@@ -751,24 +752,55 @@ export default class GitHub implements Implementation {
|
|
|
751
752
|
|
|
752
753
|
// Notes implementation, which is an abstraction to Github's PR issue comments.
|
|
753
754
|
|
|
755
|
+
/**
|
|
756
|
+
* Who the signed-in editor is, as a note records them: a display name for the
|
|
757
|
+
* pane and a stable id for the ownership check behind Edit/Resolve/Delete.
|
|
758
|
+
*/
|
|
759
|
+
async noteAuthorIdentity(): Promise<{ author: string; authorId?: string }> {
|
|
760
|
+
const currentUser = await this.currentUser({ token: this.token! });
|
|
761
|
+
return {
|
|
762
|
+
author: currentUser.login || currentUser.name || '',
|
|
763
|
+
// No id on purpose: GitHub reports the author's current login on every
|
|
764
|
+
// read, so ownership follows a rename. Recording it here would freeze it.
|
|
765
|
+
authorId: undefined,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Resolves each note's `isOwn` here rather than in the pane, because only the
|
|
771
|
+
* backend knows how its identities compare. Falls back to the display name
|
|
772
|
+
* for notes with no recorded id.
|
|
773
|
+
*/
|
|
774
|
+
private async markOwnNotes(notes: Note[]): Promise<Note[]> {
|
|
775
|
+
return markOwnNotes(notes, await this.noteAuthorIdentity());
|
|
776
|
+
}
|
|
777
|
+
|
|
754
778
|
// Notes implementation using GitHub Issues
|
|
755
779
|
async getNotes(collection: string, slug: string): Promise<Note[]> {
|
|
756
780
|
try {
|
|
757
781
|
const notes = await this.api!.getEntryNotes(collection, slug);
|
|
758
|
-
return notes.map(note => ({ ...note, entrySlug: slug }));
|
|
782
|
+
return this.markOwnNotes(notes.map(note => ({ ...note, entrySlug: slug })));
|
|
759
783
|
} catch (error) {
|
|
760
784
|
console.error('Failed to get notes:', error);
|
|
761
785
|
return [];
|
|
762
786
|
}
|
|
763
787
|
}
|
|
764
788
|
|
|
765
|
-
async addNote(
|
|
789
|
+
async addNote(
|
|
790
|
+
collection: string,
|
|
791
|
+
slug: string,
|
|
792
|
+
noteData: Omit<Note, 'id'>,
|
|
793
|
+
entryTitle?: string,
|
|
794
|
+
): Promise<Note> {
|
|
766
795
|
const currentUser = await this.currentUser({ token: this.token! });
|
|
796
|
+
const identity = await this.noteAuthorIdentity();
|
|
767
797
|
|
|
768
798
|
const note: Note = {
|
|
769
799
|
...noteData,
|
|
770
800
|
id: 'temp-' + Date.now(),
|
|
771
|
-
author:
|
|
801
|
+
author: identity.author,
|
|
802
|
+
authorId: identity.authorId,
|
|
803
|
+
isOwn: true,
|
|
772
804
|
avatarUrl: currentUser.avatar_url,
|
|
773
805
|
entrySlug: slug,
|
|
774
806
|
timestamp: noteData.timestamp || new Date().toISOString(),
|
|
@@ -776,16 +808,6 @@ export default class GitHub implements Implementation {
|
|
|
776
808
|
issueUrl: undefined,
|
|
777
809
|
};
|
|
778
810
|
|
|
779
|
-
// Get entry title for better issue naming
|
|
780
|
-
let entryTitle: string | undefined;
|
|
781
|
-
try {
|
|
782
|
-
const entryData = await this.getEntry(`${collection}/${slug}.md`);
|
|
783
|
-
const titleMatch = entryData.data.match(/^title:\s*["']?([^"'\n]+)["']?/m);
|
|
784
|
-
entryTitle = titleMatch ? titleMatch[1] : undefined;
|
|
785
|
-
} catch (error) {
|
|
786
|
-
// Entry not found or error reading, use undefined title
|
|
787
|
-
}
|
|
788
|
-
|
|
789
811
|
const { commentId, issueUrl } = await this.api!.addNoteToEntry(
|
|
790
812
|
collection,
|
|
791
813
|
slug,
|
|
@@ -883,7 +905,14 @@ export default class GitHub implements Implementation {
|
|
|
883
905
|
const unwatchFn = await this.pollingManager.watchIssueWithRetry(
|
|
884
906
|
collection,
|
|
885
907
|
slug,
|
|
886
|
-
|
|
908
|
+
{
|
|
909
|
+
...callbacks,
|
|
910
|
+
// The polling manager rebuilds notes straight from the issue's
|
|
911
|
+
// comments, so they arrive without the ownership flag getNotes adds.
|
|
912
|
+
// Without this, a poll silently strips Edit/Resolve/Delete off the
|
|
913
|
+
// editor's own notes ~15s after they appear.
|
|
914
|
+
prepareNotes: notes => this.markOwnNotes(notes),
|
|
915
|
+
},
|
|
887
916
|
5, // maxRetries - will try up to 5 times
|
|
888
917
|
2000, // retryDelay - 2 seconds between attempts
|
|
889
918
|
);
|
|
@@ -908,9 +937,9 @@ export default class GitHub implements Implementation {
|
|
|
908
937
|
if (unwatchFn) {
|
|
909
938
|
unwatchFn();
|
|
910
939
|
this.unwatchFunctions.delete(issueKey);
|
|
911
|
-
} else {
|
|
912
|
-
console.log(`[DecapNotes Polling] No active polling found for ${issueKey}`);
|
|
913
940
|
}
|
|
941
|
+
|
|
942
|
+
this.pollingManager?.stopWatching(collection, slug);
|
|
914
943
|
}
|
|
915
944
|
|
|
916
945
|
/**
|
package/dist/esm/polling.js
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
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;
|