dsh-unlag 1.8.0 → 1.8.2
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 +2 -1
- package/client.js +155 -38
- package/index.js +57 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@ dsh plugin install dsh-unlag
|
|
|
34
34
|
|
|
35
35
|
- 轮次、子代理、终端进程:照常继续。
|
|
36
36
|
- 排队消息、输入草稿、附件:重建前先提交焦点里的内容;有人没存完就拦下,不硬刷。
|
|
37
|
-
-
|
|
37
|
+
- 有未保存改动、开着弹窗、或下个页面要用的模块读不全:拦下并提示,提示里写明卡在哪一项,宁可不刷。
|
|
38
|
+
- 旧页面记着的启动清单分组变了(装过/卸过插件):按插件逐个补齐再交给新页面,不会因为清单 404 把 App 带走。
|
|
38
39
|
- 从不自己重启或退出 App。
|
|
39
40
|
|
|
40
41
|
管不到(它会拦下并说明,不会硬来):
|
package/client.js
CHANGED
|
@@ -9,6 +9,31 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
? { label: '解除卡顿', hint: '卡顿时用:换掉插件代码并重建界面;运行中的任务、排队的消息保留', blocked: '暂时无法重建界面,稍后再试', unsaved: '有未保存的改动,先保存再重试', dialog: '先关闭打开的窗口再重载', queued: '更新已排队,稍后再按一次', applying: '还在应用这次更新,稍后再按一次', restart: '这次改动要重新打开应用才会生效', failed: '插件更新没完成,稍后再按一次' } // ui-copy-ignore
|
|
10
10
|
: { label: 'Unlag', hint: 'For a sluggish interface: replaces plugin code and rebuilds the page; running turns and queued messages are kept', blocked: 'Cannot rebuild the page right now; try again', unsaved: 'Unsaved changes; save them, then retry', dialog: 'Close the open window first', queued: 'Update queued; press again shortly', applying: 'Still applying this update; press again shortly', restart: 'This change needs the app reopened before it can apply', failed: 'The plugin update did not finish; press again later' };
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Why the interface was left as it is, in words a person can act on. The checks below record
|
|
14
|
+
* the first reason they refuse for, so a press that stops can say what it saw instead of
|
|
15
|
+
* repeating one generic sentence.
|
|
16
|
+
*/
|
|
17
|
+
const REASONS = zh
|
|
18
|
+
? { list: '读不到启动信息', row: '启动信息不完整', src: '启动脚本没有地址', graph: '已装插件清单读不到', plugin: '有插件已不在运行', script: '启动脚本读不到', empty: '启动脚本是空的', covers: '启动脚本缺插件', parse: '启动脚本内容不完整' }
|
|
19
|
+
: { list: 'startup info is unavailable', row: 'startup info is incomplete', src: 'a startup script has no address', graph: 'the plugin list cannot be read', plugin: 'a plugin is no longer running', script: 'a startup script cannot be read', empty: 'a startup script is empty', covers: 'a startup script misses a plugin', parse: 'a startup script is incomplete' };
|
|
20
|
+
|
|
21
|
+
/** The last reason a readiness check refused for, or null after a check that passed. */
|
|
22
|
+
let holdReason = null;
|
|
23
|
+
|
|
24
|
+
/** Record why a rebuild is held and answer false for the check that asked. */
|
|
25
|
+
function held(reason) {
|
|
26
|
+
holdReason = reason;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The notice for one copy key, naming the reason when a readiness check stopped the press. */
|
|
31
|
+
function noticeFor(key) {
|
|
32
|
+
if (key !== 'blocked' || holdReason === null) return COPY[key];
|
|
33
|
+
const why = REASONS[holdReason] ?? REASONS.list;
|
|
34
|
+
return zh ? `暂时无法重建:${why}。稍后再点一次。` : `Cannot rebuild yet: ${why}. Press again shortly.`;
|
|
35
|
+
}
|
|
36
|
+
|
|
12
37
|
// Desktop shell only: the macOS and Windows desktop build owns the page and the running
|
|
13
38
|
// app, so this is where the button belongs. A browser tab can reload freely on its own.
|
|
14
39
|
const desktop = /Electron\//.test(navigator.userAgent || '');
|
|
@@ -104,10 +129,10 @@ window.__ModuleLoader__.load({
|
|
|
104
129
|
|
|
105
130
|
/** Read every bundle the next boot would load and prove each listed plugin is inside. */
|
|
106
131
|
async function bundlesCoverStartup(graph) {
|
|
107
|
-
if (graph === null || typeof graph !== 'object') return
|
|
132
|
+
if (graph === null || typeof graph !== 'object') return held('graph');
|
|
108
133
|
const entries = Array.isArray(graph.entries) ? graph.entries : [];
|
|
109
134
|
const batches = Array.isArray(graph.batches) ? graph.batches : [];
|
|
110
|
-
if (entries.length === 0 || batches.length === 0) return
|
|
135
|
+
if (entries.length === 0 || batches.length === 0) return held('graph');
|
|
111
136
|
const covered = new Set();
|
|
112
137
|
const sources = [];
|
|
113
138
|
for (const batch of batches) {
|
|
@@ -115,29 +140,30 @@ window.__ModuleLoader__.load({
|
|
|
115
140
|
if (url === '') continue;
|
|
116
141
|
const listed = Array.isArray(batch.entries) && batch.entries.every((id) => typeof id === 'string') ? batch.entries : [];
|
|
117
142
|
const ids = listed.length > 0 ? listed : bundledIds(url);
|
|
118
|
-
if (ids.length === 0) return
|
|
143
|
+
if (ids.length === 0) return held('row');
|
|
119
144
|
for (const id of ids) covered.add(id);
|
|
120
145
|
sources.push({ url: new URL(url, location.href), ids });
|
|
121
146
|
}
|
|
122
|
-
if (sources.length === 0) return
|
|
147
|
+
if (sources.length === 0) return held('graph');
|
|
123
148
|
for (const entry of entries) {
|
|
124
149
|
const id = typeof entry === 'string' ? entry : entry?.id;
|
|
125
|
-
if (typeof id !== 'string' || !covered.has(id)) return
|
|
150
|
+
if (typeof id !== 'string' || !covered.has(id)) return held('covers');
|
|
126
151
|
}
|
|
127
152
|
for (const source of sources) {
|
|
128
153
|
try {
|
|
129
154
|
const response = await fetch(source.url.toString(), { cache: 'no-store', credentials: 'same-origin' });
|
|
130
|
-
if (!response.ok) return
|
|
155
|
+
if (!response.ok) return held('script');
|
|
131
156
|
const body = await response.text();
|
|
132
|
-
if (body.length === 0) return
|
|
157
|
+
if (body.length === 0) return held('empty');
|
|
133
158
|
for (const id of source.ids) {
|
|
134
|
-
if (!body.includes(id)) return
|
|
159
|
+
if (!body.includes(id)) return held(source.ids.length === 1 ? 'plugin' : 'covers');
|
|
135
160
|
}
|
|
136
|
-
if (!parsesAsClassicScript(body)) return
|
|
161
|
+
if (!parsesAsClassicScript(body)) return held('parse');
|
|
137
162
|
} catch {
|
|
138
|
-
return
|
|
163
|
+
return held('script');
|
|
139
164
|
}
|
|
140
165
|
}
|
|
166
|
+
holdReason = null;
|
|
141
167
|
return true;
|
|
142
168
|
}
|
|
143
169
|
|
|
@@ -186,10 +212,78 @@ window.__ModuleLoader__.load({
|
|
|
186
212
|
return out;
|
|
187
213
|
}
|
|
188
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Whether a script has a `return` at the top level. A function body accepts one and a classic
|
|
217
|
+
* script does not, so that difference decides whether the document runs the body at all. Only
|
|
218
|
+
* depth zero counts: a return inside a callback, method or block is ordinary code, and reading
|
|
219
|
+
* one as a refusal held every real bundle back.
|
|
220
|
+
*/
|
|
221
|
+
function hasTopLevelReturn(source) {
|
|
222
|
+
let depth = 0;
|
|
223
|
+
let index = 0;
|
|
224
|
+
let previous = '';
|
|
225
|
+
while (index < source.length) {
|
|
226
|
+
const here = source[index];
|
|
227
|
+
const next = source[index + 1];
|
|
228
|
+
if (here === '/' && next === '*') {
|
|
229
|
+
const end = source.indexOf('*/', index + 2);
|
|
230
|
+
index = end < 0 ? source.length : end + 2;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (here === '/' && next === '/') {
|
|
234
|
+
const end = source.indexOf('\n', index + 2);
|
|
235
|
+
index = end < 0 ? source.length : end;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (here === '"' || here === "'" || here === '`') {
|
|
239
|
+
index += 1;
|
|
240
|
+
while (index < source.length && source[index] !== here) {
|
|
241
|
+
if (source[index] === '\\') index += 1;
|
|
242
|
+
index += 1;
|
|
243
|
+
}
|
|
244
|
+
index += 1;
|
|
245
|
+
previous = 'a';
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (here === '/' && /(?:^|[(,=:[!&|?{};+\-*%<>~^])$/u.test(previous)) {
|
|
249
|
+
// A regular expression literal; its body is pattern text, not code.
|
|
250
|
+
index += 1;
|
|
251
|
+
let inClass = false;
|
|
252
|
+
while (index < source.length) {
|
|
253
|
+
const part = source[index];
|
|
254
|
+
if (part === '\\') index += 2;
|
|
255
|
+
else if (part === '[') {
|
|
256
|
+
inClass = true;
|
|
257
|
+
index += 1;
|
|
258
|
+
} else if (part === ']') {
|
|
259
|
+
inClass = false;
|
|
260
|
+
index += 1;
|
|
261
|
+
} else if (part === '/' && !inClass) {
|
|
262
|
+
index += 1;
|
|
263
|
+
break;
|
|
264
|
+
} else if (part === '\n') break;
|
|
265
|
+
else index += 1;
|
|
266
|
+
}
|
|
267
|
+
previous = 'a';
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (here === '(' || here === '[' || here === '{') depth += 1;
|
|
271
|
+
else if (here === ')' || here === ']' || here === '}') depth = Math.max(0, depth - 1);
|
|
272
|
+
else if (depth === 0 && here === 'r' && source.startsWith('return', index)) {
|
|
273
|
+
const before = source[index - 1] ?? '';
|
|
274
|
+
const after = source[index + 6] ?? '';
|
|
275
|
+
if (!/[\w$]/u.test(before) && !/[\w$]/u.test(after)) return true;
|
|
276
|
+
}
|
|
277
|
+
if (!/\s/u.test(here)) previous = here;
|
|
278
|
+
index += 1;
|
|
279
|
+
}
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
189
283
|
function parsesAsClassicScript(body) {
|
|
190
284
|
try {
|
|
191
285
|
// A top-level return parses in a function body but not as a classic script.
|
|
192
|
-
if (
|
|
286
|
+
if (hasTopLevelReturn(body)) return false;
|
|
193
287
|
new Function(body); // eslint-disable-line no-new-func
|
|
194
288
|
return true;
|
|
195
289
|
} catch (error) {
|
|
@@ -236,9 +330,15 @@ window.__ModuleLoader__.load({
|
|
|
236
330
|
let frozen = null;
|
|
237
331
|
for (const row of rows) {
|
|
238
332
|
if (row === null || typeof row !== 'object') continue;
|
|
239
|
-
if (typeof row.kind !== 'string' || !STARTUP_ROW_KINDS.has(row.kind)) return
|
|
240
|
-
|
|
241
|
-
|
|
333
|
+
if (typeof row.kind !== 'string' || !STARTUP_ROW_KINDS.has(row.kind)) return held('row');
|
|
334
|
+
// Only the rows a document runs as scripts have to load as classic scripts. Style and
|
|
335
|
+
// markup rows carry their own text and their address is not a script at all, so reading
|
|
336
|
+
// them back would refuse a sound list over a false alarm.
|
|
337
|
+
const runs = row.kind === 'script-src' || row.kind === 'script-preload';
|
|
338
|
+
if (runs && (typeof row.src !== 'string' || row.src === '')) return held('src');
|
|
339
|
+
if (runs) scripts.push(row.src);
|
|
340
|
+
// Inline rows carry their own text and are not fetched; the queue script among them ends
|
|
341
|
+
// in a nested return that a flat scan would misread, so their text is left to the document.
|
|
242
342
|
if (row.kind !== 'global' || row.name !== '__DSH_BOOT__') continue; // ui-copy-ignore
|
|
243
343
|
let value = row.value;
|
|
244
344
|
if (typeof value === 'string') {
|
|
@@ -250,17 +350,23 @@ window.__ModuleLoader__.load({
|
|
|
250
350
|
}
|
|
251
351
|
if (value !== null && typeof value === 'object') frozen = value;
|
|
252
352
|
}
|
|
253
|
-
if (scripts.length === 0) return
|
|
254
|
-
|
|
353
|
+
if (scripts.length === 0) return held('list');
|
|
354
|
+
// The graph is read twice: one answer taken while the app re-composes says nothing about
|
|
355
|
+
// the next boot, and holding the page is the safe answer when it cannot be read at all.
|
|
356
|
+
let live = await hostBootGraph();
|
|
357
|
+
if (live === null && frozen !== null) {
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, 300)); // ui-copy-ignore
|
|
359
|
+
live = await hostBootGraph();
|
|
360
|
+
}
|
|
255
361
|
if (frozen !== null) {
|
|
256
|
-
// The frozen list decides what this document boots. Without the current
|
|
257
|
-
// nothing to prove
|
|
258
|
-
if (live === null) return
|
|
259
|
-
const liveIds = new Set((Array.isArray(live.entries) ? live.entries : []).map((entry) => (typeof entry === 'string' ? entry : entry?.id)));
|
|
362
|
+
// The frozen list decides what this document boots. Without the current plugin list there
|
|
363
|
+
// is nothing to prove every plugin in it still exists, so the page is not rebuilt.
|
|
364
|
+
if (live === null) return held('graph');
|
|
260
365
|
const kept = Array.isArray(frozen.entries) ? frozen.entries : [];
|
|
366
|
+
const liveIds = new Set((Array.isArray(live.entries) ? live.entries : []).map((entry) => (typeof entry === 'string' ? entry : entry?.id)));
|
|
261
367
|
for (const entry of kept) {
|
|
262
368
|
const id = typeof entry === 'string' ? entry : entry?.id;
|
|
263
|
-
if (typeof id === 'string' && id !== '' && !liveIds.has(id)) return
|
|
369
|
+
if (typeof id === 'string' && id !== '' && !liveIds.has(id)) return held('plugin');
|
|
264
370
|
}
|
|
265
371
|
}
|
|
266
372
|
for (const src of scripts) {
|
|
@@ -268,21 +374,23 @@ window.__ModuleLoader__.load({
|
|
|
268
374
|
try {
|
|
269
375
|
target = new URL(src, location.href).toString();
|
|
270
376
|
} catch {
|
|
271
|
-
return
|
|
377
|
+
return held('src');
|
|
272
378
|
}
|
|
273
379
|
try {
|
|
274
380
|
const response = await fetch(target, { cache: 'no-store', credentials: 'same-origin' });
|
|
275
|
-
if (!response.ok) return
|
|
381
|
+
if (!response.ok) return held('script');
|
|
276
382
|
const body = await response.text();
|
|
277
|
-
if (body.length === 0) return
|
|
278
|
-
|
|
279
|
-
|
|
383
|
+
if (body.length === 0) return held('empty');
|
|
384
|
+
const ids = bundledIds(target);
|
|
385
|
+
for (const id of ids) {
|
|
386
|
+
if (!body.includes(id)) return held(ids.length === 1 ? 'plugin' : 'covers');
|
|
280
387
|
}
|
|
281
|
-
if (!parsesAsClassicScript(body)) return
|
|
388
|
+
if (!parsesAsClassicScript(body)) return held('parse');
|
|
282
389
|
} catch {
|
|
283
|
-
return
|
|
390
|
+
return held('script');
|
|
284
391
|
}
|
|
285
392
|
}
|
|
393
|
+
holdReason = null;
|
|
286
394
|
return true;
|
|
287
395
|
}
|
|
288
396
|
|
|
@@ -362,18 +470,25 @@ window.__ModuleLoader__.load({
|
|
|
362
470
|
* @returns true when every listed plugin's module is present in the served bundles.
|
|
363
471
|
*/
|
|
364
472
|
async function startupLooksServable() {
|
|
365
|
-
|
|
473
|
+
holdReason = null;
|
|
474
|
+
// Two repeats: the app re-composes its bundles while it starts and after a plugin swap, and
|
|
475
|
+
// a list read in the middle of that can answer for a generation that is already gone.
|
|
476
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
366
477
|
const saved = await desktopStartupPayload();
|
|
367
478
|
// The desktop shell replays one startup list for every document. If it is running but its
|
|
368
479
|
// copy cannot be read, nothing here proves a new document would boot, so the page stays.
|
|
369
|
-
if (saved === null && desktopShellPresent())
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
480
|
+
if (saved === null && desktopShellPresent()) {
|
|
481
|
+
held('list');
|
|
482
|
+
} else {
|
|
483
|
+
const servable =
|
|
484
|
+
saved === null
|
|
485
|
+
? await bundlesCoverStartup((await servedBootGraph()) ?? window.__DSH_BOOT__)
|
|
486
|
+
: await frozenStartupLooksServable(saved);
|
|
487
|
+
if (servable) return true;
|
|
488
|
+
}
|
|
489
|
+
if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 300 + attempt * 300)); // ui-copy-ignore
|
|
376
490
|
}
|
|
491
|
+
if (holdReason === null) held('list');
|
|
377
492
|
return false;
|
|
378
493
|
}
|
|
379
494
|
|
|
@@ -506,7 +621,7 @@ window.__ModuleLoader__.load({
|
|
|
506
621
|
setNotice(null);
|
|
507
622
|
const stop = (key) => {
|
|
508
623
|
setPending(false);
|
|
509
|
-
setNotice(
|
|
624
|
+
setNotice(noticeFor(key));
|
|
510
625
|
};
|
|
511
626
|
const settings = await readSettings();
|
|
512
627
|
const replace = settings === null ? false : settings.plugins;
|
|
@@ -543,7 +658,8 @@ window.__ModuleLoader__.load({
|
|
|
543
658
|
window.sessionStorage.setItem(STAMP_KEY, String(Date.now()));
|
|
544
659
|
const reason = await prepareInterface();
|
|
545
660
|
if (reason !== null) {
|
|
546
|
-
|
|
661
|
+
// The log keeps the exact check that refused, so a held press can be explained later.
|
|
662
|
+
report('held', reason === 'blocked' && holdReason !== null ? `blocked:${holdReason}` : reason);
|
|
547
663
|
return stop(reason);
|
|
548
664
|
}
|
|
549
665
|
report('rebuild');
|
|
@@ -584,6 +700,7 @@ window.__ModuleLoader__.load({
|
|
|
584
700
|
const PROBE = {
|
|
585
701
|
prepareInterface,
|
|
586
702
|
startupLooksServable,
|
|
703
|
+
startupHoldReason: () => holdReason,
|
|
587
704
|
frozenStartupLooksServable,
|
|
588
705
|
desktopStartupPayload,
|
|
589
706
|
desktopShellPresent,
|
package/index.js
CHANGED
|
@@ -589,6 +589,49 @@ async function fetchBundleAt(clientModules, url, method) {
|
|
|
589
589
|
return clientModules.fetchBundle({ method, url: address })
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
/**
|
|
593
|
+
* One plugin's current module body, read back from the table so it can be put in front of a
|
|
594
|
+
* document that expects a grouping the table no longer keeps.
|
|
595
|
+
*/
|
|
596
|
+
async function currentEntryBody(clientModules, graph, id) {
|
|
597
|
+
const entries = Array.isArray(graph?.entries) ? graph.entries : []
|
|
598
|
+
const entry = entries.find((candidate) => candidate?.id === id)
|
|
599
|
+
const raw = typeof entry?.url === 'string' ? entry.url : ''
|
|
600
|
+
if (raw === '') return null
|
|
601
|
+
const target = raw.startsWith('/') ? raw : `/${raw}`
|
|
602
|
+
const response = await fetchBundleAt(clientModules, target, 'GET')
|
|
603
|
+
if (response.status !== 200) return null
|
|
604
|
+
const body = Buffer.from(await response.arrayBuffer())
|
|
605
|
+
return body.length === 0 ? null : body
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* A plugin set from a generation the table no longer groups the same way. An app that started
|
|
610
|
+
* before plugins were installed asks for exactly the sets its document listed; those sets are not
|
|
611
|
+
* kept as sets any more, but every plugin in them is still served on its own. Reading each one and
|
|
612
|
+
* answering with them in the requested order registers exactly the plugins the document expects,
|
|
613
|
+
* so a boot that would otherwise 404 and end the app comes up instead.
|
|
614
|
+
*/
|
|
615
|
+
async function regroupedBundleAnswer(clientModules, graph, request) {
|
|
616
|
+
if (request.sourceMap) return null
|
|
617
|
+
const parts = []
|
|
618
|
+
for (const id of request.ids) {
|
|
619
|
+
const body = await currentEntryBody(clientModules, graph, id)
|
|
620
|
+
if (body === null) return null
|
|
621
|
+
parts.push(body)
|
|
622
|
+
}
|
|
623
|
+
if (parts.length === 0) return null
|
|
624
|
+
/**
|
|
625
|
+
* Each entry body is a script of its own and need not end where another can begin: an entry
|
|
626
|
+
* that ends in `})` followed by one that starts with an identifier is a syntax error, and a
|
|
627
|
+
* document would run neither. A line holding just `;` between them keeps every body its own
|
|
628
|
+
* statement whatever the two sides end and start with, and leaves each body itself untouched.
|
|
629
|
+
*/
|
|
630
|
+
const body = Buffer.concat(parts.flatMap((part, index) => (index === parts.length - 1 ? [part] : [part, Buffer.from('\n;\n')])))
|
|
631
|
+
if (!request.ids.every((id) => body.includes(id))) return null
|
|
632
|
+
return { body, contentType: 'text/javascript; charset=utf-8', ids: request.ids }
|
|
633
|
+
}
|
|
634
|
+
|
|
592
635
|
/**
|
|
593
636
|
* The plugin set one stale request names, answered at the revision the app serves now. Only a body
|
|
594
637
|
* that really contains every plugin it claims is handed back; anything unsure answers null, so the
|
|
@@ -598,24 +641,24 @@ async function currentBundleAnswer(clientModules, method, url) {
|
|
|
598
641
|
const request = bundleRequest(url)
|
|
599
642
|
if (request === null) return null
|
|
600
643
|
const graph = clientModules.graph()
|
|
601
|
-
const urls = currentBundleUrls(graph, request)
|
|
602
|
-
if (urls.length === 0) return null
|
|
603
644
|
// A request already at the generation the table serves is the table's to answer.
|
|
604
645
|
if (typeof graph?.rev === 'string' && graph.rev !== '' && request.rev === graph.rev) return null
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
646
|
+
const urls = currentBundleUrls(graph, request)
|
|
647
|
+
if (urls.length > 0 && (request.rev === '' || !urls.some((target) => target.endsWith(`&rev=${request.rev}`)))) {
|
|
648
|
+
for (const target of urls) {
|
|
649
|
+
const response = await fetchBundleAt(clientModules, target, 'GET')
|
|
650
|
+
if (response.status !== 200) continue
|
|
651
|
+
const body = Buffer.from(await response.arrayBuffer())
|
|
652
|
+
if (body.length === 0) continue
|
|
653
|
+
if (!request.sourceMap && !request.ids.every((id) => body.includes(id))) continue
|
|
654
|
+
return {
|
|
655
|
+
body,
|
|
656
|
+
contentType: String(response.headers.get('content-type') ?? 'text/javascript; charset=utf-8'),
|
|
657
|
+
ids: request.ids,
|
|
658
|
+
}
|
|
616
659
|
}
|
|
617
660
|
}
|
|
618
|
-
return
|
|
661
|
+
return regroupedBundleAnswer(clientModules, graph, request)
|
|
619
662
|
}
|
|
620
663
|
|
|
621
664
|
/** Answer one stale request over the plain request/response route an app registers. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-unlag",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One press applies plugin edits right away and brings a laggy interface back; running turns, queued messages and unsent input are untouched, and nothing restarts.",
|
|
6
6
|
"license": "MIT",
|