dsh-vaultwarden 0.9.2 → 0.9.3
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/CHANGELOG.md +9 -0
- package/lib/client.js +115 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `dsh-vaultwarden` are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.9.3] — 2026-10-21
|
|
8
|
+
|
|
9
|
+
### 修复
|
|
10
|
+
|
|
11
|
+
- 修复首次加载后侧栏一直停在占位块、下方留一大片空白的问题。根因是「读取资料夹」与「读取侧栏统计」被放在同一个 `Promise.all` 里:后者决定侧栏画什么,前者只用于「移动到文件夹」,但前者一旦挂住,就会把已经返回的统计一起扣住,侧栏因此永远停在"加载中",而列表早已渲染出全部条目。
|
|
12
|
+
- 两次读取改为各自结算:各带自己的超时,谁先到谁先渲染,互不等待。资料夹读取超时 5 秒,侧栏统计 6 秒(且整条重试链共用一个上限,不是每次尝试各算一遍)。
|
|
13
|
+
- 统计读取失败时侧栏落到三个结构项并退出忙碌态,不再保留永远不会兑现的占位块。
|
|
14
|
+
- 占位骨架条的颜色由背景层色改为宿主边框色:原先在白底面板上几乎不可见,一片空白看起来像空盒子而不是"加载中"。
|
|
15
|
+
|
|
7
16
|
## [0.9.2] — 2026-10-21
|
|
8
17
|
|
|
9
18
|
### 变更
|
package/lib/client.js
CHANGED
|
@@ -396,7 +396,7 @@ const nextOffsetOf = (payload, fallback) => {
|
|
|
396
396
|
* `scripts/build.sh` stamps both constants from package.json on every
|
|
397
397
|
* build, so this fallback is never hand-maintained and cannot drift.
|
|
398
398
|
*/
|
|
399
|
-
const BUILD_VERSION = '0.9.
|
|
399
|
+
const BUILD_VERSION = '0.9.3'
|
|
400
400
|
const BUILD_REPOSITORY = 'https://github.com/BuvkB/dsh-vaultwarden'
|
|
401
401
|
/**
|
|
402
402
|
* Turn a package.json repository url into something a browser can open.
|
|
@@ -1709,7 +1709,13 @@ const nextOffsetOf = (payload, fallback) => {
|
|
|
1709
1709
|
* The pulse is in the stylesheet, so a reader who asked for reduced motion
|
|
1710
1710
|
* gets a static bar rather than a blinking one.
|
|
1711
1711
|
*/
|
|
1712
|
-
|
|
1712
|
+
// The host's border colour, not `bg2`. `bg2` is a layer tint (#f5f5f5 on
|
|
1713
|
+
// a white surface), and a bar painted in it is invisible on the panel's
|
|
1714
|
+
// own background: the rail then reads as an empty box rather than as
|
|
1715
|
+
// "still loading". The border token is by definition visible against the
|
|
1716
|
+
// surface it borders, in both themes, and no browser-support question
|
|
1717
|
+
// attaches to it the way it would to a colour mix.
|
|
1718
|
+
skeletonBar: { height: 10, borderRadius: 999, background: TOKEN.border, flex: 'none' },
|
|
1713
1719
|
/** A skeleton line: bar + optional label column, one per field row. */
|
|
1714
1720
|
skeletonField: { display: 'grid', gridTemplateColumns: '92px minmax(0, 1fr) 28px', gap: 12, alignItems: 'center' },
|
|
1715
1721
|
/** The list's own placeholder row: avatar tile + two bars + the chevron slot. */
|
|
@@ -2732,6 +2738,34 @@ const nextOffsetOf = (payload, fallback) => {
|
|
|
2732
2738
|
return /load failed|failed to fetch|networkerror|network error|fetch failed/i.test(text)
|
|
2733
2739
|
}
|
|
2734
2740
|
|
|
2741
|
+
/**
|
|
2742
|
+
* Put a ceiling on one read.
|
|
2743
|
+
*
|
|
2744
|
+
* A promise that never settles is worse than one that rejects: `await`
|
|
2745
|
+
* simply waits, so a request the host never answers — a request dropped by a
|
|
2746
|
+
* captive portal, a socket the phone's radio parked until the tab wakes —
|
|
2747
|
+
* leaves the sidebar shimmering with no rows and no error, which is the
|
|
2748
|
+
* one failure mode the reader cannot act on. The timeout turns that into an
|
|
2749
|
+
* ordinary rejection so the caller's existing fallback runs.
|
|
2750
|
+
*
|
|
2751
|
+
* Silent by construction: the timeout rejects with the same shape a failed
|
|
2752
|
+
* call would (no host code), so callers cannot tell a slow host from a
|
|
2753
|
+
* dropped one, and neither can the reader — who is told the counts are
|
|
2754
|
+
* unknown, not which of a dozen network faults produced that.
|
|
2755
|
+
*/
|
|
2756
|
+
const withTimeout = (promise, ms, label) =>
|
|
2757
|
+
new Promise((resolve, reject) => {
|
|
2758
|
+
const timer = setTimeout(() => {
|
|
2759
|
+
const error = new Error(`${label} 超时`)
|
|
2760
|
+
error.code = 'timeout'
|
|
2761
|
+
reject(error)
|
|
2762
|
+
}, ms)
|
|
2763
|
+
promise.then(
|
|
2764
|
+
(value) => { clearTimeout(timer); resolve(value) },
|
|
2765
|
+
(failure) => { clearTimeout(timer); reject(failure) },
|
|
2766
|
+
)
|
|
2767
|
+
})
|
|
2768
|
+
|
|
2735
2769
|
/** Build the RPC caller over the authenticated `/api` channel. */
|
|
2736
2770
|
const makeInvoke = (connection) => async (method, args = {}) => {
|
|
2737
2771
|
if (!connection || typeof connection.rpc?.call !== 'function') {
|
|
@@ -2781,6 +2815,30 @@ const nextOffsetOf = (payload, fallback) => {
|
|
|
2781
2815
|
* meantime, which is a worse bug than the one this fixes.
|
|
2782
2816
|
*/
|
|
2783
2817
|
const CLIPBOARD_TTL_MS = 20000
|
|
2818
|
+
/**
|
|
2819
|
+
* Ceiling on the sidebar's overview read, in milliseconds.
|
|
2820
|
+
*
|
|
2821
|
+
* A read that never settles is worse than one that rejects: `await` simply
|
|
2822
|
+
* waits, so a request the host never answers — dropped by a captive portal,
|
|
2823
|
+
* or a socket the phone's radio parked until the tab wakes — would leave the
|
|
2824
|
+
* sidebar shimmering with no rows and no error, which is the one failure
|
|
2825
|
+
* mode the reader cannot act on. The timeout turns that into an ordinary
|
|
2826
|
+
* rejection so the caller's existing fallback runs.
|
|
2827
|
+
*
|
|
2828
|
+
* Long enough that a merely slow network still gets its counts, short enough
|
|
2829
|
+
* that the rail settles into its structural places when the answer is simply
|
|
2830
|
+
* not coming: the sidebar is furniture, and a count that never arrives must
|
|
2831
|
+
* not hold the shape of the panel hostage.
|
|
2832
|
+
*/
|
|
2833
|
+
const OVERVIEW_TIMEOUT_MS = 6000
|
|
2834
|
+
/**
|
|
2835
|
+
* The same ceiling for the other read that describes a place rather than a
|
|
2836
|
+
* row. Shorter than the overview's, because nothing waits on it: the folder
|
|
2837
|
+
* list only feeds the move-to-folder step, so a slow answer is better
|
|
2838
|
+
* dropped than kept open — but it still must not stay open forever, which
|
|
2839
|
+
* is exactly how the sidebar used to get stuck.
|
|
2840
|
+
*/
|
|
2841
|
+
const READ_TIMEOUT_MS = 5000
|
|
2784
2842
|
/**
|
|
2785
2843
|
* Write `text` to the clipboard and schedule its removal.
|
|
2786
2844
|
*
|
|
@@ -5075,15 +5133,52 @@ const nextOffsetOf = (payload, fallback) => {
|
|
|
5075
5133
|
// either method, or a read that fails, leaves the sidebar on the one
|
|
5076
5134
|
// place that needs no counts at all.
|
|
5077
5135
|
await readVault(nextQuery, { quiet: Boolean(paintedSection ?? painted), identity, section: view })
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5136
|
+
// These two reads are settled INDEPENDENTLY, not awaited together.
|
|
5137
|
+
//
|
|
5138
|
+
// They used to sit in one Promise.all, which made the slower read the
|
|
5139
|
+
// gate for the faster one: the rail is drawn from `overview`, and
|
|
5140
|
+
// `folders` only feeds the move-to-folder step, but a folders call
|
|
5141
|
+
// that never settled held the already-arrived overview hostage. The
|
|
5142
|
+
// panel then showed its rows (readVault had finished) under a rail
|
|
5143
|
+
// stuck on its ghost block, forever — the list said 428 entries and
|
|
5144
|
+
// the sidebar still said "loading". That is the reported bug, and no
|
|
5145
|
+
// amount of retrying or timing out the overview can fix it, because
|
|
5146
|
+
// the overview had already succeeded.
|
|
5147
|
+
//
|
|
5148
|
+
// So each read gets its own ceiling and its own landing. Whatever
|
|
5149
|
+
// arrives is applied at once; whatever does not is dropped on the
|
|
5150
|
+
// floor by its own timer. Nothing here waits for anything else.
|
|
5151
|
+
const foldersRead = withTimeout(invoke('folders'), READ_TIMEOUT_MS, 'folders')
|
|
5152
|
+
.then((payload) => setFolders(Array.isArray(payload?.folders) ? payload.folders : []))
|
|
5153
|
+
.catch(() => { /* an older host, or a read that failed: keep the folders on screen */ })
|
|
5154
|
+
// Retry once: a single transient failure on a slow network used to
|
|
5155
|
+
// leave the rail shimmering with no rows and no way to get them — the
|
|
5156
|
+
// panel looked broken rather than busy. One quiet retry covers the
|
|
5157
|
+
// dropped first attempt without doubling the cost of a host that
|
|
5158
|
+
// genuinely lacks the method.
|
|
5159
|
+
//
|
|
5160
|
+
// ONE ceiling over the chain, not one per attempt: bounding each try
|
|
5161
|
+
// separately makes the worst case the sum of them (two six-second
|
|
5162
|
+
// waits), and the sum is exactly the case this exists for — a request
|
|
5163
|
+
// that never answers, where the retry cannot help because the first
|
|
5164
|
+
// one is still open. A single timer covers both attempts and the gap
|
|
5165
|
+
// between them.
|
|
5166
|
+
const overviewRead = withTimeout(invoke('overview').catch(() => invoke('overview')), OVERVIEW_TIMEOUT_MS, 'overview')
|
|
5167
|
+
.then((payload) => {
|
|
5168
|
+
openCache.overview = { key: identity, overview: payload, at: Date.now() }
|
|
5169
|
+
setOverview(payload)
|
|
5170
|
+
})
|
|
5171
|
+
.catch(() => {
|
|
5172
|
+
// Nothing arrived: stop pretending the rail is loading. An empty
|
|
5173
|
+
// overview renders the three structural places and lets the nav
|
|
5174
|
+
// fall out of its busy state, which is honest — the counts are
|
|
5175
|
+
// unknown, but the rail is not stuck. A rail that kept claiming
|
|
5176
|
+
// to be busy while the list underneath was already full is the
|
|
5177
|
+
// shape of a hung panel, which is the one thing this file spends
|
|
5178
|
+
// its length trying to avoid.
|
|
5179
|
+
setOverview({ failed: true, sections: [], types: [], folderList: [], folderCount: 0, unfiled: 0 })
|
|
5180
|
+
})
|
|
5181
|
+
await Promise.all([foldersRead, overviewRead])
|
|
5087
5182
|
},
|
|
5088
5183
|
[invoke, readVault],
|
|
5089
5184
|
)
|
|
@@ -6109,6 +6204,14 @@ const nextOffsetOf = (payload, fallback) => {
|
|
|
6109
6204
|
navPlaceholders(t),
|
|
6110
6205
|
)
|
|
6111
6206
|
}
|
|
6207
|
+
// The read failed and no retry is coming: the rail keeps its three
|
|
6208
|
+
// structural places and stops claiming to be busy. Ghosts here would be
|
|
6209
|
+
// a placeholder for something that is never going to arrive, which is
|
|
6210
|
+
// the shape of a hung panel — the one thing this file spends its length
|
|
6211
|
+
// trying to avoid.
|
|
6212
|
+
if (overview.failed) {
|
|
6213
|
+
return h('nav', { 'data-vw-nav': '', style: S.nav, 'aria-label': t('navSections') }, places)
|
|
6214
|
+
}
|
|
6112
6215
|
const types = overview.types ?? []
|
|
6113
6216
|
if (types.length) {
|
|
6114
6217
|
places.push(h('div', { key: 'g-types', 'data-vw-nav-group': 'types', style: S.navGroup }, `${t('sectionTypes')} (${types.length})`))
|
|
@@ -6145,7 +6248,7 @@ const nextOffsetOf = (payload, fallback) => {
|
|
|
6145
6248
|
const refreshOverview = async () => {
|
|
6146
6249
|
const identity = cacheKey(openCache.config?.config)
|
|
6147
6250
|
try {
|
|
6148
|
-
const payload = await invoke('overview')
|
|
6251
|
+
const payload = await withTimeout(invoke('overview'), OVERVIEW_TIMEOUT_MS, 'overview')
|
|
6149
6252
|
openCache.overview = { key: identity, overview: payload, at: Date.now() }
|
|
6150
6253
|
setOverview(payload)
|
|
6151
6254
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vaultwarden",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Vaultwarden/Bitwarden real-time sync for DeepSeek Harness: WebSocket push notifications, on-disk vault cache with revision probing, read/write tools and an entry browser panel. Independent implementation; see THIRD-PARTY-NOTICES.md.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|