codex-overleaf-link 1.9.5 → 1.9.7
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/README.md +23 -23
- package/extension/bootstrap/background.js +4 -0
- package/extension/bootstrap/manifest.template.json +1 -1
- package/extension/bootstrap/popup.css +169 -19
- package/extension/bootstrap/popup.html +5 -5
- package/extension/bootstrap/update.css +366 -0
- package/extension/bootstrap/update.html +55 -0
- package/extension/bootstrap/update.js +334 -0
- package/extension/src/backgroundUpdateCoordinator.js +68 -7
- package/extension/src/popup.js +135 -7
- package/extension/src/shared/compatibility.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_TYPES = Object.freeze({
|
|
5
|
+
get: 'codex-overleaf/consent-update-get-state',
|
|
6
|
+
check: 'codex-overleaf/consent-update-check',
|
|
7
|
+
install: 'codex-overleaf/consent-update-install',
|
|
8
|
+
later: 'codex-overleaf/consent-update-later',
|
|
9
|
+
retry: 'codex-overleaf/consent-update-check'
|
|
10
|
+
});
|
|
11
|
+
const ACTIVE_PHASES = new Set([
|
|
12
|
+
'checking',
|
|
13
|
+
'downloading',
|
|
14
|
+
'staged',
|
|
15
|
+
'waiting',
|
|
16
|
+
'waiting_for_idle',
|
|
17
|
+
'waiting_for_safe_point',
|
|
18
|
+
'applying',
|
|
19
|
+
'awaiting_health',
|
|
20
|
+
'awaiting_health_check',
|
|
21
|
+
'health_checking'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const elements = {
|
|
25
|
+
badge: document.getElementById('status-badge'),
|
|
26
|
+
title: document.getElementById('update-title'),
|
|
27
|
+
route: document.getElementById('version-route'),
|
|
28
|
+
detail: document.getElementById('update-detail'),
|
|
29
|
+
progressTrack: document.getElementById('progress-track'),
|
|
30
|
+
progressFill: document.getElementById('progress-fill'),
|
|
31
|
+
stages: Array.from(document.querySelectorAll('#update-stages > li')),
|
|
32
|
+
note: document.getElementById('persistence-note'),
|
|
33
|
+
details: document.getElementById('technical-details'),
|
|
34
|
+
technicalMessage: document.getElementById('technical-message'),
|
|
35
|
+
actions: document.getElementById('update-actions')
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let currentView = {};
|
|
39
|
+
let pollTimer = null;
|
|
40
|
+
let refreshTimer = null;
|
|
41
|
+
let actionPending = false;
|
|
42
|
+
|
|
43
|
+
function asObject(value) {
|
|
44
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function unwrapResponse(response) {
|
|
48
|
+
const root = asObject(response) || {};
|
|
49
|
+
if (root.ok === false) {
|
|
50
|
+
const error = new Error(root.error?.message || root.message || 'Update action failed.');
|
|
51
|
+
error.code = root.error?.code || root.code || 'update_failed';
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const candidates = [
|
|
55
|
+
root.view,
|
|
56
|
+
root.result?.view,
|
|
57
|
+
root.result,
|
|
58
|
+
root.updateConsent,
|
|
59
|
+
root.updateState,
|
|
60
|
+
root.state
|
|
61
|
+
];
|
|
62
|
+
return candidates.find(asObject) || root;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function request(action) {
|
|
66
|
+
const type = MESSAGE_TYPES[action];
|
|
67
|
+
if (!type) throw new Error(`Unsupported update action: ${action}`);
|
|
68
|
+
return unwrapResponse(await chrome.runtime.sendMessage({ type }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function phaseOf(view) {
|
|
72
|
+
const value = view.phase || view.status || view.state || view.lifecycle || 'idle';
|
|
73
|
+
return typeof value === 'string' ? value.toLowerCase().replace(/[- ]+/g, '_') : 'idle';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function versionOf(view, candidates, fallback = '') {
|
|
77
|
+
for (const key of candidates) {
|
|
78
|
+
const value = view[key];
|
|
79
|
+
if (typeof value === 'string' && value.trim()) return value.trim().replace(/^v/i, '');
|
|
80
|
+
}
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function presentationFor(view) {
|
|
85
|
+
const phase = phaseOf(view);
|
|
86
|
+
const presentations = {
|
|
87
|
+
idle: ['Stable updates', 'Automatically checks signed stable releases.', 'Ready', 'neutral'],
|
|
88
|
+
up_to_date: ['Everything is up to date', 'Extension and Native Host are on the latest stable release.', 'Up to date', 'success'],
|
|
89
|
+
checking: ['Checking for updates', 'Reading the latest signed stable release.', 'Checking', 'active'],
|
|
90
|
+
update_available: ['A stable update is ready', 'Review the target version, then update when convenient.', 'Available', 'success'],
|
|
91
|
+
downloading: ['Downloading the signed release', 'The package is being fetched and verified before any files change.', 'Downloading', 'active'],
|
|
92
|
+
staged: ['Update verified', 'The signed package is ready and waiting for a safe installation point.', 'Verified', 'active'],
|
|
93
|
+
waiting: ['Waiting for a safe point', 'Active Overleaf work will finish before the runtime changes.', 'Waiting', 'warning'],
|
|
94
|
+
waiting_for_idle: ['Waiting for a safe point', 'Active Overleaf work will finish before the runtime changes.', 'Waiting', 'warning'],
|
|
95
|
+
waiting_for_safe_point: ['Waiting for a safe point', 'Active Overleaf work will finish before the runtime changes.', 'Waiting', 'warning'],
|
|
96
|
+
applying: ['Installing the update', 'Extension and Native Host are being switched as one managed unit.', 'Installing', 'active'],
|
|
97
|
+
awaiting_health: ['Verifying the installation', 'The updated components are restarting and reconnecting.', 'Verifying', 'active'],
|
|
98
|
+
awaiting_health_check: ['Verifying the installation', 'The updated components are restarting and reconnecting.', 'Verifying', 'active'],
|
|
99
|
+
health_checking: ['Verifying the installation', 'The updated components are restarting and reconnecting.', 'Verifying', 'active'],
|
|
100
|
+
committed: ['Update complete', 'Extension and Native Host passed the post-update health check.', 'Complete', 'success'],
|
|
101
|
+
failed: ['Update could not continue', 'No further update steps will run until the failure is retried.', 'Needs attention', 'error'],
|
|
102
|
+
rolled_back: ['Previous version restored', 'The health check failed, so the managed update was rolled back safely.', 'Rolled back', 'warning'],
|
|
103
|
+
deferred: ['Update paused', 'The available update will remain ready for a later installation.', 'Later', 'neutral']
|
|
104
|
+
};
|
|
105
|
+
const [title, detail, badge, tone] = presentations[phase] || presentations.idle;
|
|
106
|
+
return { phase, title, detail, badge, tone };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function progressFor(view, phase) {
|
|
110
|
+
const raw = asObject(view.progress)?.percent ?? view.progressPercent ?? view.progress;
|
|
111
|
+
if (Number.isFinite(Number(raw))) {
|
|
112
|
+
const numeric = Number(raw);
|
|
113
|
+
return Math.max(0, Math.min(100, numeric <= 1 ? numeric * 100 : numeric));
|
|
114
|
+
}
|
|
115
|
+
const fallback = {
|
|
116
|
+
checking: 8,
|
|
117
|
+
update_available: 0,
|
|
118
|
+
downloading: 25,
|
|
119
|
+
staged: 48,
|
|
120
|
+
waiting: 55,
|
|
121
|
+
waiting_for_idle: 55,
|
|
122
|
+
waiting_for_safe_point: 55,
|
|
123
|
+
applying: 75,
|
|
124
|
+
awaiting_health: 90,
|
|
125
|
+
awaiting_health_check: 90,
|
|
126
|
+
health_checking: 90,
|
|
127
|
+
committed: 100,
|
|
128
|
+
rolled_back: 100
|
|
129
|
+
};
|
|
130
|
+
return fallback[phase] ?? 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function activeStageFor(phase) {
|
|
134
|
+
if (['downloading'].includes(phase)) return 0;
|
|
135
|
+
if (['staged', 'waiting', 'waiting_for_idle', 'waiting_for_safe_point'].includes(phase)) return 1;
|
|
136
|
+
if (['applying', 'awaiting_health', 'awaiting_health_check', 'health_checking'].includes(phase)) return 2;
|
|
137
|
+
if (phase === 'committed') return 3;
|
|
138
|
+
return -1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function errorStageFor(view, phase) {
|
|
142
|
+
if (!['failed', 'rolled_back'].includes(phase)) return -1;
|
|
143
|
+
const stage = String(view.failureStage || view.error?.stage || '').toLowerCase();
|
|
144
|
+
if (/download|verify|signature|manifest/.test(stage)) return 0;
|
|
145
|
+
if (/idle|safe|wait/.test(stage)) return 1;
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function technicalText(view) {
|
|
150
|
+
const value = view.technicalDetail || view.error?.technicalDetail || view.error?.message || view.error || view.message;
|
|
151
|
+
if (!value) return '';
|
|
152
|
+
if (typeof value === 'string') return value;
|
|
153
|
+
try {
|
|
154
|
+
return JSON.stringify(value, null, 2);
|
|
155
|
+
} catch {
|
|
156
|
+
return String(value);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function actionDefinitions(phase) {
|
|
161
|
+
if (phase === 'update_available') {
|
|
162
|
+
return [
|
|
163
|
+
{ id: 'later', label: 'Later', kind: 'quiet' },
|
|
164
|
+
{ id: 'install', label: 'Update now', kind: 'primary' }
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
if (['failed', 'rolled_back'].includes(phase)) {
|
|
168
|
+
return [
|
|
169
|
+
{ id: 'close', label: 'Close', kind: 'quiet' },
|
|
170
|
+
{ id: 'retry', label: 'Retry', kind: 'primary' }
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
if (phase === 'committed') {
|
|
174
|
+
return [{ id: 'close', label: 'Done', kind: 'primary' }];
|
|
175
|
+
}
|
|
176
|
+
if (ACTIVE_PHASES.has(phase)) {
|
|
177
|
+
return [{ id: 'close', label: 'Close window', kind: 'quiet' }];
|
|
178
|
+
}
|
|
179
|
+
return [
|
|
180
|
+
{ id: 'close', label: 'Close', kind: 'quiet' },
|
|
181
|
+
{ id: 'check', label: 'Check latest', kind: 'primary' }
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderActions(phase) {
|
|
186
|
+
elements.actions.replaceChildren(...actionDefinitions(phase).map(action => {
|
|
187
|
+
const button = document.createElement('button');
|
|
188
|
+
button.type = 'button';
|
|
189
|
+
button.dataset.action = action.id;
|
|
190
|
+
button.dataset.kind = action.kind;
|
|
191
|
+
button.textContent = action.label;
|
|
192
|
+
button.disabled = actionPending;
|
|
193
|
+
return button;
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function render(view) {
|
|
198
|
+
currentView = asObject(view) || {};
|
|
199
|
+
const presentation = presentationFor(currentView);
|
|
200
|
+
const installed = versionOf(currentView, ['currentVersion', 'installedVersion', 'extensionVersion'], chrome.runtime.getManifest().version);
|
|
201
|
+
const target = versionOf(currentView, ['latestVersion', 'targetVersion', 'availableVersion'], 'latest stable');
|
|
202
|
+
const hasTarget = target && target !== 'latest stable' && target !== installed;
|
|
203
|
+
elements.route.textContent = hasTarget ? `v${installed} → v${target}` : `Installed v${installed}`;
|
|
204
|
+
elements.title.textContent = presentation.title;
|
|
205
|
+
elements.detail.textContent = typeof currentView.detail === 'string' && currentView.detail.trim()
|
|
206
|
+
? currentView.detail.trim()
|
|
207
|
+
: presentation.detail;
|
|
208
|
+
elements.badge.textContent = presentation.badge;
|
|
209
|
+
elements.badge.dataset.tone = presentation.tone;
|
|
210
|
+
|
|
211
|
+
const progress = progressFor(currentView, presentation.phase);
|
|
212
|
+
const showProgress = ACTIVE_PHASES.has(presentation.phase) || ['committed', 'rolled_back'].includes(presentation.phase);
|
|
213
|
+
elements.progressTrack.hidden = !showProgress;
|
|
214
|
+
elements.progressTrack.dataset.indeterminate = presentation.phase === 'checking' ? 'true' : 'false';
|
|
215
|
+
elements.progressTrack.setAttribute('aria-valuenow', String(Math.round(progress)));
|
|
216
|
+
elements.progressFill.style.width = `${progress}%`;
|
|
217
|
+
|
|
218
|
+
const activeStage = activeStageFor(presentation.phase);
|
|
219
|
+
const errorStage = errorStageFor(currentView, presentation.phase);
|
|
220
|
+
elements.stages.forEach((stage, index) => {
|
|
221
|
+
let state = 'pending';
|
|
222
|
+
if (index < activeStage || activeStage === 3) state = 'done';
|
|
223
|
+
if (index === activeStage) state = 'active';
|
|
224
|
+
if (index === errorStage) state = 'error';
|
|
225
|
+
stage.dataset.state = state;
|
|
226
|
+
const marker = stage.querySelector('.stage-marker');
|
|
227
|
+
marker.textContent = state === 'done' ? '✓' : String(index + 1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const restarting = ['applying', 'awaiting_health', 'awaiting_health_check', 'health_checking'].includes(presentation.phase);
|
|
231
|
+
elements.note.textContent = restarting
|
|
232
|
+
? 'The extension may restart briefly. This window will reconnect and restore the saved progress.'
|
|
233
|
+
: 'This window can stay open while work continues elsewhere. Progress is saved automatically.';
|
|
234
|
+
|
|
235
|
+
const technical = technicalText(currentView);
|
|
236
|
+
elements.details.hidden = !technical || !['failed', 'rolled_back'].includes(presentation.phase);
|
|
237
|
+
elements.technicalMessage.textContent = technical;
|
|
238
|
+
renderActions(presentation.phase);
|
|
239
|
+
schedulePolling(presentation.phase);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderReconnecting() {
|
|
243
|
+
elements.title.textContent = 'Reconnecting after restart';
|
|
244
|
+
elements.detail.textContent = 'The saved update state will return when the extension is available again.';
|
|
245
|
+
elements.badge.textContent = 'Reconnecting';
|
|
246
|
+
elements.badge.dataset.tone = 'active';
|
|
247
|
+
elements.progressTrack.hidden = false;
|
|
248
|
+
elements.progressTrack.dataset.indeterminate = 'true';
|
|
249
|
+
elements.actions.replaceChildren();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderLocalFailure(error) {
|
|
253
|
+
render({
|
|
254
|
+
...currentView,
|
|
255
|
+
phase: 'failed',
|
|
256
|
+
detail: error?.message || 'Update action failed.',
|
|
257
|
+
technicalDetail: `${error?.code || 'update_failed'}: ${error?.message || 'Update action failed.'}`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isTransientContextError(error) {
|
|
262
|
+
return /context invalidated|receiving end does not exist|message port closed|could not establish connection/i.test(String(error?.message || error));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function refresh() {
|
|
266
|
+
try {
|
|
267
|
+
render(await request('get'));
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (isTransientContextError(error)) {
|
|
270
|
+
renderReconnecting();
|
|
271
|
+
window.setTimeout(() => window.location.reload(), 900);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
renderLocalFailure(error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function scheduleRefresh() {
|
|
279
|
+
window.clearTimeout(refreshTimer);
|
|
280
|
+
refreshTimer = window.setTimeout(refresh, 80);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function schedulePolling(phase) {
|
|
284
|
+
window.clearTimeout(pollTimer);
|
|
285
|
+
pollTimer = null;
|
|
286
|
+
if (!ACTIVE_PHASES.has(phase)) return;
|
|
287
|
+
pollTimer = window.setTimeout(async () => {
|
|
288
|
+
await refresh();
|
|
289
|
+
}, 1100);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function runAction(action) {
|
|
293
|
+
if (actionPending) return;
|
|
294
|
+
if (action === 'close') {
|
|
295
|
+
window.close();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
actionPending = true;
|
|
299
|
+
renderActions(phaseOf(currentView));
|
|
300
|
+
try {
|
|
301
|
+
render(await request(action));
|
|
302
|
+
if (action === 'later') window.close();
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (isTransientContextError(error)) {
|
|
305
|
+
renderReconnecting();
|
|
306
|
+
window.setTimeout(() => window.location.reload(), 900);
|
|
307
|
+
} else {
|
|
308
|
+
renderLocalFailure(error);
|
|
309
|
+
}
|
|
310
|
+
} finally {
|
|
311
|
+
actionPending = false;
|
|
312
|
+
if (document.visibilityState !== 'unloaded') renderActions(phaseOf(currentView));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
elements.actions.addEventListener('click', event => {
|
|
317
|
+
const button = event.target.closest('button[data-action]');
|
|
318
|
+
if (button) runAction(button.dataset.action);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
chrome.storage.onChanged.addListener((_changes, areaName) => {
|
|
323
|
+
if (areaName === 'local') scheduleRefresh();
|
|
324
|
+
});
|
|
325
|
+
} catch {
|
|
326
|
+
renderReconnecting();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const requestedAction = new URLSearchParams(window.location.search).get('action');
|
|
330
|
+
window.history.replaceState(null, '', window.location.pathname);
|
|
331
|
+
refresh().then(() => {
|
|
332
|
+
if (requestedAction && ['install', 'retry'].includes(requestedAction)) runAction(requestedAction);
|
|
333
|
+
});
|
|
334
|
+
})();
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const CHECK_INTERVAL_MINUTES = 24 * 60;
|
|
8
8
|
const SNOOZE_MS = 24 * 60 * 60 * 1000;
|
|
9
9
|
const CANDIDATE_MAX_AGE_MS = 5 * 60 * 1000;
|
|
10
|
+
const CHECK_REQUEST_TIMEOUT_MS = 15 * 1000;
|
|
10
11
|
const LEGACY_GUARD = Number.MAX_SAFE_INTEGER;
|
|
11
12
|
const OVERLEAF_MATCHES = [
|
|
12
13
|
'https://www.overleaf.com/project/*',
|
|
@@ -16,7 +17,8 @@
|
|
|
16
17
|
'codex-overleaf/consent-update-get-state',
|
|
17
18
|
'codex-overleaf/consent-update-check',
|
|
18
19
|
'codex-overleaf/consent-update-install',
|
|
19
|
-
'codex-overleaf/consent-update-later'
|
|
20
|
+
'codex-overleaf/consent-update-later',
|
|
21
|
+
'codex-overleaf/consent-update-dismiss'
|
|
20
22
|
]);
|
|
21
23
|
|
|
22
24
|
const policy = root.CodexOverleafUpdateConsent;
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
delayInMinutes: 0.5,
|
|
61
63
|
periodInMinutes: CHECK_INTERVAL_MINUTES
|
|
62
64
|
});
|
|
65
|
+
await recoverInterruptedCheck();
|
|
63
66
|
await settleTerminalConsent();
|
|
64
67
|
await armLegacyGuard();
|
|
65
68
|
await publishView();
|
|
@@ -75,6 +78,8 @@
|
|
|
75
78
|
return installUpdate();
|
|
76
79
|
case 'codex-overleaf/consent-update-later':
|
|
77
80
|
return postponeUpdate();
|
|
81
|
+
case 'codex-overleaf/consent-update-dismiss':
|
|
82
|
+
return dismissCompletedUpdate();
|
|
78
83
|
default:
|
|
79
84
|
throw codedError('unknown_update_action', 'Unknown update action.');
|
|
80
85
|
}
|
|
@@ -95,10 +100,14 @@
|
|
|
95
100
|
});
|
|
96
101
|
|
|
97
102
|
try {
|
|
98
|
-
const result = await
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
const result = await withTimeout(
|
|
104
|
+
requestNative('update.check', {
|
|
105
|
+
currentVersion: currentVersion(),
|
|
106
|
+
etag: currentState.etag || ''
|
|
107
|
+
}),
|
|
108
|
+
CHECK_REQUEST_TIMEOUT_MS,
|
|
109
|
+
codedError('update_check_timeout', 'The managed update check did not complete in time.')
|
|
110
|
+
);
|
|
102
111
|
const checkedAt = Date.now();
|
|
103
112
|
if (result.available) {
|
|
104
113
|
const nextConsent = result.latestVersion !== consent.snoozedVersion
|
|
@@ -158,6 +167,41 @@
|
|
|
158
167
|
}
|
|
159
168
|
}
|
|
160
169
|
|
|
170
|
+
async function recoverInterruptedCheck() {
|
|
171
|
+
const state = await getUpdateState();
|
|
172
|
+
if (state.state !== 'checking') return;
|
|
173
|
+
const nextState = policy.compareStableVersions(state.latestVersion, currentVersion()) > 0
|
|
174
|
+
? 'update_available'
|
|
175
|
+
: 'idle';
|
|
176
|
+
await setUpdateState({
|
|
177
|
+
...state,
|
|
178
|
+
state: nextState,
|
|
179
|
+
blocker: '',
|
|
180
|
+
blockers: [],
|
|
181
|
+
code: '',
|
|
182
|
+
message: ''
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function dismissCompletedUpdate() {
|
|
187
|
+
const state = await getUpdateState();
|
|
188
|
+
if (state.state !== 'committed') return getView();
|
|
189
|
+
const version = state.latestVersion || state.currentVersion || currentVersion();
|
|
190
|
+
await setUpdateState({
|
|
191
|
+
...state,
|
|
192
|
+
state: 'idle',
|
|
193
|
+
currentVersion: version,
|
|
194
|
+
latestVersion: version,
|
|
195
|
+
transactionId: '',
|
|
196
|
+
stagedAt: 0,
|
|
197
|
+
blocker: '',
|
|
198
|
+
blockers: [],
|
|
199
|
+
code: '',
|
|
200
|
+
message: ''
|
|
201
|
+
});
|
|
202
|
+
return getView();
|
|
203
|
+
}
|
|
204
|
+
|
|
161
205
|
async function installUpdate() {
|
|
162
206
|
let state = await getUpdateState();
|
|
163
207
|
let consent = await getConsentState();
|
|
@@ -189,10 +233,11 @@
|
|
|
189
233
|
await setUpdateState({ ...state, postponeUntil: 0, code: '', message: '' });
|
|
190
234
|
|
|
191
235
|
try {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
236
|
+
const executor = root.CodexOverleafManagedUpdateExecutor;
|
|
237
|
+
if (typeof executor?.installAuthorizedUpdate !== 'function') {
|
|
194
238
|
throw codedError('update_executor_unavailable', 'The managed update executor is unavailable.');
|
|
195
239
|
}
|
|
240
|
+
await executor.installAuthorizedUpdate();
|
|
196
241
|
return getView();
|
|
197
242
|
} catch (error) {
|
|
198
243
|
await bestEffortRevoke({
|
|
@@ -393,6 +438,22 @@
|
|
|
393
438
|
return error;
|
|
394
439
|
}
|
|
395
440
|
|
|
441
|
+
function withTimeout(promise, timeoutMs, timeoutError) {
|
|
442
|
+
return new Promise((resolve, reject) => {
|
|
443
|
+
const timer = setTimeout(() => reject(timeoutError), timeoutMs);
|
|
444
|
+
Promise.resolve(promise).then(
|
|
445
|
+
value => {
|
|
446
|
+
clearTimeout(timer);
|
|
447
|
+
resolve(value);
|
|
448
|
+
},
|
|
449
|
+
error => {
|
|
450
|
+
clearTimeout(timer);
|
|
451
|
+
reject(error);
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
396
457
|
function safeError(error) {
|
|
397
458
|
return { code: safeCode(error), message: safeMessage(error) };
|
|
398
459
|
}
|