dsh-unlag 1.9.51 → 1.9.53

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/index.js CHANGED
@@ -1312,6 +1312,7 @@ function serveBundlesAtCurrentRevision(ctx) {
1312
1312
  const ask = async (url) => {
1313
1313
  let held = null
1314
1314
  let body
1315
+ const parts = []
1315
1316
  let failed = false
1316
1317
  if (url !== originalUrl) {
1317
1318
  try {
@@ -1329,10 +1330,27 @@ function serveBundlesAtCurrentRevision(ctx) {
1329
1330
  return realWriteHead.call(res, status, ...rest)
1330
1331
  }
1331
1332
  }
1333
+ /**
1334
+ * A held 404 can still be written to in pieces. Passing those pieces through would send the
1335
+ * 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.
1337
+ */
1338
+ const realWrite = res.write
1339
+ if (typeof realWrite === 'function') {
1340
+ res.write = (chunk, ...rest) => {
1341
+ if (held !== null) {
1342
+ if (typeof chunk === 'string' || Buffer.isBuffer(chunk)) parts.push(Buffer.from(chunk))
1343
+ return true
1344
+ }
1345
+ return realWrite.call(res, chunk, ...rest)
1346
+ }
1347
+ }
1332
1348
  if (typeof realEnd === 'function') {
1333
1349
  res.end = (...args) => {
1334
1350
  if (held !== null) {
1335
- body = typeof args[0] === 'string' || Buffer.isBuffer(args[0]) ? args[0] : ''
1351
+ const last = args[0]
1352
+ if (typeof last === 'string' || Buffer.isBuffer(last)) parts.push(Buffer.from(last))
1353
+ body = parts.length === 0 ? '' : Buffer.concat(parts)
1336
1354
  return res
1337
1355
  }
1338
1356
  return realEnd.apply(res, args)
@@ -1346,6 +1364,7 @@ function serveBundlesAtCurrentRevision(ctx) {
1346
1364
  } finally {
1347
1365
  try {
1348
1366
  res.writeHead = realWriteHead
1367
+ if (realWrite !== undefined) res.write = realWrite
1349
1368
  res.end = realEnd
1350
1369
  } catch {
1351
1370
  /* the response object owns its own shape */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unlag",
3
- "version": "1.9.51",
3
+ "version": "1.9.53",
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",
@@ -15,6 +15,7 @@
15
15
  "client.js",
16
16
  "cordis.patch.yml",
17
17
  "locale/*.json",
18
+ "scripts/live-smoke.mjs",
18
19
  "README.md",
19
20
  "icon.svg"
20
21
  ],
@@ -0,0 +1,105 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { runInNewContext } from 'node:vm'
3
+
4
+ // The shipped page half, the shipped host half, and the running app: this smoke asks the real
5
+ // button whether it could rebuild right now, using the startup list the app really renders, and
6
+ // prints the answer. It never presses: only a person may swap plugin code. A host that is not
7
+ // listening is a skip, so the script is safe to run anywhere the app is not.
8
+ const HOST = process.env.DSH_UNLAG_HOST ?? 'http://127.0.0.1:52847'
9
+ const source = readFileSync(new URL('../client.js', import.meta.url), 'utf8')
10
+
11
+ async function json(path) {
12
+ const response = await fetch(`${HOST}${path}`, { headers: { 'x-dsh-unlag': '1' } })
13
+ if (!response.ok) throw new Error(`${path} answered ${response.status}`)
14
+ return response.json()
15
+ }
16
+
17
+ const stated = await json('/dsh-unlag/state').catch(() => null)
18
+ if (stated === null) {
19
+ console.log(`skip: no host at ${HOST}`)
20
+ process.exit(0)
21
+ }
22
+ const graph = (await json('/dsh-unlag/graph')).graph
23
+ const stale = (url) => String(url).replace(/rev=[0-9a-f]+/u, 'rev=0000000000ff')
24
+ const rows = [
25
+ { kind: 'script', placement: 'head', text: 'window.__ModuleLoader__={};' },
26
+ ...graph.batches.map((batch) => ({ kind: 'script-preload', src: stale(batch.url) })),
27
+ ...graph.batches.map((batch) => ({ kind: 'script-src', placement: 'head', src: stale(batch.url) })),
28
+ { kind: 'global', name: '__DSH_BOOT__', value: { ...graph, rev: '0000000000ff' } },
29
+ ]
30
+ const frozen = { ...graph, rev: '0000000000ff' }
31
+
32
+ const requests = []
33
+ const fetchForPage = async (url, init = {}) => {
34
+ const target = String(url).startsWith('/') ? `${HOST}${url}` : String(url)
35
+ requests.push(target.replace(HOST, ''))
36
+ return fetch(target, init)
37
+ }
38
+ const location = { href: `${HOST}/`, replace: () => { throw new Error('the smoke never navigates') } }
39
+ let registration
40
+ const window = {
41
+ __ModuleLoader__: { load: (value) => { registration = value } },
42
+ __DSH_UNLAG_PROBE__: true,
43
+ __DSH_BOOT__: frozen,
44
+ dshDesktopBoot: { ready: async () => ({ injections: rows }) },
45
+ sessionStorage: { getItem: () => '0', setItem() {} },
46
+ location,
47
+ dispatchEvent: () => true,
48
+ }
49
+ const sandbox = {
50
+ window,
51
+ navigator: { language: 'en-US', userAgent: 'Mozilla/5.0 (Macintosh) Electron/44.7.0' },
52
+ location,
53
+ document: { activeElement: null, querySelector: () => null },
54
+ sessionStorage: window.sessionStorage,
55
+ fetch: fetchForPage,
56
+ URL,
57
+ Response,
58
+ Buffer,
59
+ console,
60
+ setTimeout,
61
+ clearTimeout,
62
+ requestAnimationFrame: (fn) => fn(),
63
+ Event,
64
+ CustomEvent,
65
+ JSON,
66
+ RegExp,
67
+ }
68
+ sandbox.globalThis = sandbox
69
+ runInNewContext(source, sandbox)
70
+ const probe = registration.factory(() => ({ createElement: () => null })).probe
71
+
72
+ // The routes' own contract, proven against the running app: the two reads answer reads only, a
73
+ // press route without the page's header refuses, and the verify route answers a verdict. None of
74
+ // this swaps plugin code; the reload route is never called here.
75
+ const contract = []
76
+ const status = async (path, init) => {
77
+ try {
78
+ const response = await fetch(`${HOST}${path}`, init)
79
+ await response.arrayBuffer()
80
+ return response.status
81
+ } catch {
82
+ return 0
83
+ }
84
+ }
85
+ contract.push(['state GET', await status('/dsh-unlag/state'), 200])
86
+ contract.push(['state DELETE', await status('/dsh-unlag/state', { method: 'DELETE' }), 405])
87
+ contract.push(['graph PUT', await status('/dsh-unlag/graph', { method: 'PUT' }), 405])
88
+ contract.push(['reload without header', await status('/dsh-unlag/reload', { method: 'POST' }), 403])
89
+ contract.push(['verify without header', await status('/dsh-unlag/verify', { method: 'POST' }), 403])
90
+ const contractHeld = contract.every(([, got, want]) => got === want)
91
+
92
+ const running = { version: stated.version, code: stated.code, shim: stated.shim }
93
+ const verdict = await probe.startupLooksServable()
94
+ console.log(JSON.stringify({
95
+ host: HOST,
96
+ version: running.version,
97
+ shimWrapped: running.shim?.wrapped === true,
98
+ batches: graph.batches.length,
99
+ rows: rows.length,
100
+ verdict,
101
+ holdReason: verdict ? null : probe.startupHoldReason(),
102
+ asked: requests,
103
+ contract: Object.fromEntries(contract.map(([label, got, want]) => [label, `${got}${got === want ? '' : ` (wanted ${want})`}`])),
104
+ }, null, 2))
105
+ process.exitCode = verdict === true && contractHeld ? 0 : 1