@cmtlyt/lingshu-toolkit 0.5.0 → 0.7.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.
Files changed (211) hide show
  1. package/README.md +10 -0
  2. package/dist/665.js +1 -0
  3. package/dist/893.js +1 -0
  4. package/dist/react/index.js +1 -205
  5. package/dist/react/use-boolean/index.d.ts +2 -1
  6. package/dist/react/use-boolean/index.js +1 -16
  7. package/dist/react/use-controllable-value/index.d.ts +3 -3
  8. package/dist/react/use-controllable-value/index.js +1 -32
  9. package/dist/react/use-counter/index.d.ts +2 -2
  10. package/dist/react/use-counter/index.js +1 -49
  11. package/dist/react/use-force-update/index.d.ts +2 -1
  12. package/dist/react/use-force-update/index.js +1 -6
  13. package/dist/react/use-mount/index.d.ts +2 -1
  14. package/dist/react/use-mount/index.js +1 -16
  15. package/dist/react/use-ref-state/index.d.ts +3 -2
  16. package/dist/react/use-ref-state/index.js +1 -33
  17. package/dist/react/use-storage/index.d.ts +2 -1
  18. package/dist/react/use-storage/index.js +1 -15
  19. package/dist/react/use-title/index.d.ts +2 -2
  20. package/dist/react/use-title/index.js +1 -24
  21. package/dist/react/use-toggle/index.d.ts +4 -4
  22. package/dist/react/use-toggle/index.js +1 -26
  23. package/dist/react/use-valid-data/index.d.ts +5 -4
  24. package/dist/react/use-valid-data/index.js +1 -14
  25. package/dist/shared/allx/index.d.ts +2 -1
  26. package/dist/shared/allx/index.js +1 -44
  27. package/dist/shared/allx/types.d.ts +6 -0
  28. package/dist/shared/allx/utils.d.ts +9 -7
  29. package/dist/shared/allx/utils.js +1 -94
  30. package/dist/shared/animation/index.d.ts +3 -2
  31. package/dist/shared/animation/index.js +1 -77
  32. package/dist/shared/animation/types.d.ts +8 -0
  33. package/dist/shared/animation/utils.d.ts +3 -10
  34. package/dist/shared/animation/utils.js +1 -134
  35. package/dist/shared/api-controller/create-api.js +1 -79
  36. package/dist/shared/api-controller/index.js +1 -3
  37. package/dist/shared/api-controller/request.js +1 -66
  38. package/dist/shared/api-controller/types.d.ts +26 -27
  39. package/dist/shared/api-controller/utils.d.ts +6 -15
  40. package/dist/shared/api-controller/utils.js +1 -96
  41. package/dist/shared/condition-merge/index.d.ts +6 -6
  42. package/dist/shared/condition-merge/index.js +1 -30
  43. package/dist/shared/create-storage-handler/index.d.ts +4 -3
  44. package/dist/shared/create-storage-handler/index.js +1 -68
  45. package/dist/shared/data-handler/index.d.ts +4 -3
  46. package/dist/shared/data-handler/index.js +1 -77
  47. package/dist/shared/data-handler/tools.d.ts +6 -23
  48. package/dist/shared/data-handler/tools.js +1 -48
  49. package/dist/shared/data-handler/types.d.ts +20 -2
  50. package/dist/shared/data-mixed-manager/constants.js +1 -9
  51. package/dist/shared/data-mixed-manager/index.js +1 -226
  52. package/dist/shared/data-mixed-manager/types.d.ts +1 -2
  53. package/dist/shared/index.d.ts +2 -0
  54. package/dist/shared/index.js +1 -957
  55. package/dist/shared/lock-data/__test__/_helpers/memory-adapters.d.ts +95 -0
  56. package/dist/shared/lock-data/__test__/_helpers/memory-adapters.js +1 -0
  57. package/dist/shared/lock-data/__test__/playground.js +1 -0
  58. package/dist/shared/lock-data/adapters/authority.d.ts +40 -0
  59. package/dist/shared/lock-data/adapters/authority.js +1 -0
  60. package/dist/shared/lock-data/adapters/channel.d.ts +39 -0
  61. package/dist/shared/lock-data/adapters/channel.js +1 -0
  62. package/dist/shared/lock-data/adapters/index.d.ts +58 -0
  63. package/dist/shared/lock-data/adapters/index.js +1 -0
  64. package/dist/shared/lock-data/adapters/logger.d.ts +56 -0
  65. package/dist/shared/lock-data/adapters/logger.js +1 -0
  66. package/dist/shared/lock-data/adapters/session-store.d.ts +37 -0
  67. package/dist/shared/lock-data/adapters/session-store.js +1 -0
  68. package/dist/shared/lock-data/authority/epoch.d.ts +135 -0
  69. package/dist/shared/lock-data/authority/epoch.js +1 -0
  70. package/dist/shared/lock-data/authority/extract.d.ts +107 -0
  71. package/dist/shared/lock-data/authority/extract.js +1 -0
  72. package/dist/shared/lock-data/authority/index.d.ts +182 -0
  73. package/dist/shared/lock-data/authority/index.js +1 -0
  74. package/dist/shared/lock-data/authority/serialize.d.ts +35 -0
  75. package/dist/shared/lock-data/authority/serialize.js +1 -0
  76. package/dist/shared/lock-data/constants.d.ts +46 -0
  77. package/dist/shared/lock-data/constants.js +1 -0
  78. package/dist/shared/lock-data/core/actions-helpers.d.ts +163 -0
  79. package/dist/shared/lock-data/core/actions-helpers.js +1 -0
  80. package/dist/shared/lock-data/core/actions.d.ts +72 -0
  81. package/dist/shared/lock-data/core/actions.js +1 -0
  82. package/dist/shared/lock-data/core/draft.d.ts +64 -0
  83. package/dist/shared/lock-data/core/draft.js +1 -0
  84. package/dist/shared/lock-data/core/entry.d.ts +133 -0
  85. package/dist/shared/lock-data/core/entry.js +1 -0
  86. package/dist/shared/lock-data/core/fanout.d.ts +42 -0
  87. package/dist/shared/lock-data/core/fanout.js +1 -0
  88. package/dist/shared/lock-data/core/readonly-view.d.ts +49 -0
  89. package/dist/shared/lock-data/core/readonly-view.js +1 -0
  90. package/dist/shared/lock-data/core/registry.d.ts +282 -0
  91. package/dist/shared/lock-data/core/registry.js +1 -0
  92. package/dist/shared/lock-data/core/signal.d.ts +33 -0
  93. package/dist/shared/lock-data/core/signal.js +1 -0
  94. package/dist/shared/lock-data/drivers/broadcast-protocol.d.ts +71 -0
  95. package/dist/shared/lock-data/drivers/broadcast-protocol.js +1 -0
  96. package/dist/shared/lock-data/drivers/broadcast-state.d.ts +125 -0
  97. package/dist/shared/lock-data/drivers/broadcast-state.js +1 -0
  98. package/dist/shared/lock-data/drivers/broadcast.d.ts +36 -0
  99. package/dist/shared/lock-data/drivers/broadcast.js +1 -0
  100. package/dist/shared/lock-data/drivers/custom.d.ts +27 -0
  101. package/dist/shared/lock-data/drivers/custom.js +1 -0
  102. package/dist/shared/lock-data/drivers/index.d.ts +59 -0
  103. package/dist/shared/lock-data/drivers/index.js +1 -0
  104. package/dist/shared/lock-data/drivers/local.d.ts +86 -0
  105. package/dist/shared/lock-data/drivers/local.js +1 -0
  106. package/dist/shared/lock-data/drivers/storage-protocol.d.ts +67 -0
  107. package/dist/shared/lock-data/drivers/storage-protocol.js +1 -0
  108. package/dist/shared/lock-data/drivers/storage-state.d.ts +103 -0
  109. package/dist/shared/lock-data/drivers/storage-state.js +1 -0
  110. package/dist/shared/lock-data/drivers/storage.d.ts +71 -0
  111. package/dist/shared/lock-data/drivers/storage.js +1 -0
  112. package/dist/shared/lock-data/drivers/types.d.ts +73 -0
  113. package/dist/shared/lock-data/drivers/types.js +0 -0
  114. package/dist/shared/lock-data/drivers/web-locks.d.ts +123 -0
  115. package/dist/shared/lock-data/drivers/web-locks.js +1 -0
  116. package/dist/shared/lock-data/errors/index.d.ts +12 -0
  117. package/dist/shared/lock-data/errors/index.js +1 -0
  118. package/dist/shared/lock-data/errors/invalid-options-error.d.ts +11 -0
  119. package/dist/shared/lock-data/errors/invalid-options-error.js +1 -0
  120. package/dist/shared/lock-data/errors/lock-aborted-error.d.ts +10 -0
  121. package/dist/shared/lock-data/errors/lock-aborted-error.js +1 -0
  122. package/dist/shared/lock-data/errors/lock-disposed-error.d.ts +14 -0
  123. package/dist/shared/lock-data/errors/lock-disposed-error.js +1 -0
  124. package/dist/shared/lock-data/errors/lock-revoked-error.d.ts +10 -0
  125. package/dist/shared/lock-data/errors/lock-revoked-error.js +1 -0
  126. package/dist/shared/lock-data/errors/lock-timeout-error.d.ts +9 -0
  127. package/dist/shared/lock-data/errors/lock-timeout-error.js +1 -0
  128. package/dist/shared/lock-data/errors/readonly-mutation-error.d.ts +11 -0
  129. package/dist/shared/lock-data/errors/readonly-mutation-error.js +1 -0
  130. package/dist/shared/lock-data/index.d.ts +57 -0
  131. package/dist/shared/lock-data/index.js +1 -0
  132. package/dist/shared/lock-data/types.d.ts +347 -0
  133. package/dist/shared/lock-data/types.js +0 -0
  134. package/dist/shared/lock-data/utils/json-safe.d.ts +69 -0
  135. package/dist/shared/lock-data/utils/json-safe.js +1 -0
  136. package/dist/shared/logger/index.d.ts +2 -2
  137. package/dist/shared/logger/index.js +1 -10
  138. package/dist/shared/priority-queue/index.d.ts +45 -0
  139. package/dist/shared/priority-queue/index.js +1 -0
  140. package/dist/shared/priority-queue/types.d.ts +10 -0
  141. package/dist/shared/priority-queue/types.js +0 -0
  142. package/dist/shared/priority-queue/utils.d.ts +7 -0
  143. package/dist/shared/priority-queue/utils.js +1 -0
  144. package/dist/shared/throw-error/index.d.ts +11 -3
  145. package/dist/shared/throw-error/index.js +1 -10
  146. package/dist/shared/try-call/index.d.ts +3 -3
  147. package/dist/shared/try-call/index.js +1 -59
  148. package/dist/shared/types/index.js +1 -2
  149. package/dist/shared/types/pack.d.ts +2 -2
  150. package/dist/shared/types/pack.js +1 -1
  151. package/dist/shared/utils/base.d.ts +1 -1
  152. package/dist/shared/utils/base.js +1 -6
  153. package/dist/shared/utils/index.js +1 -2
  154. package/dist/shared/utils/verify.d.ts +1 -1
  155. package/dist/shared/utils/verify.js +1 -67
  156. package/dist/shared/with-resolvers/index.d.ts +5 -3
  157. package/dist/shared/with-resolvers/index.js +1 -15
  158. package/dist/vue/index.js +1 -29
  159. package/dist/vue/use-title/index.d.ts +2 -2
  160. package/dist/vue/use-title/index.js +1 -29
  161. package/package.json +27 -27
  162. package/dist/247.js +0 -66
  163. package/dist/707.js +0 -142
  164. package/dist/react/use-force-update/index.test.d.ts +0 -1
  165. package/dist/react/use-mount/index.test.d.ts +0 -1
  166. package/dist/react/use-ref-state/index.test.d.ts +0 -1
  167. package/dist/react/use-storage/index.test.d.ts +0 -1
  168. package/dist/react/use-title/index.test.d.ts +0 -1
  169. package/dist/react/use-toggle/index.test.d.ts +0 -1
  170. package/dist/react/use-valid-data/index.test.d.ts +0 -1
  171. package/dist/shared/allx/__test__/allsettled.test.d.ts +0 -1
  172. package/dist/shared/allx/__test__/basic.test.d.ts +0 -1
  173. package/dist/shared/allx/__test__/circular-dependency.test.d.ts +0 -1
  174. package/dist/shared/allx/__test__/dependency.test.d.ts +0 -1
  175. package/dist/shared/allx/__test__/edge-cases.test.d.ts +0 -1
  176. package/dist/shared/allx/__test__/error-handling.test.d.ts +0 -1
  177. package/dist/shared/allx/__test__/execution-order.test.d.ts +0 -1
  178. package/dist/shared/allx/__test__/falsy-values.test.d.ts +0 -1
  179. package/dist/shared/allx/__test__/performance.test.d.ts +0 -1
  180. package/dist/shared/allx/__test__/type-checking.test.d.ts +0 -1
  181. package/dist/shared/allx/__test__/use-cases.test.d.ts +0 -1
  182. package/dist/shared/animation/__test__/animation-pause-resume.test.d.ts +0 -1
  183. package/dist/shared/animation/__test__/animation.test.d.ts +0 -1
  184. package/dist/shared/animation/__test__/step-animation.test.d.ts +0 -1
  185. package/dist/shared/animation/__test__/utils.test.d.ts +0 -1
  186. package/dist/shared/api-controller/__test__/index.browser.test.d.ts +0 -1
  187. package/dist/shared/api-controller/__test__/index.node.test.d.ts +0 -1
  188. package/dist/shared/condition-merge/index.test-d.js +0 -108
  189. package/dist/shared/condition-merge/index.test.d.ts +0 -1
  190. package/dist/shared/create-storage-handler/index.browser.test.d.ts +0 -1
  191. package/dist/shared/create-storage-handler/index.test.d.ts +0 -1
  192. package/dist/shared/data-handler/index.test.d.ts +0 -1
  193. package/dist/shared/data-mixed-manager/__test__/basic.test.d.ts +0 -1
  194. package/dist/shared/data-mixed-manager/__test__/build-options.test.d.ts +0 -1
  195. package/dist/shared/data-mixed-manager/__test__/constructor-options.test.d.ts +0 -1
  196. package/dist/shared/data-mixed-manager/__test__/data-management.test.d.ts +0 -1
  197. package/dist/shared/data-mixed-manager/__test__/edge-cases.test.d.ts +0 -1
  198. package/dist/shared/data-mixed-manager/__test__/events.browser.test.d.ts +0 -1
  199. package/dist/shared/data-mixed-manager/__test__/events.test.d.ts +0 -1
  200. package/dist/shared/data-mixed-manager/__test__/fixed-slots.test.d.ts +0 -1
  201. package/dist/shared/data-mixed-manager/__test__/insert-mode.test.d.ts +0 -1
  202. package/dist/shared/throw-error/index.test.d.ts +0 -1
  203. package/dist/shared/try-call/index.test.d.ts +0 -1
  204. package/dist/shared/utils/__test__/base.test.d.ts +0 -1
  205. package/dist/shared/utils/__test__/verify.test.d.ts +0 -1
  206. package/dist/shared/with-resolvers/index.test.d.ts +0 -1
  207. package/dist/test/utils.d.ts +0 -13
  208. package/dist/vue/use-title/index.test.d.ts +0 -1
  209. /package/dist/{react/use-boolean/index.test.d.ts → shared/lock-data/__test__/index.test-d.d.ts} +0 -0
  210. /package/dist/{react/use-controllable-value/index.test.d.ts → shared/lock-data/__test__/integration/entry.test-d.d.ts} +0 -0
  211. /package/dist/{react/use-counter/index.test.d.ts → shared/lock-data/__test__/playground.d.ts} +0 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * BroadcastDriver 状态层:状态机定义、消息处理、竞选流程、drain
3
+ *
4
+ * 本文件不对外暴露 API;`broadcast.ts` 作为工厂聚合层消费此处导出的内部符号。
5
+ *
6
+ * ## 本地状态机
7
+ * - `idle`:无人持锁;可直接 announce
8
+ * - `holding`:本 Tab 持锁;周期广播 heartbeat
9
+ * - `remote-held`:远端持锁;监测心跳过期(DEAD_THRESHOLD 未收到 heartbeat → 回 idle)
10
+ *
11
+ * ## 竞选协议(非 force)
12
+ * 1. 广播 `announce`,启动 `REJECT_WINDOW` 窗口
13
+ * 2. 期间规则:
14
+ * - 收到 `reject(requestId 匹配)` → abandonPendingAnnounce,waiter 回队等待
15
+ * - 收到他方 `announce` → 按 (ts, requestId) 字典序仲裁;对方更早则本方 abandon
16
+ * - 收到 `heartbeat` → 存在持有者,本方 abandon 并切 remote-held
17
+ * 3. 窗口到期无拒绝 → enterHolding
18
+ *
19
+ * ## force 协议(BC-1 修复:异步等待对端 revoke 完成)
20
+ * 1. 广播 `force`,启动 `FORCE_ARBITRATION_WINDOW` 窗口
21
+ * 2. 期间收到他方 `force` → 按 (ts, token) 字典序仲裁;对方更早则本方 abandon
22
+ * 3. 窗口到期后 enterHolding;对端持有者收到 force 时立即 revoke('force')
23
+ *
24
+ * ## 持锁期间冲突检测(BC-2 修复)
25
+ * - `holding` 状态收到 `reject(holderToken != 自己)` → 双持冲突,revoke 自己
26
+ * - `holding` 状态收到 `heartbeat(token != 自己)` → 双持冲突,revoke 自己
27
+ */
28
+ /** biome-ignore-all lint/nursery/noExcessiveLinesPerFile: ignore */
29
+ import type { ChannelAdapter, LockDriverHandle } from '../types';
30
+ import { type AnnounceMessage, type ForceMessage, type HeartbeatMessage, type RejectMessage, type ReleaseMessage } from './broadcast-protocol';
31
+ import type { LockDriverDeps } from './types';
32
+ interface HoldingState {
33
+ readonly kind: 'holding';
34
+ readonly token: string;
35
+ readonly grantedAt: number;
36
+ released: boolean;
37
+ revokeCallback: ((reason: 'force' | 'timeout') => void) | null;
38
+ heartbeatTimer: ReturnType<typeof setInterval> | null;
39
+ }
40
+ interface RemoteHeldState {
41
+ readonly kind: 'remote-held';
42
+ token: string;
43
+ peerTs: number;
44
+ lastHeartbeat: number;
45
+ deadTimer: ReturnType<typeof setTimeout> | null;
46
+ }
47
+ interface IdleState {
48
+ readonly kind: 'idle';
49
+ }
50
+ type DriverState = IdleState | HoldingState | RemoteHeldState;
51
+ interface Waiter {
52
+ readonly token: string;
53
+ readonly resolve: (handle: LockDriverHandle) => void;
54
+ readonly reject: (error: Error) => void;
55
+ readonly abort: (error: Error) => void;
56
+ }
57
+ interface PendingAnnounce {
58
+ readonly requestId: string;
59
+ readonly ts: number;
60
+ readonly waiter: Waiter;
61
+ abandoned: boolean;
62
+ timer: ReturnType<typeof setTimeout> | null;
63
+ }
64
+ interface PendingForce {
65
+ readonly token: string;
66
+ readonly ts: number;
67
+ readonly waiter: Waiter;
68
+ abandoned: boolean;
69
+ timer: ReturnType<typeof setTimeout> | null;
70
+ }
71
+ interface BroadcastDriverState {
72
+ readonly deps: LockDriverDeps;
73
+ readonly channel: ChannelAdapter;
74
+ readonly senderId: string;
75
+ status: DriverState;
76
+ readonly waiters: Waiter[];
77
+ pendingAnnounce: PendingAnnounce | null;
78
+ pendingForce: PendingForce | null;
79
+ destroyed: boolean;
80
+ unsubscribe: (() => void) | null;
81
+ }
82
+ declare function handleRemoteDead(state: BroadcastDriverState, deadToken: string): void;
83
+ declare function revokeHolding(state: BroadcastDriverState, reason: 'force' | 'timeout'): void;
84
+ declare function enterHolding(state: BroadcastDriverState, token: string): LockDriverHandle;
85
+ declare function enterRemoteHeld(state: BroadcastDriverState, token: string, peerTs: number): void;
86
+ declare function abandonPendingAnnounce(state: BroadcastDriverState, reason: string): void;
87
+ declare function abandonPendingForce(state: BroadcastDriverState, reason: string): void;
88
+ /**
89
+ * 收到他方 announce:
90
+ * - 本方 holding(token 不同)→ 广播 reject;对方 handleReject 触发 abandonPendingAnnounce
91
+ * - 本方有 pendingAnnounce → 按 (ts, requestId) 字典序仲裁:
92
+ * - 我方更早 → 保持;对方在其本地也会执行相同仲裁并 abandon 自己
93
+ * - 对方更早 → 本方 abandon,回队等待
94
+ * - 本方 idle / remote-held(且无 pending)→ 不响应,让对方 announce 自然走完窗口
95
+ */
96
+ declare function handleAnnounce(state: BroadcastDriverState, msg: AnnounceMessage): void;
97
+ declare function handleReject(state: BroadcastDriverState, msg: RejectMessage): void;
98
+ declare function handleHeartbeat(state: BroadcastDriverState, msg: HeartbeatMessage): void;
99
+ declare function handleRelease(state: BroadcastDriverState, msg: ReleaseMessage): void;
100
+ /**
101
+ * 收到他方 force:
102
+ * - 本方 pendingForce → 按 (ts, token) 字典序仲裁;败方 abandon
103
+ * - 本方 holding(token 不同)→ 立即 revoke('force')
104
+ * - 切 / 刷新 remote-held
105
+ */
106
+ declare function handleForce(state: BroadcastDriverState, msg: ForceMessage): void;
107
+ declare function handleMessage(state: BroadcastDriverState, raw: unknown): void;
108
+ declare function removeWaiter(waiters: Waiter[], target: Waiter): void;
109
+ declare function pumpNextWaiter(state: BroadcastDriverState): void;
110
+ /**
111
+ * 启动 announce 竞选
112
+ *
113
+ * 前置条件(由调用方 pumpNextWaiter / acquireBroadcastLock 保证):
114
+ * - state.destroyed === false
115
+ * - state.status.kind === 'idle'
116
+ * - state.pendingAnnounce === null && state.pendingForce === null
117
+ *
118
+ * 若违反前置条件视为 driver 内部 bug,logger.error 记录后把 waiter 回队兜底
119
+ * (而非静默死等 —— BC-K 修复)
120
+ */
121
+ declare function startAnnounceCampaign(state: BroadcastDriverState, waiter: Waiter): void;
122
+ declare function startForceCampaign(state: BroadcastDriverState, waiter: Waiter): void;
123
+ declare function drainOnDestroy(state: BroadcastDriverState, buildAbortError: (token: string) => Error): void;
124
+ export type { BroadcastDriverState, HoldingState, RemoteHeldState, Waiter };
125
+ export { abandonPendingAnnounce, abandonPendingForce, drainOnDestroy, enterHolding, enterRemoteHeld, handleAnnounce, handleForce, handleHeartbeat, handleMessage, handleReject, handleRelease, handleRemoteDead, pumpNextWaiter, removeWaiter, revokeHolding, startAnnounceCampaign, startForceCampaign, };
@@ -0,0 +1 @@
1
+ import{isFunction as e}from"../../utils/index.js";import{DEAD_THRESHOLD as n,FORCE_ARBITRATION_WINDOW as t,HEARTBEAT_INTERVAL as r,REJECT_WINDOW as d,genId as a,isBroadcastMessage as s,isEarlier as o}from"./broadcast-protocol.js";function i(e){null!==e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null)}function l(e){null!==e.deadTimer&&(clearTimeout(e.deadTimer),e.deadTimer=null)}function u(e,n){let{name:t,logger:r}=e.deps;"remote-held"===e.status.kind&&e.status.token===n&&(l(e.status),r.warn(`[${t}] broadcast driver: remote holder token=${n} dead by heartbeat timeout`),e.status={kind:"idle"},w(e))}function c(n,t){if("holding"!==n.status.kind)return;let r=n.status;if(r.released)return;r.released=!0,i(r),n.status={kind:"idle"};let{name:d,logger:a}=n.deps;a.debug(`[${d}] broadcast driver: revoked token=${r.token} reason=${t}`);let s=r.revokeCallback;if(e(s))try{s(t)}catch(e){a.error(`[${d}] broadcast driver: revoke callback threw`,e)}w(n)}function k(e,n){let t={kind:"holding",token:n,grantedAt:Date.now(),released:!1,revokeCallback:null,heartbeatTimer:null};return e.status=t,t.heartbeatTimer=setInterval(()=>{t.released||e.destroyed?i(t):e.channel.postMessage({kind:"heartbeat",senderId:e.senderId,token:t.token,ts:Date.now()})},r),e.channel.postMessage({kind:"heartbeat",senderId:e.senderId,token:n,ts:t.grantedAt}),function(e,n){let{name:t,logger:r}=e.deps;return{release:()=>{if("holding"!==e.status.kind||e.status.token!==n||e.status.released)return;let d=e.status;d.released=!0,i(d),e.status={kind:"idle"},r.debug(`[${t}] broadcast driver: release token=${n}`),e.channel.postMessage({kind:"release",senderId:e.senderId,token:n}),w(e)},onRevokedByDriver:t=>{"holding"!==e.status.kind||e.status.token!==n||e.status.released||(e.status.revokeCallback=t)}}}(e,n)}function g(e,t,r){"remote-held"===e.status.kind&&l(e.status);let d={kind:"remote-held",token:t,peerTs:r,lastHeartbeat:Date.now(),deadTimer:null};e.status=d,l(d),d.deadTimer=setTimeout(()=>{u(e,d.token)},n)}function b(e,n){let t=e.pendingAnnounce;if(null===t||t.abandoned)return;t.abandoned=!0,null!==t.timer&&(clearTimeout(t.timer),t.timer=null),e.pendingAnnounce=null;let{name:r,logger:d}=e.deps;d.debug(`[${r}] broadcast driver: abandon pendingAnnounce reason=${n} token=${t.waiter.token}`),e.waiters.push(t.waiter)}function h(e,n){let t=e.pendingForce;if(null===t||t.abandoned)return;t.abandoned=!0,null!==t.timer&&(clearTimeout(t.timer),t.timer=null),e.pendingForce=null;let{name:r,logger:d}=e.deps;d.debug(`[${r}] broadcast driver: abandon pendingForce reason=${n} token=${t.token}`),e.waiters.push(t.waiter)}function f(e,n){if(n.senderId===e.senderId)return;if("holding"===e.status.kind&&!e.status.released){let t=e.status;e.channel.postMessage({kind:"reject",senderId:e.senderId,requestId:n.requestId,holderToken:t.token,holderTs:t.grantedAt});return}let t=e.pendingAnnounce;if(null!==t&&!t.abandoned){if(o(t.ts,t.requestId,n.ts,n.requestId))return;b(e,"arbitration-loss")}}function p(e,n){if(n.senderId===e.senderId)return;if("holding"===e.status.kind&&!e.status.released&&e.status.token!==n.holderToken){let{name:t,logger:r}=e.deps;r.warn(`[${t}] broadcast driver: double-hold detected (own=${e.status.token}, remote=${n.holderToken}); revoking self`),c(e,"force"),g(e,n.holderToken,n.holderTs);return}let t=e.pendingAnnounce;null===t||t.abandoned||t.requestId!==n.requestId||b(e,"rejected"),("idle"===e.status.kind||"remote-held"===e.status.kind)&&g(e,n.holderToken,n.holderTs)}function m(e,n){if(n.senderId!==e.senderId&&("holding"!==e.status.kind||e.status.token!==n.token)){if("holding"===e.status.kind&&!e.status.released){let{name:t,logger:r}=e.deps;r.warn(`[${t}] broadcast driver: double-hold detected via heartbeat (own=${e.status.token}, remote=${n.token}); revoking self`),c(e,"force"),g(e,n.token,n.ts);return}null===e.pendingAnnounce||e.pendingAnnounce.abandoned||b(e,"heartbeat-detected"),g(e,n.token,n.ts)}}function $(e,n){n.senderId!==e.senderId&&"remote-held"===e.status.kind&&e.status.token===n.token&&(l(e.status),e.status={kind:"idle"},w(e))}function v(e,n){if(n.senderId===e.senderId)return;let t=e.pendingForce;if(null!==t&&!t.abandoned){if(o(t.ts,t.token,n.ts,n.token))return;h(e,"arbitration-loss")}"holding"!==e.status.kind||e.status.released||e.status.token===n.token||c(e,"force"),g(e,n.token,n.ts)}function I(e,n){if(s(n))switch(n.kind){case"announce":f(e,n);return;case"reject":p(e,n);return;case"heartbeat":m(e,n);return;case"release":$(e,n);return;case"force":v(e,n);return;default:return}}function T(e,n){for(let t=0;t<e.length;t++)if(e[t]===n)return void e.splice(t,1)}function w(e){if(e.destroyed||"idle"!==e.status.kind||0===e.waiters.length||null!==e.pendingAnnounce||null!==e.pendingForce)return;let n=e.waiters.shift();n&&A(e,n)}function A(e,n){let{name:t,logger:r}=e.deps;if(e.destroyed)return void r.error(`[${t}] broadcast driver: startAnnounceCampaign called after destroyed`);if("idle"!==e.status.kind||null!==e.pendingAnnounce||null!==e.pendingForce){r.error(`[${t}] broadcast driver: startAnnounceCampaign precondition violated (status=${e.status.kind}, pendingAnnounce=${null!==e.pendingAnnounce}, pendingForce=${null!==e.pendingForce})`),e.waiters.push(n);return}let s=a("req"),o=Date.now(),i={requestId:s,ts:o,waiter:n,abandoned:!1,timer:null};e.pendingAnnounce=i,e.channel.postMessage({kind:"announce",senderId:e.senderId,requestId:s,token:n.token,ts:o,force:!1}),r.debug(`[${t}] broadcast driver: announce token=${n.token} reqId=${s}`),i.timer=setTimeout(()=>{if(i.timer=null,i.abandoned||e.destroyed)return;e.pendingAnnounce=null;let d=k(e,n.token);r.debug(`[${t}] broadcast driver: grant token=${n.token}`),n.resolve(d)},d)}function F(e,n){let{name:r,logger:d}=e.deps;if(e.destroyed)return void d.error(`[${r}] broadcast driver: startForceCampaign called after destroyed`);"holding"!==e.status.kind||e.status.released||c(e,"force");let a=Date.now(),s={token:n.token,ts:a,waiter:n,abandoned:!1,timer:null};e.pendingForce=s,e.channel.postMessage({kind:"force",senderId:e.senderId,token:n.token,ts:a}),d.debug(`[${r}] broadcast driver: force-announce token=${n.token} ts=${a}`),s.timer=setTimeout(()=>{if(s.timer=null,s.abandoned||e.destroyed)return;e.pendingForce=null;let t=k(e,n.token);d.debug(`[${r}] broadcast driver: grant (force) token=${n.token}`),n.resolve(t)},t)}function y(e,n){let{deps:t,waiters:r}=e,{name:d,logger:a}=t;a.debug(`[${d}] broadcast driver: destroy (waiters=${r.length}, status=${e.status.kind}, pendingAnnounce=${null!==e.pendingAnnounce}, pendingForce=${null!==e.pendingForce})`);let s=[];if(null!==e.pendingAnnounce){let{pendingAnnounce:n}=e;n.abandoned=!0,null!==n.timer&&clearTimeout(n.timer),e.pendingAnnounce=null,s.push(n.waiter)}if(null!==e.pendingForce){let{pendingForce:n}=e;n.abandoned=!0,null!==n.timer&&clearTimeout(n.timer),e.pendingForce=null,s.push(n.waiter)}for(let e=0;e<r.length;e++)s.push(r[e]);if(r.length=0,"holding"===e.status.kind){let n=e.status;i(n),n.released=!0;try{e.channel.postMessage({kind:"release",senderId:e.senderId,token:n.token})}catch(e){a.error(`[${d}] broadcast driver: release broadcast failed during destroy`,e)}}else"remote-held"===e.status.kind&&l(e.status);e.status={kind:"idle"};for(let e=0;e<s.length;e++)s[e].abort(n(s[e].token));if(null!==e.unsubscribe){try{e.unsubscribe()}catch(e){a.error(`[${d}] broadcast driver: unsubscribe threw`,e)}e.unsubscribe=null}try{e.channel.close()}catch(e){a.error(`[${d}] broadcast driver: channel.close threw`,e)}}export{b as abandonPendingAnnounce,h as abandonPendingForce,y as drainOnDestroy,k as enterHolding,g as enterRemoteHeld,f as handleAnnounce,v as handleForce,m as handleHeartbeat,I as handleMessage,p as handleReject,$ as handleRelease,u as handleRemoteDead,w as pumpNextWaiter,T as removeWaiter,c as revokeHolding,A as startAnnounceCampaign,F as startForceCampaign};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * BroadcastDriver:基于 BroadcastChannel 的跨 Tab 互斥锁
3
+ *
4
+ * 适用场景(由 pickDriver 决定):
5
+ * - 浏览器环境但不支持 `navigator.locks`(Safari < 15.4 / 老版 Firefox)
6
+ * - 支持 `BroadcastChannel`(否则继续降级到 StorageDriver)
7
+ *
8
+ * 本文件是工厂聚合层,只负责:
9
+ * - 前置依赖校验(getChannel / id 必须提供)
10
+ * - 构造 state 容器并订阅 channel
11
+ * - 暴露 acquire / destroy
12
+ *
13
+ * 协议细节、状态机、消息处理、竞选流程、drain 逻辑分别放在:
14
+ * - `./broadcast-protocol`:消息类型 + 常量 + 校验 + 仲裁工具
15
+ * - `./broadcast-state`:状态机 + 消息处理 + 竞选流程 + drainOnDestroy
16
+ *
17
+ * 前置条件:`deps.getChannel` 必须提供且返回非 null(由 pickDriver 保证);否则构造期抛错
18
+ */
19
+ import type { LockDriverContext, LockDriverHandle } from '../types';
20
+ import { type BroadcastDriverState, type Waiter } from './broadcast-state';
21
+ import type { LockDriver, LockDriverDeps } from './types';
22
+ /**
23
+ * 构造一个 waiter 并绑定 signal / timeout 的 abort 生命周期
24
+ *
25
+ * 职责:
26
+ * - `settled` 标志保证 resolve / reject / abort 互斥,只有第一次生效
27
+ * - `cleanup`:清理 timeout + signal listener
28
+ * - `abort`:把 waiter 从队列 / pending 中移除后,走 reject 兜底
29
+ */
30
+ declare function buildWaiter(ctx: LockDriverContext, state: BroadcastDriverState, resolve: (handle: LockDriverHandle) => void, reject: (error: Error) => void): Waiter;
31
+ declare function acquireBroadcastLock(state: BroadcastDriverState, ctx: LockDriverContext): Promise<LockDriverHandle>;
32
+ /**
33
+ * 创建 BroadcastDriver 实例
34
+ */
35
+ declare function createBroadcastDriver(deps: LockDriverDeps): LockDriver;
36
+ export { acquireBroadcastLock, buildWaiter, createBroadcastDriver };
@@ -0,0 +1 @@
1
+ import{throwError as e}from"../../throw-error/index.js";import{isFunction as t,isNumber as n,isString as r}from"../../utils/index.js";import{ERROR_FN_NAME as o}from"../constants.js";import{LockAbortedError as i,LockTimeoutError as u}from"../errors/index.js";import{genId as l}from"./broadcast-protocol.js";import{drainOnDestroy as a,handleMessage as s,pumpNextWaiter as d,removeWaiter as c,startAnnounceCampaign as m,startForceCampaign as b}from"./broadcast-state.js";function g(e,t,r,l){let a=!1,s=null;function m(){null!==s&&(clearTimeout(s),s=null),e.signal.removeEventListener("abort",g)}let b={token:e.token,resolve:e=>{a||(a=!0,m(),r(e))},reject:e=>{a||(a=!0,m(),l(e))},abort:e=>{if(!a){if(c(t.waiters,b),null!==t.pendingAnnounce&&t.pendingAnnounce.waiter===b){let e=t.pendingAnnounce;e.abandoned=!0,null!==e.timer&&(clearTimeout(e.timer),e.timer=null),t.pendingAnnounce=null}if(null!==t.pendingForce&&t.pendingForce.waiter===b){let e=t.pendingForce;e.abandoned=!0,null!==e.timer&&(clearTimeout(e.timer),e.timer=null),t.pendingForce=null}b.reject(e),d(t)}}};function g(){b.abort(new i(`[@cmtlyt/lingshu-toolkit#${o}]: acquire aborted (token=${e.token})`))}if(e.signal.aborted)return queueMicrotask(()=>g()),b;if(e.signal.addEventListener("abort",g,{once:!0}),n(e.acquireTimeout)&&e.acquireTimeout>0){let t=e.acquireTimeout;s=setTimeout(()=>{b.abort(new u(`[@cmtlyt/lingshu-toolkit#${o}]: acquire timed out after ${t}ms (token=${e.token})`))},t)}return b}function p(e,t){return t.signal.aborted?Promise.reject(new i(`[@cmtlyt/lingshu-toolkit#${o}]: acquire aborted (token=${t.token})`)):e.destroyed?Promise.reject(new i(`[@cmtlyt/lingshu-toolkit#${o}]: broadcast driver has been destroyed (token=${t.token})`)):new Promise((n,r)=>{let o=g(t,e,n,r);if(t.force)return void b(e,o);if("idle"===e.status.kind&&null===e.pendingAnnounce&&null===e.pendingForce)return void m(e,o);e.waiters.push(o);let{name:i,logger:u}=e.deps;u.debug(`[${i}] broadcast driver: enqueue token=${t.token}, queue=${e.waiters.length}, status=${e.status.kind}`)})}function k(n){let{id:u,getChannel:d}=n;t(d)||e(o,"broadcast driver requires getChannel factory",TypeError),r(u)&&0!==u.length||e(o,"broadcast driver requires a non-empty id",TypeError);let c=d({id:u,channel:"custom"});null===c&&e(o,"broadcast driver getChannel returned null",TypeError);let m={deps:n,channel:c,senderId:l("sender"),status:{kind:"idle"},waiters:[],pendingAnnounce:null,pendingForce:null,destroyed:!1,unsubscribe:null};function b(e){return new i(`[@cmtlyt/lingshu-toolkit#${o}]: broadcast driver destroyed (token=${e})`)}return m.unsubscribe=c.subscribe(e=>s(m,e)),{acquire:e=>p(m,e),destroy:()=>{m.destroyed||(m.destroyed=!0,a(m,b))}}}export{p as acquireBroadcastLock,g as buildWaiter,k as createBroadcastDriver};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * CustomDriver:包装用户注入的 `adapters.getLock` 为统一 `LockDriver` 接口
3
+ *
4
+ * 适用场景:`adapters.getLock` 存在时由 pickDriver 直接选中;`mode` 字段被忽略
5
+ * (对应 RFC.md「能力检测与降级」「CustomDriver」章节)
6
+ *
7
+ * 职责范围:
8
+ * - 透传 `name` / `token` / `force` / `source` / 超时 / 合并 signal 到用户工厂
9
+ * - 把 `acquireTimeout` 统一映射为 signal abort,让用户工厂只需监听 `ctx.signal`
10
+ * 即可同时响应"超时"与"外部取消"两条路径
11
+ * - 把用户返回的 `LockDriverHandle`(可能是 Promise)规范化为"同步 handle"返回给上层
12
+ * - `destroy` 不碰用户资源(用户 handle 由 actions 的 release 路径负责释放);仅
13
+ * 清理本 driver 内部持有的合并 controller / 订阅(当前无)
14
+ *
15
+ * 与其他 driver 的关键差异:
16
+ * - 不维护排队 / 心跳 / storage 订阅;完全信任用户实现的互斥语义
17
+ * - 不拒绝"用户工厂返回的 handle 缺失 onRevokedByDriver"的情况(Phase 5 状态机会
18
+ * 在 force / timeout 触发时自发广播,不强依赖 driver 上报)
19
+ */
20
+ import type { LockDriver, LockDriverDeps } from './types';
21
+ /**
22
+ * 创建 CustomDriver 实例
23
+ *
24
+ * 前置条件:`deps.userGetLock` 必须已提供(由 pickDriver 保证);否则构造期抛错
25
+ */
26
+ declare function createCustomLockDriver(deps: LockDriverDeps): LockDriver;
27
+ export { createCustomLockDriver };
@@ -0,0 +1 @@
1
+ import{throwError as e}from"../../throw-error/index.js";import{isFunction as r,isNullOrUndef as t,isNumber as o,isPromiseLike as n}from"../../utils/index.js";import{ERROR_FN_NAME as i}from"../constants.js";import{LockAbortedError as u,LockTimeoutError as a}from"../errors/index.js";function s(s){let{name:c,logger:l,userGetLock:d}=s;r(d)||e(i,"custom driver requires adapters.getLock to be a function",TypeError);let m=!1;return{acquire:async function k(k){m&&e(i,"custom driver has been destroyed",u);let{signal:f,cleanup:v,getTimeoutFired:g}=function(e,r,t){let n=new AbortController,u=!1,s=null;if(e.aborted)return n.abort(e.reason),{signal:n.signal,cleanup:()=>void 0,getTimeoutFired:()=>u};function c(){n.abort(e.reason)}return e.addEventListener("abort",c,{once:!0}),o(r)&&r>0&&(s=setTimeout(()=>{u=!0,n.abort(new a(`[@cmtlyt/lingshu-toolkit#${i}]: acquire timed out after ${r}ms (token=${t})`))},r)),{signal:n.signal,cleanup:function(){null!==s&&(clearTimeout(s),s=null),e.removeEventListener("abort",c)},getTimeoutFired:()=>u}}(k.signal,k.acquireTimeout,k.token),$={name:c,token:k.token,force:k.force,acquireTimeout:k.acquireTimeout,holdTimeout:k.holdTimeout,signal:f};try{let o=d($),u=await Promise.resolve(o);var b=k.token;if(!(u&&r(u.release))){let r=t(u)?String(u):typeof u.release;e(i,`adapters.getLock must return an object with a "release" function, got ${r} (token=${b})`,TypeError)}return v(),l.debug(`[${c}] custom driver: grant token=${k.token}`),function(e,t,o){let{name:i,logger:u}=t,a=e.release,s=e.onRevokedByDriver,c={release:()=>{let r;try{r=a.call(e)}catch(e){u.error(`[${i}] custom driver: user release threw (token=${o})`,e);return}if(n(r))return Promise.resolve(r).catch(e=>{u.error(`[${i}] custom driver: user release rejected (token=${o})`,e)})}};return r(s)&&(c.onRevokedByDriver=s.bind(e)),c}(u,s,k.token)}catch(r){throw v(),g()&&e(i,`acquire timed out after ${String(k.acquireTimeout)}ms (token=${k.token})`,a,{cause:r}),k.signal.aborted&&e(i,`acquire aborted (token=${k.token})`,u,{cause:r}),l.error(`[${c}] custom driver: user getLock rejected (token=${k.token})`,r),r}},destroy:function(){m||(m=!0,l.debug(`[${c}] custom driver: destroy`))}}}export{s as createCustomLockDriver};
@@ -0,0 +1,59 @@
1
+ /**
2
+ * drivers 层入口:能力检测 + 驱动选择 + barrel export
3
+ *
4
+ * 对应 RFC.md「能力检测与降级」:pickDriver 按以下优先级决策(首次创建 Entry 时调用一次):
5
+ *
6
+ * 1. `adapters.getLock` 存在 → CustomDriver(最高优先级,覆盖 `mode`)
7
+ * 2. `id` 未提供(纯本地只读锁)→ LocalLockDriver
8
+ * 3. `mode` 显式指定(非 `'auto'`)→ 强制使用对应 driver;能力不可用时抛错
9
+ * 4. `mode === 'auto'` 的降级链:web-locks → broadcast → storage;全不可用时抛错
10
+ *
11
+ * 能力探测:
12
+ * - navigator.locks:Web Locks API,Safari >= 15.4 / Chromium 稳定支持
13
+ * - BroadcastChannel:同源 Tab 间广播通道
14
+ * - localStorage:最通用的同步存储,探测需做"实际读写一次"防隐私模式误判
15
+ *
16
+ * 本文件**不持久化探测结果**:pickDriver 的入参已决定单次构造的 driver;同 id 二次
17
+ * 构造直接复用 registry 中的 driver 实例,不会走 pickDriver,所以无需缓存
18
+ */
19
+ import type { ResolvedAdapters } from '../adapters/index';
20
+ import type { LockDataOptions } from '../types';
21
+ import type { LockDriver } from './types';
22
+ /**
23
+ * pickDriver 的参数容器
24
+ *
25
+ * 把"需要给 driver 的能力"从 `ResolvedAdapters` 中抽出单独字段,避免 driver 层依赖
26
+ * adapters 层的完整类型(drivers 与 adapters 是平级关系,不应循环依赖)
27
+ */
28
+ interface PickDriverArgs<T> {
29
+ /** 已解析的 adapters(pickDefaultAdapters 产出) */
30
+ readonly adapters: ResolvedAdapters<T>;
31
+ /** lockData 原始 options —— 只读取 `mode` */
32
+ readonly options: Pick<LockDataOptions<T>, 'mode'>;
33
+ /** lockData 原始 id;未提供代表纯本地只读锁 */
34
+ readonly id: string | undefined;
35
+ }
36
+ /** navigator.locks 可用(Web Locks API) */
37
+ declare function hasNavigatorLocks(): boolean;
38
+ /** BroadcastChannel 可实例化(探测构造不抛错) */
39
+ declare function hasBroadcastChannel(): boolean;
40
+ /** localStorage 可实际读写(隐私模式 / quota 满时返回 false) */
41
+ declare function hasUsableLocalStorage(): boolean;
42
+ /**
43
+ * 根据能力 / mode / id 选择并构造 LockDriver
44
+ *
45
+ * 优先级(RFC.md:689-696):
46
+ * 1. `adapters.getLock` 存在 → CustomDriver(最高优先级,覆盖 mode)
47
+ * 2. `id` 未提供 → LocalLockDriver
48
+ * 3. 显式 `mode` → 强制使用,不降级(能力不可用抛错)
49
+ * 4. `mode === 'auto'`(默认)→ web-locks → broadcast → storage
50
+ */
51
+ declare function pickDriver<T>(args: PickDriverArgs<T>): LockDriver;
52
+ export { createBroadcastDriver } from './broadcast';
53
+ export { createCustomLockDriver } from './custom';
54
+ export { createLocalLockDriver } from './local';
55
+ export { createStorageDriver } from './storage';
56
+ export type { LockDriver, LockDriverDeps } from './types';
57
+ export { createWebLocksDriver } from './web-locks';
58
+ export type { PickDriverArgs };
59
+ export { hasBroadcastChannel, hasNavigatorLocks, hasUsableLocalStorage, pickDriver };
@@ -0,0 +1 @@
1
+ import{throwError as r}from"../../throw-error/index.js";import{isFunction as e,isObject as t,isString as o}from"../../utils/index.js";import{ERROR_FN_NAME as a,LOCK_PREFIX as n}from"../constants.js";import{createBroadcastDriver as i}from"./broadcast.js";import{createCustomLockDriver as c}from"./custom.js";import{createLocalLockDriver as s}from"./local.js";import{createStorageDriver as l}from"./storage.js";import{createWebLocksDriver as u}from"./web-locks.js";function m(){let r=globalThis.navigator;if(!t(r))return!1;let{locks:o}=r;return!!t(o)&&e(o.request)}function g(){let r=globalThis.BroadcastChannel;if(!e(r))return!1;try{return new r(`${n}:__pick_driver_probe__`).close(),!0}catch{return!1}}function d(){try{let r=globalThis.localStorage;if(!r)return!1;let e=`${n}:__pick_driver_probe__`;return r.setItem(e,"1"),r.removeItem(e),!0}catch{return!1}}function v(r,e,t){let{adapters:a,id:i}=r;return{name:o(i)&&i.length>0?`${n}:${i}`:`${n}:__local__`,id:i,logger:a.logger,getChannel:e?a.getChannel:void 0,userGetLock:t?a.getLock:void 0}}function b(t){let{adapters:n,options:b,id:f}=t;if(e(n.getLock))return c(v(t,!1,!0));if(!o(f)||0===f.length)return s(v(t,!1,!1));let p=b.mode||"auto";switch(p){case"web-locks":return m()||r(a,"mode='web-locks' requested but navigator.locks is unavailable in current environment",TypeError),u(v(t,!1,!1));case"broadcast":return g()||r(a,"mode='broadcast' requested but BroadcastChannel is unavailable in current environment",TypeError),i(v(t,!0,!1));case"storage":return d()||r(a,"mode='storage' requested but localStorage is unavailable in current environment",TypeError),l(v(t,!1,!1));case"auto":return m()?u(v(t,!1,!1)):g()?i(v(t,!0,!1)):d()?l(v(t,!1,!1)):void r(a,"mode='auto' requires one of navigator.locks / BroadcastChannel / localStorage to be available; got none",TypeError);default:r(a,`unknown mode: ${String(p)}`,TypeError)}}export{i as createBroadcastDriver,c as createCustomLockDriver,s as createLocalLockDriver,l as createStorageDriver,u as createWebLocksDriver,g as hasBroadcastChannel,m as hasNavigatorLocks,d as hasUsableLocalStorage,b as pickDriver};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * LocalLockDriver:进程内互斥锁
3
+ *
4
+ * 适用场景(由 pickDriver 决定):
5
+ * - 未传 id(纯本地只读锁)
6
+ * - `mode` 显式指定为 'storage' 但环境完全不可用时的最终兜底
7
+ *
8
+ * 实现要点(对应 RFC.md「LocalLockDriver」「能力检测与降级」):
9
+ * - 同 driver 实例内维护一个 FIFO 等待队列;`acquire` 产生的 `LockHandle` 在 `release`
10
+ * 调用时把下一个 waiter 从队首取出并 resolve
11
+ * - `force: true` 立即抢占:当前持有者的 `onRevokedByDriver` 以 `'force'` 回调,
12
+ * 新请求跳过队列直接持锁
13
+ * - `acquireTimeout` 用本地 `setTimeout` 计时;signal.aborted 或 timeout 触发时把
14
+ * 对应 waiter 从队列中移除并 reject
15
+ * - `destroy`:把所有等待者 reject 为 `LockAbortedError`,并清空队列;当前持有者
16
+ * `onRevokedByDriver('force')` 并让 release 变成幂等 no-op
17
+ *
18
+ * **注意**:本 driver 与 id 无关;同一进程内如有多份 LocalLockDriver 实例,它们
19
+ * 之间不互斥(由 InstanceRegistry 按 id 唯一化 driver 保证"同 id 共享同一 driver")
20
+ */
21
+ import type { LockDriverContext, LockDriverHandle } from '../types';
22
+ import type { LockDriver, LockDriverDeps } from './types';
23
+ /**
24
+ * 队列中的等待者;每次 `acquire` 未立即拿到锁时会 push 一条
25
+ *
26
+ * `force: true` 的 acquire 走 seize 快路径不入队列,所以 waiter 永远是"普通等待者"
27
+ *
28
+ * - `resolve` / `reject`:完成该次 acquire 的 Promise
29
+ * - `abort`:外部通知 waiter "放弃等待"(signal / timeout / destroy),需要解绑计时器
30
+ * + 从队列里移除自己,再把 promise reject
31
+ * - `token`:用于日志与 debug
32
+ */
33
+ interface LocalWaiter {
34
+ readonly token: string;
35
+ readonly resolve: (handle: LockDriverHandle) => void;
36
+ readonly reject: (error: Error) => void;
37
+ /** 外部请求中止等待(signal.aborted / timeout / destroy),返回时 waiter 已从队列移除 */
38
+ readonly abort: (error: Error) => void;
39
+ }
40
+ /** 当前持有者;driver 内部维护,release / revoke 时清空 */
41
+ interface LocalHolder {
42
+ readonly token: string;
43
+ /** 通知持有者被驱逐;由 driver 在 force 抢占 / destroy 时调用 */
44
+ readonly notifyRevoke: (reason: 'force' | 'timeout') => void;
45
+ /** release 幂等开关;多次 release 只有第一次会推进队列 */
46
+ released: boolean;
47
+ }
48
+ /**
49
+ * driver 闭包共享的可变状态句柄;拆出来是为了把原 `createLocalLockDriver` 超长主体拆成独立工具函数
50
+ *
51
+ * 通过引用共享 holder 指针:每个工具函数都能读写同一个 holder / waiters / destroyed
52
+ */
53
+ interface LocalDriverState {
54
+ readonly name: string;
55
+ readonly logger: LockDriverDeps['logger'];
56
+ readonly waiters: LocalWaiter[];
57
+ holder: LocalHolder | null;
58
+ destroyed: boolean;
59
+ }
60
+ /**
61
+ * 把队首 waiter 出队并授予锁;若队列为空则保持空闲
62
+ *
63
+ * 这里不做 signal 校验(waiter 进入队列时已注册监听器,signal.aborted 会自行出队)
64
+ */
65
+ declare function pumpNextWaiter(state: LocalDriverState): void;
66
+ /**
67
+ * 从队列移除指定 waiter(waiter 放弃等待时调用)
68
+ *
69
+ * 使用索引 for 是因为 Array.findIndex + splice 对热路径的两次遍历并不划算
70
+ */
71
+ declare function removeWaiter(waiters: LocalWaiter[], target: LocalWaiter): void;
72
+ /**
73
+ * 构造 waiter 并把它 enqueue 到队列;返回 Promise 在拿到锁 / abort 时 settle
74
+ *
75
+ * 拆分为独立函数是为了控制 `createLocalLockDriver` 的函数行数(biome noExcessiveLinesPerFunction)
76
+ */
77
+ declare function enqueueWaiter(state: LocalDriverState, ctx: LockDriverContext): Promise<LockDriverHandle>;
78
+ /**
79
+ * 创建一个 LocalLockDriver 实例
80
+ *
81
+ * driver 为"按 id 单例"(由 InstanceRegistry 管理),本函数只负责实例化,
82
+ * 不关心 id 是否存在(name 已由 pickDriver 拼好)
83
+ */
84
+ declare function createLocalLockDriver(deps: LockDriverDeps): LockDriver;
85
+ export type { LocalDriverState, LocalWaiter };
86
+ export { createLocalLockDriver, enqueueWaiter, pumpNextWaiter, removeWaiter };
@@ -0,0 +1 @@
1
+ import{throwError as e}from"../../throw-error/index.js";import{isFunction as r,isNumber as t}from"../../utils/index.js";import{ERROR_FN_NAME as o}from"../constants.js";import{LockAbortedError as l,LockTimeoutError as n}from"../errors/index.js";function i(e,t,o){let{name:l,logger:n}=e,i=null,d=null;return{handle:{release:()=>{let{holder:r}=e;r&&r.token===t&&!r.released&&(r.released=!0,e.holder=null,n.debug(`[${l}] local driver: release by token=${t}`),o())},onRevokedByDriver:e=>{if(i=e,null!==d)try{i(d)}catch(e){n.error(`[${l}] local driver: revoke callback threw`,e)}}},notifyRevoke:e=>{if(d=e,r(i))try{i(e)}catch(e){n.error(`[${l}] local driver: revoke callback threw`,e)}}}}function d(e){let{name:r,logger:t,waiters:o}=e;if(e.holder||0===o.length)return;let l=o.shift();if(!l)return;let{handle:n,notifyRevoke:a}=i(e,l.token,()=>d(e));e.holder={token:l.token,notifyRevoke:a,released:!1},t.debug(`[${r}] local driver: grant token=${l.token}`),l.resolve(n)}function a(e,r){for(let t=0;t<e.length;t++)if(e[t]===r)return void e.splice(t,1)}function u(e,r){let{name:i,logger:d,waiters:u}=e;return new Promise((e,c)=>{let s=!1,k=null;function f(){null!==k&&(clearTimeout(k),k=null),r.signal.removeEventListener("abort",$)}let h={token:r.token,resolve:r=>{s||(s=!0,f(),e(r))},reject:e=>{s||(s=!0,f(),c(e))},abort:e=>{s||(a(u,h),h.reject(e))}};function $(){h.abort(new l(`[@cmtlyt/lingshu-toolkit#${o}]: acquire aborted (token=${r.token})`))}if(t(r.acquireTimeout)&&r.acquireTimeout>0){let e=r.acquireTimeout;k=setTimeout(()=>{h.abort(new n(`[@cmtlyt/lingshu-toolkit#${o}]: acquire timed out after ${e}ms (token=${r.token})`))},e)}r.signal.addEventListener("abort",$,{once:!0}),u.push(h),d.debug(`[${i}] local driver: enqueue token=${r.token}, queue size=${u.length}`)})}async function c(r,t){let{name:n,logger:a}=r;if(r.destroyed&&e(o,"local driver has been destroyed",l),t.signal.aborted&&e(o,`acquire aborted before start (token=${t.token})`,l),t.force)return function(e,r){let{name:t,logger:o}=e;if(e.holder){let l=e.holder;l.released=!0,e.holder=null,o.debug(`[${t}] local driver: force-seize from token=${l.token} by token=${r}`),l.notifyRevoke("force")}let{handle:l,notifyRevoke:n}=i(e,r,()=>d(e));return e.holder={token:r,notifyRevoke:n,released:!1},o.debug(`[${t}] local driver: grant (force) token=${r}`),l}(r,t.token);if(!r.holder){let{handle:e,notifyRevoke:o}=i(r,t.token,()=>d(r));return r.holder={token:t.token,notifyRevoke:o,released:!1},a.debug(`[${n}] local driver: grant (fast-path) token=${t.token}`),e}return u(r,t)}function s(e){let{name:r,logger:t}=e,n={name:r,logger:t,waiters:[],holder:null,destroyed:!1};return{acquire:e=>c(n,e),destroy:()=>{n.destroyed||(n.destroyed=!0,function(e){let{name:r,logger:t,waiters:n}=e;if(t.debug(`[${r}] local driver: destroy (waiters=${n.length}, holding=${e.holder?"yes":"no"})`),e.holder){let r=e.holder;r.released=!0,e.holder=null,r.notifyRevoke("force")}let i=n.slice();n.length=0;for(let e=0;e<i.length;e++)i[e].abort(new l(`[@cmtlyt/lingshu-toolkit#${o}]: local driver destroyed (token=${i[e].token})`))}(n))}}}export{s as createLocalLockDriver,u as enqueueWaiter,d as pumpNextWaiter,a as removeWaiter};
@@ -0,0 +1,67 @@
1
+ /**
2
+ * StorageDriver 协议层:存储格式定义、常量、校验、nonce 生成
3
+ *
4
+ * ## 存储格式(对应 RFC.md「StorageDriver 协议」)
5
+ * key:`${LOCK_PREFIX}:${id}:driver-lock`
6
+ * value:JSON
7
+ * ```
8
+ * {
9
+ * "holder": { "token": string, "heartbeat": number, "nonce": string } | null,
10
+ * "queue": Array<{ "token": string, "ts": number }>,
11
+ * "rev": number
12
+ * }
13
+ * ```
14
+ *
15
+ * ## 关键字段
16
+ * - `holder.nonce`:每次 holder 写入生成一次的随机值;CAS verify 时用 token + nonce
17
+ * 双重匹配(ST-1 修复)。两个 Tab 并发写入时,后写者覆盖先写者;先写者读回验证发现
18
+ * token 相同但 nonce 不同 → 判定竞争失败,退避重试
19
+ * - `queue`:本地 FIFO 等待队列的持久化视图;所有 Tab 共享,入队 / 出队均走 CAS 重试
20
+ * - `rev`:每次写入递增,辅助调试丢更新问题;storage 事件可能在 rev 相同时也触发
21
+ *
22
+ * ## 时间常量
23
+ * - HEARTBEAT_INTERVAL=500ms:下调自 1000ms,缩短 force 抢占被原持有者发现的最大延迟(ST-6)
24
+ * - DEAD_THRESHOLD=2500ms:>= 4 个心跳周期,避免系统短时停顿误判崩溃
25
+ * - POLL_INTERVAL=250ms:同 Tab 多实例场景下 storage 事件不触发,用 polling 兜底
26
+ * - WRITE_RETRY_MAX=3:CAS / 入队 / 出队的最大重试次数(ST-5)
27
+ * - WRITE_RETRY_JITTER_MAX=20ms:重试前随机退避 0~20ms,分散并发写者
28
+ */
29
+ /** 心跳周期(ms);持有者每此毫秒更新一次 holder.heartbeat */
30
+ declare const HEARTBEAT_INTERVAL = 500;
31
+ /** 崩溃阈值(ms);`now - holder.heartbeat > 此值` 视为远端崩溃 */
32
+ declare const DEAD_THRESHOLD = 2500;
33
+ /** 同 Tab 多实例的 polling 兜底周期(storage 事件不跨同 Tab 触发) */
34
+ declare const POLL_INTERVAL = 250;
35
+ /** CAS / 入队 / 出队的最大重试次数 */
36
+ declare const WRITE_RETRY_MAX = 3;
37
+ interface StorageHolder {
38
+ readonly token: string;
39
+ readonly heartbeat: number;
40
+ /** 随机 nonce;CAS verify 时用 token + nonce 双重匹配 */
41
+ readonly nonce: string;
42
+ }
43
+ interface StorageQueueEntry {
44
+ readonly token: string;
45
+ readonly ts: number;
46
+ }
47
+ interface StorageLockValue {
48
+ readonly holder: StorageHolder | null;
49
+ readonly queue: readonly StorageQueueEntry[];
50
+ readonly rev: number;
51
+ }
52
+ declare const EMPTY_VALUE: StorageLockValue;
53
+ declare function isStorageLockValue(value: unknown): value is StorageLockValue;
54
+ /**
55
+ * 生成 holder 的随机 nonce
56
+ *
57
+ * 同 `broadcast-protocol.genId`:不依赖 crypto.randomUUID,保持广兼容
58
+ */
59
+ declare function genNonce(): string;
60
+ /** 生成 waiter token(driver 内部排队用,与用户传入的 ctx.token 区分) */
61
+ declare function genWaiterId(): string;
62
+ /** 计算下一次重试的退避时长(0~WRITE_RETRY_JITTER_MAX ms 的随机数) */
63
+ declare function nextRetryJitter(): number;
64
+ /** 判定 holder 是否已崩溃(heartbeat 超过阈值未更新) */
65
+ declare function isHolderDead(holder: StorageHolder): boolean;
66
+ export type { StorageHolder, StorageLockValue, StorageQueueEntry };
67
+ export { DEAD_THRESHOLD, EMPTY_VALUE, genNonce, genWaiterId, HEARTBEAT_INTERVAL, isHolderDead, isStorageLockValue, nextRetryJitter, POLL_INTERVAL, WRITE_RETRY_MAX, };
@@ -0,0 +1 @@
1
+ import{isArray as e,isNumber as t,isObject as n,isString as r}from"../../utils/index.js";let o=500,u=2500,i=250,a=3,l={holder:null,queue:[],rev:0};function c(e){return t(e)&&Number.isFinite(e)}function f(t){var o,u;if(!n(t)||!c(t.rev)||!e(t.queue))return!1;for(let e=0;e<t.queue.length;e++)if(!(n(o=t.queue[e])&&r(o.token)&&c(o.ts)))return!1;return null===t.holder||!!(n(u=t.holder)&&r(u.token)&&c(u.heartbeat)&&r(u.nonce))}function d(){return`n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`}function h(){return`w_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`}function E(){return Math.floor(20*Math.random())}function _(e){return Date.now()-e.heartbeat>u}export{u as DEAD_THRESHOLD,l as EMPTY_VALUE,o as HEARTBEAT_INTERVAL,i as POLL_INTERVAL,a as WRITE_RETRY_MAX,d as genNonce,h as genWaiterId,_ as isHolderDead,f as isStorageLockValue,E as nextRetryJitter};
@@ -0,0 +1,103 @@
1
+ /**
2
+ * StorageDriver 状态层:状态机 + CAS 读写 + 队列操作 + 心跳 + drain
3
+ *
4
+ * ## CAS 模式(ST-1 修复)
5
+ * 读取 → 判定(idle / dead-holder / queue-head-ready)→ 生成新 holder(带 nonce)→
6
+ * 写入 → 再读回 verify(token + nonce 必须都匹配)。verify 失败 → 随机退避 → 重试;
7
+ * 超过 WRITE_RETRY_MAX 次 → 放弃本次尝试,等 storage 事件 / polling 触发下一轮
8
+ *
9
+ * ## 状态机
10
+ * - `idle`:无人持锁(本 Tab 视角)
11
+ * - `holding`:本 Tab 持锁;周期心跳 + 读 verify(发现被覆盖 → revoke('force'))
12
+ *
13
+ * 注意:storage driver **不维护 remote-held 状态**,因为 storage 是"随时可读的权威";
14
+ * 需要知道远端持有者状态时直接 readStorage(),比内存状态机更可靠
15
+ *
16
+ * ## 队列(ST-5 修复)
17
+ * 本地队列 `state.waiters`(driver 内存)+ storage 队列 `value.queue`(跨 Tab 持久化)
18
+ * 两者通过 waiter.token 关联;入队 / 出队均走 CAS 重试(最多 WRITE_RETRY_MAX 次)
19
+ */
20
+ import type { LockDriverHandle } from '../types';
21
+ import type { LockDriverDeps } from './types';
22
+ interface HoldingState {
23
+ readonly kind: 'holding';
24
+ readonly token: string;
25
+ /** 本 Tab 写入 holder 时生成的 nonce;CAS verify 依据 */
26
+ readonly nonce: string;
27
+ released: boolean;
28
+ revokeCallback: ((reason: 'force' | 'timeout') => void) | null;
29
+ heartbeatTimer: ReturnType<typeof setInterval> | null;
30
+ }
31
+ interface IdleState {
32
+ readonly kind: 'idle';
33
+ }
34
+ type DriverLocalState = IdleState | HoldingState;
35
+ interface Waiter {
36
+ readonly token: string;
37
+ readonly resolve: (handle: LockDriverHandle) => void;
38
+ readonly reject: (error: Error) => void;
39
+ readonly abort: (error: Error) => void;
40
+ /**
41
+ * 查询 waiter 是否已 settled(resolve / reject / abort 任一)
42
+ *
43
+ * pump / force 路径需要在拿到 grant 后 resolve 前检查:若已 settled 说明 waiter
44
+ * 已被 abort / timeout / signal 终结,此时抢到的 storage 锁应立即释放,避免泄漏(S-4 修复)
45
+ */
46
+ readonly isSettled: () => boolean;
47
+ }
48
+ interface StorageDriverState {
49
+ readonly deps: LockDriverDeps;
50
+ readonly storage: Storage;
51
+ readonly key: string;
52
+ status: DriverLocalState;
53
+ readonly waiters: Waiter[];
54
+ destroyed: boolean;
55
+ /** 并发保护:pumpNextWaiter 进行中;避免 storage 事件 + polling 同时触发多路 tryAcquire */
56
+ pumping: boolean;
57
+ unsubscribeStorageEvent: (() => void) | null;
58
+ pollTimer: ReturnType<typeof setInterval> | null;
59
+ }
60
+ declare function enqueueInStorage(state: StorageDriverState, token: string): Promise<boolean>;
61
+ interface AcquireGrant {
62
+ readonly token: string;
63
+ readonly nonce: string;
64
+ }
65
+ declare function tryAcquire(state: StorageDriverState, token: string, force: boolean): Promise<AcquireGrant | null>;
66
+ declare function revokeHolding(state: StorageDriverState, reason: 'force' | 'timeout'): void;
67
+ declare function removeWaiter(waiters: Waiter[], target: Waiter): void;
68
+ /**
69
+ * 尝试推进队首 waiter
70
+ *
71
+ * SS-1 修复:加 `state.pumping` 并发保护 —— storage 事件 + polling 可能并发触发,
72
+ * 必须保证同一时刻只有一个 `tryAcquire` 流程在跑
73
+ */
74
+ declare function pumpNextWaiter(state: StorageDriverState): void;
75
+ /**
76
+ * 在 storage 中释放 holder(幂等;仅当 token + nonce 匹配才真释放)
77
+ */
78
+ declare function releaseHolderInStorage(state: StorageDriverState, token: string, nonce: string): void;
79
+ declare function enterHolding(state: StorageDriverState, token: string, nonce: string): LockDriverHandle;
80
+ /**
81
+ * 当 storage 发生外部变更(其他 Tab 写入 / polling 检测到 holder 崩溃)时触发
82
+ *
83
+ * 职责:
84
+ * 1. 若本 Tab holding 且 holder 在 storage 中已被覆盖 → revoke('force')
85
+ * 2. 若本 Tab idle 且有 waiter → 尝试 pump
86
+ */
87
+ declare function handleExternalChange(state: StorageDriverState): void;
88
+ /**
89
+ * 订阅 window 的 storage 事件(跨 Tab 通知)
90
+ *
91
+ * 注意:storage 事件不跨同 Tab 触发,同 Tab 多实例需 polling 兜底(见 startPolling)
92
+ */
93
+ declare function subscribeStorageEvent(state: StorageDriverState): (() => void) | null;
94
+ declare function startPolling(state: StorageDriverState): ReturnType<typeof setInterval>;
95
+ /**
96
+ * 判断当前是否应立即尝试抢锁(快路径,避开入队 → 等事件 → pump 的流程)
97
+ *
98
+ * 条件:本 Tab idle + 无其他本地 waiter + (storage 无 holder 或 holder 已崩溃) + 队列空
99
+ */
100
+ declare function canFastAcquire(state: StorageDriverState): false | true;
101
+ declare function drainOnDestroy(state: StorageDriverState, buildAbortError: (token: string) => Error): void;
102
+ export type { AcquireGrant, StorageDriverState, Waiter };
103
+ export { canFastAcquire, drainOnDestroy, enqueueInStorage, enterHolding, handleExternalChange, pumpNextWaiter, releaseHolderInStorage, removeWaiter, revokeHolding, startPolling, subscribeStorageEvent, tryAcquire, };
@@ -0,0 +1 @@
1
+ import{isFunction as e}from"../../utils/index.js";import{EMPTY_VALUE as t,HEARTBEAT_INTERVAL as r,POLL_INTERVAL as n,WRITE_RETRY_MAX as o,genNonce as l,isHolderDead as a,isStorageLockValue as s,nextRetryJitter as u}from"./storage-protocol.js";function i(e){let r,{storage:n,key:o,deps:l}=e;try{r=n.getItem(o)}catch(e){return l.logger.warn(`[${l.name}] storage driver: getItem failed at key=${o}`,e),t}if(null===r||""===r)return t;try{let e=JSON.parse(r);if(s(e))return e;return l.logger.warn(`[${l.name}] storage driver: malformed value at key=${o}; treating as empty`),t}catch(e){return l.logger.warn(`[${l.name}] storage driver: JSON.parse failed at key=${o}`,e),t}}function d(e,t){let{storage:r,key:n,deps:o}=e;try{return r.setItem(n,JSON.stringify(t)),"success"}catch(e){return o.logger.warn(`[${o.name}] storage driver: setItem failed`,e),"abort"}}function c(e,t){var r;return r=()=>(function(e,t){if(e.destroyed)return"abort";let r=i(e);for(let e=0;e<r.queue.length;e++)if(r.queue[e].token===t)return"success";if("abort"===d(e,{holder:r.holder,queue:[...r.queue,{token:t,ts:Date.now()}],rev:r.rev+1}))return"abort";let n=i(e);for(let e=0;e<n.queue.length;e++)if(n.queue[e].token===t)return"success";return"retry"})(e,t),new Promise(e=>{let t=0,n=()=>{let l=r();"success"===l?e(!0):"abort"===l||++t>=o?e(!1):setTimeout(n,u())};n()})}function g(e,t,r){return new Promise(n=>{let s=0,c=()=>{let g=function(e,t,r){if(e.destroyed)return"abort";let n=i(e),{holder:o,queue:s}=n;if(null!==o&&!a(o)&&!r||!r&&s.length>0&&s[0].token!==t)return"cannot-acquire";let u=l(),c=Date.now(),g=s.filter(e=>e.token!==t);if("abort"===d(e,{holder:{token:t,heartbeat:c,nonce:u},queue:g,rev:n.rev+1}))return"abort";let h=i(e);return null===h.holder||h.holder.token!==t||h.holder.nonce!==u?"retry":{token:t,nonce:u}}(e,t,r);"abort"===g||"cannot-acquire"===g?n(null):"retry"!==g?n(g):++s>=o?n(null):setTimeout(c,u())};c()})}function h(e){null!==e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null)}function f(t,r){if("holding"!==t.status.kind)return;let n=t.status;if(n.released)return;n.released=!0,h(n),t.status={kind:"idle"};let{name:o,logger:l}=t.deps;l.debug(`[${o}] storage driver: revoked token=${n.token} reason=${r}`);let a=n.revokeCallback;if(e(a))try{a(r)}catch(e){l.error(`[${o}] storage driver: revoke callback threw`,e)}v(t)}function k(e,t){for(let r=0;r<e.length;r++)if(e[r]===t)return void e.splice(r,1)}function v(e){if(e.destroyed||e.pumping||"idle"!==e.status.kind||0===e.waiters.length)return;let[t]=e.waiters;e.pumping=!0,g(e,t.token,!1).then(r=>{if(e.pumping=!1,null===r)return;if(e.destroyed){b(e,t.token,r.nonce),k(e.waiters,t);return}if("idle"!==e.status.kind)return void b(e,t.token,r.nonce);let[n]=e.waiters;if(n!==t)return void b(e,t.token,r.nonce);if(t.isSettled()){e.waiters.shift(),b(e,t.token,r.nonce);return}e.waiters.shift();let o=m(e,t.token,r.nonce);e.deps.logger.debug(`[${e.deps.name}] storage driver: grant token=${t.token}`),t.resolve(o)})}function b(e,t,r){let n=i(e);null===n.holder||n.holder.token!==t||n.holder.nonce!==r||d(e,{holder:null,queue:n.queue,rev:n.rev+1})}function m(e,t,n){let o={kind:"holding",token:t,nonce:n,released:!1,revokeCallback:null,heartbeatTimer:null};return e.status=o,o.heartbeatTimer=setInterval(()=>{if(o.released||e.destroyed)return void h(o);let t=i(e);null===t.holder||t.holder.token!==o.token||t.holder.nonce!==o.nonce?f(e,"force"):d(e,{holder:{token:o.token,heartbeat:Date.now(),nonce:o.nonce},queue:t.queue,rev:t.rev+1})},r),function(e,t,r){let{name:n,logger:o}=e.deps;return{release:()=>{if("holding"!==e.status.kind||e.status.token!==t||e.status.nonce!==r||e.status.released)return;let l=e.status;l.released=!0,h(l),e.status={kind:"idle"},o.debug(`[${n}] storage driver: release token=${t}`),b(e,t,r),v(e)},onRevokedByDriver:n=>{"holding"!==e.status.kind||e.status.token!==t||e.status.nonce!==r||e.status.released||(e.status.revokeCallback=n)}}}(e,t,n)}function p(e){if(e.destroyed)return;let t=i(e);if("holding"===e.status.kind&&!e.status.released){(null===t.holder||t.holder.token!==e.status.token||t.holder.nonce!==e.status.nonce)&&f(e,"force");return}"idle"===e.status.kind&&e.waiters.length>0&&(null===t.holder||a(t.holder))&&v(e)}function y(t){let r=globalThis;if(!e(r.addEventListener))return t.deps.logger.warn(`[${t.deps.name}] storage driver: globalThis.addEventListener unavailable; cross-tab notification disabled`),null;let n=e=>{if(e.storageArea===t.storage&&(e.key===t.key||null===e.key))try{p(t)}catch(e){t.deps.logger.error(`[${t.deps.name}] storage driver: handleExternalChange threw`,e)}};return r.addEventListener("storage",n),()=>{r.removeEventListener?.("storage",n)}}function w(e){return setInterval(()=>{try{p(e)}catch(t){e.deps.logger.error(`[${e.deps.name}] storage driver: polling threw`,t)}},n)}function $(e){if("idle"!==e.status.kind||e.waiters.length>0)return!1;let t=i(e);return!(t.queue.length>0)&&(null===t.holder||a(t.holder))}function q(e,t){let{deps:r,waiters:n}=e,{name:o,logger:l}=r;if(l.debug(`[${o}] storage driver: destroy (waiters=${n.length}, status=${e.status.kind})`),null!==e.pollTimer&&(clearInterval(e.pollTimer),e.pollTimer=null),null!==e.unsubscribeStorageEvent){try{e.unsubscribeStorageEvent()}catch(e){l.error(`[${o}] storage driver: unsubscribe storage event threw`,e)}e.unsubscribeStorageEvent=null}if("holding"===e.status.kind){let t=e.status;h(t),t.released=!0,b(e,t.token,t.nonce)}e.status={kind:"idle"};let a=n.slice();if(n.length=0,a.length>0)try{let t=i(e),r=new Set;for(let e=0;e<a.length;e++)r.add(a[e].token);let n=t.queue.filter(e=>!r.has(e.token));n.length!==t.queue.length&&d(e,{holder:t.holder,queue:n,rev:t.rev+1})}catch(e){l.error(`[${o}] storage driver: batch dequeue failed during destroy`,e)}for(let e=0;e<a.length;e++)a[e].abort(t(a[e].token))}export{$ as canFastAcquire,q as drainOnDestroy,c as enqueueInStorage,m as enterHolding,p as handleExternalChange,v as pumpNextWaiter,b as releaseHolderInStorage,k as removeWaiter,f as revokeHolding,w as startPolling,y as subscribeStorageEvent,g as tryAcquire};