@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,182 @@
1
+ /**
2
+ * StorageAuthority 主类:跨进程权威副本的读写 + 推送 + 拉取统一收口
3
+ *
4
+ * 对应 RFC.md「StorageAuthority(localStorage 权威副本)」章节。
5
+ *
6
+ * 职责边界(与 LockDriver 互不干扰):
7
+ * - 权威 snapshot 读写
8
+ * - 跨 Tab 推送(authority.subscribe)/ 激活时主动拉取(pageshow / visibilitychange)
9
+ * - 会话纪元(epoch)生命周期管理(首次 resolveEpoch + 常驻 session-probe 响应)
10
+ * - 与锁调度完全无关(acquire / release / revoke 都不经过此处)
11
+ *
12
+ * 三条读路径共享同一应用流程:
13
+ * | 触发源 | 时机 | source | 数据来源 |
14
+ * | --------------------- | ------------------------------- | ------------------- | ---------------------------- |
15
+ * | acquire 时 pull | driver.acquire 成功、进入 recipe 前 | 'pull-on-acquire' | authority.read() 同步读 |
16
+ * | authority.subscribe | 其他进程写入触发订阅回调 | 'storage-event' | 回调直接传入 newValue |
17
+ * | 激活时主动 pull | pageshow / visibilitychange | 'pageshow' 等 | authority.read() 同步读 |
18
+ *
19
+ * 一条写路径:
20
+ * commit 成功 → rev++ → authority.write(serialize) → 触发 onCommit
21
+ *
22
+ * wrapper 方案下的契约(对应 fixes/api-getvalue-only-redesign.md §14.2 缺口 2):
23
+ * - 不再注入 `applySnapshot` 钩子 —— 远程同步通过 `host.applyRemote(next)` 完成原子覆写,
24
+ * authority 不感知 wrapper / dataRef 实现细节
25
+ * - 不再注入 `clone` 函数 —— 内部用 `cloneByJson` 完成 emit 事件的 snapshot 隔离
26
+ * - 事件触发统一走 `emit` 回调,由调用方接到 listenersFanout
27
+ */
28
+ import type { AuthorityAdapter, ChannelAdapter, CommitSource, LockDataMutation, LoggerAdapter, Persistence, SessionStoreAdapter, SyncSource } from '../types';
29
+ import { type ResolveEpochResult } from './epoch';
30
+ /**
31
+ * StorageAuthority 宿主契约(Entry 的最小子集)
32
+ *
33
+ * 设计动机:authority 不感知 Entry 完整结构,避免循环依赖;同时通过 `applyRemote` 方法
34
+ * 把"如何写入 dataRef.current"的实现细节封装到 Entry 内部,authority 只负责调用该方法
35
+ *
36
+ * 字段语义:
37
+ * - `applyRemote(next)`:远程同步入口;内部走 `cloneByJson(next)` + 赋值 `dataRef.current`,
38
+ * 与 emit 链解耦(emit 由 authority 自己负责)
39
+ * - `rev` / `lastAppliedRev` 读写双向;`epoch` 由 `StorageAuthority` 内部在首次
40
+ * `resolveEpoch` 后回写
41
+ */
42
+ interface StorageAuthorityHost<T extends object> {
43
+ /**
44
+ * 远程同步入口:把 awaited / 远程 snapshot 写入 `dataRef.current`
45
+ *
46
+ * 调用方语义:authority 拿到远端最新 snapshot 后,先 `host.applyRemote(snapshot)` 完成
47
+ * 内部状态切换,再由 authority 自己 `emitSync(...)` 通知 listener。Entry 内部实现
48
+ * 必须保证 `applyRemote` 走 JSON 拷贝隔离(详见 core/entry.ts buildApplyRemote)
49
+ */
50
+ readonly applyRemote: (next: T) => void;
51
+ /** 单调递增版本号;commit 时 `rev++` */
52
+ rev: number;
53
+ /** 已应用的最大 rev;commit 后同步更新;subscribe 回调时用于去重 */
54
+ lastAppliedRev: number;
55
+ /** 当前 Tab 的会话纪元;首次 resolveEpoch 完成后由此类回写 */
56
+ epoch: string | null;
57
+ }
58
+ /**
59
+ * StorageAuthority 的构造依赖集合
60
+ *
61
+ * adapters 三件套允许为 null:authority 或 channel 不可用时降级为
62
+ * 对应功能 no-op,保证 lockData 在任何环境下都能跑
63
+ */
64
+ interface StorageAuthorityDeps<T extends object> {
65
+ readonly host: StorageAuthorityHost<T>;
66
+ readonly authority: AuthorityAdapter | null;
67
+ readonly channel: ChannelAdapter | null;
68
+ readonly sessionStore: SessionStoreAdapter | null;
69
+ readonly persistence: Persistence;
70
+ readonly sessionProbeTimeout?: number;
71
+ readonly logger: LoggerAdapter;
72
+ /** onSync 事件触发回调;上层接到 listenersFanout */
73
+ readonly emitSync: (event: {
74
+ source: SyncSource;
75
+ rev: number;
76
+ snapshot: T;
77
+ }) => void;
78
+ /** onCommit 事件触发回调;上层接到 listenersFanout */
79
+ readonly emitCommit: (event: {
80
+ source: CommitSource;
81
+ token: string;
82
+ rev: number;
83
+ mutations: readonly LockDataMutation[];
84
+ snapshot: T;
85
+ }) => void;
86
+ }
87
+ /**
88
+ * StorageAuthority 对外暴露的 API
89
+ */
90
+ interface StorageAuthority<T extends object> {
91
+ /**
92
+ * 初始化:resolveEpoch + 常驻 session-probe 响应 + 初次 pull + 订阅推送通道
93
+ *
94
+ * 返回 `Promise<ResolveEpochResult>`;调用方(`core/entry.ts`)会把此 Promise 与
95
+ * getValue Promise 合成后挂到 `Entry.dataReadyPromise` 对外暴露
96
+ *
97
+ * 多次调用 init 是非法的:宿主自行保证只调用一次
98
+ */
99
+ init: () => Promise<ResolveEpochResult>;
100
+ /** 手动拉取(acquire 时使用):等价于一次 source='pull-on-acquire' 的 readIfNewer + 应用 */
101
+ pullOnAcquire: () => void;
102
+ /**
103
+ * commit 成功后的写路径:`rev++` → `authority.write` → emit onCommit
104
+ *
105
+ * `mutations` 由 Draft 层(`core/draft.ts`)提供;commit 流程为空 mutations 时也可调用
106
+ * `snapshot` 必须是已隔离的独立副本(调用方走 `cloneByJson`,authority.write 之后
107
+ * 宿主可能继续改 dataRef.current,独立副本保证 listener 看到的是 commit 当时的值)
108
+ */
109
+ onCommitSuccess: (event: {
110
+ source: CommitSource;
111
+ token: string;
112
+ mutations: readonly LockDataMutation[];
113
+ snapshot: T;
114
+ }) => void;
115
+ /**
116
+ * 销毁:解绑所有订阅(authority.subscribe / pageshow / visibilitychange / session-probe)
117
+ * + close channel;幂等
118
+ */
119
+ dispose: () => void;
120
+ }
121
+ /**
122
+ * StorageAuthority 的内部可变状态容器
123
+ *
124
+ * 从 `createStorageAuthority` 中抽离,让生命周期函数(init / pullOnAcquire /
125
+ * onCommitSuccess / dispose)可以作为顶层纯函数独立存在;否则这些函数作为闭包
126
+ * 会让 `createStorageAuthority` 单函数行数超过 biome `noExcessiveLinesPerFunction`
127
+ * 默认阈值(100 行)
128
+ */
129
+ interface AuthorityState {
130
+ readonly unsubscribers: Array<() => void>;
131
+ disposed: boolean;
132
+ initialized: boolean;
133
+ }
134
+ /**
135
+ * 根据远端 raw 应用到 host;三条读路径共享
136
+ *
137
+ * 命中条件 see extract.ts: `readIfNewer`
138
+ *
139
+ * wrapper 方案下:调用 `host.applyRemote(nextSnapshot)` 完成原子覆写,authority 不感知
140
+ * dataRef 实现细节;emitSync 的 snapshot 走 `cloneByJson` 拷贝隔离,避免 listener mutate
141
+ * 影响内部 dataRef.current
142
+ */
143
+ declare function applyAuthorityIfNewer<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>, source: SyncSource, raw: string | null): void;
144
+ /**
145
+ * 订阅激活时 pull:pageshow / visibilitychange → authority.read() + 应用
146
+ *
147
+ * 仅在浏览器环境(`window` / `document` 可用)注册;
148
+ * 非浏览器环境由自定义 AuthorityAdapter.subscribe 在合适时机回调即可
149
+ */
150
+ declare function attachActivationPullSubscription<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>): void;
151
+ /**
152
+ * 执行 init 流程:常驻 probe 响应 → resolveEpoch → 推送/激活订阅 → 初次 pull
153
+ */
154
+ declare function performInit<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>): Promise<ResolveEpochResult>;
155
+ /**
156
+ * 执行 pullOnAcquire 流程:dispose / authority 缺失时 no-op
157
+ */
158
+ declare function performPullOnAcquire<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>): void;
159
+ /**
160
+ * 执行 onCommitSuccess 写路径:rev 自增 → authority.write → emitCommit
161
+ */
162
+ declare function performCommitSuccess<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>, event: {
163
+ source: CommitSource;
164
+ token: string;
165
+ mutations: readonly LockDataMutation[];
166
+ snapshot: T;
167
+ }): void;
168
+ /**
169
+ * 执行 dispose 流程:解绑所有订阅 + channel.close;幂等
170
+ */
171
+ declare function performDispose<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>): void;
172
+ /**
173
+ * 创建 StorageAuthority 实例
174
+ *
175
+ * 仅负责:state 初始化 + 绑定 deps 闭包 + 返回 API 表面
176
+ * 具体生命周期逻辑由顶层 `perform*` / `attach*` 纯函数承担
177
+ *
178
+ * 立即执行:不做初始化(resolveEpoch / 订阅);这些在 `init()` 中异步触发
179
+ */
180
+ declare function createStorageAuthority<T extends object>(deps: StorageAuthorityDeps<T>): StorageAuthority<T>;
181
+ export type { AuthorityState, StorageAuthority, StorageAuthorityDeps, StorageAuthorityHost };
182
+ export { applyAuthorityIfNewer, attachActivationPullSubscription, createStorageAuthority, performCommitSuccess, performDispose, performInit, performPullOnAcquire, };
@@ -0,0 +1 @@
1
+ import{isObject as e,isString as t}from"../../utils/index.js";import{cloneByJson as r}from"../utils/json-safe.js";import{resolveEpoch as i,subscribeSessionProbe as o}from"./epoch.js";import{readIfNewer as s}from"./extract.js";import{serializeAuthority as n}from"./serialize.js";function a(t,i,o,n){if(t.disposed)return;let{host:a,logger:c,emitSync:u}=i,l=s({lastAppliedRev:a.lastAppliedRev,epoch:a.epoch},n);if(!l)return;if(!e(l.snapshot))return void c.warn(`[lockData] authority snapshot is not an object (source=${o}), skip apply`);let p=l.snapshot;try{a.applyRemote(p)}catch(e){c.error(`[lockData] host.applyRemote failed (source=${o}, rev=${l.rev})`,e);return}a.rev=l.rev,a.lastAppliedRev=l.rev;try{u({source:o,rev:l.rev,snapshot:r(p)})}catch(e){c.error(`[lockData] emitSync listener threw (source=${o})`,e)}}function c(e,t){let{authority:r}=t;if(!r||void 0===globalThis.window||"u"<typeof document)return;let i=i=>{i.persisted&&a(e,t,"pageshow",r.read())},o=()=>{"visible"===document.visibilityState&&a(e,t,"visibilitychange",r.read())};window.addEventListener("pageshow",i),document.addEventListener("visibilitychange",o),e.unsubscribers.push(()=>{window.removeEventListener("pageshow",i),document.removeEventListener("visibilitychange",o)})}async function u(e,t){let{host:r,authority:s,channel:n,sessionStore:u,persistence:l,sessionProbeTimeout:p,logger:h}=t;if(e.initialized)return h.warn("[lockData] StorageAuthority.init called twice, ignore the second call"),{epoch:r.epoch||"persistent",effectivePersistence:l,authorityCleared:!1};e.initialized=!0,function(e,t){if("session"!==t.persistence||!t.channel)return;let r=o(t.channel,()=>t.host.epoch);e.unsubscribers.push(r)}(e,t);let d=await i({persistence:l,sessionStore:u,channel:n,authority:s?{remove:()=>s.remove()}:null,sessionProbeTimeout:p,logger:h});return e.disposed||(r.epoch=d.epoch,!function(e,t){let{authority:r}=t;if(!r)return;let i=r.subscribe(r=>{a(e,t,"storage-event",r)});e.unsubscribers.push(i)}(e,t),c(e,t),s&&!d.authorityCleared&&a(e,t,"pull-on-acquire",s.read())),d}function l(e,t){let{authority:r}=t;!e.disposed&&r&&a(e,t,"pull-on-acquire",r.read())}function p(e,r,i){if(e.disposed)return;let{host:o,authority:s,logger:a,emitCommit:c}=r;if(o.rev++,o.lastAppliedRev=o.rev,s&&t(o.epoch)){let e=n(o.rev,Date.now(),o.epoch,i.snapshot);s.write(e)}try{c({source:i.source,token:i.token,rev:o.rev,mutations:i.mutations,snapshot:i.snapshot})}catch(e){a.error(`[lockData] emitCommit listener threw (source=${i.source})`,e)}}function h(e,t){if(e.disposed)return;e.disposed=!0;let{channel:r,logger:i}=t;for(let t=0;t<e.unsubscribers.length;t++)try{e.unsubscribers[t]()}catch(e){i.warn("[lockData] StorageAuthority dispose: unsubscriber threw",e)}if(e.unsubscribers.length=0,r)try{r.close()}catch(e){i.warn("[lockData] StorageAuthority dispose: channel.close threw",e)}}function d(e){let t={unsubscribers:[],disposed:!1,initialized:!1};return{init:()=>u(t,e),pullOnAcquire:()=>l(t,e),onCommitSuccess:r=>p(t,e,r),dispose:()=>h(t,e)}}export{a as applyAuthorityIfNewer,c as attachActivationPullSubscription,d as createStorageAuthority,p as performCommitSuccess,h as performDispose,u as performInit,l as performPullOnAcquire};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 权威副本存储格式序列化
3
+ *
4
+ * 对应 RFC.md「存储格式(固化契约)」章节。
5
+ *
6
+ * 固化字段顺序:`rev → ts → epoch → snapshot`
7
+ *
8
+ * 固化理由:
9
+ * 1. `JSON.stringify({ rev, ts, epoch, snapshot })` 在 JS 规范上不保证字段顺序
10
+ * (V8 / SpiderMonkey 实测按插入顺序,但不作为契约),手动拼接避免任何引擎差异
11
+ * 2. `rev` 固定首位:`extractRev` 用锚定开头的正则即可安全提取,不被 snapshot 内容干扰
12
+ * 3. `epoch` 固定在 snapshot 之前:`extractEpoch` 在小范围内匹配,快路径开销与 value 总长无关
13
+ * 4. `snapshot` 固定尾部:用户数据可能包含 `"rev"` / `"epoch"` 等字面量,放在尾部避免正则锚定出错
14
+ *
15
+ * snapshot 的序列化统一走 `JSON.stringify`,不做字段顺序保证(snapshot 内部由用户控制)
16
+ */
17
+ interface AuthoritySerializedParts {
18
+ readonly rev: number;
19
+ readonly ts: number;
20
+ readonly epoch: string;
21
+ readonly snapshot: unknown;
22
+ }
23
+ /**
24
+ * 序列化权威副本 value
25
+ *
26
+ * 产物形如:`{"rev":42,"ts":1714198800123,"epoch":"ab12...","snapshot":{...}}`
27
+ *
28
+ * @param rev 单调递增版本号
29
+ * @param ts 写入时间戳(ms,来自 `Date.now()`)
30
+ * @param epoch 会话纪元;`persistence === 'persistent'` 时固定为 `'persistent'`,否则为 UUID 字符串
31
+ * @param snapshot 用户数据快照;由调用方保证已通过 `cloneByJson` 深克隆(JSON 拷贝隔离)
32
+ */
33
+ declare function serializeAuthority(rev: number, ts: number, epoch: string, snapshot: unknown): string;
34
+ export type { AuthoritySerializedParts };
35
+ export { serializeAuthority };
@@ -0,0 +1 @@
1
+ function t(t,i,r,e){return`{"rev":${t},"ts":${i},"epoch":${JSON.stringify(r)},"snapshot":${JSON.stringify(e)}}`}export{t as serializeAuthority};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * lock-data 模块的全局常量
3
+ *
4
+ * 所有 key / 默认值统一收敛到此文件,避免散落在各模块中导致跨 Tab 契约漂移。
5
+ * 详见 RFC.md「默认值总览」「StorageAuthority / 存储格式」章节。
6
+ */
7
+ /**
8
+ * 跨 Tab 存储 key 的统一前缀
9
+ *
10
+ * 为什么不用 `lingshu:lock-data`:包含作者 scope 可避免与其他未来集成到页面的
11
+ * 第三方锁库(同样用 lock-data 命名)发生 localStorage key 冲突
12
+ */
13
+ declare const LOCK_PREFIX = "@cmtlyt/lingshu-toolkit:lockData";
14
+ /**
15
+ * "永不超时"标记
16
+ *
17
+ * 使用 unique symbol(而非 Infinity / -1 / 0)是为了:
18
+ * 1. 在 TypeScript 层完整区别于"未设置"和任意数值
19
+ * 2. 避免 setTimeout 对非法数值的静默降级
20
+ * 3. 让业务侧必须显式 import 才能使用,降低误用概率
21
+ */
22
+ declare const NEVER_TIMEOUT: unique symbol;
23
+ /**
24
+ * 默认抢锁超时(毫秒)
25
+ *
26
+ * 5000ms 是协作类场景的经验值:
27
+ * - 大于人类主动操作的响应延迟阈值(2s)
28
+ * - 小于用户感知到"卡住"的耐心上限(10s)
29
+ */
30
+ declare const DEFAULT_TIMEOUT = 5000;
31
+ /**
32
+ * session-probe 探测响应的等待窗口(毫秒)
33
+ *
34
+ * 仅首次启动(sessionStorage 无 epoch 时)阻塞;刷新 / bfcache 恢复走快路径跳过探测。
35
+ * 100ms 已覆盖同源 Tab 间 BroadcastChannel 的典型回响延迟(<30ms)。
36
+ */
37
+ declare const DEFAULT_SESSION_PROBE_TIMEOUT = 100;
38
+ /**
39
+ * persistent 策略下固定使用的 epoch 值
40
+ *
41
+ * 使用常量字符串而非随机 uuid,保证跨会话重开仍能匹配同一权威副本。
42
+ */
43
+ declare const PERSISTENT_EPOCH = "persistent";
44
+ /** 统一用于 throwError 第一参数的函数名,保证错误消息前缀一致 */
45
+ declare const ERROR_FN_NAME = "lockData";
46
+ export { DEFAULT_SESSION_PROBE_TIMEOUT, DEFAULT_TIMEOUT, ERROR_FN_NAME, LOCK_PREFIX, NEVER_TIMEOUT, PERSISTENT_EPOCH };
@@ -0,0 +1 @@
1
+ let E="@cmtlyt/lingshu-toolkit:lockData",t=Symbol("@cmtlyt/lingshu-toolkit:lockData#NEVER_TIMEOUT"),T=5e3,l=100,_="persistent",o="lockData";export{l as DEFAULT_SESSION_PROBE_TIMEOUT,T as DEFAULT_TIMEOUT,o as ERROR_FN_NAME,E as LOCK_PREFIX,t as NEVER_TIMEOUT,_ as PERSISTENT_EPOCH};
@@ -0,0 +1,163 @@
1
+ /**
2
+ * actions.ts 的内部辅助:纯函数式工具 + 内部状态接口
3
+ *
4
+ * 拆分动机:actions.ts 主体由「createActions 状态机闭包」+「跨调用的纯函数辅助」
5
+ * 两部分组成,后者完全独立可测,且 biome `noExcessiveLinesPerFile.maxLines: 500`
6
+ * 要求拆分,遂把以下章节迁移到本模块:
7
+ * - 错误辅助:throwDisposed / isAbortLike / translateAcquireError
8
+ * - timeout 归一化:resolveAcquireTimeout / resolveHoldTimeout / toMilliseconds
9
+ * - signal 合并:buildAcquireSignal + AcquireSignalBundle
10
+ * - driver handle 释放:releaseDriverHandle / safeReleaseHandle
11
+ * - token + buildAcquireName + issueToken
12
+ * - applyInPlace:replace 路径专用的原地覆写
13
+ * - 内部状态:ActionsInternalState / createInitialState / enqueueWrite
14
+ * - signal 自动 dispose 桥接:attachSignalAutoDispose / noop
15
+ *
16
+ * 本模块只对 actions.ts 内部使用;不通过 index.ts 对外导出
17
+ */
18
+ import type { ResolvedLoggerAdapter } from '../adapters/logger';
19
+ import type { ActionCallOptions, LockDataOptions, LockDriverHandle, LockPhase, TimeoutValue } from '../types';
20
+ import type { Entry } from './registry';
21
+ import { type SignalLike } from './signal';
22
+ /**
23
+ * 构造 driver `acquire` 入参的 `name`
24
+ *
25
+ * 必须基于 `entry.lockId`(语义判定用真实 id),而不是 `entry.id`(展示用占位):
26
+ * - Registry 路径:lockId === id,行为与历史一致(`${LOCK_PREFIX}:<真实 id>`)
27
+ * - Standalone(无 id)路径:lockId === undefined,fallback 到 `${LOCK_PREFIX}:__local__`,
28
+ * 与 `drivers/index.ts::buildDriverDeps` 的占位 name 保持一致;CustomDriver 透传给
29
+ * 用户的 `getLock` 时也会拿到这个 fallback,而非伪 `__local__` 真实 id
30
+ *
31
+ * 详见 `src/shared/lock-data/fixes/standalone-id-leak.md` §3.5
32
+ */
33
+ declare function buildAcquireName<T extends object>(entry: Entry<T>): string;
34
+ interface TokenSeqHolder {
35
+ tokenSeq: number;
36
+ }
37
+ /**
38
+ * 发放新 token;格式:`${LOCK_PREFIX}:${id}:token:${seq}`
39
+ *
40
+ * 仅用于事件 token 字段追踪,无需全局唯一;进程重启从 0 开始不会造成混淆
41
+ */
42
+ declare function issueToken(holder: TokenSeqHolder, id: string): string;
43
+ /**
44
+ * 从 options / callOpts 决议本次调用的抢锁超时
45
+ *
46
+ * 优先级:`callOpts.acquireTimeout` > `options.timeout` > `DEFAULT_TIMEOUT`
47
+ */
48
+ declare function resolveAcquireTimeout<T>(options: LockDataOptions<T>, callOpts: ActionCallOptions | undefined): TimeoutValue;
49
+ /** 同 resolveAcquireTimeout,维度换为 holdTimeout */
50
+ declare function resolveHoldTimeout<T>(options: LockDataOptions<T>, callOpts: ActionCallOptions | undefined): TimeoutValue;
51
+ /** 把 TimeoutValue 归一化为毫秒数;NEVER_TIMEOUT 返回 null 表示"不计时" */
52
+ declare function toMilliseconds(value: TimeoutValue): number | null;
53
+ interface AcquireSignalBundle {
54
+ readonly signal: AbortSignal;
55
+ readonly dispose: () => void;
56
+ /** acquireTimeout 触发用的 AbortController(null 表示 NEVER_TIMEOUT 不计时) */
57
+ readonly timeoutController: AbortController | null;
58
+ }
59
+ /**
60
+ * 把 options.signal / callOpts.signal / acquireTimeout / disposedSignal 合成一个派生 signal
61
+ *
62
+ * 返回 `dispose`:清理 timer + 内部 anySignal 的监听;调用方在 acquire 完成 / 失败时都要调用
63
+ */
64
+ declare function buildAcquireSignal(baseSignals: readonly SignalLike[], acquireTimeoutMs: number | null): AcquireSignalBundle;
65
+ /** 抛 LockDisposedError 辅助 */
66
+ declare function throwDisposed(cause?: unknown): never;
67
+ /**
68
+ * driver.acquire 抛错时按 signal 原因翻译错误类型
69
+ *
70
+ * - 超时 controller 触发 → LockTimeoutError
71
+ * - 其他 AbortError / TimeoutError → LockAbortedError
72
+ * - 其他错误原样透传(driver 内部故障、自定义 driver 抛错)
73
+ */
74
+ declare function translateAcquireError(error: unknown, timeoutController: AbortController | null): Error;
75
+ /**
76
+ * 调用 driver handle.release;异步 release 的错误通过 logger.warn 兜底,
77
+ * 不让 actions 的"还锁成功"被异步失败反向污染
78
+ *
79
+ * 严谨 thenable 鸭子类型判定:三重守卫过滤 undefined/null/primitive,Promise.resolve
80
+ * 把最小 thenable(只有 .then 没有 .catch)正规化为 Promise 再挂 catch,避免
81
+ * `(result as Promise<void>).catch` 在最小 thenable 上抛 "catch is not a function"
82
+ * 回归测试:actions.browser.test.ts 第 13 组 describe「driver.release 返回最小 rejected thenable」
83
+ */
84
+ declare function releaseDriverHandle(handle: LockDriverHandle, logger: ResolvedLoggerAdapter): void;
85
+ /**
86
+ * 独立调用 handle.release(用于 dispose-race 场景,此时 currentHandle 可能未设置)
87
+ *
88
+ * 严谨 thenable 鸭子类型判定:result 类型是 unknown(driver.release 的实际返回值可能
89
+ * 偏离契约),通过 isObject + 'then' in + isFunction 三重守卫过滤 null/primitive;
90
+ * Promise.resolve 把最小 thenable(只有 .then 没有 .catch)正规化为 Promise 再挂 catch
91
+ * 回归测试:actions.browser.test.ts 第 13 组 describe「dispose-race:acquire 期间 dispose 触发 → safeReleaseHandle 处理最小 thenable 不抛 TypeError」
92
+ */
93
+ declare function safeReleaseHandle(handle: LockDriverHandle, logger: ResolvedLoggerAdapter): void;
94
+ /**
95
+ * 把 `target` 的全部自有字段替换为 `next` 的字段(原地修改)
96
+ *
97
+ * 通过 Draft Proxy 调用以保证 set / delete 走 mutation log,享受统一的回滚保护:
98
+ * - 数组:`length = 0` 后 `push(...)` 还原;其他自有数字键 / `length` 不会泄漏
99
+ * - 对象:先 `delete` 多余键,再 `Reflect.set` 复制 `next` 的键
100
+ *
101
+ * 形态错配(target 是数组而 next 是对象,或反之)立即抛 `TypeError`,事务统一 rollback
102
+ *
103
+ * 历史位置:曾位于 `core/registry.ts`;wrapper 方案下 registry 不再做就地覆写
104
+ * (commit / 远程同步全部走 `entry.applyRemote(next)` 的新引用赋值),applyInPlace
105
+ * 仅 `actions.replace` 还在使用,遂迁移到本模块
106
+ */
107
+ declare function applyInPlace<T extends object>(target: T, next: T): void;
108
+ /**
109
+ * Actions 的内部可变状态;所有字段集中在此避免散落的闭包变量
110
+ *
111
+ * token 语义:
112
+ * - `currentToken`:当前 acquire 发放的 token;release / revoke / dispose 后仍保留
113
+ * 用于还锁 / 撤销事件的 token 字段;下次 acquire 会被覆盖
114
+ * - `aliveToken`:当前持有的"有效"token;revoke 后置空 —— 区分"这个 token 是否仍能
115
+ * commit",解决 acquiring 期被 revoke 后 await 仍回来的 race
116
+ *
117
+ * `writeChain` 用于写操作严格 FIFO 串行,详见 fixes/concurrent-acquire-serialize.md
118
+ */
119
+ interface ActionsInternalState {
120
+ phase: LockPhase;
121
+ /** 当前持有的 driver handle;非 holding 状态下必为 null */
122
+ currentHandle: LockDriverHandle | null;
123
+ /** 最近一次 acquire 发放的 token;每次 acquire 覆盖一次 */
124
+ currentToken: string;
125
+ /** 当前 "仍然有效" 的 token;release / revoke / dispose 后置空字符串 */
126
+ aliveToken: string;
127
+ /** token 单调序号;用于 issueToken */
128
+ tokenSeq: number;
129
+ /** holdTimeout 定时器句柄 */
130
+ holdTimer: ReturnType<typeof setTimeout> | null;
131
+ /** 当前持锁是否由 getLock 发起(影响 update 完成后是否自动 release) */
132
+ acquiredByGetLock: boolean;
133
+ /** dispose 终态标记;之后所有调用 reject LockDisposedError */
134
+ disposed: boolean;
135
+ /** 写操作串行链:update / replace / getLock 通过此 Promise 链严格 FIFO 排队 */
136
+ writeChain: Promise<void>;
137
+ }
138
+ declare function createInitialState(): ActionsInternalState;
139
+ /**
140
+ * 写操作串行化排队 helper
141
+ *
142
+ * 关键设计点:
143
+ * 1. `state.writeChain.then(task, task)` —— 无论前一个任务成功或失败,下一个任务都会
144
+ * 继续执行;保证 FIFO 严格串行
145
+ * 2. `state.writeChain = next.then(swallow, swallow)` —— 链尾用空函数吞掉 rejection,
146
+ * 下一个排队者不会被前一个失败污染;调用方拿到的是 `next` 本身
147
+ *
148
+ * 详见 src/shared/lock-data/fixes/concurrent-acquire-serialize.md
149
+ */
150
+ declare function enqueueWrite<R>(state: ActionsInternalState, task: () => Promise<R>): Promise<R>;
151
+ declare function clearHoldTimer(state: ActionsInternalState): void;
152
+ declare function noop(): void;
153
+ /**
154
+ * 为 options.signal 注册 abort 监听;触发时等价于自动 dispose
155
+ *
156
+ * 返回 unbind 函数:actions 主动 dispose 时调用,避免悬挂监听
157
+ *
158
+ * 若 signal 构造期已 aborted:通过 queueMicrotask 延迟触发,保证 createActions 完整
159
+ * 返回后再进入 dispose 路径,避免构造期半初始化状态被观察
160
+ */
161
+ declare function attachSignalAutoDispose(signal: AbortSignal | undefined, triggerDispose: () => void): () => void;
162
+ export type { AcquireSignalBundle, ActionsInternalState, TokenSeqHolder };
163
+ export { applyInPlace, attachSignalAutoDispose, buildAcquireName, buildAcquireSignal, clearHoldTimer, createInitialState, enqueueWrite, issueToken, noop, releaseDriverHandle, resolveAcquireTimeout, resolveHoldTimeout, safeReleaseHandle, throwDisposed, toMilliseconds, translateAcquireError, };
@@ -0,0 +1 @@
1
+ import{createError as e,throwError as r}from"../../throw-error/index.js";import{isFunction as t,isObject as n}from"../../utils/index.js";import{DEFAULT_TIMEOUT as o,ERROR_FN_NAME as i,LOCK_PREFIX as l,NEVER_TIMEOUT as a}from"../constants.js";import{LockAbortedError as u,LockDisposedError as s,LockTimeoutError as c}from"../errors/index.js";import{anySignal as d}from"./signal.js";function m(e){return void 0===e.lockId?`${l}:__local__`:`${l}:${e.lockId}`}function h(e,r){return e.tokenSeq++,`${l}:${r}:token:${e.tokenSeq}`}function f(e,r){return r&&void 0!==r.acquireTimeout?r.acquireTimeout:void 0!==e.timeout?e.timeout:o}function p(e,r){return r&&void 0!==r.holdTimeout?r.holdTimeout:void 0!==e.timeout?e.timeout:o}function T(e){return e===a?null:e}function k(e,r){let t=null===r?null:new AbortController,n=null===t?null:setTimeout(()=>t.abort(new DOMException("acquire timeout","TimeoutError")),r),o=d([...e,t?t.signal:null]);return{signal:o.signal,dispose:()=>{null!==n&&clearTimeout(n),o.dispose()},timeoutController:t}}function v(e){r(i,"actions disposed",s,{cause:e})}function y(r,t){return t&&t.signal.aborted?e(i,"acquire timeout",c,{cause:r}):!function(e){if(!n(e))return!1;let{name:r}=e;return"AbortError"===r||"TimeoutError"===r}(r)?r:e(i,"acquire aborted",u,{cause:r})}function b(e,r){let o;try{o=e.release()}catch(e){r.warn("[lockData] driver.release threw (sync)",e);return}n(o)&&"then"in o&&t(o.then)&&Promise.resolve(o).catch(e=>{r.warn("[lockData] driver.release threw (async)",e)})}function w(e,r){let o;try{o=e.release()}catch(e){r.warn("[lockData] handle.release threw (dispose-race)",e);return}n(o)&&"then"in o&&t(o.then)&&Promise.resolve(o).catch(e=>{r.warn("[lockData] handle.release threw (dispose-race async)",e)})}function q(e,t){let n=Array.isArray(e),o=Array.isArray(t);if(n!==o&&r(i,`replace shape mismatch: target is ${n?"array":"object"}, next is ${o?"array":"object"}`,TypeError),n){e.length=0;for(let r=0;r<t.length;r++)e.push(t[r]);return}let l=Object.keys(e);for(let r=0;r<l.length;r++){let n=l[r];Object.hasOwn(t,n)||Reflect.deleteProperty(e,n)}let a=Object.keys(t);for(let r=0;r<a.length;r++){let n=a[r];Reflect.set(e,n,t[n])}}function g(){return{phase:"idle",currentHandle:null,currentToken:"",aliveToken:"",tokenSeq:0,holdTimer:null,acquiredByGetLock:!1,disposed:!1,writeChain:Promise.resolve()}}function A(e,r){let t=()=>{},n=e.writeChain.then(r,r);return e.writeChain=n.then(t,t),n}function j(e){null!==e.holdTimer&&(clearTimeout(e.holdTimer),e.holdTimer=null)}function D(){}function E(e,r){if(!(e instanceof AbortSignal))return D;if(e.aborted)return queueMicrotask(r),D;let t=()=>{r()};return e.addEventListener("abort",t,{once:!0}),()=>{e.removeEventListener("abort",t)}}export{q as applyInPlace,E as attachSignalAutoDispose,m as buildAcquireName,k as buildAcquireSignal,j as clearHoldTimer,g as createInitialState,A as enqueueWrite,h as issueToken,D as noop,b as releaseDriverHandle,f as resolveAcquireTimeout,p as resolveHoldTimeout,w as safeReleaseHandle,v as throwDisposed,T as toMilliseconds,y as translateAcquireError};
@@ -0,0 +1,72 @@
1
+ /**
2
+ * LockDataActions:状态机 + 事务式写入的对外 API 实现
3
+ *
4
+ * 对应 RFC.md「Actions 实现要点」章节(L930-964)。
5
+ *
6
+ * 状态机:
7
+ * `idle` → `acquiring` → `holding` → `committing` → `released` / `revoked` / `disposed`
8
+ *
9
+ * 职责:
10
+ * - 抢锁 / 还锁 / 持锁期事务(update / replace)
11
+ * - 合并 AbortSignal:`options.signal` + `callOpts.signal` + `acquireTimeout` + 内部 dispose controller
12
+ * - `holdTimeout` 定时器 / `onRevokedByDriver` 桥接 → 触发 revoke + 广播 `onRevoked`
13
+ * - 通过 `entry.dataReadyPromise` 等待异步初始化完成,`failed` 态直接 reject `LockDisposedError`
14
+ * - dispose 时调用 `releaseFromRegistry` 释放引用计数;Entry 销毁由 Registry 负责
15
+ *
16
+ * 职责边界(不做什么):
17
+ * - 不直接销毁 Entry(refCount 归零时由 Registry 的 releaseEntry 触发 teardowns)
18
+ * - 不关心 driver / authority 的具体实现(只通过抽象接口交互)
19
+ * - 不管 Entry 销毁后的清理(通过 registerTeardown 登记的回调由 Registry 负责逆序运行)
20
+ */
21
+ import type { ActionCallOptions, LockDataActions, LockDataOptions } from '../types';
22
+ import { type ActionsInternalState } from './actions-helpers';
23
+ import type { Entry } from './registry';
24
+ /**
25
+ * Actions 构造依赖
26
+ *
27
+ * 使用依赖注入而非直接 import registry:便于测试用 stub registry 隔离,
28
+ * 也让 entry.ts 的组装链路显式可见(Registry 实例从 entry.ts 传入)
29
+ */
30
+ interface ActionsDeps<T extends object> {
31
+ /** 共享的 Entry;Actions 不拥有它,只读取 / 标记 rev++ */
32
+ readonly entry: Entry<T>;
33
+ /** 本实例原始 options(listeners / signal / timeout 等);长期持有 */
34
+ readonly options: LockDataOptions<T>;
35
+ /**
36
+ * 释放 Entry 的引用计数通道;Actions.dispose 调用
37
+ *
38
+ * 无 id 场景传入 `() => void` no-op(Entry 独占,无 Registry 跟踪)
39
+ */
40
+ readonly releaseFromRegistry: () => void;
41
+ }
42
+ /**
43
+ * 进入抢锁流程前的前置检查:
44
+ * - disposed 终态 → reject LockDisposedError
45
+ * - dataReadyPromise 存在(异步初始化未就绪)→ await(等待期不计入 acquireTimeout);
46
+ * reject 通道由 `prepareEntryData` 负责包装为 `LockDisposedError(cause=...)`,本处直接透传
47
+ *
48
+ * 半极简方案(设计文档 §12):删除 `dataReadyState/dataReadyError` 字段后,仅靠
49
+ * `dataReadyPromise !== null` 一个标志位就能完整表达"是否需要等待初始化"
50
+ */
51
+ declare function ensureDataReady<T extends object>(deps: ActionsDeps<T>, state: ActionsInternalState): Promise<void>;
52
+ /**
53
+ * 执行一次 acquire;拿到 handle 后启动 holdTimeout + onRevokedByDriver 绑定
54
+ *
55
+ * 失败路径:把错误翻译成 LockTimeoutError / LockAbortedError 抛出,并把 phase 回落到 'idle'
56
+ */
57
+ declare function performAcquire<T extends object>(deps: ActionsDeps<T>, state: ActionsInternalState, disposedSignal: AbortSignal, callOpts: ActionCallOptions | undefined, force: boolean): Promise<void>;
58
+ /**
59
+ * 构造 LockDataActions 实例
60
+ *
61
+ * 所有状态封装在 `ActionsInternalState`;对外暴露的 Actions 方法是纯闭包,
62
+ * 不泄漏内部 state 引用
63
+ */
64
+ declare function createActions<T extends object>(deps: ActionsDeps<T>): LockDataActions<T>;
65
+ interface ActionsTestHooks {
66
+ readonly doDispose: () => void;
67
+ readonly disposedController: AbortController;
68
+ }
69
+ /** 从 createActions 返回值上取出测试钩子;仅供 __test__ 使用 */
70
+ declare function getTestHooks<T extends object>(actions: LockDataActions<T>): ActionsTestHooks;
71
+ export type { ActionsDeps, ActionsTestHooks };
72
+ export { createActions, ensureDataReady, getTestHooks, performAcquire };
@@ -0,0 +1 @@
1
+ import{createError as e,throwError as t}from"../../throw-error/index.js";import{isFunction as r,isObject as o}from"../../utils/index.js";import{ERROR_FN_NAME as a}from"../constants.js";import{LockAbortedError as i,LockRevokedError as n}from"../errors/index.js";import{assertJsonSafeInput as s,cloneByJson as l}from"../utils/json-safe.js";import{applyInPlace as d,attachSignalAutoDispose as c,buildAcquireName as u,buildAcquireSignal as p,clearHoldTimer as y,createInitialState as f,enqueueWrite as m,issueToken as g,noop as k,releaseDriverHandle as v,resolveAcquireTimeout as h,resolveHoldTimeout as T,safeReleaseHandle as w,throwDisposed as j,toMilliseconds as q,translateAcquireError as b}from"./actions-helpers.js";import{createDraftSession as R}from"./draft.js";import{fanoutCommit as H,fanoutLockStateChange as L,fanoutRevoked as B}from"./fanout.js";function x(e,t,r,o){t.phase=r,L(e.entry.listenersSet,{phase:r,token:o},e.entry.adapters.logger)}function A(e,t,r){if(""===t.aliveToken)return;let o=t.aliveToken;t.aliveToken="",t.acquiredByGetLock=!1,y(t),S(e,t),x(e,t,"revoked",o),B(e.entry.listenersSet,{reason:r,token:o},e.entry.adapters.logger)}function S(e,t){let r=t.currentHandle;r&&(t.currentHandle=null,v(r,e.entry.adapters.logger))}async function D(e,t){t.disposed&&j();let{entry:r}=e;null!==r.dataReadyPromise&&(await r.dataReadyPromise,t.disposed&&j())}async function G(e,o,i,s,l){var d,c,y,f,m;let k,v,{entry:R,options:H}=e,L=h(H,s),B=q(L),S=T(H,s),D=p([H.signal,s?.signal,i],B),G=g(o,R.id);o.currentToken=G,o.aliveToken=G,x(e,o,"acquiring",G);try{v=await R.driver.acquire({name:u(R),token:G,force:l,acquireTimeout:L,holdTimeout:S,signal:D.signal})}catch(t){throw o.disposed&&j(t),o.aliveToken="",x(e,o,"idle",G),b(t,D.timeoutController)}finally{D.dispose()}(o.disposed||o.aliveToken!==G)&&(w(v,e.entry.adapters.logger),o.disposed&&j(),t(a,"lock revoked before activation",n)),o.currentHandle=v,d=e,c=o,r((y=v).onRevokedByDriver)&&y.onRevokedByDriver(e=>{A(d,c,e)}),f=e,m=o,null!==(k=q(S))&&(m.holdTimer=setTimeout(()=>{m.holdTimer=null,A(f,m,"timeout")},k)),x(e,o,"holding",G),R.authority&&R.authority.pullOnAcquire()}async function _(e,i,s,d){let{entry:c}=e,u=R(c.dataRef.current),p=i.currentToken;x(e,i,"committing",p);let y=!1;try{let c=d(u.draft);o(c)&&"then"in c&&r(c.then)&&await c,i.aliveToken!==p&&t(a,"lock revoked during recipe",n);let f=u.commit();y=!0,function(e,t,r,o,a){let{entry:i}=e,n=l(i.dataRef.current);i.authority?i.authority.onCommitSuccess({source:r,token:o,mutations:a,snapshot:n}):(i.rev++,i.lastAppliedRev=i.rev,H(i.listenersSet,{source:r,token:o,rev:i.rev,mutations:a,snapshot:n},i.adapters.logger)),x(e,t,"holding",o)}(e,i,s,p,f)}finally{y||u.rollback(),u.dispose()}}function C(e,t){if(t.disposed&&j(),"holding"!==t.phase&&"committing"!==t.phase)return;let r=t.currentToken;t.aliveToken="",y(t),S(e,t),t.acquiredByGetLock=!1,x(e,t,"released",r),x(e,t,"idle",r)}function P(n){let u=f(),p=new AbortController,v=k,h=()=>{if(u.disposed)return;if(u.disposed=!0,v(),v=k,p.signal.aborted||p.abort(e(a,"actions disposed",i)),""!==u.aliveToken){let e=u.aliveToken;u.aliveToken="",y(u),S(n,u),B(n.entry.listenersSet,{reason:"dispose",token:e},n.entry.adapters.logger)}let t=g(u,n.entry.id);x(n,u,"disposed",t),n.releaseFromRegistry()};v=c(n.options.signal,h);let T=()=>{u.disposed&&j()},w=async(e,t)=>{if(await D(n,u),"holding"===u.phase&&""!==u.aliveToken)return{alreadyHeld:!0};let r=e?.force===!0;return await G(n,u,p.signal,e,r),"getLock"===t&&(u.acquiredByGetLock=!0),{alreadyHeld:!1}},q=e=>{e||u.acquiredByGetLock||"holding"===u.phase&&C(n,u)},b={get isHolding(){return"holding"===u.phase||"committing"===u.phase},update:async(e,o)=>(T(),r(e)||t(a,"update requires a recipe function",TypeError),m(u,async()=>{T();let{alreadyHeld:t}=await w(o,"update");try{await _(n,u,"update",e)}finally{q(t)}})),replace:async(e,r)=>(T(),o(e)||t(a,"replace requires a non-null object",TypeError),s(e,"lockData actions.replace(next)"),m(u,async()=>{T();let{alreadyHeld:t}=await w(r,"replace");try{await _(n,u,"replace",t=>{d(t,e)})}finally{q(t)}})),snapshot:()=>(T(),l(n.entry.dataRef.current)),getLock:async e=>(T(),m(u,async()=>{T(),await w(e,"getLock")})),release(){C(n,u)},async dispose(){h()}};return Object.defineProperty(b,"__testHooks",{value:{doDispose:h,disposedController:p},enumerable:!1,configurable:!1,writable:!1}),b}function E(e){return e.__testHooks}export{P as createActions,D as ensureDataReady,E as getTestHooks,G as performAcquire};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 事务式 Draft:锁持有期间对底层 data 的可写代理
3
+ *
4
+ * 实现要点(对应 RFC.md「事务式 Draft」「Draft Proxy 行为」):
5
+ * - set / deleteProperty 同时:① push 到 mutation log ② 原地写入 target ③ 在 snapshot 中首次记录 prevValue
6
+ * - validity 置否(revoke / abort / recipe 结束)后任何写入立即抛 LockRevokedError
7
+ * - 惰性子代理:get 到对象 / 数组时递归构造子 draft,共享同一 ctx,路径前缀累加
8
+ * - rollback 按 snapshot 逆序 + prevValue 恢复 target,避免整树深拷贝
9
+ *
10
+ * ----------------------------------------------------------------
11
+ * **JSON-only 契约**(重要)
12
+ *
13
+ * Draft 仅支持 JSON 安全类型:plain object / array / string / number(不含 NaN/Infinity)/
14
+ * boolean / null。**禁止** Set / Map / Date / RegExp / class 实例 / function / symbol /
15
+ * bigint / undefined / 循环引用 等。
16
+ *
17
+ * 历史版本曾对 Set / Map 提供 collection proxy 跟踪,但「`map.get(key)` 取出的对象引用
18
+ * 直接深改」会绕过 proxy trap,事务的 commit / rollback 语义会被静默破坏。lock-data 的
19
+ * 数据本身要参与跨 Tab 同步与持久化序列化,集合类型在 JSON 上下文里只会持续制造类似缺陷,
20
+ * 因此从设计上移除支持,并在入口与每次写入处显式校验。
21
+ * ----------------------------------------------------------------
22
+ *
23
+ * ----------------------------------------------------------------
24
+ * MIGRATION NOTE(对应 RFC.md 决策 #32 「外部化前瞻」):
25
+ * 当前实现 self-contained,不对外导出。未来若出现第二个使用者(表单草稿 /
26
+ * 乐观更新 / 编辑器临时操作等),可按 RFC 预留的通用化 API 骨架
27
+ * (shared/transactional-draft)抽离;lock-data 用薄适配层接回。
28
+ * ----------------------------------------------------------------
29
+ */
30
+ import type { LockDataMutation } from '../types';
31
+ /**
32
+ * 事务式 Draft 会话句柄
33
+ *
34
+ * **JSON-only 契约**:仅支持 plain object / array / string / number(不含 NaN/Infinity)/
35
+ * boolean / null。传入或后续写入 Set / Map / Date / RegExp / class 实例 / function /
36
+ * symbol / bigint / undefined / 循环引用 会抛 `TypeError`。
37
+ */
38
+ interface DraftSession<T extends object> {
39
+ readonly draft: T;
40
+ readonly mutations: readonly LockDataMutation[];
41
+ commit: () => readonly LockDataMutation[];
42
+ rollback: () => void;
43
+ dispose: () => void;
44
+ }
45
+ /**
46
+ * 创建一个事务式 Draft 会话
47
+ *
48
+ * **JSON-only 契约**:`target` 及后续所有写入值必须是 JSON 安全类型 ——
49
+ * plain object / array / string / number(不含 NaN/Infinity)/ boolean / null。
50
+ * 传入 Set / Map / Date / RegExp / class 实例 / function / symbol / bigint /
51
+ * undefined / 循环引用 等会立即抛 `TypeError`。
52
+ *
53
+ * 集合类容器(Set / Map)虽常用,但其内部对象引用读取会绕过 proxy trap 导致
54
+ * 事务的 commit / rollback 语义被静默破坏;lock-data 的数据本身要参与跨 Tab
55
+ * 同步与持久化序列化,故从设计上仅允许 JSON 安全类型。如果业务层确有 Set / Map
56
+ * 语义需求,建议改用:`Set<T>` → `T[]`(去重逻辑放在 recipe 内);
57
+ * `Map<K, V>` → `Record<string, V>` 或 `{ key: K; value: V }[]`。
58
+ *
59
+ * @param target - 待包装的可变对象,必须是 JSON 安全的 plain object 或 array
60
+ * @throws {TypeError} target 包含非 JSON 安全类型 / 循环引用 时抛出
61
+ */
62
+ declare function createDraftSession<T extends object>(target: T): DraftSession<T>;
63
+ export type { DraftSession };
64
+ export { createDraftSession };
@@ -0,0 +1 @@
1
+ import{throwError as t}from"../../throw-error/index.js";import{ERROR_FN_NAME as e}from"../constants.js";import{LockRevokedError as r}from"../errors/index.js";import{assertJsonSafe as i}from"../utils/json-safe.js";function o(t,e,r,i){let o=`${i}::${String(r)}`;if(t.has(o))return;let a=Object.hasOwn(e,r),s=a?Reflect.get(e,r):void 0;t.set(o,{target:e,key:r,existed:a,prevValue:s})}function a(i){i.isValid||t(e,"draft is no longer valid (lock revoked / aborted)",r)}function s(t){i(t,[],new WeakSet,"draft");let e={validity:{isValid:!0},mutations:[],snapshot:new Map};return{draft:function t(e,r,s,n){return new Proxy(e,{get(e,i,o){let a=Reflect.get(e,i,o);if("object"!=typeof a||null===a)return a;let l=`${n}::${String(i)}`;return t(a,r,[...s,i],l)},set:(t,e,l)=>(a(r.validity),i(l,[...s,e],new WeakSet,"draft"),o(r.snapshot,t,e,n),r.mutations.push({path:[...s,e],op:"set",value:l}),Reflect.set(t,e,l)),deleteProperty:(t,e)=>(a(r.validity),o(r.snapshot,t,e,n),r.mutations.push({path:[...s,e],op:"delete"}),Reflect.deleteProperty(t,e))})}(t,e,[],"root"),get mutations(){return e.mutations},commit:()=>(a(e.validity),e.validity.isValid=!1,Object.freeze(e.mutations.map(t=>Object.freeze({...t,path:Object.freeze([...t.path])})))),rollback:()=>{!function(t){let e=Array.from(t.values()).reverse();for(let t=0;t<e.length;t++){let r=e[t];if(r.existed){Reflect.set(r.target,r.key,r.prevValue);continue}Reflect.deleteProperty(r.target,r.key)}}(e.snapshot),e.validity.isValid=!1,e.mutations.length=0,e.snapshot.clear()},dispose:()=>{e.validity.isValid=!1}}}export{s as createDraftSession};