@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.
- package/dist/665.js +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -1
- package/dist/shared/lock-data/__test__/_helpers/memory-adapters.d.ts +95 -0
- package/dist/shared/lock-data/__test__/_helpers/memory-adapters.js +1 -0
- package/dist/shared/lock-data/__test__/index.test-d.d.ts +1 -0
- package/dist/shared/lock-data/__test__/integration/entry.test-d.d.ts +1 -0
- package/dist/shared/lock-data/__test__/playground.d.ts +1 -0
- package/dist/shared/lock-data/__test__/playground.js +1 -0
- package/dist/shared/lock-data/adapters/authority.d.ts +40 -0
- package/dist/shared/lock-data/adapters/authority.js +1 -0
- package/dist/shared/lock-data/adapters/channel.d.ts +39 -0
- package/dist/shared/lock-data/adapters/channel.js +1 -0
- package/dist/shared/lock-data/adapters/index.d.ts +58 -0
- package/dist/shared/lock-data/adapters/index.js +1 -0
- package/dist/shared/lock-data/adapters/logger.d.ts +56 -0
- package/dist/shared/lock-data/adapters/logger.js +1 -0
- package/dist/shared/lock-data/adapters/session-store.d.ts +37 -0
- package/dist/shared/lock-data/adapters/session-store.js +1 -0
- package/dist/shared/lock-data/authority/epoch.d.ts +135 -0
- package/dist/shared/lock-data/authority/epoch.js +1 -0
- package/dist/shared/lock-data/authority/extract.d.ts +107 -0
- package/dist/shared/lock-data/authority/extract.js +1 -0
- package/dist/shared/lock-data/authority/index.d.ts +182 -0
- package/dist/shared/lock-data/authority/index.js +1 -0
- package/dist/shared/lock-data/authority/serialize.d.ts +35 -0
- package/dist/shared/lock-data/authority/serialize.js +1 -0
- package/dist/shared/lock-data/constants.d.ts +46 -0
- package/dist/shared/lock-data/constants.js +1 -0
- package/dist/shared/lock-data/core/actions-helpers.d.ts +163 -0
- package/dist/shared/lock-data/core/actions-helpers.js +1 -0
- package/dist/shared/lock-data/core/actions.d.ts +72 -0
- package/dist/shared/lock-data/core/actions.js +1 -0
- package/dist/shared/lock-data/core/draft.d.ts +64 -0
- package/dist/shared/lock-data/core/draft.js +1 -0
- package/dist/shared/lock-data/core/entry.d.ts +133 -0
- package/dist/shared/lock-data/core/entry.js +1 -0
- package/dist/shared/lock-data/core/fanout.d.ts +42 -0
- package/dist/shared/lock-data/core/fanout.js +1 -0
- package/dist/shared/lock-data/core/readonly-view.d.ts +49 -0
- package/dist/shared/lock-data/core/readonly-view.js +1 -0
- package/dist/shared/lock-data/core/registry.d.ts +282 -0
- package/dist/shared/lock-data/core/registry.js +1 -0
- package/dist/shared/lock-data/core/signal.d.ts +33 -0
- package/dist/shared/lock-data/core/signal.js +1 -0
- package/dist/shared/lock-data/drivers/broadcast-protocol.d.ts +71 -0
- package/dist/shared/lock-data/drivers/broadcast-protocol.js +1 -0
- package/dist/shared/lock-data/drivers/broadcast-state.d.ts +125 -0
- package/dist/shared/lock-data/drivers/broadcast-state.js +1 -0
- package/dist/shared/lock-data/drivers/broadcast.d.ts +36 -0
- package/dist/shared/lock-data/drivers/broadcast.js +1 -0
- package/dist/shared/lock-data/drivers/custom.d.ts +27 -0
- package/dist/shared/lock-data/drivers/custom.js +1 -0
- package/dist/shared/lock-data/drivers/index.d.ts +59 -0
- package/dist/shared/lock-data/drivers/index.js +1 -0
- package/dist/shared/lock-data/drivers/local.d.ts +86 -0
- package/dist/shared/lock-data/drivers/local.js +1 -0
- package/dist/shared/lock-data/drivers/storage-protocol.d.ts +67 -0
- package/dist/shared/lock-data/drivers/storage-protocol.js +1 -0
- package/dist/shared/lock-data/drivers/storage-state.d.ts +103 -0
- package/dist/shared/lock-data/drivers/storage-state.js +1 -0
- package/dist/shared/lock-data/drivers/storage.d.ts +71 -0
- package/dist/shared/lock-data/drivers/storage.js +1 -0
- package/dist/shared/lock-data/drivers/types.d.ts +73 -0
- package/dist/shared/lock-data/drivers/types.js +0 -0
- package/dist/shared/lock-data/drivers/web-locks.d.ts +123 -0
- package/dist/shared/lock-data/drivers/web-locks.js +1 -0
- package/dist/shared/lock-data/errors/index.d.ts +12 -0
- package/dist/shared/lock-data/errors/index.js +1 -0
- package/dist/shared/lock-data/errors/invalid-options-error.d.ts +11 -0
- package/dist/shared/lock-data/errors/invalid-options-error.js +1 -0
- package/dist/shared/lock-data/errors/lock-aborted-error.d.ts +10 -0
- package/dist/shared/lock-data/errors/lock-aborted-error.js +1 -0
- package/dist/shared/lock-data/errors/lock-disposed-error.d.ts +14 -0
- package/dist/shared/lock-data/errors/lock-disposed-error.js +1 -0
- package/dist/shared/lock-data/errors/lock-revoked-error.d.ts +10 -0
- package/dist/shared/lock-data/errors/lock-revoked-error.js +1 -0
- package/dist/shared/lock-data/errors/lock-timeout-error.d.ts +9 -0
- package/dist/shared/lock-data/errors/lock-timeout-error.js +1 -0
- package/dist/shared/lock-data/errors/readonly-mutation-error.d.ts +11 -0
- package/dist/shared/lock-data/errors/readonly-mutation-error.js +1 -0
- package/dist/shared/lock-data/index.d.ts +57 -0
- package/dist/shared/lock-data/index.js +1 -0
- package/dist/shared/lock-data/types.d.ts +347 -0
- package/dist/shared/lock-data/types.js +0 -0
- package/dist/shared/lock-data/utils/json-safe.d.ts +69 -0
- package/dist/shared/lock-data/utils/json-safe.js +1 -0
- package/dist/shared/throw-error/index.d.ts +10 -3
- package/dist/shared/throw-error/index.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InstanceRegistry:同 id 进程内单例池
|
|
3
|
+
*
|
|
4
|
+
* 对应 RFC.md「InstanceRegistry(同 id 进程内单例)」章节。
|
|
5
|
+
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* - 按 id 缓存 Entry;同 id 再次 lockData(...) 复用同一份 dataRef / driver / adapters / authority
|
|
8
|
+
* - refCount 管理:每次 lockData(...) +1,actions.dispose() -1,归零时销毁 Entry
|
|
9
|
+
* - listenersSet 管理:每实例独立保留一份 listeners,driver 事件向全部 fanout
|
|
10
|
+
* - dataReadyPromise 共享:getValue 返回 Promise 时,同 id 多实例共享同一个就绪 Promise;
|
|
11
|
+
* resolve 后由外部 factory 调用 `entry.applyRemote(awaited)` 把 dataRef.current 重新赋值
|
|
12
|
+
* - initOptions 冲突检查:非 listeners 字段与首次注册不一致时走 logger.warn,以首次为准
|
|
13
|
+
*
|
|
14
|
+
* 设计边界:
|
|
15
|
+
* - 本模块**不感知** actions 状态机 / Draft / fanout / authority 的具体实现
|
|
16
|
+
* - Entry 的构造(pickDriver / pickDefaultAdapters / 初始化 data)由外部 `EntryFactory` 完成
|
|
17
|
+
* - Registry 通过 `EntryFactoryContext.registerTeardown` 把销毁回调通道注入给 factory;
|
|
18
|
+
* StorageAuthority / fanout 等模块通过此回调登记清理,Registry 在 refCount 归零时逆序运行
|
|
19
|
+
* - 无 id 场景不进入本 Registry(由 entry.ts 直接构造独立 Entry)
|
|
20
|
+
*
|
|
21
|
+
* wrapper 方案契约(与旧契约的根本差异):
|
|
22
|
+
* - `Entry.dataRef` 引用本身在 Entry 生命周期内永不变更
|
|
23
|
+
* - `Entry.dataRef.current` 在以下场景被重新赋值(`= JSON.parse(JSON.stringify(next))`):
|
|
24
|
+
* ① 异步 getValue resolve 后;② commit 成功后;③ `entry.applyRemote(next)` 远程同步
|
|
25
|
+
* - readonly view 通过 wrapper Proxy 跟随 `dataRef.current`,所有用户读取都看到最新值
|
|
26
|
+
*/
|
|
27
|
+
import type { ResolvedAdapters } from '../adapters/index';
|
|
28
|
+
import type { StorageAuthority } from '../authority/index';
|
|
29
|
+
import type { LockDriver } from '../drivers/index';
|
|
30
|
+
import type { LockDataListeners, LockDataOptions, LockMode, Persistence, SyncMode, TimeoutValue } from '../types';
|
|
31
|
+
/**
|
|
32
|
+
* Entry 结构:同 id 共享的全部状态
|
|
33
|
+
*
|
|
34
|
+
* 字段说明对应 RFC「Entry 结构关键字段」表格。
|
|
35
|
+
*
|
|
36
|
+
* 引用稳定性(wrapper 方案):
|
|
37
|
+
* - `dataRef` 引用本身在 Entry 生命周期内永不变更
|
|
38
|
+
* - `dataRef.current` 在 commit / `applyRemote` / 异步 getValue resolve 时被重新赋值
|
|
39
|
+
* - readonly view 通过 wrapper Proxy(target = dataRef)跟随 `dataRef.current`
|
|
40
|
+
*/
|
|
41
|
+
interface Entry<T extends object> {
|
|
42
|
+
/**
|
|
43
|
+
* 锁的展示用 id;用于日志、错误消息、Registry slot key 等"对外稳定文本"输出
|
|
44
|
+
*
|
|
45
|
+
* - Registry 路径:等于真实 id(非空字符串)
|
|
46
|
+
* - Standalone(无 id)路径:占位字符串 `'__local__'`
|
|
47
|
+
*
|
|
48
|
+
* **重要**:永远不要拿这个字段去做"是否有真实 id"的语义判定 —— standalone 路径
|
|
49
|
+
* 它只是占位。语义判定请使用 `lockId` 字段(详见下方)
|
|
50
|
+
*/
|
|
51
|
+
readonly id: string;
|
|
52
|
+
/**
|
|
53
|
+
* 真实锁 id;用于"是否启用跨 Tab 能力"的语义判定
|
|
54
|
+
*
|
|
55
|
+
* - Registry 路径:与 `id` 同值(必为非空字符串)
|
|
56
|
+
* - Standalone(无 id)路径:`undefined`,由此驱动 `pickDriver` 走 LocalLockDriver、
|
|
57
|
+
* `syncMode='storage-authority'` 不启用 authority、driver acquire `name` 走本地占位
|
|
58
|
+
*
|
|
59
|
+
* 详见 `src/shared/lock-data/fixes/standalone-id-leak.md`
|
|
60
|
+
*/
|
|
61
|
+
readonly lockId: string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* 共享数据 wrapper 引用
|
|
64
|
+
*
|
|
65
|
+
* - `dataRef` 引用本身在 Entry 生命周期内永不变更
|
|
66
|
+
* - `dataRef.current` 在 commit / `applyRemote` / 异步 getValue resolve 时被重新赋值
|
|
67
|
+
* - readonly view 通过 wrapper Proxy(target = dataRef)跟随 `dataRef.current`
|
|
68
|
+
*
|
|
69
|
+
* 不直接暴露 `T` 引用(而是包一层 `{ current: T }`)是为了让 wrapper Proxy 始终持有
|
|
70
|
+
* 一个稳定的 target,commit / 远程同步只需替换 `dataRef.current`,无需重建 view
|
|
71
|
+
*/
|
|
72
|
+
readonly dataRef: {
|
|
73
|
+
current: T;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* 远程同步入口:替换 `dataRef.current`
|
|
77
|
+
*
|
|
78
|
+
* 内部执行 `dataRef.current = JSON.parse(JSON.stringify(next))`,
|
|
79
|
+
* 由 authority 远程 push / 异步 getValue resolve 等场景调用
|
|
80
|
+
*
|
|
81
|
+
* JSON 拷贝隔离契约:调用方传入的 `next` 对象与内部 `dataRef.current` 完全隔离,
|
|
82
|
+
* 任一方的后续 mutate 都不会影响另一方
|
|
83
|
+
*/
|
|
84
|
+
readonly applyRemote: (next: T) => void;
|
|
85
|
+
/** 共享锁驱动实例;由首次 pickDriver 产出,Entry 销毁时 destroy */
|
|
86
|
+
readonly driver: LockDriver;
|
|
87
|
+
/** 已解析适配器集合 */
|
|
88
|
+
readonly adapters: ResolvedAdapters<T>;
|
|
89
|
+
/**
|
|
90
|
+
* 跨 Tab 权威副本(对应 RFC L646)
|
|
91
|
+
*
|
|
92
|
+
* - `syncMode === 'storage-authority'` 时由 factory 构造并注入
|
|
93
|
+
* - 其他 syncMode / 无 id 场景下为 null
|
|
94
|
+
*
|
|
95
|
+
* 生命周期:factory 创建后通过 registerTeardown 注册 `authority.dispose`,
|
|
96
|
+
* Entry 销毁时随 teardown 统一释放
|
|
97
|
+
*/
|
|
98
|
+
readonly authority: StorageAuthority<T> | null;
|
|
99
|
+
/** 每实例独立的 listeners;driver 事件向全部 fanout */
|
|
100
|
+
readonly listenersSet: Set<LockDataListeners<T>>;
|
|
101
|
+
/** 首次注册的冻结配置;用于后续同 id 实例的冲突检查 */
|
|
102
|
+
readonly initOptions: FrozenInitOptions;
|
|
103
|
+
/**
|
|
104
|
+
* 异步初始化未就绪场景的等待依据
|
|
105
|
+
*
|
|
106
|
+
* - 同步路径下为 `null`(Entry 构造瞬间即已就绪)
|
|
107
|
+
* - 异步路径下持有合成 Promise,resolve 时表示 `dataRef.current` 已被赋值为真实值
|
|
108
|
+
*
|
|
109
|
+
* 真实用途场景(详见 fixes/api-getvalue-only-redesign.md §14.3):
|
|
110
|
+
* ① 同 Tab 二次 lockData 调用方命中已存在 Entry 但首次调用尚未 resolve
|
|
111
|
+
* ② authority.init 等待异步初始化完成后再做远程拉取
|
|
112
|
+
* ③ 异步初始化期间 Entry 提前注册 + 二次调用方共享
|
|
113
|
+
*
|
|
114
|
+
* 异步初始化失败时 Promise reject,所有持有此 Entry 的同 Tab 调用方
|
|
115
|
+
* 在 action 时通过 `ensureDataReady` 抛 `LockDisposedError`(cause 携带原始原因)
|
|
116
|
+
*/
|
|
117
|
+
readonly dataReadyPromise: Promise<void> | null;
|
|
118
|
+
/**
|
|
119
|
+
* 注册 Entry 销毁回调;refCount 归零时逆序调用
|
|
120
|
+
*
|
|
121
|
+
* 调用异常隔离:单个回调抛错通过 logger.warn 捕获,继续运行后续回调
|
|
122
|
+
* Entry 已进入销毁流程后再调用本方法,回调被静默丢弃
|
|
123
|
+
*/
|
|
124
|
+
readonly registerTeardown: (teardown: () => void) => void;
|
|
125
|
+
/** 引用计数;每次 lockData(...) +1,actions.dispose() -1;归零销毁 Entry */
|
|
126
|
+
refCount: number;
|
|
127
|
+
/** 当前数据的权威单调序号;commit 成功 +1,初始 0 */
|
|
128
|
+
rev: number;
|
|
129
|
+
/** 最近一次应用 authority snapshot 的 rev;与 rev 分离用于去重 */
|
|
130
|
+
lastAppliedRev: number;
|
|
131
|
+
/** 当前 Tab 所属会话纪元;persistent 策略为 'persistent',session 策略首次为 null */
|
|
132
|
+
epoch: string | null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 首次注册冻结的配置子集,用于冲突检查
|
|
136
|
+
*
|
|
137
|
+
* 仅记录 RFC 要求"跨实例必须一致"的字段;listeners / signal / getValue / adapters 等
|
|
138
|
+
* 每实例独立字段不参与冲突检查
|
|
139
|
+
*/
|
|
140
|
+
interface FrozenInitOptions {
|
|
141
|
+
readonly timeout: TimeoutValue | undefined;
|
|
142
|
+
readonly mode: LockMode | undefined;
|
|
143
|
+
readonly syncMode: SyncMode | undefined;
|
|
144
|
+
readonly persistence: Persistence | undefined;
|
|
145
|
+
readonly sessionProbeTimeout: number | undefined;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* EntryFactory 调用上下文
|
|
149
|
+
*
|
|
150
|
+
* Registry 把生命周期通道(`registerTeardown`)通过此对象注入 factory;
|
|
151
|
+
* factory 组装 Entry 时直接把 `registerTeardown` 写进 Entry 字段
|
|
152
|
+
*/
|
|
153
|
+
interface EntryFactoryContext {
|
|
154
|
+
/** 供 factory 写进 Entry 的 `registerTeardown`;Registry 归零时使用这些回调 */
|
|
155
|
+
readonly registerTeardown: (teardown: () => void) => void;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Entry 构造工厂:由外部(entry.ts)注入,避免 Registry 直接依赖 driver / adapter 层
|
|
159
|
+
*
|
|
160
|
+
* 工厂契约:
|
|
161
|
+
* 1. 解析 adapters(pickDefaultAdapters)→ 解析 driver(pickDriver)
|
|
162
|
+
* 2. 按 `prepareEntryData` 准备 `dataRef` / `applyRemote` / `dataReadyPromise`
|
|
163
|
+
* (同步路径直接得到首值;异步路径下 Entry 构造延迟到 awaited resolve 之后)
|
|
164
|
+
* 3. 把 `ctx.registerTeardown` 写入返回 Entry 的 `registerTeardown` 字段
|
|
165
|
+
* 4. refCount 初始 1,listenersSet 含当次 options.listeners(若提供)
|
|
166
|
+
* 5. 把入参 `id` 写入 `Entry.id`、`lockId` 写入 `Entry.lockId`
|
|
167
|
+
*
|
|
168
|
+
* 工厂抛错时 Registry 不会把条目放入 Map —— **partial 资源的清理由 factory 自己负责**
|
|
169
|
+
* (factory 应当在内部用 try/catch 处理中途失败;例如已构造的 driver、已注册的订阅等)。
|
|
170
|
+
* Registry 不介入 partial 构造链,避免在无 logger 可用的场景被迫使用 console 兜底
|
|
171
|
+
*
|
|
172
|
+
* 参数语义:
|
|
173
|
+
* - `id`:展示用 id(必为非空字符串);Registry 路径下 = 真实 id,
|
|
174
|
+
* standalone 路径下 = 占位 `'__local__'`
|
|
175
|
+
* - `lockId`:真实 id;Registry 路径下与 `id` 同值,standalone 路径下为 `undefined`
|
|
176
|
+
* 下游(pickDriver / attachAuthority / driver acquire name)必须基于此参数
|
|
177
|
+
* 做"是否有真实 id"的语义判定,详见 `fixes/standalone-id-leak.md`
|
|
178
|
+
*/
|
|
179
|
+
type EntryFactory<T extends object> = (id: string, lockId: string | undefined, options: LockDataOptions<T>, ctx: EntryFactoryContext) => Entry<T>;
|
|
180
|
+
/** Registry 对外 API */
|
|
181
|
+
interface InstanceRegistry {
|
|
182
|
+
/**
|
|
183
|
+
* 获取或创建指定 id 的 Entry
|
|
184
|
+
*
|
|
185
|
+
* - 命中已存在 Entry:refCount++ + 加入 listenersSet + 冲突检查 + 返回
|
|
186
|
+
* - 首次创建:调用 factory 构造 Entry 并注册
|
|
187
|
+
*
|
|
188
|
+
* 命中已存在 Entry 时**不会**等待 dataReadyPromise:Actions 层通过
|
|
189
|
+
* `await entry.dataReadyPromise` 自行感知初始化失败,由 Actions 层抛 LockDisposedError
|
|
190
|
+
*
|
|
191
|
+
* @throws 仅在 id 为空字符串时抛错(其他参数合法性由调用方保证)
|
|
192
|
+
*/
|
|
193
|
+
getOrCreateEntry: <T extends object>(id: string, options: LockDataOptions<T>, factory: EntryFactory<T>) => Entry<T>;
|
|
194
|
+
/**
|
|
195
|
+
* 释放指定实例对 Entry 的持有
|
|
196
|
+
*
|
|
197
|
+
* - 若传入 listeners 则从 listenersSet 移除(Set.delete 幂等)
|
|
198
|
+
* - refCount--;归零时逆序运行 teardownCallbacks → driver.destroy() → registry.delete(id)
|
|
199
|
+
* - 未命中 id / refCount 已为 0 时 no-op(幂等)
|
|
200
|
+
*/
|
|
201
|
+
releaseEntry: <T extends object>(id: string, listeners: LockDataListeners<T> | undefined) => void;
|
|
202
|
+
/** 仅用于测试 / 调试 */
|
|
203
|
+
readonly peek: {
|
|
204
|
+
has: (id: string) => boolean;
|
|
205
|
+
size: () => number;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** 冻结 options 子集用于冲突检查 */
|
|
209
|
+
declare function freezeInitOptions<T extends object>(options: LockDataOptions<T>): FrozenInitOptions;
|
|
210
|
+
/**
|
|
211
|
+
* 创建一个独立的 InstanceRegistry
|
|
212
|
+
*
|
|
213
|
+
* 通常单进程只创建一个(由 entry.ts 持有模块级单例),但允许测试构造隔离实例
|
|
214
|
+
*/
|
|
215
|
+
declare function createInstanceRegistry(): InstanceRegistry;
|
|
216
|
+
/**
|
|
217
|
+
* `prepareEntryData` 的产物;由 EntryFactory 用来组装 Entry 字段
|
|
218
|
+
*
|
|
219
|
+
* 字段语义:
|
|
220
|
+
* - `firstValue`:进入 Entry 时 `dataRef.current` 的首个值(已经过 JSON 拷贝隔离)
|
|
221
|
+
* - `dataReadyPromise`:异步路径下的就绪等待依据;同步路径下为 `null`
|
|
222
|
+
*
|
|
223
|
+
* 与旧版 `InitialDataPatch` 的差异:
|
|
224
|
+
* - 不再返回引用稳定的 `data`(wrapper 方案下 `dataRef.current` 可被重新赋值,无需引用稳定)
|
|
225
|
+
* - 不再返回 `dataReadyState` / `dataReadyError` 字段(这两个字段已删除)
|
|
226
|
+
* - 同步抛错路径不返回 patch,而是直接抛 `LockDisposedError`(Entry 不构造)
|
|
227
|
+
*/
|
|
228
|
+
interface EntryInitialData<T extends object> {
|
|
229
|
+
/**
|
|
230
|
+
* 进入 Entry 时 `dataRef.current` 的初始值
|
|
231
|
+
*
|
|
232
|
+
* - **同步路径**:已经过 `assertJsonSafeInput` + `cloneByJson` 隔离,是真实首值
|
|
233
|
+
* - **异步路径**:占位 `{} as T`;真实首值通过 `dataReadyPromise` resolve 时携带的值,
|
|
234
|
+
* 由 EntryFactory 在 resolve 后调用 `entry.applyRemote(awaited)` 写入 `dataRef.current`
|
|
235
|
+
*/
|
|
236
|
+
readonly firstValue: T;
|
|
237
|
+
/**
|
|
238
|
+
* 异步就绪通道(同步路径为 `null`)
|
|
239
|
+
*
|
|
240
|
+
* - 异步路径 resolve 时携带 awaited 真实首值(已经过 `assertJsonSafeInput` 校验);
|
|
241
|
+
* EntryFactory 拿到该值后 `applyRemote(awaited)` 写入 `dataRef.current`
|
|
242
|
+
* - 异步路径 reject 时携带 `LockDisposedError`(cause 字段保留原始 reject 原因)
|
|
243
|
+
* - 同步路径下首值已写入 `firstValue`,无需异步通道
|
|
244
|
+
*
|
|
245
|
+
* 通道资源严禁外泄:仅 `EntryFactory` 内部使用;`Entry.dataReadyPromise` 是
|
|
246
|
+
* 经过 `.then(() => undefined)` 抹平后的 `Promise<void>`,对外只暴露就绪与否
|
|
247
|
+
*/
|
|
248
|
+
readonly dataReadyPromise: Promise<T> | null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 按 `options.getValue` 形态准备 Entry 的首值 + dataReadyPromise
|
|
252
|
+
*
|
|
253
|
+
* 两种形态(types 层已强制 `getValue` 必传):
|
|
254
|
+
*
|
|
255
|
+
* 1. **同步路径**:`getValue()` 返回非 PromiseLike
|
|
256
|
+
* - 同步抛错 → 抛 `LockDisposedError`(Entry 不构造,不进 registry)
|
|
257
|
+
* - 顶层数组 / 非 JSON-safe 值 → 抛 `InvalidOptionsError` / `TypeError`(Entry 不构造)
|
|
258
|
+
* - 正常返回 → `firstValue = cloneByJson(returned)`,`dataReadyPromise = null`
|
|
259
|
+
*
|
|
260
|
+
* 2. **异步路径**:`getValue()` 返回 Promise / thenable
|
|
261
|
+
* - 占位 `firstValue = {} as T`;真实首值通过 `dataReadyPromise` resolve 时携带(仅一次 await)
|
|
262
|
+
* - resolve 后 `assertJsonSafeInput` 校验 awaited,校验失败 → reject `LockDisposedError`
|
|
263
|
+
* - 原始 Promise reject → reject `LockDisposedError`(cause 携带原始原因)
|
|
264
|
+
* - **Entry 构造延迟到 awaited resolve 之后**:调用方 `lockData()` 在 resolve 前不返回元组
|
|
265
|
+
*
|
|
266
|
+
* 校验闸单点收敛:所有进入 `dataRef.current` 的值(同步 / 异步)都在本函数内统一走
|
|
267
|
+
* `assertJsonSafeInput`,调用方拿到 firstValue 时已是 JSON 安全状态
|
|
268
|
+
*/
|
|
269
|
+
declare function prepareEntryData<T extends object>(id: string, options: LockDataOptions<T>): EntryInitialData<T>;
|
|
270
|
+
/**
|
|
271
|
+
* 构造 LockDisposedError,cause 字段携带 getValue 原始 reject 原因
|
|
272
|
+
*
|
|
273
|
+
* 由本模块(同步抛错路径)+ Actions 层(异步 dataReadyPromise reject 路径)共同调用,
|
|
274
|
+
* 对外统一抛 LockDisposedError。
|
|
275
|
+
*
|
|
276
|
+
* 放在本模块的理由:`LockDisposedError + cause` 的错误格式是 Registry 对外的数据契约
|
|
277
|
+
* (RFC 规定"错误 cause 字段携带 getValue 原始 reject 原因")—— 让同一处代码产出
|
|
278
|
+
* 同步路径与异步路径下的 LockDisposedError,避免契约漂移
|
|
279
|
+
*/
|
|
280
|
+
declare function createFailedInitError(id: string, cause: unknown): Error;
|
|
281
|
+
export type { Entry, EntryFactory, EntryFactoryContext, EntryInitialData, FrozenInitOptions, InstanceRegistry };
|
|
282
|
+
export { createFailedInitError, createInstanceRegistry, freezeInitOptions, prepareEntryData };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createError as e}from"../../throw-error/index.js";import{isFunction as t,isObject as r}from"../../utils/index.js";import{withResolvers as o}from"../../with-resolvers/index.js";import{ERROR_FN_NAME as n}from"../constants.js";import{LockDisposedError as i}from"../errors/index.js";import{assertJsonSafeInput as s,cloneByJson as a}from"../utils/json-safe.js";function l(e){return Object.freeze({timeout:e.timeout,mode:e.mode,syncMode:e.syncMode,persistence:e.persistence,sessionProbeTimeout:e.sessionProbeTimeout})}function u(){let o=new Map;return{getOrCreateEntry:(i,s,a)=>{if(0===i.length)throw e(n,"InstanceRegistry requires a non-empty id",TypeError);let u=o.get(i);if(u){let{entry:e}=u;e.refCount++;let{listeners:t}=s;return r(t)&&e.listenersSet.add(t),!function(e,t,r,o){let n=["timeout","mode","syncMode","persistence","sessionProbeTimeout"];for(let i=0;i<n.length;i++){let s=n[i];t[s]!==r[s]&&o.warn(`[lockData] option conflict on id=${e} (field=${String(s)}, first=${String(r[s])}, incoming=${String(t[s])}), using first registered value`)}}(i,l(s),e.initOptions,e.adapters.logger),e}let c=[],d={value:!0},f=a(i,i,s,{registerTeardown:e=>{!t(e)||d.value&&c.push(e)}});return o.set(i,{entry:f,teardowns:c,alive:d}),f},releaseEntry:(e,t)=>{let r=o.get(e);if(!r||r.entry.refCount<=0)return;let{entry:n,teardowns:i,alive:s}=r;void 0!==t&&n.listenersSet.delete(t),n.refCount--,n.refCount>0||(s.value=!1,o.delete(e),function(e,t){let{driver:r,adapters:o,id:n}=e,{logger:i}=o;for(let e=t.length-1;e>=0;e--)try{t[e]()}catch(e){i.warn(`[lockData] teardown callback threw on id=${n}`,e)}try{r.destroy()}catch(e){i.warn(`[lockData] driver.destroy threw on id=${n}`,e)}}(n,i))},peek:{has:e=>o.has(e),size:()=>o.size}}}function c(r,i){var l,u,c;let m,p,{getValue:g}=i;if(!t(g))throw e(n,`lockData id=${r} requires options.getValue (function)`,TypeError);try{p=g()}catch(e){throw f(r,e)}return null===(l=p)||"object"!=typeof l||"function"!=typeof l.then?(s(p,"lockData getValue() result"),{firstValue:a(p),dataReadyPromise:null}):(u=r,c=p,m=o(),Promise.resolve(c).then(e=>{try{s(e,"lockData getValue() result")}catch(e){m.reject(f(u,e));return}m.resolve(e)},e=>{m.reject(f(u,e))}),m.promise.catch(d),{firstValue:{},dataReadyPromise:m.promise})}function d(){}function f(t,r){return e(n,`lockData id=${t} initialization failed during getValue()`,i,{cause:r})}export{f as createFailedInitError,u as createInstanceRegistry,l as freezeInitOptions,c as prepareEntryData};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AbortSignal 合并工具
|
|
3
|
+
*
|
|
4
|
+
* 为什么不直接用 `AbortSignal.any`:
|
|
5
|
+
* - 较老 Safari / Node 18 不支持,而 lock-data 目标环境包括 SSR / Node 多进程
|
|
6
|
+
* - 需要在 polyfill 路径里正确处理"构造时已有 signal 处于 aborted 态"的边界
|
|
7
|
+
*
|
|
8
|
+
* 语义与 `AbortSignal.any` 一致:任一输入 signal abort 即派生 signal abort;
|
|
9
|
+
* 若构造时已有任何输入为 aborted,则派生 signal 立即 aborted
|
|
10
|
+
*/
|
|
11
|
+
type SignalLike = AbortSignal | null | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* 合并任意数量的 AbortSignal;null / undefined 会被过滤
|
|
14
|
+
*
|
|
15
|
+
* 返回值:
|
|
16
|
+
* - `signal`:合并后的派生 signal
|
|
17
|
+
* - `dispose`:手动解绑所有监听(避免长生命周期 signal 泄漏)
|
|
18
|
+
*/
|
|
19
|
+
declare function anySignal(signals: readonly SignalLike[]): {
|
|
20
|
+
signal: AbortSignal;
|
|
21
|
+
dispose: () => void;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* 把已有 signal(若未完成)+ 超时合成一个新 signal
|
|
25
|
+
*
|
|
26
|
+
* 返回值包含 `dispose`,用于提前清理 setTimeout(如 action 正常完成时)
|
|
27
|
+
*/
|
|
28
|
+
declare function signalWithTimeout(baseSignal: SignalLike, timeoutMs: number): {
|
|
29
|
+
signal: AbortSignal;
|
|
30
|
+
dispose: () => void;
|
|
31
|
+
};
|
|
32
|
+
export type { SignalLike };
|
|
33
|
+
export { anySignal, signalWithTimeout };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){let t=e.filter(e=>e instanceof AbortSignal);if("function"==typeof AbortSignal.any)return{signal:AbortSignal.any(t),dispose:n};let o=new AbortController,r=t.find(e=>e.aborted);if(void 0!==r)return o.abort(r.reason),{signal:o.signal,dispose:n};let i=[],a=()=>{for(let e=0;e<i.length;e++)i[e]();i.length=0};for(let e=0;e<t.length;e++){let n=t[e],r=()=>{o.abort(n.reason),a()};n.addEventListener("abort",r,{once:!0}),i.push(()=>n.removeEventListener("abort",r))}return{signal:o.signal,dispose:a}}function n(){}function t(n,t){let o=new AbortController,r=setTimeout(()=>o.abort(new DOMException("timeout","TimeoutError")),t),i=e([n,o.signal]);function a(){l()}let l=()=>{clearTimeout(r),i.dispose(),i.signal.removeEventListener("abort",a)};return i.signal.aborted?l():i.signal.addEventListener("abort",a,{once:!0}),{signal:i.signal,dispose:l}}export{e as anySignal,t as signalWithTimeout};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BroadcastDriver 协议层:消息类型定义、常量、校验
|
|
3
|
+
*
|
|
4
|
+
* 消息类型(所有消息都带 `senderId`,接收方据此判定是否来自自己 —— BroadcastChannel 不
|
|
5
|
+
* 回环,但用户可能替换为自定义 ChannelAdapter,因此本 driver 不依赖"不回环"假设):
|
|
6
|
+
* - `announce`:抢锁请求广播(requestId + token + ts + force)
|
|
7
|
+
* - `reject`:持有者对 announce 的拒绝响应(带 holderToken + holderTs)
|
|
8
|
+
* - `release`:持有者主动释放
|
|
9
|
+
* - `force`:强制抢占通知(带 ts 仲裁字段)
|
|
10
|
+
* - `heartbeat`:持有者周期心跳
|
|
11
|
+
*
|
|
12
|
+
* 仲裁规则:`isEarlier(tsA, idA, tsB, idB)`;时间戳小者优先,时间戳相等时字典序小者优先;
|
|
13
|
+
* 两端独立执行相同仲裁得出一致结果(基于消息内容是全局确定的:(ts, id) 一旦发出就不变)
|
|
14
|
+
*/
|
|
15
|
+
/** announce 的拒绝等待窗口(ms);窗口内无 reject / 无更早的他方 announce → 拿锁 */
|
|
16
|
+
declare const REJECT_WINDOW = 50;
|
|
17
|
+
/** force 抢占的仲裁等待窗口(ms);给对端时间响应 force 并可能反向广播 force 仲裁 */
|
|
18
|
+
declare const FORCE_ARBITRATION_WINDOW = 50;
|
|
19
|
+
/** 心跳周期(ms) */
|
|
20
|
+
declare const HEARTBEAT_INTERVAL = 1000;
|
|
21
|
+
/** 崩溃阈值(ms);连续此毫秒未收到 heartbeat → 视为远端崩溃回 idle */
|
|
22
|
+
declare const DEAD_THRESHOLD = 3000;
|
|
23
|
+
interface AnnounceMessage {
|
|
24
|
+
readonly kind: 'announce';
|
|
25
|
+
readonly senderId: string;
|
|
26
|
+
readonly requestId: string;
|
|
27
|
+
readonly token: string;
|
|
28
|
+
readonly ts: number;
|
|
29
|
+
readonly force: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface RejectMessage {
|
|
32
|
+
readonly kind: 'reject';
|
|
33
|
+
readonly senderId: string;
|
|
34
|
+
readonly requestId: string;
|
|
35
|
+
readonly holderToken: string;
|
|
36
|
+
readonly holderTs: number;
|
|
37
|
+
}
|
|
38
|
+
interface ReleaseMessage {
|
|
39
|
+
readonly kind: 'release';
|
|
40
|
+
readonly senderId: string;
|
|
41
|
+
readonly token: string;
|
|
42
|
+
}
|
|
43
|
+
interface ForceMessage {
|
|
44
|
+
readonly kind: 'force';
|
|
45
|
+
readonly senderId: string;
|
|
46
|
+
readonly token: string;
|
|
47
|
+
readonly ts: number;
|
|
48
|
+
}
|
|
49
|
+
interface HeartbeatMessage {
|
|
50
|
+
readonly kind: 'heartbeat';
|
|
51
|
+
readonly senderId: string;
|
|
52
|
+
readonly token: string;
|
|
53
|
+
readonly ts: number;
|
|
54
|
+
}
|
|
55
|
+
type BroadcastMessage = AnnounceMessage | RejectMessage | ReleaseMessage | ForceMessage | HeartbeatMessage;
|
|
56
|
+
/**
|
|
57
|
+
* 严格校验每个 kind 的必需字段(BC-6 修复)
|
|
58
|
+
*
|
|
59
|
+
* 运行时消息可能来自任意源(错位的 channel / 用户误用 / 注入),必须 shape 校验后再 narrow
|
|
60
|
+
*/
|
|
61
|
+
declare function isBroadcastMessage(value: unknown): value is BroadcastMessage;
|
|
62
|
+
/** 生成请求 / sender 的唯一 id;不使用 `crypto.randomUUID()` 以保持广兼容 */
|
|
63
|
+
declare function genId(prefix: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* 仲裁:`(tsA, idA)` 是否严格先于 `(tsB, idB)`
|
|
66
|
+
*
|
|
67
|
+
* 时间戳小者优先;时间戳相等时字符串字典序小者优先。两端独立执行一致
|
|
68
|
+
*/
|
|
69
|
+
declare function isEarlier(tsA: number, idA: string, tsB: number, idB: string): boolean;
|
|
70
|
+
export type { AnnounceMessage, BroadcastMessage, ForceMessage, HeartbeatMessage, RejectMessage, ReleaseMessage };
|
|
71
|
+
export { DEAD_THRESHOLD, FORCE_ARBITRATION_WINDOW, genId, HEARTBEAT_INTERVAL, isBroadcastMessage, isEarlier, REJECT_WINDOW, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isBoolean as e,isNumber as r,isObject as t,isString as n}from"../../utils/index.js";let s=50,o=50,i=1e3,u=3e3;function a(e){return r(e)&&Number.isFinite(e)}function c(r){if(!t(r)||!n(r.senderId))return!1;switch(r.kind){case"announce":return n(r.requestId)&&n(r.token)&&a(r.ts)&&e(r.force);case"reject":return n(r.requestId)&&n(r.holderToken)&&a(r.holderTs);case"release":return n(r.token);case"force":case"heartbeat":return n(r.token)&&a(r.ts);default:return!1}}function d(e){return`${e}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`}function f(e,r,t,n){return e!==t?e<t:r<n}export{u as DEAD_THRESHOLD,o as FORCE_ARBITRATION_WINDOW,i as HEARTBEAT_INTERVAL,s as REJECT_WINDOW,d as genId,c as isBroadcastMessage,f as isEarlier};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BroadcastDriver 状态层:状态机定义、消息处理、竞选流程、drain
|
|
3
|
+
*
|
|
4
|
+
* 本文件不对外暴露 API;`broadcast.ts` 作为工厂聚合层消费此处导出的内部符号。
|
|
5
|
+
*
|
|
6
|
+
* ## 本地状态机
|
|
7
|
+
* - `idle`:无人持锁;可直接 announce
|
|
8
|
+
* - `holding`:本 Tab 持锁;周期广播 heartbeat
|
|
9
|
+
* - `remote-held`:远端持锁;监测心跳过期(DEAD_THRESHOLD 未收到 heartbeat → 回 idle)
|
|
10
|
+
*
|
|
11
|
+
* ## 竞选协议(非 force)
|
|
12
|
+
* 1. 广播 `announce`,启动 `REJECT_WINDOW` 窗口
|
|
13
|
+
* 2. 期间规则:
|
|
14
|
+
* - 收到 `reject(requestId 匹配)` → abandonPendingAnnounce,waiter 回队等待
|
|
15
|
+
* - 收到他方 `announce` → 按 (ts, requestId) 字典序仲裁;对方更早则本方 abandon
|
|
16
|
+
* - 收到 `heartbeat` → 存在持有者,本方 abandon 并切 remote-held
|
|
17
|
+
* 3. 窗口到期无拒绝 → enterHolding
|
|
18
|
+
*
|
|
19
|
+
* ## force 协议(BC-1 修复:异步等待对端 revoke 完成)
|
|
20
|
+
* 1. 广播 `force`,启动 `FORCE_ARBITRATION_WINDOW` 窗口
|
|
21
|
+
* 2. 期间收到他方 `force` → 按 (ts, token) 字典序仲裁;对方更早则本方 abandon
|
|
22
|
+
* 3. 窗口到期后 enterHolding;对端持有者收到 force 时立即 revoke('force')
|
|
23
|
+
*
|
|
24
|
+
* ## 持锁期间冲突检测(BC-2 修复)
|
|
25
|
+
* - `holding` 状态收到 `reject(holderToken != 自己)` → 双持冲突,revoke 自己
|
|
26
|
+
* - `holding` 状态收到 `heartbeat(token != 自己)` → 双持冲突,revoke 自己
|
|
27
|
+
*/
|
|
28
|
+
/** biome-ignore-all lint/nursery/noExcessiveLinesPerFile: ignore */
|
|
29
|
+
import type { ChannelAdapter, LockDriverHandle } from '../types';
|
|
30
|
+
import { type AnnounceMessage, type ForceMessage, type HeartbeatMessage, type RejectMessage, type ReleaseMessage } from './broadcast-protocol';
|
|
31
|
+
import type { LockDriverDeps } from './types';
|
|
32
|
+
interface HoldingState {
|
|
33
|
+
readonly kind: 'holding';
|
|
34
|
+
readonly token: string;
|
|
35
|
+
readonly grantedAt: number;
|
|
36
|
+
released: boolean;
|
|
37
|
+
revokeCallback: ((reason: 'force' | 'timeout') => void) | null;
|
|
38
|
+
heartbeatTimer: ReturnType<typeof setInterval> | null;
|
|
39
|
+
}
|
|
40
|
+
interface RemoteHeldState {
|
|
41
|
+
readonly kind: 'remote-held';
|
|
42
|
+
token: string;
|
|
43
|
+
peerTs: number;
|
|
44
|
+
lastHeartbeat: number;
|
|
45
|
+
deadTimer: ReturnType<typeof setTimeout> | null;
|
|
46
|
+
}
|
|
47
|
+
interface IdleState {
|
|
48
|
+
readonly kind: 'idle';
|
|
49
|
+
}
|
|
50
|
+
type DriverState = IdleState | HoldingState | RemoteHeldState;
|
|
51
|
+
interface Waiter {
|
|
52
|
+
readonly token: string;
|
|
53
|
+
readonly resolve: (handle: LockDriverHandle) => void;
|
|
54
|
+
readonly reject: (error: Error) => void;
|
|
55
|
+
readonly abort: (error: Error) => void;
|
|
56
|
+
}
|
|
57
|
+
interface PendingAnnounce {
|
|
58
|
+
readonly requestId: string;
|
|
59
|
+
readonly ts: number;
|
|
60
|
+
readonly waiter: Waiter;
|
|
61
|
+
abandoned: boolean;
|
|
62
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
63
|
+
}
|
|
64
|
+
interface PendingForce {
|
|
65
|
+
readonly token: string;
|
|
66
|
+
readonly ts: number;
|
|
67
|
+
readonly waiter: Waiter;
|
|
68
|
+
abandoned: boolean;
|
|
69
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
70
|
+
}
|
|
71
|
+
interface BroadcastDriverState {
|
|
72
|
+
readonly deps: LockDriverDeps;
|
|
73
|
+
readonly channel: ChannelAdapter;
|
|
74
|
+
readonly senderId: string;
|
|
75
|
+
status: DriverState;
|
|
76
|
+
readonly waiters: Waiter[];
|
|
77
|
+
pendingAnnounce: PendingAnnounce | null;
|
|
78
|
+
pendingForce: PendingForce | null;
|
|
79
|
+
destroyed: boolean;
|
|
80
|
+
unsubscribe: (() => void) | null;
|
|
81
|
+
}
|
|
82
|
+
declare function handleRemoteDead(state: BroadcastDriverState, deadToken: string): void;
|
|
83
|
+
declare function revokeHolding(state: BroadcastDriverState, reason: 'force' | 'timeout'): void;
|
|
84
|
+
declare function enterHolding(state: BroadcastDriverState, token: string): LockDriverHandle;
|
|
85
|
+
declare function enterRemoteHeld(state: BroadcastDriverState, token: string, peerTs: number): void;
|
|
86
|
+
declare function abandonPendingAnnounce(state: BroadcastDriverState, reason: string): void;
|
|
87
|
+
declare function abandonPendingForce(state: BroadcastDriverState, reason: string): void;
|
|
88
|
+
/**
|
|
89
|
+
* 收到他方 announce:
|
|
90
|
+
* - 本方 holding(token 不同)→ 广播 reject;对方 handleReject 触发 abandonPendingAnnounce
|
|
91
|
+
* - 本方有 pendingAnnounce → 按 (ts, requestId) 字典序仲裁:
|
|
92
|
+
* - 我方更早 → 保持;对方在其本地也会执行相同仲裁并 abandon 自己
|
|
93
|
+
* - 对方更早 → 本方 abandon,回队等待
|
|
94
|
+
* - 本方 idle / remote-held(且无 pending)→ 不响应,让对方 announce 自然走完窗口
|
|
95
|
+
*/
|
|
96
|
+
declare function handleAnnounce(state: BroadcastDriverState, msg: AnnounceMessage): void;
|
|
97
|
+
declare function handleReject(state: BroadcastDriverState, msg: RejectMessage): void;
|
|
98
|
+
declare function handleHeartbeat(state: BroadcastDriverState, msg: HeartbeatMessage): void;
|
|
99
|
+
declare function handleRelease(state: BroadcastDriverState, msg: ReleaseMessage): void;
|
|
100
|
+
/**
|
|
101
|
+
* 收到他方 force:
|
|
102
|
+
* - 本方 pendingForce → 按 (ts, token) 字典序仲裁;败方 abandon
|
|
103
|
+
* - 本方 holding(token 不同)→ 立即 revoke('force')
|
|
104
|
+
* - 切 / 刷新 remote-held
|
|
105
|
+
*/
|
|
106
|
+
declare function handleForce(state: BroadcastDriverState, msg: ForceMessage): void;
|
|
107
|
+
declare function handleMessage(state: BroadcastDriverState, raw: unknown): void;
|
|
108
|
+
declare function removeWaiter(waiters: Waiter[], target: Waiter): void;
|
|
109
|
+
declare function pumpNextWaiter(state: BroadcastDriverState): void;
|
|
110
|
+
/**
|
|
111
|
+
* 启动 announce 竞选
|
|
112
|
+
*
|
|
113
|
+
* 前置条件(由调用方 pumpNextWaiter / acquireBroadcastLock 保证):
|
|
114
|
+
* - state.destroyed === false
|
|
115
|
+
* - state.status.kind === 'idle'
|
|
116
|
+
* - state.pendingAnnounce === null && state.pendingForce === null
|
|
117
|
+
*
|
|
118
|
+
* 若违反前置条件视为 driver 内部 bug,logger.error 记录后把 waiter 回队兜底
|
|
119
|
+
* (而非静默死等 —— BC-K 修复)
|
|
120
|
+
*/
|
|
121
|
+
declare function startAnnounceCampaign(state: BroadcastDriverState, waiter: Waiter): void;
|
|
122
|
+
declare function startForceCampaign(state: BroadcastDriverState, waiter: Waiter): void;
|
|
123
|
+
declare function drainOnDestroy(state: BroadcastDriverState, buildAbortError: (token: string) => Error): void;
|
|
124
|
+
export type { BroadcastDriverState, HoldingState, RemoteHeldState, Waiter };
|
|
125
|
+
export { abandonPendingAnnounce, abandonPendingForce, drainOnDestroy, enterHolding, enterRemoteHeld, handleAnnounce, handleForce, handleHeartbeat, handleMessage, handleReject, handleRelease, handleRemoteDead, pumpNextWaiter, removeWaiter, revokeHolding, startAnnounceCampaign, startForceCampaign, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isFunction as e}from"../../utils/index.js";import{DEAD_THRESHOLD as n,FORCE_ARBITRATION_WINDOW as t,HEARTBEAT_INTERVAL as r,REJECT_WINDOW as d,genId as a,isBroadcastMessage as s,isEarlier as o}from"./broadcast-protocol.js";function i(e){null!==e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null)}function l(e){null!==e.deadTimer&&(clearTimeout(e.deadTimer),e.deadTimer=null)}function u(e,n){let{name:t,logger:r}=e.deps;"remote-held"===e.status.kind&&e.status.token===n&&(l(e.status),r.warn(`[${t}] broadcast driver: remote holder token=${n} dead by heartbeat timeout`),e.status={kind:"idle"},w(e))}function c(n,t){if("holding"!==n.status.kind)return;let r=n.status;if(r.released)return;r.released=!0,i(r),n.status={kind:"idle"};let{name:d,logger:a}=n.deps;a.debug(`[${d}] broadcast driver: revoked token=${r.token} reason=${t}`);let s=r.revokeCallback;if(e(s))try{s(t)}catch(e){a.error(`[${d}] broadcast driver: revoke callback threw`,e)}w(n)}function k(e,n){let t={kind:"holding",token:n,grantedAt:Date.now(),released:!1,revokeCallback:null,heartbeatTimer:null};return e.status=t,t.heartbeatTimer=setInterval(()=>{t.released||e.destroyed?i(t):e.channel.postMessage({kind:"heartbeat",senderId:e.senderId,token:t.token,ts:Date.now()})},r),e.channel.postMessage({kind:"heartbeat",senderId:e.senderId,token:n,ts:t.grantedAt}),function(e,n){let{name:t,logger:r}=e.deps;return{release:()=>{if("holding"!==e.status.kind||e.status.token!==n||e.status.released)return;let d=e.status;d.released=!0,i(d),e.status={kind:"idle"},r.debug(`[${t}] broadcast driver: release token=${n}`),e.channel.postMessage({kind:"release",senderId:e.senderId,token:n}),w(e)},onRevokedByDriver:t=>{"holding"!==e.status.kind||e.status.token!==n||e.status.released||(e.status.revokeCallback=t)}}}(e,n)}function g(e,t,r){"remote-held"===e.status.kind&&l(e.status);let d={kind:"remote-held",token:t,peerTs:r,lastHeartbeat:Date.now(),deadTimer:null};e.status=d,l(d),d.deadTimer=setTimeout(()=>{u(e,d.token)},n)}function b(e,n){let t=e.pendingAnnounce;if(null===t||t.abandoned)return;t.abandoned=!0,null!==t.timer&&(clearTimeout(t.timer),t.timer=null),e.pendingAnnounce=null;let{name:r,logger:d}=e.deps;d.debug(`[${r}] broadcast driver: abandon pendingAnnounce reason=${n} token=${t.waiter.token}`),e.waiters.push(t.waiter)}function h(e,n){let t=e.pendingForce;if(null===t||t.abandoned)return;t.abandoned=!0,null!==t.timer&&(clearTimeout(t.timer),t.timer=null),e.pendingForce=null;let{name:r,logger:d}=e.deps;d.debug(`[${r}] broadcast driver: abandon pendingForce reason=${n} token=${t.token}`),e.waiters.push(t.waiter)}function f(e,n){if(n.senderId===e.senderId)return;if("holding"===e.status.kind&&!e.status.released){let t=e.status;e.channel.postMessage({kind:"reject",senderId:e.senderId,requestId:n.requestId,holderToken:t.token,holderTs:t.grantedAt});return}let t=e.pendingAnnounce;if(null!==t&&!t.abandoned){if(o(t.ts,t.requestId,n.ts,n.requestId))return;b(e,"arbitration-loss")}}function p(e,n){if(n.senderId===e.senderId)return;if("holding"===e.status.kind&&!e.status.released&&e.status.token!==n.holderToken){let{name:t,logger:r}=e.deps;r.warn(`[${t}] broadcast driver: double-hold detected (own=${e.status.token}, remote=${n.holderToken}); revoking self`),c(e,"force"),g(e,n.holderToken,n.holderTs);return}let t=e.pendingAnnounce;null===t||t.abandoned||t.requestId!==n.requestId||b(e,"rejected"),("idle"===e.status.kind||"remote-held"===e.status.kind)&&g(e,n.holderToken,n.holderTs)}function m(e,n){if(n.senderId!==e.senderId&&("holding"!==e.status.kind||e.status.token!==n.token)){if("holding"===e.status.kind&&!e.status.released){let{name:t,logger:r}=e.deps;r.warn(`[${t}] broadcast driver: double-hold detected via heartbeat (own=${e.status.token}, remote=${n.token}); revoking self`),c(e,"force"),g(e,n.token,n.ts);return}null===e.pendingAnnounce||e.pendingAnnounce.abandoned||b(e,"heartbeat-detected"),g(e,n.token,n.ts)}}function $(e,n){n.senderId!==e.senderId&&"remote-held"===e.status.kind&&e.status.token===n.token&&(l(e.status),e.status={kind:"idle"},w(e))}function v(e,n){if(n.senderId===e.senderId)return;let t=e.pendingForce;if(null!==t&&!t.abandoned){if(o(t.ts,t.token,n.ts,n.token))return;h(e,"arbitration-loss")}"holding"!==e.status.kind||e.status.released||e.status.token===n.token||c(e,"force"),g(e,n.token,n.ts)}function I(e,n){if(s(n))switch(n.kind){case"announce":f(e,n);return;case"reject":p(e,n);return;case"heartbeat":m(e,n);return;case"release":$(e,n);return;case"force":v(e,n);return;default:return}}function T(e,n){for(let t=0;t<e.length;t++)if(e[t]===n)return void e.splice(t,1)}function w(e){if(e.destroyed||"idle"!==e.status.kind||0===e.waiters.length||null!==e.pendingAnnounce||null!==e.pendingForce)return;let n=e.waiters.shift();n&&A(e,n)}function A(e,n){let{name:t,logger:r}=e.deps;if(e.destroyed)return void r.error(`[${t}] broadcast driver: startAnnounceCampaign called after destroyed`);if("idle"!==e.status.kind||null!==e.pendingAnnounce||null!==e.pendingForce){r.error(`[${t}] broadcast driver: startAnnounceCampaign precondition violated (status=${e.status.kind}, pendingAnnounce=${null!==e.pendingAnnounce}, pendingForce=${null!==e.pendingForce})`),e.waiters.push(n);return}let s=a("req"),o=Date.now(),i={requestId:s,ts:o,waiter:n,abandoned:!1,timer:null};e.pendingAnnounce=i,e.channel.postMessage({kind:"announce",senderId:e.senderId,requestId:s,token:n.token,ts:o,force:!1}),r.debug(`[${t}] broadcast driver: announce token=${n.token} reqId=${s}`),i.timer=setTimeout(()=>{if(i.timer=null,i.abandoned||e.destroyed)return;e.pendingAnnounce=null;let d=k(e,n.token);r.debug(`[${t}] broadcast driver: grant token=${n.token}`),n.resolve(d)},d)}function F(e,n){let{name:r,logger:d}=e.deps;if(e.destroyed)return void d.error(`[${r}] broadcast driver: startForceCampaign called after destroyed`);"holding"!==e.status.kind||e.status.released||c(e,"force");let a=Date.now(),s={token:n.token,ts:a,waiter:n,abandoned:!1,timer:null};e.pendingForce=s,e.channel.postMessage({kind:"force",senderId:e.senderId,token:n.token,ts:a}),d.debug(`[${r}] broadcast driver: force-announce token=${n.token} ts=${a}`),s.timer=setTimeout(()=>{if(s.timer=null,s.abandoned||e.destroyed)return;e.pendingForce=null;let t=k(e,n.token);d.debug(`[${r}] broadcast driver: grant (force) token=${n.token}`),n.resolve(t)},t)}function y(e,n){let{deps:t,waiters:r}=e,{name:d,logger:a}=t;a.debug(`[${d}] broadcast driver: destroy (waiters=${r.length}, status=${e.status.kind}, pendingAnnounce=${null!==e.pendingAnnounce}, pendingForce=${null!==e.pendingForce})`);let s=[];if(null!==e.pendingAnnounce){let{pendingAnnounce:n}=e;n.abandoned=!0,null!==n.timer&&clearTimeout(n.timer),e.pendingAnnounce=null,s.push(n.waiter)}if(null!==e.pendingForce){let{pendingForce:n}=e;n.abandoned=!0,null!==n.timer&&clearTimeout(n.timer),e.pendingForce=null,s.push(n.waiter)}for(let e=0;e<r.length;e++)s.push(r[e]);if(r.length=0,"holding"===e.status.kind){let n=e.status;i(n),n.released=!0;try{e.channel.postMessage({kind:"release",senderId:e.senderId,token:n.token})}catch(e){a.error(`[${d}] broadcast driver: release broadcast failed during destroy`,e)}}else"remote-held"===e.status.kind&&l(e.status);e.status={kind:"idle"};for(let e=0;e<s.length;e++)s[e].abort(n(s[e].token));if(null!==e.unsubscribe){try{e.unsubscribe()}catch(e){a.error(`[${d}] broadcast driver: unsubscribe threw`,e)}e.unsubscribe=null}try{e.channel.close()}catch(e){a.error(`[${d}] broadcast driver: channel.close threw`,e)}}export{b as abandonPendingAnnounce,h as abandonPendingForce,y as drainOnDestroy,k as enterHolding,g as enterRemoteHeld,f as handleAnnounce,v as handleForce,m as handleHeartbeat,I as handleMessage,p as handleReject,$ as handleRelease,u as handleRemoteDead,w as pumpNextWaiter,T as removeWaiter,c as revokeHolding,A as startAnnounceCampaign,F as startForceCampaign};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BroadcastDriver:基于 BroadcastChannel 的跨 Tab 互斥锁
|
|
3
|
+
*
|
|
4
|
+
* 适用场景(由 pickDriver 决定):
|
|
5
|
+
* - 浏览器环境但不支持 `navigator.locks`(Safari < 15.4 / 老版 Firefox)
|
|
6
|
+
* - 支持 `BroadcastChannel`(否则继续降级到 StorageDriver)
|
|
7
|
+
*
|
|
8
|
+
* 本文件是工厂聚合层,只负责:
|
|
9
|
+
* - 前置依赖校验(getChannel / id 必须提供)
|
|
10
|
+
* - 构造 state 容器并订阅 channel
|
|
11
|
+
* - 暴露 acquire / destroy
|
|
12
|
+
*
|
|
13
|
+
* 协议细节、状态机、消息处理、竞选流程、drain 逻辑分别放在:
|
|
14
|
+
* - `./broadcast-protocol`:消息类型 + 常量 + 校验 + 仲裁工具
|
|
15
|
+
* - `./broadcast-state`:状态机 + 消息处理 + 竞选流程 + drainOnDestroy
|
|
16
|
+
*
|
|
17
|
+
* 前置条件:`deps.getChannel` 必须提供且返回非 null(由 pickDriver 保证);否则构造期抛错
|
|
18
|
+
*/
|
|
19
|
+
import type { LockDriverContext, LockDriverHandle } from '../types';
|
|
20
|
+
import { type BroadcastDriverState, type Waiter } from './broadcast-state';
|
|
21
|
+
import type { LockDriver, LockDriverDeps } from './types';
|
|
22
|
+
/**
|
|
23
|
+
* 构造一个 waiter 并绑定 signal / timeout 的 abort 生命周期
|
|
24
|
+
*
|
|
25
|
+
* 职责:
|
|
26
|
+
* - `settled` 标志保证 resolve / reject / abort 互斥,只有第一次生效
|
|
27
|
+
* - `cleanup`:清理 timeout + signal listener
|
|
28
|
+
* - `abort`:把 waiter 从队列 / pending 中移除后,走 reject 兜底
|
|
29
|
+
*/
|
|
30
|
+
declare function buildWaiter(ctx: LockDriverContext, state: BroadcastDriverState, resolve: (handle: LockDriverHandle) => void, reject: (error: Error) => void): Waiter;
|
|
31
|
+
declare function acquireBroadcastLock(state: BroadcastDriverState, ctx: LockDriverContext): Promise<LockDriverHandle>;
|
|
32
|
+
/**
|
|
33
|
+
* 创建 BroadcastDriver 实例
|
|
34
|
+
*/
|
|
35
|
+
declare function createBroadcastDriver(deps: LockDriverDeps): LockDriver;
|
|
36
|
+
export { acquireBroadcastLock, buildWaiter, createBroadcastDriver };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{throwError as e}from"../../throw-error/index.js";import{isFunction as t,isNumber as n,isString as r}from"../../utils/index.js";import{ERROR_FN_NAME as o}from"../constants.js";import{LockAbortedError as i,LockTimeoutError as u}from"../errors/index.js";import{genId as l}from"./broadcast-protocol.js";import{drainOnDestroy as a,handleMessage as s,pumpNextWaiter as d,removeWaiter as c,startAnnounceCampaign as m,startForceCampaign as b}from"./broadcast-state.js";function g(e,t,r,l){let a=!1,s=null;function m(){null!==s&&(clearTimeout(s),s=null),e.signal.removeEventListener("abort",g)}let b={token:e.token,resolve:e=>{a||(a=!0,m(),r(e))},reject:e=>{a||(a=!0,m(),l(e))},abort:e=>{if(!a){if(c(t.waiters,b),null!==t.pendingAnnounce&&t.pendingAnnounce.waiter===b){let e=t.pendingAnnounce;e.abandoned=!0,null!==e.timer&&(clearTimeout(e.timer),e.timer=null),t.pendingAnnounce=null}if(null!==t.pendingForce&&t.pendingForce.waiter===b){let e=t.pendingForce;e.abandoned=!0,null!==e.timer&&(clearTimeout(e.timer),e.timer=null),t.pendingForce=null}b.reject(e),d(t)}}};function g(){b.abort(new i(`[@cmtlyt/lingshu-toolkit#${o}]: acquire aborted (token=${e.token})`))}if(e.signal.aborted)return queueMicrotask(()=>g()),b;if(e.signal.addEventListener("abort",g,{once:!0}),n(e.acquireTimeout)&&e.acquireTimeout>0){let t=e.acquireTimeout;s=setTimeout(()=>{b.abort(new u(`[@cmtlyt/lingshu-toolkit#${o}]: acquire timed out after ${t}ms (token=${e.token})`))},t)}return b}function p(e,t){return t.signal.aborted?Promise.reject(new i(`[@cmtlyt/lingshu-toolkit#${o}]: acquire aborted (token=${t.token})`)):e.destroyed?Promise.reject(new i(`[@cmtlyt/lingshu-toolkit#${o}]: broadcast driver has been destroyed (token=${t.token})`)):new Promise((n,r)=>{let o=g(t,e,n,r);if(t.force)return void b(e,o);if("idle"===e.status.kind&&null===e.pendingAnnounce&&null===e.pendingForce)return void m(e,o);e.waiters.push(o);let{name:i,logger:u}=e.deps;u.debug(`[${i}] broadcast driver: enqueue token=${t.token}, queue=${e.waiters.length}, status=${e.status.kind}`)})}function k(n){let{id:u,getChannel:d}=n;t(d)||e(o,"broadcast driver requires getChannel factory",TypeError),r(u)&&0!==u.length||e(o,"broadcast driver requires a non-empty id",TypeError);let c=d({id:u,channel:"custom"});null===c&&e(o,"broadcast driver getChannel returned null",TypeError);let m={deps:n,channel:c,senderId:l("sender"),status:{kind:"idle"},waiters:[],pendingAnnounce:null,pendingForce:null,destroyed:!1,unsubscribe:null};function b(e){return new i(`[@cmtlyt/lingshu-toolkit#${o}]: broadcast driver destroyed (token=${e})`)}return m.unsubscribe=c.subscribe(e=>s(m,e)),{acquire:e=>p(m,e),destroy:()=>{m.destroyed||(m.destroyed=!0,a(m,b))}}}export{p as acquireBroadcastLock,g as buildWaiter,k as createBroadcastDriver};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CustomDriver:包装用户注入的 `adapters.getLock` 为统一 `LockDriver` 接口
|
|
3
|
+
*
|
|
4
|
+
* 适用场景:`adapters.getLock` 存在时由 pickDriver 直接选中;`mode` 字段被忽略
|
|
5
|
+
* (对应 RFC.md「能力检测与降级」「CustomDriver」章节)
|
|
6
|
+
*
|
|
7
|
+
* 职责范围:
|
|
8
|
+
* - 透传 `name` / `token` / `force` / `source` / 超时 / 合并 signal 到用户工厂
|
|
9
|
+
* - 把 `acquireTimeout` 统一映射为 signal abort,让用户工厂只需监听 `ctx.signal`
|
|
10
|
+
* 即可同时响应"超时"与"外部取消"两条路径
|
|
11
|
+
* - 把用户返回的 `LockDriverHandle`(可能是 Promise)规范化为"同步 handle"返回给上层
|
|
12
|
+
* - `destroy` 不碰用户资源(用户 handle 由 actions 的 release 路径负责释放);仅
|
|
13
|
+
* 清理本 driver 内部持有的合并 controller / 订阅(当前无)
|
|
14
|
+
*
|
|
15
|
+
* 与其他 driver 的关键差异:
|
|
16
|
+
* - 不维护排队 / 心跳 / storage 订阅;完全信任用户实现的互斥语义
|
|
17
|
+
* - 不拒绝"用户工厂返回的 handle 缺失 onRevokedByDriver"的情况(Phase 5 状态机会
|
|
18
|
+
* 在 force / timeout 触发时自发广播,不强依赖 driver 上报)
|
|
19
|
+
*/
|
|
20
|
+
import type { LockDriver, LockDriverDeps } from './types';
|
|
21
|
+
/**
|
|
22
|
+
* 创建 CustomDriver 实例
|
|
23
|
+
*
|
|
24
|
+
* 前置条件:`deps.userGetLock` 必须已提供(由 pickDriver 保证);否则构造期抛错
|
|
25
|
+
*/
|
|
26
|
+
declare function createCustomLockDriver(deps: LockDriverDeps): LockDriver;
|
|
27
|
+
export { createCustomLockDriver };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{throwError as e}from"../../throw-error/index.js";import{isFunction as r,isNullOrUndef as t,isNumber as o,isPromiseLike as n}from"../../utils/index.js";import{ERROR_FN_NAME as i}from"../constants.js";import{LockAbortedError as u,LockTimeoutError as a}from"../errors/index.js";function s(s){let{name:c,logger:l,userGetLock:d}=s;r(d)||e(i,"custom driver requires adapters.getLock to be a function",TypeError);let m=!1;return{acquire:async function k(k){m&&e(i,"custom driver has been destroyed",u);let{signal:f,cleanup:v,getTimeoutFired:g}=function(e,r,t){let n=new AbortController,u=!1,s=null;if(e.aborted)return n.abort(e.reason),{signal:n.signal,cleanup:()=>void 0,getTimeoutFired:()=>u};function c(){n.abort(e.reason)}return e.addEventListener("abort",c,{once:!0}),o(r)&&r>0&&(s=setTimeout(()=>{u=!0,n.abort(new a(`[@cmtlyt/lingshu-toolkit#${i}]: acquire timed out after ${r}ms (token=${t})`))},r)),{signal:n.signal,cleanup:function(){null!==s&&(clearTimeout(s),s=null),e.removeEventListener("abort",c)},getTimeoutFired:()=>u}}(k.signal,k.acquireTimeout,k.token),$={name:c,token:k.token,force:k.force,acquireTimeout:k.acquireTimeout,holdTimeout:k.holdTimeout,signal:f};try{let o=d($),u=await Promise.resolve(o);var b=k.token;if(!(u&&r(u.release))){let r=t(u)?String(u):typeof u.release;e(i,`adapters.getLock must return an object with a "release" function, got ${r} (token=${b})`,TypeError)}return v(),l.debug(`[${c}] custom driver: grant token=${k.token}`),function(e,t,o){let{name:i,logger:u}=t,a=e.release,s=e.onRevokedByDriver,c={release:()=>{let r;try{r=a.call(e)}catch(e){u.error(`[${i}] custom driver: user release threw (token=${o})`,e);return}if(n(r))return Promise.resolve(r).catch(e=>{u.error(`[${i}] custom driver: user release rejected (token=${o})`,e)})}};return r(s)&&(c.onRevokedByDriver=s.bind(e)),c}(u,s,k.token)}catch(r){throw v(),g()&&e(i,`acquire timed out after ${String(k.acquireTimeout)}ms (token=${k.token})`,a,{cause:r}),k.signal.aborted&&e(i,`acquire aborted (token=${k.token})`,u,{cause:r}),l.error(`[${c}] custom driver: user getLock rejected (token=${k.token})`,r),r}},destroy:function(){m||(m=!0,l.debug(`[${c}] custom driver: destroy`))}}}export{s as createCustomLockDriver};
|