dsh-zen-remote 1.1.16 → 2.0.0
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/AGENTS.md +54 -9
- package/README.md +249 -190
- package/README.zh-CN.md +247 -201
- package/assets/home.png +0 -0
- package/assets/info.png +0 -0
- package/assets/pairing.png +0 -0
- package/assets/session.png +0 -0
- package/assets/sheet.png +0 -0
- package/cordis.patch.yml +12 -11
- package/cordis.patch.yml.example +24 -9
- package/docs/interface.md +57 -2
- package/docs/remote-access.en.md +99 -58
- package/docs/remote-access.md +99 -42
- package/dsh-push.mjs +67 -54
- package/lan-gate.mjs +88 -20
- package/lib/activity.js +168 -0
- package/lib/activity.js.map +1 -0
- package/lib/admin-routes.js +526 -0
- package/lib/admin-routes.js.map +1 -0
- package/lib/client-pairing.js +224 -0
- package/lib/client-pairing.js.map +1 -0
- package/lib/client-routes.js +674 -0
- package/lib/client-routes.js.map +1 -0
- package/lib/client.js +10051 -4777
- package/lib/config.js +346 -0
- package/lib/config.js.map +1 -0
- package/lib/fetch-route-intercept.js +418 -0
- package/lib/fetch-route-intercept.js.map +1 -0
- package/lib/fingerprint.js +363 -0
- package/lib/fingerprint.js.map +1 -0
- package/lib/http.js +69 -0
- package/lib/http.js.map +1 -0
- package/lib/index.js +675 -54
- package/lib/index.js.map +1 -1
- package/lib/intercept-shape.js +81 -0
- package/lib/intercept-shape.js.map +1 -0
- package/lib/intercept.js +3040 -0
- package/lib/intercept.js.map +1 -0
- package/lib/lan-gate-server.cjs +532 -121
- package/lib/merge-streams.js +1165 -0
- package/lib/merge-streams.js.map +1 -0
- package/lib/relay-access.js +496 -0
- package/lib/relay-access.js.map +1 -0
- package/lib/relay-client.js +1303 -0
- package/lib/relay-client.js.map +1 -0
- package/lib/relay-filter.js +282 -0
- package/lib/relay-filter.js.map +1 -0
- package/lib/relay-server.js +1792 -0
- package/lib/relay-server.js.map +1 -0
- package/lib/restart-fields.js +34 -0
- package/lib/restart-fields.js.map +1 -0
- package/lib/restart-watcher.js +106 -0
- package/lib/restart-watcher.js.map +1 -0
- package/lib/session-reference.js +157 -0
- package/lib/session-reference.js.map +1 -0
- package/lib/share-export.js +16 -1
- package/lib/share-export.js.map +1 -1
- package/lib/share-ops.js +86 -0
- package/lib/share-ops.js.map +1 -0
- package/lib/share-store.js +294 -0
- package/lib/share-store.js.map +1 -0
- package/lib/types/activity.d.ts +99 -0
- package/lib/types/activity.d.ts.map +1 -0
- package/lib/types/admin-routes.d.ts +129 -0
- package/lib/types/admin-routes.d.ts.map +1 -0
- package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -1
- package/lib/types/client/MobileHome.d.ts +1 -1
- package/lib/types/client/MobileHome.d.ts.map +1 -1
- package/lib/types/client/MobileHomeChips.d.ts +1 -1
- package/lib/types/client/MobileHomeChips.d.ts.map +1 -1
- package/lib/types/client/MobileSessionHeader.d.ts +13 -2
- package/lib/types/client/MobileSessionHeader.d.ts.map +1 -1
- package/lib/types/client/MobileSessionInfo.d.ts +20 -3
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
- package/lib/types/client/RemoteComposerBanner.d.ts +42 -0
- package/lib/types/client/RemoteComposerBanner.d.ts.map +1 -0
- package/lib/types/client/RemoteHeaderIcon.d.ts +37 -0
- package/lib/types/client/RemoteHeaderIcon.d.ts.map +1 -0
- package/lib/types/client/RemoteShareMenu.d.ts +28 -0
- package/lib/types/client/RemoteShareMenu.d.ts.map +1 -0
- package/lib/types/client/RemoteStatusIcon.d.ts +36 -0
- package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -0
- package/lib/types/client/compat/desktop.d.ts +21 -0
- package/lib/types/client/compat/desktop.d.ts.map +1 -0
- package/lib/types/client/compat/types.d.ts +57 -0
- package/lib/types/client/compat/types.d.ts.map +1 -1
- package/lib/types/client/current-session-report.d.ts +37 -0
- package/lib/types/client/current-session-report.d.ts.map +1 -0
- package/lib/types/client/effects/header-status.d.ts.map +1 -1
- package/lib/types/client/effects/keyboard-guard.d.ts.map +1 -1
- package/lib/types/client/effects/permission-sheet.d.ts +39 -0
- package/lib/types/client/effects/permission-sheet.d.ts.map +1 -0
- package/lib/types/client/effects/remote-session.d.ts +25 -0
- package/lib/types/client/effects/remote-session.d.ts.map +1 -0
- package/lib/types/client/effects/turn-fold.d.ts +9 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +187 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/remote-fetch.d.ts +51 -0
- package/lib/types/client/remote-fetch.d.ts.map +1 -0
- package/lib/types/client/remote-group-expansion.d.ts +41 -0
- package/lib/types/client/remote-group-expansion.d.ts.map +1 -0
- package/lib/types/client/remote-share-css.d.ts +19 -0
- package/lib/types/client/remote-share-css.d.ts.map +1 -0
- package/lib/types/client/remote-share-register.d.ts +113 -0
- package/lib/types/client/remote-share-register.d.ts.map +1 -0
- package/lib/types/client/remote-status-css.d.ts +15 -0
- package/lib/types/client/remote-status-css.d.ts.map +1 -0
- package/lib/types/client/remote-status-register.d.ts +46 -0
- package/lib/types/client/remote-status-register.d.ts.map +1 -0
- package/lib/types/client/settings/SettingsSection.d.ts +46 -0
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -0
- package/lib/types/client/settings/register-settings.d.ts +58 -0
- package/lib/types/client/settings/register-settings.d.ts.map +1 -0
- package/lib/types/client/settings/settings-css.d.ts +10 -0
- package/lib/types/client/settings/settings-css.d.ts.map +1 -0
- package/lib/types/client/share/share-preview.d.ts.map +1 -1
- package/lib/types/client/styles/composer.css.d.ts +1 -1
- package/lib/types/client/styles/composer.css.d.ts.map +1 -1
- package/lib/types/client/styles/header.css.d.ts +1 -1
- package/lib/types/client/styles/header.css.d.ts.map +1 -1
- package/lib/types/client/styles/info.css.d.ts +1 -1
- package/lib/types/client/styles/info.css.d.ts.map +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts +2 -0
- package/lib/types/client/styles/remote-session.css.d.ts.map +1 -0
- package/lib/types/client-data/current-session.d.ts +41 -0
- package/lib/types/client-data/current-session.d.ts.map +1 -0
- package/lib/types/client-data/remote-group-expansion.d.ts +76 -0
- package/lib/types/client-data/remote-group-expansion.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +148 -0
- package/lib/types/client-data/remote-status.d.ts.map +1 -0
- package/lib/types/client-data/settings-form.d.ts +774 -0
- package/lib/types/client-data/settings-form.d.ts.map +1 -0
- package/lib/types/client-data/shares.d.ts +211 -0
- package/lib/types/client-data/shares.d.ts.map +1 -0
- package/lib/types/client-pairing.d.ts +114 -0
- package/lib/types/client-pairing.d.ts.map +1 -0
- package/lib/types/client-routes.d.ts +112 -0
- package/lib/types/client-routes.d.ts.map +1 -0
- package/lib/types/config.d.ts +200 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/fetch-route-intercept.d.ts +156 -0
- package/lib/types/fetch-route-intercept.d.ts.map +1 -0
- package/lib/types/fingerprint.d.ts +164 -0
- package/lib/types/fingerprint.d.ts.map +1 -0
- package/lib/types/http.d.ts +41 -0
- package/lib/types/http.d.ts.map +1 -0
- package/lib/types/index.d.ts +56 -24
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept-shape.d.ts +32 -0
- package/lib/types/intercept-shape.d.ts.map +1 -0
- package/lib/types/intercept.d.ts +432 -0
- package/lib/types/intercept.d.ts.map +1 -0
- package/lib/types/merge-streams.d.ts +393 -0
- package/lib/types/merge-streams.d.ts.map +1 -0
- package/lib/types/relay-access.d.ts +310 -0
- package/lib/types/relay-access.d.ts.map +1 -0
- package/lib/types/relay-client.d.ts +274 -0
- package/lib/types/relay-client.d.ts.map +1 -0
- package/lib/types/relay-filter.d.ts +103 -0
- package/lib/types/relay-filter.d.ts.map +1 -0
- package/lib/types/relay-server.d.ts +208 -0
- package/lib/types/relay-server.d.ts.map +1 -0
- package/lib/types/restart-fields.d.ts +22 -0
- package/lib/types/restart-fields.d.ts.map +1 -0
- package/lib/types/restart-watcher.d.ts +83 -0
- package/lib/types/restart-watcher.d.ts.map +1 -0
- package/lib/types/session-reference.d.ts +66 -0
- package/lib/types/session-reference.d.ts.map +1 -0
- package/lib/types/share-export.d.ts +14 -1
- package/lib/types/share-export.d.ts.map +1 -1
- package/lib/types/share-ops.d.ts +79 -0
- package/lib/types/share-ops.d.ts.map +1 -0
- package/lib/types/share-store.d.ts +87 -0
- package/lib/types/share-store.d.ts.map +1 -0
- package/lib/types/virtual-id.d.ts +38 -0
- package/lib/types/virtual-id.d.ts.map +1 -0
- package/lib/virtual-id.js +52 -0
- package/lib/virtual-id.js.map +1 -0
- package/llms.txt +17 -14
- package/package.json +60 -50
- package/scripts/build-client.mjs +5 -3
- package/scripts/check-host-shadow.mjs +19 -2
- package/scripts/check-settings-form.mjs +933 -0
- package/scripts/check-shares.mjs +397 -0
- package/scripts/check-upload-endpoint.mjs +59 -3
- package/scripts/sync-doc-version.mjs +8 -15
- package/scripts/verify-fingerprint-live.mjs +111 -0
- package/src/activity.ts +218 -0
- package/src/admin-routes.ts +627 -0
- package/src/client/MobileAttachButton.tsx +2 -2
- package/src/client/MobileAttachChips.tsx +3 -3
- package/src/client/MobileDrawerFooter.tsx +7 -4
- package/src/client/MobileHome.tsx +20 -16
- package/src/client/MobileHomeChips.tsx +16 -16
- package/src/client/MobileNavOverlay.tsx +2 -2
- package/src/client/MobileNavToggle.tsx +3 -3
- package/src/client/MobileSessionHeader.tsx +63 -42
- package/src/client/MobileSessionInfo.tsx +118 -17
- package/src/client/RemoteComposerBanner.tsx +76 -0
- package/src/client/RemoteHeaderIcon.tsx +103 -0
- package/src/client/RemoteShareMenu.tsx +57 -0
- package/src/client/RemoteStatusIcon.tsx +91 -0
- package/src/client/compat/desktop.ts +22 -0
- package/src/client/compat/types.ts +62 -0
- package/src/client/current-session-report.ts +164 -0
- package/src/client/effects/header-status.ts +21 -17
- package/src/client/effects/keyboard-guard.ts +65 -1
- package/src/client/effects/permission-sheet.ts +149 -0
- package/src/client/effects/remote-session.ts +68 -0
- package/src/client/effects/turn-fold.ts +52 -0
- package/src/client/index.tsx +151 -36
- package/src/client/locales.ts +396 -2
- package/src/client/remote-fetch.ts +125 -0
- package/src/client/remote-group-expansion.ts +190 -0
- package/src/client/remote-share-css.ts +54 -0
- package/src/client/remote-share-register.ts +244 -0
- package/src/client/remote-status-css.ts +64 -0
- package/src/client/remote-status-register.ts +107 -0
- package/src/client/settings/SettingsSection.tsx +1229 -0
- package/src/client/settings/register-settings.ts +116 -0
- package/src/client/settings/settings-css.ts +236 -0
- package/src/client/share/share-preview.tsx +6 -3
- package/src/client/share/share-sheet.tsx +3 -3
- package/src/client/styles/composer.css.ts +119 -26
- package/src/client/styles/header.css.ts +7 -7
- package/src/client/styles/info.css.ts +74 -0
- package/src/client/styles/remote-session.css.ts +251 -0
- package/src/client-data/current-session.ts +57 -0
- package/src/client-data/remote-group-expansion.ts +145 -0
- package/src/client-data/remote-status.ts +374 -0
- package/src/client-data/settings-form.ts +1371 -0
- package/src/client-data/shares.ts +467 -0
- package/src/client-pairing.ts +244 -0
- package/src/client-routes.ts +732 -0
- package/src/config.ts +447 -0
- package/src/fetch-route-intercept.ts +510 -0
- package/src/fingerprint.ts +408 -0
- package/src/http.ts +68 -0
- package/src/index.ts +719 -62
- package/src/intercept-shape.ts +87 -0
- package/src/intercept.ts +3362 -0
- package/src/merge-streams.ts +1272 -0
- package/src/relay-access.ts +669 -0
- package/src/relay-client.ts +1514 -0
- package/src/relay-filter.ts +291 -0
- package/src/relay-server.ts +2025 -0
- package/src/restart-fields.ts +36 -0
- package/src/restart-watcher.ts +140 -0
- package/src/session-reference.ts +156 -0
- package/src/share-export.ts +26 -1
- package/src/share-ops.ts +114 -0
- package/src/share-store.ts +373 -0
- package/src/virtual-id.ts +59 -0
package/src/activity.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-activity tracking for the shared remote table (T22c): the bridge
|
|
3
|
+
* from DSH's session event stream to share-store bookkeeping, plus the idle
|
|
4
|
+
* sweeper that puts away sessions gone quiet. Pure logic — no DSH imports,
|
|
5
|
+
* no filesystem, and timers only through injectable functions — so the tests
|
|
6
|
+
* can drive every tick from a fake clock (test/activity.test.cjs).
|
|
7
|
+
*
|
|
8
|
+
* The event mapping is the rule set agreed for "what counts as activity":
|
|
9
|
+
* every session event means the session moved, EXCEPT `session/end-seed`,
|
|
10
|
+
* which DSH appends by itself whenever a session is merely opened (see the
|
|
11
|
+
* main repo's docs/spike-relay.md §2.1 pitfall 2) — counting it would reset
|
|
12
|
+
* the idle clock every time someone looks at a conversation. A turn marks
|
|
13
|
+
* the session busy for its whole span (`turn/start` → `turn/end`), so
|
|
14
|
+
* waiting on an approval or an answer — both happen INSIDE a turn — never
|
|
15
|
+
* idles out either.
|
|
16
|
+
*
|
|
17
|
+
* T22c-fix added parent propagation: subagent sessions never enter the
|
|
18
|
+
* shared table themselves, but their events (a background child grinding on
|
|
19
|
+
* after the parent's own turn ended) must keep the ANCESTORS' idle clocks
|
|
20
|
+
* fresh — {@link createParentIndex} records the child→parent links from
|
|
21
|
+
* session headers, and the tracker touches every ancestor along the chain.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ShareStore } from './share-store.js'
|
|
25
|
+
|
|
26
|
+
/** What `createActivityTracker` hands back: one call per session event. */
|
|
27
|
+
export interface ActivityTracker {
|
|
28
|
+
/** Report one session event. No-op for unshared sessions (share-store
|
|
29
|
+
* ignores strangers) and for the `session/end-seed` marker. */
|
|
30
|
+
onEvent(sessionId: string, eventType: string): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Auto-appended when a session is opened; a snapshot of attention, not activity. */
|
|
34
|
+
const END_SEED_EVENT = 'session/end-seed'
|
|
35
|
+
|
|
36
|
+
/** Map one DSH session event onto the shared table's clock rules. `parentOf`
|
|
37
|
+
* (optional) reports the parent of a subagent session; when given, a child's
|
|
38
|
+
* event also refreshes every ancestor's clock (touch only — see below). */
|
|
39
|
+
export function createActivityTracker(
|
|
40
|
+
store: ShareStore,
|
|
41
|
+
parentOf: (id: string) => string | undefined = () => undefined,
|
|
42
|
+
): ActivityTracker {
|
|
43
|
+
// Walk UP from the session itself, touching each ancestor once. Bounded
|
|
44
|
+
// like share-store's own isAccessible walk: 16 hops, and a repeated id
|
|
45
|
+
// (a hostile/corrupt parent chain) stops the walk dead instead of looping.
|
|
46
|
+
const touchAncestors = (sessionId: string): void => {
|
|
47
|
+
const seen = new Set<string>([sessionId])
|
|
48
|
+
let current = sessionId
|
|
49
|
+
for (let hop = 0; hop < MAX_ANCESTOR_HOPS; hop += 1) {
|
|
50
|
+
let parent: string | undefined
|
|
51
|
+
try {
|
|
52
|
+
parent = parentOf(current)
|
|
53
|
+
} catch {
|
|
54
|
+
// A throwing parentOf means "no parent from here".
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (parent === undefined || parent === '' || seen.has(parent)) return
|
|
58
|
+
seen.add(parent)
|
|
59
|
+
// Touch ONLY: a child's turn start/end says nothing about whether the
|
|
60
|
+
// parent is running or waiting — the parent's own turn events govern
|
|
61
|
+
// its busy flag.
|
|
62
|
+
store.touch(parent)
|
|
63
|
+
current = parent
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
onEvent(sessionId, eventType) {
|
|
68
|
+
if (eventType === END_SEED_EVENT) return
|
|
69
|
+
// The child itself is handled exactly as before (a shared child would
|
|
70
|
+
// get the full mapping; an unshared one is ignored by the store).
|
|
71
|
+
if (eventType === 'turn/start') {
|
|
72
|
+
// Busy AND touched: the touch stamps "activity began here", which is
|
|
73
|
+
// the clock the table falls back to after a restart (busy is not
|
|
74
|
+
// persisted — see the note on startSweeper).
|
|
75
|
+
store.setBusy(sessionId, true)
|
|
76
|
+
store.touch(sessionId)
|
|
77
|
+
} else if (eventType === 'turn/end') {
|
|
78
|
+
// No touch of its own: share-store re-stamps lastActivityAt when busy
|
|
79
|
+
// flips to false, so the idle span restarts at the turn's actual end.
|
|
80
|
+
store.setBusy(sessionId, false)
|
|
81
|
+
} else {
|
|
82
|
+
store.touch(sessionId)
|
|
83
|
+
}
|
|
84
|
+
// A subagent's motion is its ancestors' motion too: a background child
|
|
85
|
+
// (run_in_background) keeps working after the parent's own turn ended,
|
|
86
|
+
// and without this the parent idles out mid-orchestration. The
|
|
87
|
+
// end-seed marker never reaches here — a session being OPENED (even a
|
|
88
|
+
// subagent one) is a look, not motion, and must not defer anyone's
|
|
89
|
+
// idle clock.
|
|
90
|
+
touchAncestors(sessionId)
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The slice of a live DSH Session the parent index reads: the session id
|
|
96
|
+
* plus the two subagent-relevant header fields. Declared structurally so
|
|
97
|
+
* this module never imports DSH types (an optional peer dependency) and the
|
|
98
|
+
* tests can feed plain objects. Field names per DSH 0.2.0 dsh-session's
|
|
99
|
+
* `validateSessionHeader`: `origin` (only legal value `'subagent'`) and
|
|
100
|
+
* `parentSession` (must be a string). */
|
|
101
|
+
export interface ObservableSession {
|
|
102
|
+
id?: unknown
|
|
103
|
+
header?: { origin?: unknown; parentSession?: unknown } | undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Remembers which sessions are subagents of which, as reported by
|
|
107
|
+
* {@link ParentIndex.observe}. */
|
|
108
|
+
export interface ParentIndex {
|
|
109
|
+
/** Record one session's parent link when it IS a subagent; every other
|
|
110
|
+
* shape (top-level sessions, id-less or header-less stubs, undefined) is
|
|
111
|
+
* a no-op. */
|
|
112
|
+
observe(session: ObservableSession | undefined): void
|
|
113
|
+
/** The recorded parent of `id`, or undefined when unknown. */
|
|
114
|
+
parentOf(id: string): string | undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* ponytail: 父子表上限 5000 条,超出按最早插入淘汰。这是内存里的纯派生数据
|
|
119
|
+
* (重启后由 session/event 重建),淘汰只发生在「第 5001 个新子会话」出现时,
|
|
120
|
+
* 按插入序删最早的一条;反复 observe 已有 id 只覆盖值、不动位置。上限挡的是
|
|
121
|
+
* 长驻进程里无限累积的子会话 id,5000 对单机并发子智能体绰绰有余。
|
|
122
|
+
*/
|
|
123
|
+
/** Entries the parent index keeps before the oldest insertion is evicted. */
|
|
124
|
+
const MAX_PARENT_ENTRIES = 5000
|
|
125
|
+
|
|
126
|
+
/** Same-walk ancestor bound as share-store's isAccessible: 16 hops. */
|
|
127
|
+
const MAX_ANCESTOR_HOPS = 16
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The child→parent table backing subagent activity propagation (T22c-fix):
|
|
131
|
+
* the tracker refreshes ancestors' clocks and the relay's `isAccessible`
|
|
132
|
+
* walks it to decide whether a subagent session is reachable. Pure in-memory
|
|
133
|
+
* — rebuilt from live `session/event` traffic after every restart, so there
|
|
134
|
+
* is nothing to persist and nothing to invalidate.
|
|
135
|
+
*/
|
|
136
|
+
export function createParentIndex(): ParentIndex {
|
|
137
|
+
// Insertion order is the eviction order: Map preserves it.
|
|
138
|
+
const parents = new Map<string, string>()
|
|
139
|
+
return {
|
|
140
|
+
observe(session) {
|
|
141
|
+
const header = session?.header
|
|
142
|
+
if (header === undefined || header.origin !== 'subagent') return
|
|
143
|
+
const { parentSession } = header
|
|
144
|
+
if (typeof parentSession !== 'string' || parentSession === '') return
|
|
145
|
+
const id = session?.id
|
|
146
|
+
// A self-parent would poison every future walk into a 1-cycle; the
|
|
147
|
+
// seen-set in the walkers already survives it, but there is no honest
|
|
148
|
+
// link to record in the first place.
|
|
149
|
+
if (typeof id !== 'string' || id === '' || id === parentSession) return
|
|
150
|
+
if (!parents.has(id) && parents.size >= MAX_PARENT_ENTRIES) {
|
|
151
|
+
const oldest = parents.keys().next().value
|
|
152
|
+
if (oldest !== undefined) parents.delete(oldest)
|
|
153
|
+
}
|
|
154
|
+
parents.set(id, parentSession)
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
parentOf(id) {
|
|
158
|
+
return parents.get(id)
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Duck-typed timer halves: Node's setInterval/clearInterval at runtime,
|
|
164
|
+
* fakes in tests. Loose on purpose — the real NodeJS.Timeout type would
|
|
165
|
+
* fight the contravariant `clearInterval` parameter under strictFunctionTypes. */
|
|
166
|
+
type StartInterval = (callback: () => void, ms: number) => unknown
|
|
167
|
+
type ClearInterval = (handle: unknown) => void
|
|
168
|
+
|
|
169
|
+
export interface SweeperOptions {
|
|
170
|
+
/** The shared table to sweep. */
|
|
171
|
+
store: ShareStore
|
|
172
|
+
/** Read fresh before EVERY sweep: `idleHours` is a volatile row setting
|
|
173
|
+
* that can change without a restart, so it is never snapshotted here. */
|
|
174
|
+
getIdleHours: () => number
|
|
175
|
+
/** Sweep cadence in ms. Default 60_000 — a minute late is fine for a
|
|
176
|
+
* threshold measured in hours. */
|
|
177
|
+
intervalMs?: number
|
|
178
|
+
/** Injectable timer halves (tests drive ticks by hand). */
|
|
179
|
+
setIntervalImpl?: StartInterval
|
|
180
|
+
clearIntervalImpl?: ClearInterval
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* ponytail: 忙碌状态不写盘(share-store 的持久化刻意省略 busy)。一个会话在回合
|
|
185
|
+
* 中(比如等审批)经历了 DSH 重启,重启后按空闲处理,计时起点仍是回合开始时
|
|
186
|
+
* `touch` 打下的时间戳;如果那时距重启已超过 idleHours,重启后的第一次扫描就会
|
|
187
|
+
* 关闭它的远程。DSH 重启本身会中断正在进行的回合,所以这种情况下关闭远程是
|
|
188
|
+
* 可以接受的。
|
|
189
|
+
*
|
|
190
|
+
* Start the idle sweeper: every interval, first feed the table the CURRENT
|
|
191
|
+
* idle budget, then close whatever has been quiet past it. Returns the stop
|
|
192
|
+
* function (the host half hands it to ctx.effect). Two failure disciplines:
|
|
193
|
+
* a throwing getIdleHours or sweep is swallowed — the timer must survive to
|
|
194
|
+
* try again next tick — and the underlying timer is unref()ed so the sweeper
|
|
195
|
+
* alone never keeps the DSH process alive.
|
|
196
|
+
*/
|
|
197
|
+
export function startSweeper(options: SweeperOptions): () => void {
|
|
198
|
+
const { store, getIdleHours, intervalMs = 60_000 } = options
|
|
199
|
+
const setIntervalImpl: StartInterval = options.setIntervalImpl ?? ((callback, ms) => setInterval(callback, ms))
|
|
200
|
+
const clearIntervalImpl: ClearInterval =
|
|
201
|
+
options.clearIntervalImpl ?? ((handle) => clearInterval(handle as ReturnType<typeof setInterval>))
|
|
202
|
+
const tick = () => {
|
|
203
|
+
try {
|
|
204
|
+
store.setIdleHours(getIdleHours())
|
|
205
|
+
store.sweep()
|
|
206
|
+
} catch {
|
|
207
|
+
// Anything thrown here (a hostile getIdleHours, a disk hiccup in
|
|
208
|
+
// sweep's persist) must not kill the timer: the next interval retries.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const handle = setIntervalImpl(tick, intervalMs)
|
|
212
|
+
if (typeof handle === 'object' && handle !== null && typeof (handle as { unref?: unknown }).unref === 'function') {
|
|
213
|
+
;(handle as { unref: () => void }).unref()
|
|
214
|
+
}
|
|
215
|
+
return () => {
|
|
216
|
+
clearIntervalImpl(handle)
|
|
217
|
+
}
|
|
218
|
+
}
|