@khorsheed/dsh-ankh-guard 0.4.1 → 0.4.2

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.4.2(2026-09-30)
4
+
5
+ - **修复重启浮层卡死**:标签页带着 pending handoff 时,若 cutover 已不经由本标签页收口(操作员 restore、handoff off、监督链楔死),服务端对该 cutoverId 永远回 409,「正在重启」浮层会无限重试不下台。现在每 8 次失败 ACK 花一次 poll——`idle` 即证明没有在途事务,丢弃 pending 并收浮层。sessionStorage 按标签页存活,所以旧实例上已卡住的标签页不受影响此修复——关掉重开即可(3080 实测案例)
6
+
3
7
  ## 0.4.1(2026-09-30)
4
8
 
5
9
  修复 2026-09-29/30 的 0.2.0-rc.2 切换事故暴露的三个缺口:
package/lib/client.js CHANGED
@@ -159,6 +159,8 @@ window.__ModuleLoader__.load({
159
159
  let timer;
160
160
  let pending = consumeFallbackFragment() ?? readPending();
161
161
  let cap;
162
+ /** Consecutive failed ACKs for the current pending handoff. */
163
+ let ackFailures = 0;
162
164
  let errorRetryMs = ERROR_RETRY_MIN_MS;
163
165
  let firstFailureAt;
164
166
  let request;
@@ -226,6 +228,22 @@ window.__ModuleLoader__.load({
226
228
  location.replace(returnPath);
227
229
  return;
228
230
  }
231
+ } else {
232
+ ackFailures += 1;
233
+ if (ackFailures >= 8) {
234
+ ackFailures = 0;
235
+ const probe = await post({
236
+ version: 1,
237
+ operation: "poll",
238
+ capability: cap ??= capability()
239
+ }, current.signal);
240
+ if (stale()) return;
241
+ if (probe.ok && (await probe.json()).state === "idle") {
242
+ sessionStorage.removeItem(PENDING_KEY);
243
+ sessionStorage.removeItem(CAPABILITY_KEY);
244
+ pending = null;
245
+ }
246
+ }
229
247
  }
230
248
  errorRetryMs = ERROR_RETRY_MIN_MS;
231
249
  firstFailureAt = void 0;
@@ -278,6 +296,7 @@ window.__ModuleLoader__.load({
278
296
  };
279
297
  pending = nextPending;
280
298
  writePending(nextPending);
299
+ ackFailures = 0;
281
300
  waitingOverlay();
282
301
  location.reload();
283
302
  return;
@@ -299,6 +318,7 @@ window.__ModuleLoader__.load({
299
318
  };
300
319
  pending = nextPending;
301
320
  writePending(nextPending);
321
+ ackFailures = 0;
302
322
  waitingOverlay();
303
323
  location.replace(launch.href);
304
324
  return;
@@ -155,6 +155,8 @@ export function apply(_ctx) {
155
155
  let timer;
156
156
  let pending = consumeFallbackFragment() ?? readPending();
157
157
  let cap;
158
+ /** Consecutive failed ACKs for the current pending handoff. */
159
+ let ackFailures = 0;
158
160
  let errorRetryMs = ERROR_RETRY_MIN_MS;
159
161
  let firstFailureAt;
160
162
  let request;
@@ -231,6 +233,29 @@ export function apply(_ctx) {
231
233
  return;
232
234
  }
233
235
  }
236
+ else {
237
+ // A failed ACK can be permanent: the cutover settled without this tab
238
+ // (operator restore, handoff off, a wedged supervisor) and the server
239
+ // keeps answering 409 'waiting'/'stale' for a dead cutoverId — the
240
+ // veil would otherwise stay up forever (observed 2026-09-30). Every
241
+ // few failures, spend one poll: 'idle' proves nothing is in flight,
242
+ // so the pending handoff is dead — drop it and let the ordinary
243
+ // poll loop remove the veil on the next tick.
244
+ ackFailures += 1;
245
+ if (ackFailures >= 8) {
246
+ ackFailures = 0;
247
+ const probe = await post({
248
+ version: 1, operation: 'poll', capability: cap ??= capability(),
249
+ }, current.signal);
250
+ if (stale())
251
+ return;
252
+ if (probe.ok && (await probe.json()).state === 'idle') {
253
+ sessionStorage.removeItem(PENDING_KEY);
254
+ sessionStorage.removeItem(CAPABILITY_KEY);
255
+ pending = null;
256
+ }
257
+ }
258
+ }
234
259
  errorRetryMs = ERROR_RETRY_MIN_MS;
235
260
  firstFailureAt = undefined;
236
261
  schedule(ACTIVE_RETRY_MS);
@@ -287,6 +312,7 @@ export function apply(_ctx) {
287
312
  };
288
313
  pending = nextPending;
289
314
  writePending(nextPending);
315
+ ackFailures = 0;
290
316
  waitingOverlay();
291
317
  location.reload();
292
318
  return;
@@ -307,6 +333,7 @@ export function apply(_ctx) {
307
333
  };
308
334
  pending = nextPending;
309
335
  writePending(nextPending);
336
+ ackFailures = 0;
310
337
  waitingOverlay();
311
338
  // The bearer remains a local variable for the shortest possible time.
312
339
  location.replace(launch.href);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@khorsheed/dsh-ankh-guard",
3
3
  "description": "Hard gate for self-modification restarts: a green-build credential bound to the git HEAD, checked before any restart of the running instance",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",