@cmtlyt/lingshu-toolkit 0.6.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 (90) hide show
  1. package/dist/665.js +1 -1
  2. package/dist/shared/index.d.ts +1 -0
  3. package/dist/shared/index.js +1 -1
  4. package/dist/shared/lock-data/__test__/_helpers/memory-adapters.d.ts +95 -0
  5. package/dist/shared/lock-data/__test__/_helpers/memory-adapters.js +1 -0
  6. package/dist/shared/lock-data/__test__/index.test-d.d.ts +1 -0
  7. package/dist/shared/lock-data/__test__/integration/entry.test-d.d.ts +1 -0
  8. package/dist/shared/lock-data/__test__/playground.d.ts +1 -0
  9. package/dist/shared/lock-data/__test__/playground.js +1 -0
  10. package/dist/shared/lock-data/adapters/authority.d.ts +40 -0
  11. package/dist/shared/lock-data/adapters/authority.js +1 -0
  12. package/dist/shared/lock-data/adapters/channel.d.ts +39 -0
  13. package/dist/shared/lock-data/adapters/channel.js +1 -0
  14. package/dist/shared/lock-data/adapters/index.d.ts +58 -0
  15. package/dist/shared/lock-data/adapters/index.js +1 -0
  16. package/dist/shared/lock-data/adapters/logger.d.ts +56 -0
  17. package/dist/shared/lock-data/adapters/logger.js +1 -0
  18. package/dist/shared/lock-data/adapters/session-store.d.ts +37 -0
  19. package/dist/shared/lock-data/adapters/session-store.js +1 -0
  20. package/dist/shared/lock-data/authority/epoch.d.ts +135 -0
  21. package/dist/shared/lock-data/authority/epoch.js +1 -0
  22. package/dist/shared/lock-data/authority/extract.d.ts +107 -0
  23. package/dist/shared/lock-data/authority/extract.js +1 -0
  24. package/dist/shared/lock-data/authority/index.d.ts +182 -0
  25. package/dist/shared/lock-data/authority/index.js +1 -0
  26. package/dist/shared/lock-data/authority/serialize.d.ts +35 -0
  27. package/dist/shared/lock-data/authority/serialize.js +1 -0
  28. package/dist/shared/lock-data/constants.d.ts +46 -0
  29. package/dist/shared/lock-data/constants.js +1 -0
  30. package/dist/shared/lock-data/core/actions-helpers.d.ts +163 -0
  31. package/dist/shared/lock-data/core/actions-helpers.js +1 -0
  32. package/dist/shared/lock-data/core/actions.d.ts +72 -0
  33. package/dist/shared/lock-data/core/actions.js +1 -0
  34. package/dist/shared/lock-data/core/draft.d.ts +64 -0
  35. package/dist/shared/lock-data/core/draft.js +1 -0
  36. package/dist/shared/lock-data/core/entry.d.ts +133 -0
  37. package/dist/shared/lock-data/core/entry.js +1 -0
  38. package/dist/shared/lock-data/core/fanout.d.ts +42 -0
  39. package/dist/shared/lock-data/core/fanout.js +1 -0
  40. package/dist/shared/lock-data/core/readonly-view.d.ts +49 -0
  41. package/dist/shared/lock-data/core/readonly-view.js +1 -0
  42. package/dist/shared/lock-data/core/registry.d.ts +282 -0
  43. package/dist/shared/lock-data/core/registry.js +1 -0
  44. package/dist/shared/lock-data/core/signal.d.ts +33 -0
  45. package/dist/shared/lock-data/core/signal.js +1 -0
  46. package/dist/shared/lock-data/drivers/broadcast-protocol.d.ts +71 -0
  47. package/dist/shared/lock-data/drivers/broadcast-protocol.js +1 -0
  48. package/dist/shared/lock-data/drivers/broadcast-state.d.ts +125 -0
  49. package/dist/shared/lock-data/drivers/broadcast-state.js +1 -0
  50. package/dist/shared/lock-data/drivers/broadcast.d.ts +36 -0
  51. package/dist/shared/lock-data/drivers/broadcast.js +1 -0
  52. package/dist/shared/lock-data/drivers/custom.d.ts +27 -0
  53. package/dist/shared/lock-data/drivers/custom.js +1 -0
  54. package/dist/shared/lock-data/drivers/index.d.ts +59 -0
  55. package/dist/shared/lock-data/drivers/index.js +1 -0
  56. package/dist/shared/lock-data/drivers/local.d.ts +86 -0
  57. package/dist/shared/lock-data/drivers/local.js +1 -0
  58. package/dist/shared/lock-data/drivers/storage-protocol.d.ts +67 -0
  59. package/dist/shared/lock-data/drivers/storage-protocol.js +1 -0
  60. package/dist/shared/lock-data/drivers/storage-state.d.ts +103 -0
  61. package/dist/shared/lock-data/drivers/storage-state.js +1 -0
  62. package/dist/shared/lock-data/drivers/storage.d.ts +71 -0
  63. package/dist/shared/lock-data/drivers/storage.js +1 -0
  64. package/dist/shared/lock-data/drivers/types.d.ts +73 -0
  65. package/dist/shared/lock-data/drivers/types.js +0 -0
  66. package/dist/shared/lock-data/drivers/web-locks.d.ts +123 -0
  67. package/dist/shared/lock-data/drivers/web-locks.js +1 -0
  68. package/dist/shared/lock-data/errors/index.d.ts +12 -0
  69. package/dist/shared/lock-data/errors/index.js +1 -0
  70. package/dist/shared/lock-data/errors/invalid-options-error.d.ts +11 -0
  71. package/dist/shared/lock-data/errors/invalid-options-error.js +1 -0
  72. package/dist/shared/lock-data/errors/lock-aborted-error.d.ts +10 -0
  73. package/dist/shared/lock-data/errors/lock-aborted-error.js +1 -0
  74. package/dist/shared/lock-data/errors/lock-disposed-error.d.ts +14 -0
  75. package/dist/shared/lock-data/errors/lock-disposed-error.js +1 -0
  76. package/dist/shared/lock-data/errors/lock-revoked-error.d.ts +10 -0
  77. package/dist/shared/lock-data/errors/lock-revoked-error.js +1 -0
  78. package/dist/shared/lock-data/errors/lock-timeout-error.d.ts +9 -0
  79. package/dist/shared/lock-data/errors/lock-timeout-error.js +1 -0
  80. package/dist/shared/lock-data/errors/readonly-mutation-error.d.ts +11 -0
  81. package/dist/shared/lock-data/errors/readonly-mutation-error.js +1 -0
  82. package/dist/shared/lock-data/index.d.ts +57 -0
  83. package/dist/shared/lock-data/index.js +1 -0
  84. package/dist/shared/lock-data/types.d.ts +347 -0
  85. package/dist/shared/lock-data/types.js +0 -0
  86. package/dist/shared/lock-data/utils/json-safe.d.ts +69 -0
  87. package/dist/shared/lock-data/utils/json-safe.js +1 -0
  88. package/dist/shared/throw-error/index.d.ts +10 -3
  89. package/dist/shared/throw-error/index.js +1 -1
  90. package/package.json +2 -2
@@ -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};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * StorageDriver:基于 localStorage 的跨 Tab 互斥锁
3
+ *
4
+ * 适用场景(由 pickDriver 决定):
5
+ * - 浏览器环境但既不支持 `navigator.locks`,也不支持 `BroadcastChannel`
6
+ * - 显式 `mode='storage'` 强制指定
7
+ *
8
+ * 本文件是工厂聚合层:
9
+ * - 前置依赖校验(id 必传 / localStorage 可用)
10
+ * - 构造 state 容器 + 订阅 storage 事件 + 启动 polling
11
+ * - 暴露 acquire / destroy
12
+ *
13
+ * 协议细节、状态机、CAS 读写、队列操作、drain 逻辑分别放在:
14
+ * - `./storage-protocol`:存储格式 + 常量 + 校验 + nonce 生成
15
+ * - `./storage-state`:状态机 + CAS 读写 + 队列 + 心跳 + drain
16
+ */
17
+ import type { LockDriverContext, LockDriverHandle } from '../types';
18
+ import { type StorageDriverState, type Waiter } from './storage-state';
19
+ import type { LockDriver, LockDriverDeps } from './types';
20
+ /**
21
+ * 能力探测:localStorage 是否可实际读写
22
+ *
23
+ * 与 adapters/authority 的探测同构;pickDriver 已做一次但保留防御性兜底,
24
+ * 覆盖单元测试直接实例化的场景
25
+ */
26
+ declare function hasUsableLocalStorage(): boolean;
27
+ /**
28
+ * 构造 waiter 并绑定 signal / timeout 的 abort 生命周期
29
+ *
30
+ * 与 broadcast driver 的 buildWaiter 同构;主要差异:
31
+ * - 没有 pendingAnnounce / pendingForce 分支(storage driver 的抢锁是 CAS 直接决断,
32
+ * 没有"进行中的竞选"需要清理)
33
+ * - abort 时需要 removeWaiter + pumpNextWaiter,让下一个 waiter 继续抢
34
+ */
35
+ declare function buildWaiter(ctx: LockDriverContext, state: StorageDriverState, resolve: (handle: LockDriverHandle) => void, reject: (error: Error) => void): Waiter;
36
+ /**
37
+ * force 路径的专用入队 + 抢锁流程
38
+ *
39
+ * force 不走 FIFO 队列 —— 直接 CAS 覆盖 holder;成功即进入 holding,失败交由 abort 处理
40
+ */
41
+ declare function acquireForceLock(state: StorageDriverState, waiter: Waiter): void;
42
+ /**
43
+ * 快路径抢锁成功时的后处理
44
+ *
45
+ * 把 `acquireNonForceLock` 的快路径 `.then` 回调拆出为独立函数,
46
+ * 降低外层函数圈复杂度;职责不变 —— 根据 destroyed / settled / status 决定:
47
+ * - 直接授予 handle
48
+ * - 释放刚抢到的锁并 abort waiter
49
+ * - 释放刚抢到的锁并降级到慢路径入队
50
+ */
51
+ declare function handleFastPathGrant(state: StorageDriverState, waiter: Waiter, grantNonce: string): void;
52
+ /**
53
+ * 非 force 路径的抢锁流程
54
+ *
55
+ * 1. 快路径:本地 idle + 无其他 waiter + storage 可直接抢 → tryAcquire 一次,成功即返回
56
+ * 2. 慢路径:入 storage 队列 + 本地队列 → 等 pumpNextWaiter(由 storage 事件 / polling / release 触发)
57
+ */
58
+ declare function acquireNonForceLock(state: StorageDriverState, waiter: Waiter): void;
59
+ /**
60
+ * 把 waiter 加入 storage 队列 + 本地队列
61
+ *
62
+ * storage 入队失败(quota / 写冲突重试耗尽)不阻塞 —— 让 waiter 本地排队等 polling / heartbeat
63
+ * 超时触发 pump;最坏情况下会触发 acquireTimeout
64
+ */
65
+ declare function enqueueSlowPath(state: StorageDriverState, waiter: Waiter): void;
66
+ declare function acquireStorageLock(state: StorageDriverState, ctx: LockDriverContext): Promise<LockDriverHandle>;
67
+ /**
68
+ * 创建 StorageDriver 实例
69
+ */
70
+ declare function createStorageDriver(deps: LockDriverDeps): LockDriver;
71
+ export { acquireForceLock, acquireNonForceLock, acquireStorageLock, buildWaiter, createStorageDriver, enqueueSlowPath, handleFastPathGrant, hasUsableLocalStorage, };
@@ -0,0 +1 @@
1
+ import{throwError as e}from"../../throw-error/index.js";import{isNumber as t,isString as r}from"../../utils/index.js";import{ERROR_FN_NAME as o,LOCK_PREFIX as n}from"../constants.js";import{LockAbortedError as i,LockTimeoutError as s}from"../errors/index.js";import{canFastAcquire as a,drainOnDestroy as l,enqueueInStorage as u,enterHolding as d,pumpNextWaiter as c,releaseHolderInStorage as k,removeWaiter as g,revokeHolding as f,startPolling as m,subscribeStorageEvent as $,tryAcquire as v}from"./storage-state.js";function b(){try{let e=globalThis.localStorage;if(!e)return!1;let t=`${n}:__storage_driver_probe__`;return e.setItem(t,"1"),e.removeItem(t),!0}catch{return!1}}function h(e,r,n,a){let l=!1,u=null;function d(){null!==u&&(clearTimeout(u),u=null),e.signal.removeEventListener("abort",f)}let k={token:e.token,resolve:e=>{l||(l=!0,d(),n(e))},reject:e=>{l||(l=!0,d(),a(e))},abort:e=>{l||(g(r.waiters,k),k.reject(e),c(r))},isSettled:()=>l};function f(){k.abort(new i(`[@cmtlyt/lingshu-toolkit#${o}]: acquire aborted (token=${e.token})`))}if(e.signal.aborted)return queueMicrotask(()=>f()),k;if(e.signal.addEventListener("abort",f,{once:!0}),t(e.acquireTimeout)&&e.acquireTimeout>0){let t=e.acquireTimeout;u=setTimeout(()=>{k.abort(new s(`[@cmtlyt/lingshu-toolkit#${o}]: acquire timed out after ${t}ms (token=${e.token})`))},t)}return k}function y(e,t){let{name:r,logger:n}=e.deps;v(e,t.token,!0).then(s=>{if(null===s)return void t.abort(new i(`[@cmtlyt/lingshu-toolkit#${o}]: force acquire failed after retries (token=${t.token})`));if(e.destroyed){k(e,t.token,s.nonce),t.abort(new i(`[@cmtlyt/lingshu-toolkit#${o}]: storage driver destroyed during force acquire (token=${t.token})`));return}if(t.isSettled())return void k(e,t.token,s.nonce);"holding"!==e.status.kind||e.status.released||f(e,"force");let a=d(e,t.token,s.nonce);n.debug(`[${r}] storage driver: grant (force) token=${t.token}`),t.resolve(a)})}function p(e,t,r){let{name:n,logger:s}=e.deps;if(e.destroyed){k(e,t.token,r),t.abort(new i(`[@cmtlyt/lingshu-toolkit#${o}]: storage driver destroyed during fast acquire (token=${t.token})`));return}if(t.isSettled())return void k(e,t.token,r);if("idle"!==e.status.kind){k(e,t.token,r),w(e,t);return}let a=d(e,t.token,r);s.debug(`[${n}] storage driver: grant (fast-path) token=${t.token}`),t.resolve(a)}function q(e,t){a(e)?v(e,t.token,!1).then(r=>{null!==r?p(e,t,r.nonce):t.isSettled()||w(e,t)}):w(e,t)}function w(e,t){let{name:r,logger:o}=e.deps;e.waiters.push(t),o.debug(`[${r}] storage driver: enqueue token=${t.token}, queue=${e.waiters.length}`),u(e,t.token).then(n=>{n||o.warn(`[${r}] storage driver: enqueueInStorage failed after retries (token=${t.token}); relying on timeout/polling`),c(e)})}function S(e,t){return e.destroyed?Promise.reject(new i(`[@cmtlyt/lingshu-toolkit#${o}]: storage driver has been destroyed (token=${t.token})`)):new Promise((r,o)=>{let n=h(t,e,r,o);t.force?y(e,n):q(e,n)})}function T(t){let{id:s}=t;r(s)&&0!==s.length||e(o,"storage driver requires a non-empty id",TypeError),b()||e(o,"storage driver requires a usable localStorage",TypeError);let a={deps:t,storage:globalThis.localStorage,key:`${n}:${s}:driver-lock`,status:{kind:"idle"},waiters:[],destroyed:!1,pumping:!1,unsubscribeStorageEvent:null,pollTimer:null};function u(e){return new i(`[@cmtlyt/lingshu-toolkit#${o}]: storage driver destroyed (token=${e})`)}return a.unsubscribeStorageEvent=$(a),a.pollTimer=m(a),{acquire:e=>S(a,e),destroy:()=>{a.destroyed||(a.destroyed=!0,l(a,u))}}}export{y as acquireForceLock,q as acquireNonForceLock,S as acquireStorageLock,h as buildWaiter,T as createStorageDriver,w as enqueueSlowPath,p as handleFastPathGrant,b as hasUsableLocalStorage};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * drivers 层的内部类型契约
3
+ *
4
+ * 对应 RFC.md「架构分层」与「能力检测与降级」章节:
5
+ * - `LockDriver`:drivers 层统一抽象;Entry 持有 driver,所有 action 走 `driver.acquire`
6
+ * 拿到一个 `LockHandle`;driver 是**按 id 进程内单例**的(由 InstanceRegistry 持有)
7
+ * - `LockHandle`(已在 `../types` 中以 `LockDriverHandle` 名义向用户暴露):单次持有
8
+ * 的生命周期对象,`release` 还锁、`onRevokedByDriver` 桥接驱逐事件
9
+ * - `LockDriverDeps`:driver 构造依赖;不同 driver 子集不同,工厂函数签名统一
10
+ *
11
+ * 本文件仅导出**内部类型**,不对外 re-export。
12
+ */
13
+ import type { ResolvedLoggerAdapter } from '../adapters/logger';
14
+ import type { ChannelAdapter, ChannelAdapterContext, LockDataAdapters, LockDriverContext, LockDriverHandle } from '../types';
15
+ /**
16
+ * driver 构造依赖;由 `pickDriver` 从 `entry.adapters` 中分拣后传入
17
+ *
18
+ * 字段设计原则:
19
+ * - `logger` 对所有 driver 必传;driver 内部统一用此 logger 输出 warn / error / debug
20
+ * - `name` 已是拼好 `${LOCK_PREFIX}:${id}` 的完整锁作用域名;driver 无需关心前缀规则
21
+ * - `getChannel` 仅 `BroadcastDriver` 需要;其他 driver 允许为 undefined
22
+ * - `userGetLock` 仅 `CustomDriver` 需要;其他 driver 为 undefined
23
+ *
24
+ * **关于 `userGetLock` 的 `unknown` 泛型**:
25
+ * `LockDataAdapters<T>` 的泛型参数 `_T` 在 `getLock` 签名中并不出现(getLock
26
+ * 只接触锁调度上下文,不接触数据类型);此处用 `unknown` 表示"本 driver 层对
27
+ * 数据类型不可见",等价于 `LockDataAdapters<any>['getLock']` 但无 any 污染
28
+ */
29
+ interface LockDriverDeps {
30
+ /** 已拼前缀的锁作用域名 `${LOCK_PREFIX}:${id}`;无 id 场景下为 `${LOCK_PREFIX}:__local__` 占位 */
31
+ readonly name: string;
32
+ /** lockData 的原始 id;CustomDriver / 日志输出时需要(未 scope 化) */
33
+ readonly id: string | undefined;
34
+ /** Resolved logger(三方法齐全,下游可直接调用无需 guard) */
35
+ readonly logger: ResolvedLoggerAdapter;
36
+ /** 工厂:提供广播通道(仅 BroadcastDriver 消费) */
37
+ readonly getChannel?: (ctx: ChannelAdapterContext) => ChannelAdapter | null;
38
+ /** 用户注入的自定义 `adapters.getLock`(仅 CustomDriver 消费) */
39
+ readonly userGetLock?: LockDataAdapters<unknown>['getLock'];
40
+ }
41
+ /**
42
+ * 锁驱动统一抽象
43
+ *
44
+ * 所有 driver 实现均为"进程内 + id 作用域"单例;由 InstanceRegistry 首次创建
45
+ * Entry 时构造一次,Entry 销毁时调用 `destroy()` 清理内部长生命周期资源
46
+ * (心跳定时器、订阅、BroadcastChannel 实例等)
47
+ *
48
+ * 并发语义:
49
+ * - `acquire` 可被同一 driver 实例并发调用(例如同进程内两个 lockData 实例,均指向
50
+ * 同一 id),driver 内部负责**串行化**(FIFO 排队 / WebLocks 原生排队 / token 协议)
51
+ * - `force: true` 的 `acquire` 会让当前持有者的 `LockHandle.onRevokedByDriver` 以
52
+ * `'force'` 回调并立即释放;当前持有者后续 `release` 仍需可调用(幂等 no-op)
53
+ * - `acquireTimeout` 在 `acquire` 内部处理;到期抛 `LockTimeoutError`
54
+ * - `signal.aborted`(acquiring 阶段)抛 `LockAbortedError`
55
+ */
56
+ interface LockDriver {
57
+ /**
58
+ * 抢锁;返回一个单次持有的 `LockHandle`
59
+ *
60
+ * 抛错语义:
61
+ * - `LockTimeoutError`:acquireTimeout 到期
62
+ * - `LockAbortedError`:ctx.signal aborted / driver 已 destroy
63
+ * - 其他错误:driver 内部故障(logger.error 后原样透传)
64
+ */
65
+ readonly acquire: (ctx: LockDriverContext) => Promise<LockDriverHandle>;
66
+ /**
67
+ * Entry 引用计数归零 / dispose 时调用;清理 driver 内部长生命周期资源
68
+ *
69
+ * 幂等:重复调用 no-op;`destroy` 后再次 `acquire` 必抛 `LockAbortedError`
70
+ */
71
+ readonly destroy: () => void;
72
+ }
73
+ export type { LockDriver, LockDriverDeps };
File without changes
@@ -0,0 +1,123 @@
1
+ /**
2
+ * WebLocksDriver:基于 `navigator.locks` 的跨 Tab 互斥锁实现(首选 driver)
3
+ *
4
+ * 适用场景(由 pickDriver 决定):
5
+ * - `mode === 'auto'` 且运行时检测到 `navigator.locks`(现代浏览器 / Chromium 内核环境)
6
+ * - `mode === 'web-locks'` 强制指定
7
+ *
8
+ * 实现要点(对应 RFC.md「WebLocksDriver(首选)」):
9
+ * - 核心 API:`navigator.locks.request(name, { mode: 'exclusive', steal, signal }, callback)`
10
+ * - `callback` 持锁期间必须返回一个 Promise;锁会一直持有直到该 Promise settle
11
+ * → 这里构造一个外部可 resolve 的 `holdPromise`,`LockHandle.release` 就是 resolve 它
12
+ * - `force: true` → `steal: true`;原持有者的 callback 会以 `AbortError` reject,
13
+ * 捕获后把原 handle 的 `onRevokedByDriver('force')` 触发
14
+ * - `acquireTimeout` → `AbortController.abort()`;`navigator.locks.request` 会 reject
15
+ * `AbortError`(注意需要与 steal 场景区分:本 handle 还没拿到锁就 abort,而非被抢)
16
+ * - `signal.aborted`(外部 signal)同 `acquireTimeout` 通过合并 AbortController 统一处理
17
+ * - `destroy`:对所有仍持有锁的 handle 调用 release;等待中的 acquire 由各自的 signal
18
+ * 负责清理(destroy 会广播 abort 给内部 controller)
19
+ *
20
+ * 与其他 driver 的关键差异:
21
+ * - 互斥语义由浏览器保证(跨 Tab),无需自研排队协议
22
+ * - `release` 是**同步**触发(resolve holdPromise),但底层 navigator.locks 的清理
23
+ * 是微任务队列,所以上层看到的 release Promise 下一轮 tick 才 settle
24
+ */
25
+ import type { LockDriverContext } from '../types';
26
+ import type { LockDriver, LockDriverDeps } from './types';
27
+ /**
28
+ * Web Locks API 的最小化类型定义
29
+ *
30
+ * 为什么不直接用 `lib.dom` 内置 types:
31
+ * - TypeScript 4.x / 早期 5.x 的 lib.dom 对 `LockManager` 的定义不完整
32
+ * (缺 `steal` / `ifAvailable` 字段);本地定义保证 strict 下可编译
33
+ * - 仅声明 driver 内部实际用到的子集,不追求全面
34
+ */
35
+ interface WebLockRequestOptions {
36
+ mode?: 'exclusive' | 'shared';
37
+ ifAvailable?: boolean;
38
+ steal?: boolean;
39
+ signal?: AbortSignal;
40
+ }
41
+ interface WebLockManager {
42
+ readonly request: <T>(name: string, options: WebLockRequestOptions, callback: (lock: unknown) => Promise<T> | T) => Promise<T>;
43
+ }
44
+ /**
45
+ * 从运行时获取 `navigator.locks`;不存在则返回 null
46
+ *
47
+ * 正常路径下 pickDriver 会在选中本 driver 前做能力检测,本函数兜底覆盖两种边缘场景:
48
+ * 1. 上层绕过 pickDriver 直接实例化本 driver(如单元测试)
49
+ * 2. 运行时 `navigator` 对象缺失(非浏览器环境 + Node 22+ 的部分运行时)
50
+ * 返回 null 时由 `createWebLocksDriver` 构造期抛错,不会静默降级
51
+ */
52
+ declare function getWebLockManager(): WebLockManager | null;
53
+ /**
54
+ * 内部持有态:一次成功 acquire 产生一条
55
+ *
56
+ * - `holdPromise`:callback 期间返回给 navigator.locks 的 Promise;resolve 时锁释放
57
+ * - `resolveHold`:从外部 resolve holdPromise 的闭包引用,`release` 调用它
58
+ * - `revokeCallback`:用户(actions 层)通过 `onRevokedByDriver` 注册的回调
59
+ * - `released`:幂等开关
60
+ */
61
+ interface WebLockHolding {
62
+ readonly token: string;
63
+ readonly holdPromise: Promise<void>;
64
+ readonly resolveHold: () => void;
65
+ revokeCallback: ((reason: 'force' | 'timeout') => void) | null;
66
+ released: boolean;
67
+ }
68
+ /**
69
+ * 把 `ctx.signal` 与 `acquireTimeout` 合并为单一 AbortSignal 传给 navigator.locks
70
+ *
71
+ * 为什么需要合并:
72
+ * - navigator.locks.request 只接受单个 signal 作为"放弃抢锁"的开关
73
+ * - 需要同时响应"外部 signal abort"与"acquireTimeout 到期"两条路径
74
+ * - 用 `AbortController` 手动合并比 `AbortSignal.any` 兼容面更广
75
+ *
76
+ * 返回的 cleanup 必须在 `navigator.locks.request` settle 后调用,清理 timer + listener
77
+ */
78
+ declare function mergeSignalWithTimeout(externalSignal: AbortSignal, acquireTimeout: LockDriverContext['acquireTimeout'], token: string): {
79
+ signal: AbortSignal;
80
+ cleanup: () => void;
81
+ getTimeoutFired: () => boolean;
82
+ };
83
+ /** 判定错误是否属于 navigator.locks abort 类型(DOMException 'AbortError') */
84
+ declare function isAbortLikeError(error: unknown): boolean;
85
+ /** 顶层辅助函数共享的 driver 级依赖容器 */
86
+ interface DriverScope {
87
+ readonly holdings: Set<WebLockHolding>;
88
+ readonly logger: LockDriverDeps['logger'];
89
+ readonly driverName: string;
90
+ }
91
+ /**
92
+ * 已拿到锁后被 steal / force 驱逐的处理路径
93
+ *
94
+ * W3C 规范:原持有者的 `navigator.locks.request` 返回 Promise 以 AbortError reject;
95
+ * 必须显式 `resolveHold()` 避免 callback 里的 holdPromise 永远挂起(虽然 navigator.locks
96
+ * 此时已释放锁,但不 resolve 会造成本地 Promise 泄漏 + 后续 release 的幂等判定失效)
97
+ *
98
+ * 提至模块顶层,通过 `scope` 容器传入 driver 级依赖,降低 `createWebLocksDriver` 的
99
+ * linesPerFunction(biome noExcessiveLinesPerFunction = 100)
100
+ */
101
+ declare function handleStealRejection(seized: WebLockHolding, scope: DriverScope): void;
102
+ /**
103
+ * 处理 navigator.locks.request 的 settle —— 分三种情况:
104
+ * 1. resolve 路径(正常 release)→ 兜底清理 holding
105
+ * 2. reject + 已持有 → steal 路径,触发 onRevokedByDriver('force')
106
+ * 3. reject + 未持有 → 未拿到锁就被 abort / 非法参数等,把错误传给 acquire 入口
107
+ */
108
+ declare function wireRequestSettle(requestPromise: Promise<unknown>, getHolding: () => WebLockHolding | null, rejectGranted: (error: unknown) => void, scope: DriverScope): void;
109
+ declare function createWebLocksDriver(deps: LockDriverDeps): LockDriver;
110
+ /**
111
+ * destroy 时排空 holdings:把所有未 released 的 holding 同步释放
112
+ *
113
+ * 提至模块顶层的原因:
114
+ * - `holding.released === true` 的 false 分支在正常运行路径下不可达(release/handleStealRejection
115
+ * 都同步从 holdings 删除 + 置 released=true),属于防御性死代码
116
+ * - 抽出顶层后允许测试直接构造混合 holdings 集合(含已 released 的元素),命中防御分支
117
+ *
118
+ * 复制一份再遍历,避免 resolveHold 触发的副作用修改 `holdings`
119
+ * 用 Array.from + for 循环代替 forEach —— biome.useIterableCallbackReturn 禁止 forEach 回调有返回值
120
+ */
121
+ declare function drainHoldingsOnDestroy(holdings: Set<WebLockHolding>): void;
122
+ export type { DriverScope, WebLockHolding, WebLockManager };
123
+ export { createWebLocksDriver, drainHoldingsOnDestroy, getWebLockManager, handleStealRejection, isAbortLikeError, mergeSignalWithTimeout, wireRequestSettle, };
@@ -0,0 +1 @@
1
+ import{throwError as e}from"../../throw-error/index.js";import{isFunction as r,isNumber as t,isObject as o}from"../../utils/index.js";import{withResolvers as l}from"../../with-resolvers/index.js";import{ERROR_FN_NAME as n}from"../constants.js";import{LockAbortedError as i,LockTimeoutError as a}from"../errors/index.js";function s(){if("u"<typeof navigator)return null;let{locks:e}=navigator;return e||null}function d(e,r,o){let l=new AbortController,i=!1,s=null;if(e.aborted)return l.abort(e.reason),{signal:l.signal,cleanup:()=>void 0,getTimeoutFired:()=>i};function d(){l.abort(e.reason)}return e.addEventListener("abort",d,{once:!0}),t(r)&&r>0&&(s=setTimeout(()=>{i=!0,l.abort(new a(`[@cmtlyt/lingshu-toolkit#${n}]: acquire timed out after ${r}ms (token=${o})`))},r)),{signal:l.signal,cleanup:function(){null!==s&&(clearTimeout(s),s=null),e.removeEventListener("abort",d)},getTimeoutFired:()=>i}}function u(e){if(!o(e))return!1;let{name:r}=e;return"AbortError"===r}function c(e,t){if(e.released)return;let{holdings:o,logger:l,driverName:n}=t;if(e.released=!0,o.delete(e),e.resolveHold(),l.debug(`[${n}] web-locks driver: revoked by steal token=${e.token}`),r(e.revokeCallback))try{e.revokeCallback("force")}catch(e){l.error(`[${n}] web-locks driver: revoke callback threw`,e)}}function k(e,r,t,o){let{holdings:l}=o;e.then(()=>{let e=r();e&&!e.released&&(e.released=!0,l.delete(e))}).catch(e=>{let l=r();l?c(l,o):t(e)})}function v(r){let{name:t,logger:o}=r,c=s();c||e(n,"web-locks driver requires navigator.locks; use auto mode or fallback driver",TypeError);let v=new Set,m=!1;return{acquire:async function r(r){m&&e(n,"web-locks driver has been destroyed",i);let{signal:s,cleanup:b,getTimeoutFired:f}=d(r.signal,r.acquireTimeout,r.token),g=l(),$=null,w=l(),h=!0===r.force?{mode:"exclusive",steal:!0}:{mode:"exclusive",signal:s};k(c.request(t,h,()=>($={token:r.token,holdPromise:g.promise,resolveHold:g.resolve,revokeCallback:null,released:!1},v.add($),o.debug(`[${t}] web-locks driver: grant token=${r.token} steal=${!0===r.force}`),w.resolve($),g.promise)),()=>$,w.reject,{holdings:v,logger:o,driverName:t});try{var y;let l=await w.promise;return b(),m&&(l.released=!0,v.delete(l),l.resolveHold(),e(n,`web-locks driver destroyed during acquire (token=${r.token})`,i)),y=l,{release:()=>{y.released||(y.released=!0,v.delete(y),y.resolveHold(),o.debug(`[${t}] web-locks driver: release token=${y.token}`))},onRevokedByDriver:e=>{y.revokeCallback=e}}}catch(l){throw b(),f()&&e(n,`acquire timed out after ${String(r.acquireTimeout)}ms (token=${r.token})`,a,{cause:l}),(u(l)||r.signal.aborted)&&e(n,`acquire aborted (token=${r.token})`,i,{cause:l}),o.error(`[${t}] web-locks driver: request failed (token=${r.token})`,l),l}},destroy:function(){m||(m=!0,o.debug(`[${t}] web-locks driver: destroy (active holdings=${v.size})`),b(v))}}}function b(e){let r=Array.from(e);for(let t=0;t<r.length;t++){let o=r[t];o.released||(o.released=!0,e.delete(o),o.resolveHold())}}export{v as createWebLocksDriver,b as drainHoldingsOnDestroy,s as getWebLockManager,c as handleStealRejection,u as isAbortLikeError,d as mergeSignalWithTimeout,k as wireRequestSettle};