dsh-unlag 1.9.64 → 1.9.66

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 +17 -6
  2. package/index.js +51 -33
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -15,8 +15,8 @@ window.__ModuleLoader__.load({
15
15
  * repeating one generic sentence.
16
16
  */
17
17
  const REASONS = zh
18
- ? { list: '读不到启动信息', slow: '启动信息读得太慢', row: '启动信息不完整', src: '启动脚本没有地址', graph: '已装插件清单读不到', plugin: '有插件已不在运行', script: '启动脚本读不到', empty: '启动脚本是空的', covers: '启动脚本缺插件', parse: '启动脚本内容不完整', size: '启动脚本太大', verify: '插件后端没有应答' }
19
- : { list: 'startup info is unavailable', slow: 'the startup list answered too slowly', 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', size: 'a startup script is too large', verify: 'the plugin is not answering' };
18
+ ? { list: '读不到启动信息', slow: '启动信息读得太慢', row: '启动信息不完整', src: '启动脚本没有地址', graph: '已装插件清单读不到', plugin: '有插件已不在运行', script: '启动脚本读不到', empty: '启动脚本是空的', covers: '启动脚本缺插件', parse: '启动脚本内容不完整', size: '启动脚本太大', verify: '插件后端没有应答', screen: '页面状态读不到' }
19
+ : { list: 'startup info is unavailable', slow: 'the startup list answered too slowly', 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', size: 'a startup script is too large', verify: 'the plugin is not answering', screen: 'the page state cannot be read' };
20
20
 
21
21
  /** The last reason a readiness check refused for, or null after a check that passed. */
22
22
  let holdReason = null;
@@ -611,10 +611,21 @@ window.__ModuleLoader__.load({
611
611
  */
612
612
  async function prepareInterface() {
613
613
  if (!(await startupLooksServable())) return 'blocked';
614
- if (screenHasOpenWindow()) return 'dialog';
615
- await commitFocusedField();
616
- const ready = await otherPluginsAreReady();
617
- if (!ready || stillHeldBack()) return 'unsaved';
614
+ /**
615
+ * Everything below reads the page itself: the document, the events it answers, the fields it
616
+ * holds. A page that cannot be read is not a page that can be left running behind a fresh
617
+ * document, and a throw here would end the press as a failure with no reason on screen, so
618
+ * an unreadable page holds the rebuild and says so.
619
+ */
620
+ try {
621
+ if (screenHasOpenWindow()) return 'dialog';
622
+ await commitFocusedField();
623
+ const ready = await otherPluginsAreReady();
624
+ if (!ready || stillHeldBack()) return 'unsaved';
625
+ } catch {
626
+ held('screen');
627
+ return 'blocked';
628
+ }
618
629
  return null;
619
630
  }
620
631
 
package/index.js CHANGED
@@ -1754,6 +1754,45 @@ export async function verifyStartup(clientModules, rows) {
1754
1754
  return verifyStartupRows(graph, (url) => readStartupScript(clientModules, url), rows)
1755
1755
  }
1756
1756
 
1757
+ /** What one press is told when the update it waits for did not answer in time. */
1758
+ const STILL_APPLYING = Object.freeze({ status: 'applying', warnings: [], pending: [], restartReason: null, failedEntries: [] });
1759
+
1760
+ /**
1761
+ * The answer a press waits for, bounded in time. The update is asked for at most once and its
1762
+ * answer is settled the same way whether it is a promise, a plain value, or a promise that cannot
1763
+ * even be awaited: the page is always told one status, and nothing a plugin swap answers can turn
1764
+ * the route into an error.
1765
+ * @returns {{ outcome: object, timeRanOut: boolean, work: Promise<unknown> }}
1766
+ */
1767
+ export async function answerReload(controller, limitMs) {
1768
+ let work = null
1769
+ try {
1770
+ work = Promise.resolve(controller.reloadNow())
1771
+ } catch (error) {
1772
+ log(`a press could not be started: ${error?.message ?? error}`)
1773
+ return { outcome: { status: 'failed', warnings: [], pending: [], restartReason: null, failedEntries: [] }, timeRanOut: false, work: Promise.resolve() }
1774
+ }
1775
+ let timer = null
1776
+ let timeRanOut = false
1777
+ const ranOut = new Promise((resolve) => {
1778
+ timer = setTimeout(() => {
1779
+ timeRanOut = true
1780
+ resolve(STILL_APPLYING)
1781
+ }, limitMs)
1782
+ })
1783
+ try {
1784
+ const outcome = await Promise.race([work.catch(() => null), ranOut])
1785
+ // Reading the status can throw too; an answer that cannot be read is not one the page can use.
1786
+ try {
1787
+ if (outcome !== null && typeof outcome === 'object' && typeof outcome.status === 'string') return { outcome, timeRanOut, work }
1788
+ } catch {}
1789
+ // An update that answers nothing usable is still a press that has to be answered.
1790
+ return { outcome: { status: 'failed', warnings: [], pending: [], restartReason: null, failedEntries: [] }, timeRanOut, work }
1791
+ } finally {
1792
+ if (timer !== null) clearTimeout(timer)
1793
+ }
1794
+ }
1795
+
1757
1796
  /**
1758
1797
  * The entry points the header button calls. Each answers with the current switches so the page
1759
1798
  * never guesses: with both off it does nothing, and a turn still running is reported rather than
@@ -1888,42 +1927,21 @@ function serveButton(ctx, controller, config) {
1888
1927
  if (buttonAllowed(req) === false) return sendJson(res, 403, { status: 'forbidden' })
1889
1928
  if (!settings.reloadPlugins) return sendJson(res, 200, { status: 'off' })
1890
1929
  const limit = Number(process.env.DSH_UNLAG_ANSWER_MS ?? '') || RELOAD_ANSWER_MS
1891
- let timer = null
1892
- let timedOut = false
1893
- const stillApplying = new Promise((resolve) => {
1894
- timer = setTimeout(() => {
1895
- // Only the timer's own answer means the update outlasted the page: an update that
1896
- // says "applying" itself is a press that arrived during another one, not a slow one.
1897
- timedOut = true
1898
- resolve({ status: 'applying', warnings: [], pending: [], restartReason: null, failedEntries: [] })
1899
- }, limit)
1900
- })
1901
- let answeredEarly = false
1902
- try {
1903
- const work = controller.reloadNow()
1904
- /**
1905
- * The answer the page waits for is bounded, but the update is not. Whatever the update
1906
- * did after the page was told it is still applying is written down here: a replacement
1907
- * that failed a moment later must not look like one that succeeded.
1908
- */
1930
+ const { outcome, timeRanOut, work } = await answerReload(controller, limit)
1931
+ /**
1932
+ * The answer the page waits for is bounded, but the update is not. Whatever the update
1933
+ * did after the page was told it is still applying is written down here: a replacement
1934
+ * that failed a moment later must not look like one that succeeded.
1935
+ */
1936
+ if (outcome.status === 'applying') {
1937
+ if (timeRanOut) log('the update outlasted the answer the page waits for: the press is told it is still applying')
1938
+ else log('a press arrived while an update is already applying: the press is told it is still applying')
1909
1939
  work.then(
1910
- (late) => {
1911
- if (answeredEarly) log(`the update finished after the page was told it is still applying: ${String(late?.status ?? 'no status')}`)
1912
- },
1913
- (error) => {
1914
- if (answeredEarly) log(`the update failed after the page was told it is still applying: ${error?.message ?? error}`)
1915
- },
1940
+ (late) => log(`the update finished after the page was told it is still applying: ${String(late?.status ?? 'no status')}`),
1941
+ (error) => log(`the update failed after the page was told it is still applying: ${error?.message ?? error}`),
1916
1942
  )
1917
- const outcome = await Promise.race([work, stillApplying])
1918
- if (outcome?.status === 'applying') {
1919
- answeredEarly = true
1920
- if (timedOut) log('the update outlasted the answer the page waits for: the press is told it is still applying')
1921
- else log('a press arrived while an update is already applying: the press is told it is still applying')
1922
- }
1923
- sendJson(res, 200, outcome)
1924
- } finally {
1925
- if (timer !== null) clearTimeout(timer)
1926
1943
  }
1944
+ sendJson(res, 200, outcome)
1927
1945
  } catch (error) {
1928
1946
  log(`cannot apply a plugin reload: ${error?.message ?? error}`)
1929
1947
  sendJson(res, 500, { status: 'failed', detail: String(error?.message ?? error) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unlag",
3
- "version": "1.9.64",
3
+ "version": "1.9.66",
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",