dsh-sessions-manager 3.4.2 → 3.5.1
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.en.md +6 -2
- package/README.md +6 -2
- package/lib/client.js +78 -21
- package/lib/client.js.map +2 -2
- package/lib/index.js +272 -85
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +58 -14
- package/src/client/logic.js +8 -0
- package/src/compat/capabilities.js +34 -0
- package/src/compat/persistence.js +72 -0
- package/src/index.js +113 -60
- package/src/zstd-frame.js +68 -34
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sessions-manager",
|
|
3
3
|
"description": "DSH 设置面板会话管理器:归档 / 恢复 / 彻底删除 / 移动到其他工作区,带工作区标签与会话日期;统一「会话管理」面板。Session manager for the DeepSeek Harness settings panel — archive / restore / permanently delete / move sessions across workspaces, with workspace tags & session dates in one unified panel.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.5.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/client/index.jsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
14
|
-
import { canDropOnWorkspace, dotStateFor, sessionForNodes, starredOf, workspaceForNodes } from './logic.js'
|
|
14
|
+
import { authoritativeTitleForFirstPaint, canDropOnWorkspace, dotStateFor, sessionForNodes, starredOf, workspaceForNodes } from './logic.js'
|
|
15
15
|
|
|
16
16
|
export const inject = ['slots']
|
|
17
17
|
|
|
@@ -219,6 +219,7 @@ function installSettingsNavIcons(ctx) {
|
|
|
219
219
|
function SessionPanel({ workspacesSvc }) {
|
|
220
220
|
const [sessions, setSessions] = useState(null)
|
|
221
221
|
const [workspaces, setWorkspaces] = useState([])
|
|
222
|
+
const [capabilities, setCapabilities] = useState(null)
|
|
222
223
|
const initialPrefs = useRef(loadPanelPrefs()).current
|
|
223
224
|
// 注意:'storage' 已从可持久化取值中移除(它不再是视图),旧版残留的
|
|
224
225
|
// filter='storage' 会自动回落到 'all',避免落到一个已不存在的界面。
|
|
@@ -276,14 +277,16 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
276
277
|
Promise.all([
|
|
277
278
|
postJSON('/archived-sessions/sessions', {}),
|
|
278
279
|
postJSON('/archived-sessions/workspaces', {}),
|
|
280
|
+
postJSON('/archived-sessions/capabilities', {}),
|
|
279
281
|
])
|
|
280
|
-
.then(([s, works]) => {
|
|
282
|
+
.then(([s, works, caps]) => {
|
|
281
283
|
// Permanently-purged sessions must never re-appear here, even if DSH's
|
|
282
284
|
// in-memory session index still lists them after the file was unlinked.
|
|
283
285
|
const purged = dsmLoadPurged()
|
|
284
286
|
const visible = (s.items || []).filter((x) => !purged.has(String(x.sessionId)))
|
|
285
287
|
setSessions(visible)
|
|
286
288
|
setWorkspaces(works.items || [])
|
|
289
|
+
setCapabilities(caps || null)
|
|
287
290
|
setSelected({})
|
|
288
291
|
setDelTarget(null)
|
|
289
292
|
setConfirmBatch(false)
|
|
@@ -393,6 +396,9 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
393
396
|
const w = workspaces.find((x) => x.workspaceId === id)
|
|
394
397
|
return w ? w.path : ''
|
|
395
398
|
}
|
|
399
|
+
const actionCapability = (name) => (capabilities && capabilities.actions && capabilities.actions[name]) || { available: false, reason: '正在检查当前 DSH 的兼容能力…' }
|
|
400
|
+
const canPurge = actionCapability('purge')
|
|
401
|
+
const canMove = actionCapability('move')
|
|
396
402
|
|
|
397
403
|
const archivedList = sessions ? sessions.filter((x) => x.archived) : []
|
|
398
404
|
const activeList = sessions ? sessions.filter((x) => !x.archived) : []
|
|
@@ -473,7 +479,12 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
473
479
|
const verifyTrash = () => {
|
|
474
480
|
setTrashBusy('__verify')
|
|
475
481
|
postJSON('/archived-sessions/trash/verify', {})
|
|
476
|
-
.then((r) => {
|
|
482
|
+
.then((r) => {
|
|
483
|
+
setTrashBusy(null); setTrashCheck(r)
|
|
484
|
+
if (r.missing) showToast(`发现 ${r.missing} 条日志缺失`)
|
|
485
|
+
else if (r.unverified) showToast(`${r.unverified} 条日志位置由当前 Runtime 管理,无法直接核验`)
|
|
486
|
+
else showToast('回收站校验通过')
|
|
487
|
+
})
|
|
477
488
|
.catch((e) => { setTrashBusy(null); setError(String((e && e.message) || e)) })
|
|
478
489
|
}
|
|
479
490
|
|
|
@@ -554,6 +565,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
554
565
|
}
|
|
555
566
|
|
|
556
567
|
const doMove = (it) => {
|
|
568
|
+
if (!canMove.available) { setError(canMove.reason || '当前环境不支持跨工作区移动'); return }
|
|
557
569
|
const targetPath = moveMode === 'new' ? newPath.trim() : wsPath(targetWs)
|
|
558
570
|
if (!targetPath) { setError('请选择已有工作区或输入新的目标目录路径'); return }
|
|
559
571
|
setBusy(it.sessionId)
|
|
@@ -591,6 +603,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
591
603
|
}
|
|
592
604
|
|
|
593
605
|
const openMoveFor = (it) => {
|
|
606
|
+
if (!canMove.available) { setError(canMove.reason || '当前环境不支持跨工作区移动'); return }
|
|
594
607
|
if (openMove === it.sessionId) { setOpenMove(null); return }
|
|
595
608
|
setTargetWs(workspaces.length ? (targetWs || workspaces[0].workspaceId) : '')
|
|
596
609
|
setMoveMode('existing')
|
|
@@ -663,15 +676,20 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
663
676
|
>⋯</button>
|
|
664
677
|
{openMenu === it.sessionId && (
|
|
665
678
|
<div className="more-menu" role="menu">
|
|
666
|
-
{items.map(([id, label]) =>
|
|
679
|
+
{items.map(([id, label]) => {
|
|
680
|
+
const blocked = id === 'move' && !canMove.available
|
|
681
|
+
return (
|
|
667
682
|
<button
|
|
668
683
|
key={id}
|
|
669
684
|
type="button"
|
|
670
685
|
role="menuitem"
|
|
671
686
|
className={'more-item' + (id === 'delete' ? ' more-item-danger' : '')}
|
|
687
|
+
disabled={blocked}
|
|
688
|
+
title={blocked ? canMove.reason : undefined}
|
|
672
689
|
onClick={() => runMenu(id, it)}
|
|
673
690
|
>{label}</button>
|
|
674
|
-
|
|
691
|
+
)
|
|
692
|
+
})}
|
|
675
693
|
</div>
|
|
676
694
|
)}
|
|
677
695
|
</div>
|
|
@@ -1034,9 +1052,10 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
1034
1052
|
</select>
|
|
1035
1053
|
</div>
|
|
1036
1054
|
<div className="sess-field"><label>数据检查</label><button type="button" className="archv-btn" disabled={trashBusy !== null} onClick={verifyTrash}>{trashBusy === '__verify' ? '校验中…' : '校验日志完整性'}</button></div>
|
|
1037
|
-
<div className="sess-field"><label>永久清理</label><button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !trash.length} onClick={() => setPurgeTarget('__all')}>清空回收站</button></div>
|
|
1055
|
+
<div className="sess-field"><label>永久清理</label><button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !trash.length || !canPurge.available} title={!canPurge.available ? canPurge.reason : undefined} onClick={() => setPurgeTarget('__all')}>清空回收站</button></div>
|
|
1038
1056
|
</div>
|
|
1039
|
-
{
|
|
1057
|
+
{!canPurge.available && <div className="archv-empty" role="status">{canPurge.reason}</div>}
|
|
1058
|
+
{trashCheck && <div className={trashCheck.missing ? 'archv-err' : 'archv-empty'} role="status">校验完成:{trashCheck.healthy} 条正常,{trashCheck.missing} 条日志缺失,{trashCheck.unverified || 0} 条无法直接核验。</div>}
|
|
1040
1059
|
{trash.length === 0 ? (
|
|
1041
1060
|
<div className="dsm-trash-empty">回收站为空。删除的会话会先进入这里,可恢复或彻底删除。</div>
|
|
1042
1061
|
) : (
|
|
@@ -1047,7 +1066,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
1047
1066
|
<span className="dsm-trash-date">{fmtDate(t.deletedAt)}</span>
|
|
1048
1067
|
<span className="dsm-trash-actions">
|
|
1049
1068
|
<button type="button" className="archv-btn" disabled={trashBusy !== null} onClick={() => restoreTrash(t.sessionId)}>恢复</button>
|
|
1050
|
-
<button type="button" className="archv-btn archv-del" disabled={trashBusy !== null} onClick={() => setPurgeTarget(t.sessionId)}>彻底删除</button>
|
|
1069
|
+
<button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !canPurge.available} title={!canPurge.available ? canPurge.reason : undefined} onClick={() => setPurgeTarget(t.sessionId)}>彻底删除</button>
|
|
1051
1070
|
</span>
|
|
1052
1071
|
</div>
|
|
1053
1072
|
))}
|
|
@@ -1110,7 +1129,7 @@ const SIDEBAR_AUG_CSS = `
|
|
|
1110
1129
|
.dsm-sub-cur{font-size:11px;color:var(--dsw-alias-state-business-primary);flex:none;margin-left:8px}
|
|
1111
1130
|
.dsm-dot{position:absolute;left:6px;top:50%;transform:translateY(-50%);width:8px;height:8px;border-radius:50%;box-sizing:border-box;cursor:pointer;pointer-events:auto;z-index:1}
|
|
1112
1131
|
.dsm-dot-unread-manual{background:var(--dsw-alias-state-business-primary)}
|
|
1113
|
-
.dsm-dot-waiting{background:var(--dsw-alias-state-
|
|
1132
|
+
.dsm-dot-waiting{background:var(--dsw-alias-state-warn-primary,#F59E0B)}
|
|
1114
1133
|
.dsm-dot-unread{background:var(--dsw-alias-state-success-primary)}
|
|
1115
1134
|
.dsm-drag-source{opacity:.48}
|
|
1116
1135
|
.dsm-drop-target{position:relative;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)!important;outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px;border-radius:8px}
|
|
@@ -1169,6 +1188,14 @@ let dsmTrashIds = null
|
|
|
1169
1188
|
let dsmServerPurgedIds = new Set()
|
|
1170
1189
|
let dsmAuthoritativeTitles = new Map()
|
|
1171
1190
|
let dsmTrashTick = 0
|
|
1191
|
+
let dsmCapabilities = null
|
|
1192
|
+
async function dsmLoadCapabilities() {
|
|
1193
|
+
try { dsmCapabilities = await postJSON('/archived-sessions/capabilities', {}) } catch (e) { /* unknown stays safely unavailable */ }
|
|
1194
|
+
return dsmCapabilities
|
|
1195
|
+
}
|
|
1196
|
+
function dsmActionCapability(name) {
|
|
1197
|
+
return dsmCapabilities && dsmCapabilities.actions && dsmCapabilities.actions[name]
|
|
1198
|
+
}
|
|
1172
1199
|
async function dsmLoadTrashIds() {
|
|
1173
1200
|
try {
|
|
1174
1201
|
const r = await postJSON('/archived-sessions/sidebar-state', {})
|
|
@@ -1385,6 +1412,11 @@ function installSidebarSessionMenuAug() {
|
|
|
1385
1412
|
const move = mk('移动会话', ICON_MOVE, false)
|
|
1386
1413
|
const del = mk('删除会话', ICON_DEL, true)
|
|
1387
1414
|
const mark = mk(dsmLoadManual().has(info.id) ? '标记已读' : '标记未读', ICON_UNREAD, false)
|
|
1415
|
+
const moveCapability = dsmActionCapability('move')
|
|
1416
|
+
if (!moveCapability || !moveCapability.available) {
|
|
1417
|
+
move.btn.disabled = true
|
|
1418
|
+
move.btn.title = (moveCapability && moveCapability.reason) || '正在检查当前版本的移动能力'
|
|
1419
|
+
}
|
|
1388
1420
|
if (mark.btn.firstChild) mark.btn.firstChild.style.color = 'var(--dsw-alias-state-business-primary)'
|
|
1389
1421
|
const chev = document.createElement('span')
|
|
1390
1422
|
chev.style.cssText = 'margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:1'
|
|
@@ -1433,7 +1465,7 @@ function installSidebarSessionMenuAug() {
|
|
|
1433
1465
|
// Left-side per-session status dot (DOM shim). The color is driven by DSH's
|
|
1434
1466
|
// REAL session status, read from the row's own StateDot ([data-state]):
|
|
1435
1467
|
// 手动标记未读(蓝) = 用户在 ⋯ 菜单或点击圆点手动标记,localStorage 持久化(最高优先级)
|
|
1436
|
-
// 工作中(黄) = DSH state 'running'
|
|
1468
|
+
// 工作中(黄) = DSH state 'ongoing'(旧版兼容 'running')
|
|
1437
1469
|
// 需用户反馈(琥珀) = DSH state 'warning'(有追问需用户反馈)
|
|
1438
1470
|
// 完成后未读(绿) = DSH state 'done' 且用户尚未读过(read 集合)
|
|
1439
1471
|
// 完成已读(不显示) = DSH state 'done' 且已读过(read 集合持久化)
|
|
@@ -1471,7 +1503,7 @@ function installSidebarStatusDots() {
|
|
|
1471
1503
|
}
|
|
1472
1504
|
// Dot colors per the user's scheme. We read DSH's REAL session status from the
|
|
1473
1505
|
// row's own StateDot (its [data-state] attr) and recolor it:
|
|
1474
|
-
// running
|
|
1506
|
+
// ongoing/running → 黄 工作中
|
|
1475
1507
|
// warning → 琥珀 有追问需用户反馈
|
|
1476
1508
|
// done → 绿 完成后未读(读过则不再显示)
|
|
1477
1509
|
// error → 红 出错/需关注(与 DSH 自带 StateDot 一致)
|
|
@@ -1479,11 +1511,12 @@ function installSidebarStatusDots() {
|
|
|
1479
1511
|
const COLOR = {
|
|
1480
1512
|
manual: 'var(--dsw-alias-state-business-primary)', // 蓝 手动标记未读
|
|
1481
1513
|
running: '#EAB308', // 黄 工作中 (DSH running)
|
|
1482
|
-
feedback: 'var(--dsw-alias-state-
|
|
1514
|
+
feedback: 'var(--dsw-alias-state-warn-primary, #F59E0B)', // 琥珀 需用户反馈 (DSH warning)
|
|
1483
1515
|
done: 'var(--dsw-alias-state-success-primary)', // 绿 完成后未读 (DSH done)
|
|
1484
1516
|
error: 'var(--dsw-alias-state-error-primary)', // 红 出错/需关注 (DSH error)
|
|
1485
1517
|
}
|
|
1486
1518
|
const manualUnread = dsmLoadManual()
|
|
1519
|
+
const titleInitializedRows = new WeakSet()
|
|
1487
1520
|
const paint = () => {
|
|
1488
1521
|
// Recompute the active session on every paint so the active row is never
|
|
1489
1522
|
// shown from a stale `curActive` (the click that changes aria-selected
|
|
@@ -1517,12 +1550,20 @@ function installSidebarStatusDots() {
|
|
|
1517
1550
|
// Session is opened. Paint the latest log-folded title from the host
|
|
1518
1551
|
// authority without materializing the Session or changing its log.
|
|
1519
1552
|
const authoritativeTitle = dsmAuthoritativeTitles.get(id)
|
|
1520
|
-
if (
|
|
1553
|
+
if (!titleInitializedRows.has(row)) {
|
|
1521
1554
|
const node = rowNode(row) || {}
|
|
1522
1555
|
const expected = new Set([node.title, node.displayTitle, node.name].filter((value) => typeof value === 'string'))
|
|
1523
1556
|
const spans = [...row.children].filter((el) => el.tagName === 'SPAN' && !el.querySelector('[data-state]') && (el.textContent || '').trim())
|
|
1524
1557
|
const titleEl = spans.find((el) => expected.has((el.textContent || '').trim())) || spans[0]
|
|
1525
|
-
if (titleEl
|
|
1558
|
+
if (titleEl) {
|
|
1559
|
+
const correction = authoritativeTitleForFirstPaint({
|
|
1560
|
+
firstPaint: true,
|
|
1561
|
+
rendered: titleEl.textContent || '',
|
|
1562
|
+
authoritative: authoritativeTitle || '',
|
|
1563
|
+
})
|
|
1564
|
+
if (correction) titleEl.textContent = correction
|
|
1565
|
+
titleInitializedRows.add(row)
|
|
1566
|
+
}
|
|
1526
1567
|
}
|
|
1527
1568
|
// Read DSH's REAL session status from the row's own StateDot and recolor
|
|
1528
1569
|
// it with the user's scheme (we also hide DSH's dot so only ours shows).
|
|
@@ -1647,6 +1688,8 @@ function installSidebarWorkspaceDrag() {
|
|
|
1647
1688
|
}, true)
|
|
1648
1689
|
|
|
1649
1690
|
document.addEventListener('dragstart', (event) => {
|
|
1691
|
+
const moveCapability = dsmActionCapability('move')
|
|
1692
|
+
if (!moveCapability || !moveCapability.available) return
|
|
1650
1693
|
const row = eventRow(event)
|
|
1651
1694
|
const item = row && sessionForRow(row)
|
|
1652
1695
|
if (!item || moving) return
|
|
@@ -1729,6 +1772,7 @@ function installSidebarWorkspaceDrag() {
|
|
|
1729
1772
|
}
|
|
1730
1773
|
|
|
1731
1774
|
export function apply(ctx) {
|
|
1775
|
+
dsmLoadCapabilities()
|
|
1732
1776
|
installSettingsNavIcons(ctx)
|
|
1733
1777
|
installSidebarSessionMenuAug()
|
|
1734
1778
|
installSidebarStatusDots()
|
package/src/client/logic.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export function dotStateFor({ manualUnread = false, dataState = '', isActive = false } = {}) {
|
|
11
11
|
if (manualUnread) return 'manual'
|
|
12
12
|
switch (dataState) {
|
|
13
|
+
case 'ongoing':
|
|
13
14
|
case 'running': return 'running'
|
|
14
15
|
case 'warning': return 'feedback'
|
|
15
16
|
case 'error': return 'error'
|
|
@@ -18,6 +19,13 @@ export function dotStateFor({ manualUnread = false, dataState = '', isActive = f
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
// 权威标题只修正新出现 DOM 行的冷态标题。行一旦完成首次 paint,后续文本
|
|
23
|
+
// 变化属于 DSH Core 的实时更新(例如重命名),插件不得再用轮询快照覆盖。
|
|
24
|
+
export function authoritativeTitleForFirstPaint({ firstPaint = false, rendered = '', authoritative = '' } = {}) {
|
|
25
|
+
if (!firstPaint || !authoritative || rendered === authoritative) return null
|
|
26
|
+
return authoritative
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
// 拖拽迁移前置校验:同工作区拦截(workspacePath 相等即拒绝)。
|
|
22
30
|
// 无 workspacePath 的会话(如侧栏 live 行尚未同步)放行,由 host 最终裁决。
|
|
23
31
|
export function canDropOnWorkspace(item, target) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function action(available, reason = null) {
|
|
2
|
+
return { available: !!available, reason: available ? null : reason }
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function detectCapabilities({ persistence, workspaceRegistry }) {
|
|
6
|
+
const handleApi = !!(persistence && typeof persistence.open === 'function')
|
|
7
|
+
const legacyRead = !!(persistence && typeof persistence.readFrom === 'function')
|
|
8
|
+
const legacyLocate = !!(persistence && (typeof persistence.locate === 'function'
|
|
9
|
+
|| (persistence.backend && typeof persistence.backend.locate === 'function')))
|
|
10
|
+
const workspaceInternals = !!(workspaceRegistry
|
|
11
|
+
&& workspaceRegistry.headers && workspaceRegistry.sessionPaths
|
|
12
|
+
&& typeof workspaceRegistry.replaceHeaderIndex === 'function')
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
persistence: handleApi ? 'session-handle' : 'legacy',
|
|
16
|
+
actions: {
|
|
17
|
+
read: action(legacyRead || handleApi, '当前 DSH 未提供可识别的会话读取接口'),
|
|
18
|
+
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === 'function'), '当前 DSH 未提供归档接口'),
|
|
19
|
+
trash: action(legacyRead || handleApi, '当前 DSH 无法读取会话,不能安全移入回收站'),
|
|
20
|
+
restoreTrash: action(true),
|
|
21
|
+
purge: action(!handleApi && legacyLocate, '当前 DSH 版本尚未提供经过验证的安全永久删除能力'),
|
|
22
|
+
move: action(!handleApi && legacyRead && legacyLocate && workspaceInternals, '当前 DSH 版本尚未提供经过验证的跨工作区迁移能力'),
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function requireCapability(capabilities, name) {
|
|
28
|
+
const value = capabilities && capabilities.actions && capabilities.actions[name]
|
|
29
|
+
if (value && value.available) return
|
|
30
|
+
const error = new Error((value && value.reason) || `当前环境不支持 ${name}`)
|
|
31
|
+
error.status = 409
|
|
32
|
+
error.code = 'DSM_CAPABILITY_UNAVAILABLE'
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Compatibility boundary for the two DSH persistence generations supported by
|
|
2
|
+
// dsh-sessions-manager. Business code consumes normalized headers and complete
|
|
3
|
+
// inspections; it never needs to know whether DSH returned a legacy header or
|
|
4
|
+
// a handle-era SessionPersistenceSnapshot.
|
|
5
|
+
|
|
6
|
+
function asHeader(value) {
|
|
7
|
+
if (!value || typeof value !== 'object') return null
|
|
8
|
+
const candidate = value.header && typeof value.header === 'object' ? value.header : value
|
|
9
|
+
return candidate.id == null ? null : candidate
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizePersistenceEntry(value) {
|
|
13
|
+
const header = asHeader(value)
|
|
14
|
+
if (!header) return null
|
|
15
|
+
const snapshot = value && value.header === header ? value : null
|
|
16
|
+
return {
|
|
17
|
+
header,
|
|
18
|
+
snapshot,
|
|
19
|
+
id: String(header.id),
|
|
20
|
+
sizeBytes: snapshot && Number.isFinite(snapshot.sizeBytes) ? Number(snapshot.sizeBytes) : null,
|
|
21
|
+
eventCount: snapshot && Number.isSafeInteger(snapshot.eventCount) ? snapshot.eventCount : null,
|
|
22
|
+
revision: snapshot ? snapshot.revision : null,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizePersistenceList(values) {
|
|
27
|
+
if (!Array.isArray(values)) return []
|
|
28
|
+
return values.map(normalizePersistenceEntry).filter(Boolean)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createPersistenceAdapter(service) {
|
|
32
|
+
if (!service || typeof service.list !== 'function') throw new TypeError('sessionPersistence.list is required')
|
|
33
|
+
|
|
34
|
+
async function listEntries(options) {
|
|
35
|
+
return normalizePersistenceList(await service.list(options))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function readSession(id, offset = 0) {
|
|
39
|
+
if (typeof service.readFrom === 'function') {
|
|
40
|
+
const result = await service.readFrom(id, offset)
|
|
41
|
+
return {
|
|
42
|
+
meta: result && result.meta ? result.meta : null,
|
|
43
|
+
inheritedEventCount: result && Number.isSafeInteger(result.inheritedEventCount) ? result.inheritedEventCount : 0,
|
|
44
|
+
events: result && Array.isArray(result.events) ? result.events : [],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (typeof service.open !== 'function') throw new Error('当前 DSH 持久化服务不支持读取会话')
|
|
48
|
+
const handle = await service.open(id, 'read')
|
|
49
|
+
if (!handle || typeof handle.read !== 'function' || typeof handle.close !== 'function') {
|
|
50
|
+
try { if (handle && typeof handle.close === 'function') await handle.close() } catch {}
|
|
51
|
+
throw new Error('DSH 返回了无效的 SessionHandle')
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const events = await handle.read(offset)
|
|
55
|
+
return {
|
|
56
|
+
meta: handle.header || handle.meta || null,
|
|
57
|
+
inheritedEventCount: Number.isSafeInteger(handle.inheritedEventCount) ? handle.inheritedEventCount : 0,
|
|
58
|
+
events: Array.isArray(events) ? events : [...(events || [])],
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
await handle.close()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function locate(header) {
|
|
66
|
+
if (typeof service.locate === 'function') return service.locate(header)
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const kind = typeof service.open === 'function' ? 'session-handle' : 'legacy'
|
|
71
|
+
return { kind, listEntries, readSession, locate }
|
|
72
|
+
}
|