dsh-unlag 1.9.54 → 1.9.56

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.
Files changed (3) hide show
  1. package/client.js +24 -2
  2. package/index.js +50 -34
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -432,6 +432,22 @@ window.__ModuleLoader__.load({
432
432
  }
433
433
  }
434
434
 
435
+ /**
436
+ * The stamp written when the plugin code was last loaded. A replacement that was applied
437
+ * without changing which bundles are served keeps the startup list's revision, so the graph
438
+ * alone cannot tell a lost answer from a failed press; the code stamp can.
439
+ */
440
+ async function hostCodeStamp() {
441
+ try {
442
+ const response = await withTimeout(fetch(`${HOST_BASE}/state`, { cache: 'no-store', credentials: 'same-origin' }));
443
+ if (!response.ok) return null;
444
+ const data = await withTimeout(response.json());
445
+ return typeof data?.code === 'string' ? data.code : null;
446
+ } catch {
447
+ return null;
448
+ }
449
+ }
450
+
435
451
  /** Whether a plugin no longer in the running set still has its script answered. */
436
452
  async function hostShimAnswersGone() {
437
453
  try {
@@ -683,13 +699,19 @@ window.__ModuleLoader__.load({
683
699
  */
684
700
  const before = await hostBootGraph();
685
701
  const revBefore = typeof before?.rev === 'string' ? before.rev : null;
702
+ const codeBefore = await hostCodeStamp();
686
703
  let status = await withTimeout(replacePlugins()).catch(() => 'failed');
687
- if (status === 'failed' && revBefore !== null) {
704
+ if (status === 'failed' && (revBefore !== null || codeBefore !== null)) {
688
705
  for (const wait of [400, 900, 1800]) { // ui-copy-ignore
689
706
  await new Promise((resolve) => setTimeout(resolve, wait));
690
707
  const after = await hostBootGraph();
691
708
  const revNow = typeof after?.rev === 'string' ? after.rev : null;
692
- if (revNow !== null && revNow !== revBefore) {
709
+ if (revNow !== null && revBefore !== null && revNow !== revBefore) {
710
+ status = 'done';
711
+ break;
712
+ }
713
+ const codeNow = await hostCodeStamp();
714
+ if (codeNow !== null && codeBefore !== null && codeNow !== codeBefore) {
693
715
  status = 'done';
694
716
  break;
695
717
  }
package/index.js CHANGED
@@ -1309,6 +1309,8 @@ function serveBundlesAtCurrentRevision(ctx) {
1309
1309
  }
1310
1310
  const realWriteHead = res.writeHead
1311
1311
  const realEnd = res.end
1312
+ const realWriteRef = res.write
1313
+ let canHold = true
1312
1314
  const ask = async (url) => {
1313
1315
  let held = null
1314
1316
  let body
@@ -1321,24 +1323,14 @@ function serveBundlesAtCurrentRevision(ctx) {
1321
1323
  /* a frozen request keeps its own address */
1322
1324
  }
1323
1325
  }
1324
- if (typeof realWriteHead === 'function') {
1325
- res.writeHead = (status, ...rest) => {
1326
- if (status === 404) {
1327
- held = { status, headers: rest[0] }
1328
- return res
1329
- }
1330
- return realWriteHead.call(res, status, ...rest)
1331
- }
1332
- }
1333
1326
  /**
1334
1327
  * A held 404 can still be written to in pieces. Passing those pieces through would send the
1335
1328
  * implicit 200 headers Node writes on a first `write`, and the real answer could then never
1336
- * be sent; so the pieces are kept with the body and the socket stays untouched.
1329
+ * be sent; so the pieces are kept with the body and the socket stays untouched. A held answer
1330
+ * also owes the caller the callbacks it passed: dropping one would leave a handler that waits
1331
+ * for the flush awaiting forever, and the answer after it would never run.
1337
1332
  */
1338
1333
  const realWrite = res.write
1339
- // A held answer still owes the caller the callbacks it passed: dropping one would leave a
1340
- // handler that waits for the flush awaiting forever, and the fallback answer would never
1341
- // run. They are answered on the next turn once the held body is collected.
1342
1334
  const answerCallback = (args) => {
1343
1335
  for (let at = args.length - 1; at >= 0; at -= 1) {
1344
1336
  if (typeof args[at] === 'function') {
@@ -1358,26 +1350,48 @@ function serveBundlesAtCurrentRevision(ctx) {
1358
1350
  }
1359
1351
  }
1360
1352
  }
1361
- if (typeof realWrite === 'function') {
1362
- res.write = (chunk, ...rest) => {
1363
- if (held !== null) {
1364
- if (typeof chunk === 'string' || Buffer.isBuffer(chunk)) parts.push(Buffer.from(chunk))
1365
- answerCallback(rest)
1366
- return true
1353
+ /**
1354
+ * Installing the hold means writing to the response object. A response that refuses that is
1355
+ * answered as the table left it instead of thrown at, because this runs in an async route
1356
+ * handler where a rejection would be an unhandled one.
1357
+ */
1358
+ if (canHold) {
1359
+ try {
1360
+ if (typeof realWrite === 'function') {
1361
+ res.write = (chunk, ...rest) => {
1362
+ if (held !== null) {
1363
+ if (typeof chunk === 'string' || Buffer.isBuffer(chunk)) parts.push(Buffer.from(chunk))
1364
+ answerCallback(rest)
1365
+ return true
1366
+ }
1367
+ return realWrite.call(res, chunk, ...rest)
1368
+ }
1367
1369
  }
1368
- return realWrite.call(res, chunk, ...rest)
1369
- }
1370
- }
1371
- if (typeof realEnd === 'function') {
1372
- res.end = (...args) => {
1373
- if (held !== null) {
1374
- const last = args[0]
1375
- if (typeof last === 'string' || Buffer.isBuffer(last)) parts.push(Buffer.from(last))
1376
- body = parts.length === 0 ? '' : Buffer.concat(parts)
1377
- answerCallback(args)
1378
- return res
1370
+ if (typeof realEnd === 'function') {
1371
+ res.end = (...args) => {
1372
+ if (held !== null) {
1373
+ const last = args[0]
1374
+ if (typeof last === 'string' || Buffer.isBuffer(last)) parts.push(Buffer.from(last))
1375
+ body = parts.length === 0 ? '' : Buffer.concat(parts)
1376
+ answerCallback(args)
1377
+ return res
1378
+ }
1379
+ return realEnd.apply(res, args)
1380
+ }
1379
1381
  }
1380
- return realEnd.apply(res, args)
1382
+ if (typeof realWriteHead === 'function') {
1383
+ res.writeHead = (status, ...rest) => {
1384
+ if (status === 404) {
1385
+ held = { status, headers: rest[0] }
1386
+ return res
1387
+ }
1388
+ return realWriteHead.call(res, status, ...rest)
1389
+ }
1390
+ }
1391
+ } catch (error) {
1392
+ canHold = false
1393
+ held = null
1394
+ log(`cannot hold a 404 for a moment: ${oneLine(error?.message ?? error)}`)
1381
1395
  }
1382
1396
  }
1383
1397
  try {
@@ -1387,9 +1401,11 @@ function serveBundlesAtCurrentRevision(ctx) {
1387
1401
  log(`cannot answer a bundle request: ${error?.message ?? error}`)
1388
1402
  } finally {
1389
1403
  try {
1390
- res.writeHead = realWriteHead
1391
- if (realWrite !== undefined) res.write = realWrite
1392
- res.end = realEnd
1404
+ if (canHold) {
1405
+ res.writeHead = realWriteHead
1406
+ if (realWriteRef !== undefined) res.write = realWriteRef
1407
+ res.end = realEnd
1408
+ }
1393
1409
  } catch {
1394
1410
  /* the response object owns its own shape */
1395
1411
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unlag",
3
- "version": "1.9.54",
3
+ "version": "1.9.56",
4
4
  "type": "module",
5
5
  "description": "A press applies plugin edits right away and brings a laggy interface back; automatic reloading stays off unless you turn it on, so the press is the only trigger, and nothing restarts.",
6
6
  "license": "MIT",