@arcaneorion/dsh-model-channel-manager 0.3.13 → 0.3.15
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 +57 -0
- package/package.json +3 -2
- package/src/client.js +70 -20
- package/src/index.js +16 -1
package/README.md
CHANGED
|
@@ -187,6 +187,33 @@ dsh plugin --profile web add @arcaneorion/dsh-model-channel-manager
|
|
|
187
187
|
|
|
188
188
|
host 侧兜底:`rewireRoutes` 发现配置组数 > 实际路由数时 `console.warn` 列出被丢弃的组 ID(此前是静默丢弃——H08:3 组保存、路由只有 1 条,界面仍显示「已保存」)。允许清空全部组(空列表合法)。
|
|
189
189
|
|
|
190
|
+
## 保存时的 revision 冲突(0.3.14:自噪声识别 + 自动重试)
|
|
191
|
+
|
|
192
|
+
**症状**:保存后提示「部分保存:提供商配置 已生效;轮询组配置 失败——轮询组配置已被其他页面修改(版本冲突),请点「刷新」后重试」,且要点多次才成功。
|
|
193
|
+
|
|
194
|
+
**根因**(dsh-settings 源码事实):`describe()` 里
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
raw = JSON.stringify([fiber.uid, schema.toJSON(), entry.options.config ?? {}])
|
|
198
|
+
revision = previous.revision + Number(previous.raw !== raw)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**revision 就是「该行 raw config 的 JSON 指纹」**。而本插件的 settings 行同时承载健康投影——宿主每 5s 的 digest 叶写、`runtime`、`testResults` 都会改动该行 raw config,revision 因此不断前进。client 加载时记下的 revision 在几秒内必然过期,保存被 `settings/conflict` 拒绝:**提示里的「其他页面」其实是插件自己写的健康数据**。providers 走的是另一个行(`llm-pi-ai`),没有这种自噪声,所以它总是先成功——这正是「部分保存」的原因。
|
|
202
|
+
|
|
203
|
+
**修法**:
|
|
204
|
+
|
|
205
|
+
- client:写入前**重新读取 revision**(消除过期);仍冲突时比较远端值与我们加载时的基线
|
|
206
|
+
(`groups`+`providerOrder` / `providers`,用键序无关的规范 JSON 比较)——
|
|
207
|
+
**远端没变 = 自噪声 → 用新 revision 重试(最多 3 次);远端真的变了 → 才报冲突**。
|
|
208
|
+
既消除误报,也保留真正的多页面冲突保护(审计 F05 的语义)。
|
|
209
|
+
- host:`digest` 内容未变时不写(60s 心跳保底),从源头减少 revision 噪声。
|
|
210
|
+
|
|
211
|
+
回归:`tests/revision-conflict.test.cjs`。
|
|
212
|
+
|
|
213
|
+
> 真正的根治是把健康/任务通道搬出 settings 行(审计建议的 Remote + storageDomain 路线)——
|
|
214
|
+
> 那时本行 revision 只随用户配置变化,连重试都不需要。当前修法在不改通信架构的前提下
|
|
215
|
+
> 同时消除了症状与误报。
|
|
216
|
+
|
|
190
217
|
## 密钥写入(凭据引用虚拟化)
|
|
191
218
|
|
|
192
219
|
- 面板主视图只出现「API Key」输入框:**粘贴或输入后失焦即自动写入** DSH 凭据存储(`~/.dsh/.credentials.yaml`,0600,write-only 读不回),无手动按钮;清空输入框不会删除已存 key。上游 llm-pi-ai 的供应商 profile 只有 `apiKeyEnv` 一个密钥字段(凭据引用名),不存在内联 key 的选项——secrets 不进 settings.yaml、不随 `settings.describe` 下发,是有意的安全设计。
|
|
@@ -279,6 +306,32 @@ react 经 `require('react')`;样式用 `ctx.effect` 自管理;`dsh.client: {
|
|
|
279
306
|
- 回归:`tests/timeout-abort.test.cjs`(契约断言 + 挂起流行为级验证——3s 关闭预算内
|
|
280
307
|
完成,不等满 5s 挂起)
|
|
281
308
|
|
|
309
|
+
## 引擎集成测试(0.3.15:真正跑一遍 apply → adapter → 引擎)
|
|
310
|
+
|
|
311
|
+
此前所有引擎测试都是**源级断言或逻辑复刻**,抓不到「作用域/引用」类缺陷:0.3.5 的 F13
|
|
312
|
+
重构把 `let cursor = …` 改成两个分支内赋值却丢了声明,ESM 严格模式下每次走虚拟路由都抛
|
|
313
|
+
`ReferenceError: cursor is not defined`(用户看到的「本轮运行失败」),而当时 32 个测试全绿。
|
|
314
|
+
|
|
315
|
+
`tests/engine-integration.test.cjs` 用最小 Cordis ctx 替身(只实现插件用到的 8 个面,
|
|
316
|
+
`ctx.timeout` 按 `cordis-plugin-timer` 的真实双形态实现)真正执行:
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
apply(ctx, Config({...})) → 捕获 llm.registerAdapter 的 adapter
|
|
320
|
+
→ adapter.stream({provider: 'roundrobin/<组>'}) → 断言真实 llm.stream 调用序列
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
覆盖:路由分发、round-robin 指针原子预留(F13 并发均分)、单候选重试与 failover 顺序、
|
|
324
|
+
`NO_ADAPTER` 终止块、`prepareCall` 快照路径、effect disposer 可执行(卸载不留悬挂 interval)。
|
|
325
|
+
**验证过它抓得住原 bug**:临时删掉 `let cursor = 0;` → 7 项里 5 项立即失败。
|
|
326
|
+
|
|
327
|
+
另加静态守卫 `tests/no-undeclared-assignment.test.cjs`:用 acorn 解析两个源文件,
|
|
328
|
+
找出所有「赋值给从未声明标识符」的目标(F18 的 `idx`、这次的 `cursor` 都是这一类)。
|
|
329
|
+
这是文件级 no-undef(比词法作用域宽松,宁可漏报不误报),恰好覆盖最危险的形态。
|
|
330
|
+
|
|
331
|
+
> 顺带观察(非缺陷):`speedTest.enabled` 默认开启,所以**组内第一次请求会后台触发一次
|
|
332
|
+
> 全候选测速**(每个候选一次真实请求)。UI 文案是「开启自动测速」,语义一致;不想付这份
|
|
333
|
+
> 额度就在组设置里取消勾选。
|
|
334
|
+
|
|
282
335
|
## 踩坑速记(本项目,按严重程度)
|
|
283
336
|
|
|
284
337
|
1. **settings 服务异步初始化**:host 插件 `ctx.get('settings')` 在 apply 时为 undefined → 整个引擎静默不生效(无报错、无命名空间)。必须 `ctx.inject(['settings'], ...)`。
|
|
@@ -316,6 +369,10 @@ react 经 `require('react')`;样式用 `ctx.effect` 自管理;`dsh.client: {
|
|
|
316
369
|
31. **超时守卫 dispose 时必须 settle promise(0.3.13,审计 R3)**:`timed()` 的 guard 若在 fiber dispose 时只 `clearTimeout`,promise 永不 reject——`await Promise.race([inner.next(), guard.promise])` 就永久挂住,在飞测试既不结束也不写终态,客户端只能等到 66s 假超时(表现与「host 未处理」一模一样)。**规则:任何挂在 `ctx.effect` 上的定时器,dispose 时不仅要清定时器,还要让等待它的 promise settle**(这里 reject `ABORTED`),否则卸载路径会留下永久悬挂的 await。配套:新进程启动时清扫上次遗留的 `running` 条目(补 `ABORTED` 终态)——进程内在飞任务都有 fiber 生命周期,重启后见到的 running 一定是遗留的。回归:`tests/runtime-hardening.test.cjs`。
|
|
317
370
|
32. **静默失败要有出口(0.3.13,审计 R2)**:loader 的 `_commitVolatile` 在 `resolveConfig` 抛错时只 `logger.warn` 后返回 true——document 已落盘、fiber 引用不更新、**不发 `loader/volatile-update`**。而本插件的任务通道只在 volatile-update 里消费请求,于是请求被彻底忽略、客户端 66s 超时,宿主侧却「什么都没发生」。同理 `.loose(true)` 的 schema 兜底会把整棵 volatile 子树换成空对象而不报错(踩坑 28)。**规则:凡是「静默丢弃用户动作」的路径都要有可见出口**——① 未消费的请求打日志(每 nonce 一次);② 周期性兜底重试(5s,复用同一消费判定,每 nonce 一次);③ 关键子树从有到无时告警。回归:`tests/runtime-hardening.test.cjs`。
|
|
318
371
|
|
|
372
|
+
33. **settings 的 revision 是「raw config 的 JSON 指纹」,会被插件自己的健康写入推高(0.3.14)**:`describe()` 里 `revision += raw !== previous.raw`,`raw = JSON.stringify([fiber.uid, schema.toJSON(), entry.options.config])`。本插件的行同时承载健康投影(digest 每 5s、runtime、testResults),所以 client 手里的 revision 几秒内必然过期,保存被 `settings/conflict` 拒绝,提示却是「已被其他页面修改」——**其实「其他页面」就是插件自己**;providers 在另一个行(llm-pi-ai)没有自噪声,于是表现为「部分保存:提供商成功、轮询组失败,点多次才成功」。两条教训:① **带 revision 的写入必须「写入前重读」**,加载时记下的 revision 只在「该行没有其他写入者」时才有效;② 冲突要**分类**——比较远端值与加载基线(键序无关的规范 JSON),远端没变就是自噪声(重试),真的变了才是冲突(报错)。根治方向是把高频数据搬出该行(Remote/storageDomain)。回归:`tests/revision-conflict.test.cjs`。
|
|
373
|
+
|
|
374
|
+
34. **重构时把「声明 + 赋值」拆成分支内赋值,却丢了声明(0.3.15,用户看到的「本轮运行失败 cursor is not defined」)**:F13 把 `let cursor = strategy === 'primary' ? 0 : …` 改成 `if (round-robin) { cursor = …; } else { cursor = …; }` —— 两处赋值都在,**声明没了**。ESM 恒为严格模式,赋值未声明标识符直接抛 `ReferenceError`,于是**每一次走虚拟路由的请求都失败**;更糟的是这个错误只有真正跑到那一行才暴露:注册、目录、模型菜单、源级测试全部正常(32 项全绿)。教训:① **局部变量改写分支结构时,声明必须留在分支之外**(`let x;` + 分支内只赋值);② **引擎需要「真跑一遍」的集成测试**——源级断言/逻辑复刻挡不住这类缺陷,见 `tests/engine-integration.test.cjs`;③ 加一道静态守卫:acorn 扫「赋值给未声明标识符」(`tests/no-undeclared-assignment.test.cjs`),同一类缺陷的 F18(`idx`)也会被它抓住。
|
|
375
|
+
|
|
319
376
|
## 0.3.11 修复后的自愈路径(无需手工清库)
|
|
320
377
|
|
|
321
378
|
重启后:schema 接受历史字符串 nonce → `records` 重新可见 → `migrateFrom` 把旧流水**合并去重**进 domain(`my-opencode-go` 桶已有新事件,按 `ts|provider|model` 去重后并入)→ path-ops 真删除 settings 里的 `records` → digest 重算包含历史。日志会打印 `legacy health imported into domain and cleared from settings`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcaneorion/dsh-model-channel-manager",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"yaml": "^2.9.0"
|
|
40
|
+
"yaml": "^2.9.0",
|
|
41
|
+
"acorn": "^8.18.0"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"@deepseek-ai/cordis": "^4.0.4",
|
package/src/client.js
CHANGED
|
@@ -1435,6 +1435,46 @@ window.__ModuleLoader__.load({
|
|
|
1435
1435
|
if (r && r.ok === false) throw new Error((r.error && (r.error.message || r.error)) || 'request failed')
|
|
1436
1436
|
return r && r.value !== undefined ? r.value : r
|
|
1437
1437
|
}
|
|
1438
|
+
// 冲突判定:宿主以 settings/conflict 拒绝(dsh-settings 的 SettingsConflictError)
|
|
1439
|
+
const isConflict = (err) => !!(err && (err.code === 'settings/conflict' || /conflict/i.test(String(err.message || ''))))
|
|
1440
|
+
// 规范 JSON(对象键排序)——用于「远端是否真的改过我们关心的字段」的比较,
|
|
1441
|
+
// 不受键序影响(providers/groups 的对象键序在落盘与投影之间可能不同)
|
|
1442
|
+
const canonicalJson = (v) => {
|
|
1443
|
+
if (Array.isArray(v)) return '[' + v.map(canonicalJson).join(',') + ']'
|
|
1444
|
+
if (v && typeof v === 'object') return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonicalJson(v[k])).join(',') + '}'
|
|
1445
|
+
return JSON.stringify(v === undefined ? null : v)
|
|
1446
|
+
}
|
|
1447
|
+
const sameJson = (a, b) => canonicalJson(a) === canonicalJson(b)
|
|
1448
|
+
const readNsDesc = async (ns) => {
|
|
1449
|
+
const resp = await apiRef.settings.describe({})
|
|
1450
|
+
const d = unwrap(resp)
|
|
1451
|
+
return ((d && d.namespaces) || []).find((n) => n && n.ns === ns) || null
|
|
1452
|
+
}
|
|
1453
|
+
// 带 revision 的保存 + 「自噪声冲突」自动重试(0.3.14):
|
|
1454
|
+
//
|
|
1455
|
+
// 为什么必须这样:本插件的 settings 行(model-channel-manager)同时承载健康投影,
|
|
1456
|
+
// 宿主每 5s 的 digest 叶写都会改动该行的 raw config;而 describe() 的 revision
|
|
1457
|
+
// 正是「raw config 的 JSON 指纹」(dsh-settings:revision += raw !== previous.raw)。
|
|
1458
|
+
// 于是 client 手里那份 revision 在几秒内必然过期,保存被 settings/conflict 拒绝——
|
|
1459
|
+
// 提示里的「其他页面修改」其实是插件自己写的健康数据。
|
|
1460
|
+
//
|
|
1461
|
+
// 策略:① 每次写入前取最新 revision(消除过期);② 仍冲突时比较远端值与我们加载时
|
|
1462
|
+
// 的基线:没变 = 纯 revision 噪声 → 用新 revision 重试;真的变了 = 真冲突 → 报错。
|
|
1463
|
+
const saveWithRevisionRetry = async ({ ns, baseline, remoteOf, write, conflictMessage, attempts = 3 }) => {
|
|
1464
|
+
let lastError = null
|
|
1465
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1466
|
+
const desc = await readNsDesc(ns).catch(() => null)
|
|
1467
|
+
const rev = desc && typeof desc.revision === 'number' ? desc.revision : undefined
|
|
1468
|
+
const r = await write(rev)
|
|
1469
|
+
if (!(r && r.ok === false)) return r
|
|
1470
|
+
lastError = r.error
|
|
1471
|
+
if (!isConflict(r.error)) throw new Error((r.error && (r.error.message || r.error)) || (ns + ' save failed'))
|
|
1472
|
+
const now = await readNsDesc(ns).catch(() => null)
|
|
1473
|
+
if (!sameJson(remoteOf(now), baseline)) throw new Error(conflictMessage)
|
|
1474
|
+
// 远端未变:属于插件自身健康写入造成的 revision 噪声 → 取新 revision 重试
|
|
1475
|
+
}
|
|
1476
|
+
throw new Error(conflictMessage.replace(/,请点「刷新」后重试/, '') + '(已自动重试 ' + attempts + ' 次仍未成功)' + (lastError ? ':' + String(lastError.message || lastError) : ''))
|
|
1477
|
+
}
|
|
1438
1478
|
|
|
1439
1479
|
// 按持久化的 providerOrder 重排 providers 的键序:settings-file 的 patchNode
|
|
1440
1480
|
// 对 map 键序盲(拖拽纯重排在文件层是零 diff),顺序由 model-channels ns 里的
|
|
@@ -1654,33 +1694,43 @@ window.__ModuleLoader__.load({
|
|
|
1654
1694
|
for (const k of allKeys) ops.push({ op: 'unset', path: ['providers', k] })
|
|
1655
1695
|
for (const k of keys) ops.push({ op: 'set', path: ['providers', k], value: cleanProviders[k] })
|
|
1656
1696
|
if (ops.length === 0) return Promise.resolve()
|
|
1657
|
-
// F05:带 revision
|
|
1658
|
-
//
|
|
1659
|
-
return
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1697
|
+
// F05 + 0.3.14:带 revision 保存;冲突若只是宿主侧 revision 噪声(远端 providers
|
|
1698
|
+
// 与我们加载时一致)则自动重试,真的被别处改过才提示「刷新后重试」。
|
|
1699
|
+
return saveWithRevisionRetry({
|
|
1700
|
+
ns: 'llm-pi-ai',
|
|
1701
|
+
baseline: (state && state.providers) || {},
|
|
1702
|
+
remoteOf: (desc) => (desc && desc.value && desc.value.providers) || {},
|
|
1703
|
+
write: (rev) => apiRef.settings.mutate({ ns: 'llm-pi-ai', ops, expectedRevision: rev }).then((resp) => {
|
|
1704
|
+
const r = resp && resp.result ? resp.result : resp
|
|
1705
|
+
return r
|
|
1706
|
+
}),
|
|
1707
|
+
conflictMessage: 'llm-pi-ai 配置已被其他页面修改(版本冲突),请点「刷新」后重试',
|
|
1666
1708
|
})
|
|
1667
1709
|
})() : Promise.resolve()
|
|
1668
1710
|
// 顺序持久化:patchNode 对 map 键序盲,拖拽顺序写进 providerOrder 数组(同一次 update 落盘);
|
|
1669
1711
|
// 即使 channelsDraft 为空(无轮询组),只要 draft 非空也要写——这是排序的唯二持久化时机
|
|
1670
1712
|
const orderPatch = draft ? { providerOrder: Object.keys(cleanProviders) } : {}
|
|
1671
|
-
const
|
|
1672
|
-
ns: 'model-channels',
|
|
1673
|
-
expectedRevision: (state && state.nsRevisions && state.nsRevisions.channels) || undefined,
|
|
1713
|
+
const channelsPatch = Object.assign({
|
|
1674
1714
|
// 命名单一身份:写入时统一 virtualModel.name = 组 id——旧的独立呈现名
|
|
1675
1715
|
// (如遗留的 group-1)在下一次保存时自动归一,无需迁移
|
|
1676
|
-
|
|
1677
|
-
})
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1716
|
+
groups: (channelsDraft || []).map((g) => Object.assign({}, g, { virtualModel: Object.assign({}, g.virtualModel, { name: g.id }) }))
|
|
1717
|
+
}, orderPatch)
|
|
1718
|
+
// 0.3.14:本行同时承载健康投影,宿主每 5s 的 digest 叶写会不断推高它的 revision
|
|
1719
|
+
// (revision = raw config 的 JSON 指纹)——旧实现用加载时的旧 revision,几乎必然
|
|
1720
|
+
// 撞 settings/conflict,于是出现「部分保存:轮询组配置失败……点击多次才能成功」。
|
|
1721
|
+
// 现在每次写入前取最新 revision;冲突时只有远端 groups/providerOrder 真的变了才报冲突。
|
|
1722
|
+
const p2 = saveWithRevisionRetry({
|
|
1723
|
+
ns: 'model-channels',
|
|
1724
|
+
baseline: { groups: channels || [], providerOrder: (state && state.providerOrder) || [] },
|
|
1725
|
+
remoteOf: (desc) => ({
|
|
1726
|
+
groups: (desc && desc.value && desc.value.groups) || [],
|
|
1727
|
+
providerOrder: (desc && desc.value && desc.value.providerOrder) || [],
|
|
1728
|
+
}),
|
|
1729
|
+
write: (rev) => apiRef.settings.update({ ns: 'model-channels', expectedRevision: rev, patch: channelsPatch }).then((resp) => {
|
|
1730
|
+
const r = resp && resp.result ? resp.result : resp
|
|
1731
|
+
return r
|
|
1732
|
+
}),
|
|
1733
|
+
conflictMessage: '轮询组配置已被其他页面修改(版本冲突),请点「刷新」后重试',
|
|
1684
1734
|
})
|
|
1685
1735
|
// F06(审计 C10,0.3.8):两域用 allSettled 而非 all——半成功不再「整单失败」
|
|
1686
1736
|
// 掩盖已提交的那半。各自报告,失败域给出明确指引;部分成功时提示里列明
|
package/src/index.js
CHANGED
|
@@ -628,6 +628,10 @@ export function apply(ctx, config) {
|
|
|
628
628
|
// 推进 rt.currentIndex,两个并发请求都从 first 开始(并发不均分)。
|
|
629
629
|
// 现在:选定时立即占位推进(同步读改写,JS 单线程内原子),失败不回滚
|
|
630
630
|
// (失败反馈走冷却,指针保持前进语义——轮询分布比严格顺序更重要)。
|
|
631
|
+
// 0.3.15:`cursor` 必须先声明——0.3.5 重构时把原来的 `let cursor = …`
|
|
632
|
+
// 改成了分支内赋值却没保留声明,ESM 严格模式下每次走虚拟路由都抛
|
|
633
|
+
// `ReferenceError: cursor is not defined`(本轮运行失败)。
|
|
634
|
+
let cursor = 0;
|
|
631
635
|
if (strategy === 'round-robin') {
|
|
632
636
|
cursor = rt.currentIndex % order.length;
|
|
633
637
|
rt.currentIndex = (cursor + 1) % order.length; // 本次请求已占用 cursor 槽位
|
|
@@ -1177,6 +1181,8 @@ export function apply(ctx, config) {
|
|
|
1177
1181
|
// digest:写进 settings health 子树的小投影(client 健康页渲染用)。
|
|
1178
1182
|
// 聚合上移 host,client 不再拉原始流水全量 describe——settings 里不再出现 records。
|
|
1179
1183
|
let digestTimer = null;
|
|
1184
|
+
let lastDigestJson = null; // 上次落盘的 digest 内容(相同则跳过,减少 revision 噪声)
|
|
1185
|
+
let lastDigestWriteAt = 0;
|
|
1180
1186
|
const DIGEST_INTERVAL = 5000;
|
|
1181
1187
|
const buildDigest = () => {
|
|
1182
1188
|
// 与旧 client HealthPanel 聚合口径一致:total/success/ttft/latency/token(计费口径
|
|
@@ -1220,7 +1226,16 @@ export function apply(ctx, config) {
|
|
|
1220
1226
|
const flushDigest = () => {
|
|
1221
1227
|
digestTimer = null;
|
|
1222
1228
|
if (bus !== null) {
|
|
1223
|
-
|
|
1229
|
+
const digest = buildDigest();
|
|
1230
|
+
// 0.3.14:内容没变就不写。settings 行的 revision 是 raw config 的 JSON 指纹
|
|
1231
|
+
// (dsh-settings describe:revision += raw !== previous.raw),每一次无意义的写
|
|
1232
|
+
// 都会让 client 手里的 revision 过期,表现为保存时的 settings/conflict。
|
|
1233
|
+
// 60s 心跳保证 digestAt 不会永远停住。
|
|
1234
|
+
const json = JSON.stringify(digest);
|
|
1235
|
+
if (json === lastDigestJson && Date.now() - lastDigestWriteAt < 60000) return;
|
|
1236
|
+
lastDigestJson = json;
|
|
1237
|
+
lastDigestWriteAt = Date.now();
|
|
1238
|
+
bus.writeHealth({ digest, digestAt: Date.now() }).catch(() => { });
|
|
1224
1239
|
}
|
|
1225
1240
|
};
|
|
1226
1241
|
const scheduleDigest = () => {
|