@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.
- package/README.md +10 -0
- package/dist/665.js +1 -0
- package/dist/893.js +1 -0
- package/dist/react/index.js +1 -205
- package/dist/react/use-boolean/index.d.ts +2 -1
- package/dist/react/use-boolean/index.js +1 -16
- package/dist/react/use-controllable-value/index.d.ts +3 -3
- package/dist/react/use-controllable-value/index.js +1 -32
- package/dist/react/use-counter/index.d.ts +2 -2
- package/dist/react/use-counter/index.js +1 -49
- package/dist/react/use-force-update/index.d.ts +2 -1
- package/dist/react/use-force-update/index.js +1 -6
- package/dist/react/use-mount/index.d.ts +2 -1
- package/dist/react/use-mount/index.js +1 -16
- package/dist/react/use-ref-state/index.d.ts +3 -2
- package/dist/react/use-ref-state/index.js +1 -33
- package/dist/react/use-storage/index.d.ts +2 -1
- package/dist/react/use-storage/index.js +1 -15
- package/dist/react/use-title/index.d.ts +2 -2
- package/dist/react/use-title/index.js +1 -24
- package/dist/react/use-toggle/index.d.ts +4 -4
- package/dist/react/use-toggle/index.js +1 -26
- package/dist/react/use-valid-data/index.d.ts +5 -4
- package/dist/react/use-valid-data/index.js +1 -14
- package/dist/shared/allx/index.d.ts +2 -1
- package/dist/shared/allx/index.js +1 -44
- package/dist/shared/allx/types.d.ts +6 -0
- package/dist/shared/allx/utils.d.ts +9 -7
- package/dist/shared/allx/utils.js +1 -94
- package/dist/shared/animation/index.d.ts +3 -2
- package/dist/shared/animation/index.js +1 -77
- package/dist/shared/animation/types.d.ts +8 -0
- package/dist/shared/animation/utils.d.ts +3 -10
- package/dist/shared/animation/utils.js +1 -134
- package/dist/shared/api-controller/create-api.js +1 -79
- package/dist/shared/api-controller/index.js +1 -3
- package/dist/shared/api-controller/request.js +1 -66
- package/dist/shared/api-controller/types.d.ts +26 -27
- package/dist/shared/api-controller/utils.d.ts +6 -15
- package/dist/shared/api-controller/utils.js +1 -96
- package/dist/shared/condition-merge/index.d.ts +6 -6
- package/dist/shared/condition-merge/index.js +1 -30
- package/dist/shared/create-storage-handler/index.d.ts +4 -3
- package/dist/shared/create-storage-handler/index.js +1 -68
- package/dist/shared/data-handler/index.d.ts +4 -3
- package/dist/shared/data-handler/index.js +1 -77
- package/dist/shared/data-handler/tools.d.ts +6 -23
- package/dist/shared/data-handler/tools.js +1 -48
- package/dist/shared/data-handler/types.d.ts +20 -2
- package/dist/shared/data-mixed-manager/constants.js +1 -9
- package/dist/shared/data-mixed-manager/index.js +1 -226
- package/dist/shared/data-mixed-manager/types.d.ts +1 -2
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -957
- 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__/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/logger/index.d.ts +2 -2
- package/dist/shared/logger/index.js +1 -10
- package/dist/shared/priority-queue/index.d.ts +45 -0
- package/dist/shared/priority-queue/index.js +1 -0
- package/dist/shared/priority-queue/types.d.ts +10 -0
- package/dist/shared/priority-queue/types.js +0 -0
- package/dist/shared/priority-queue/utils.d.ts +7 -0
- package/dist/shared/priority-queue/utils.js +1 -0
- package/dist/shared/throw-error/index.d.ts +11 -3
- package/dist/shared/throw-error/index.js +1 -10
- package/dist/shared/try-call/index.d.ts +3 -3
- package/dist/shared/try-call/index.js +1 -59
- package/dist/shared/types/index.js +1 -2
- package/dist/shared/types/pack.d.ts +2 -2
- package/dist/shared/types/pack.js +1 -1
- package/dist/shared/utils/base.d.ts +1 -1
- package/dist/shared/utils/base.js +1 -6
- package/dist/shared/utils/index.js +1 -2
- package/dist/shared/utils/verify.d.ts +1 -1
- package/dist/shared/utils/verify.js +1 -67
- package/dist/shared/with-resolvers/index.d.ts +5 -3
- package/dist/shared/with-resolvers/index.js +1 -15
- package/dist/vue/index.js +1 -29
- package/dist/vue/use-title/index.d.ts +2 -2
- package/dist/vue/use-title/index.js +1 -29
- package/package.json +27 -27
- package/dist/247.js +0 -66
- package/dist/707.js +0 -142
- package/dist/react/use-force-update/index.test.d.ts +0 -1
- package/dist/react/use-mount/index.test.d.ts +0 -1
- package/dist/react/use-ref-state/index.test.d.ts +0 -1
- package/dist/react/use-storage/index.test.d.ts +0 -1
- package/dist/react/use-title/index.test.d.ts +0 -1
- package/dist/react/use-toggle/index.test.d.ts +0 -1
- package/dist/react/use-valid-data/index.test.d.ts +0 -1
- package/dist/shared/allx/__test__/allsettled.test.d.ts +0 -1
- package/dist/shared/allx/__test__/basic.test.d.ts +0 -1
- package/dist/shared/allx/__test__/circular-dependency.test.d.ts +0 -1
- package/dist/shared/allx/__test__/dependency.test.d.ts +0 -1
- package/dist/shared/allx/__test__/edge-cases.test.d.ts +0 -1
- package/dist/shared/allx/__test__/error-handling.test.d.ts +0 -1
- package/dist/shared/allx/__test__/execution-order.test.d.ts +0 -1
- package/dist/shared/allx/__test__/falsy-values.test.d.ts +0 -1
- package/dist/shared/allx/__test__/performance.test.d.ts +0 -1
- package/dist/shared/allx/__test__/type-checking.test.d.ts +0 -1
- package/dist/shared/allx/__test__/use-cases.test.d.ts +0 -1
- package/dist/shared/animation/__test__/animation-pause-resume.test.d.ts +0 -1
- package/dist/shared/animation/__test__/animation.test.d.ts +0 -1
- package/dist/shared/animation/__test__/step-animation.test.d.ts +0 -1
- package/dist/shared/animation/__test__/utils.test.d.ts +0 -1
- package/dist/shared/api-controller/__test__/index.browser.test.d.ts +0 -1
- package/dist/shared/api-controller/__test__/index.node.test.d.ts +0 -1
- package/dist/shared/condition-merge/index.test-d.js +0 -108
- package/dist/shared/condition-merge/index.test.d.ts +0 -1
- package/dist/shared/create-storage-handler/index.browser.test.d.ts +0 -1
- package/dist/shared/create-storage-handler/index.test.d.ts +0 -1
- package/dist/shared/data-handler/index.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/basic.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/build-options.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/constructor-options.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/data-management.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/edge-cases.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/events.browser.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/events.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/fixed-slots.test.d.ts +0 -1
- package/dist/shared/data-mixed-manager/__test__/insert-mode.test.d.ts +0 -1
- package/dist/shared/throw-error/index.test.d.ts +0 -1
- package/dist/shared/try-call/index.test.d.ts +0 -1
- package/dist/shared/utils/__test__/base.test.d.ts +0 -1
- package/dist/shared/utils/__test__/verify.test.d.ts +0 -1
- package/dist/shared/with-resolvers/index.test.d.ts +0 -1
- package/dist/test/utils.d.ts +0 -13
- package/dist/vue/use-title/index.test.d.ts +0 -1
- /package/dist/{react/use-boolean/index.test.d.ts → shared/lock-data/__test__/index.test-d.d.ts} +0 -0
- /package/dist/{react/use-controllable-value/index.test.d.ts → shared/lock-data/__test__/integration/entry.test-d.d.ts} +0 -0
- /package/dist/{react/use-counter/index.test.d.ts → shared/lock-data/__test__/playground.d.ts} +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lockData 主入口:组装 Registry / Adapters / Driver / Authority / Actions / ReadonlyView
|
|
3
|
+
*
|
|
4
|
+
* 对应 RFC.md「架构分层」「InstanceRegistry」「能力检测与降级」章节。
|
|
5
|
+
*
|
|
6
|
+
* 流程总览(wrapper 方案 + 单参数 API):
|
|
7
|
+
* lockData(options)
|
|
8
|
+
* ├─ 顶层数组运行时拒绝(assertNotTopLevelArray,类型层已禁止;防擦除)
|
|
9
|
+
* ├─ 参数校验(id / getValue / syncMode / listeners 等结构层)
|
|
10
|
+
* ├─ 分派:id 存在 → defaultRegistry.getOrCreateEntry(id, options, factory)
|
|
11
|
+
* │ id 缺失 → 直接执行 factory(无 Registry 跟踪)
|
|
12
|
+
* │
|
|
13
|
+
* ├─ factory 内部(entryFactory):
|
|
14
|
+
* │ ├─ pickDefaultAdapters(options.adapters)
|
|
15
|
+
* │ ├─ prepareEntryData(id, options) —— 同步抛错走 LockDisposedError;
|
|
16
|
+
* │ │ 异步返回 { firstValue, dataReadyPromise }(首值 resolve 后由
|
|
17
|
+
* │ │ `applyRemote` 写入 dataRef.current)
|
|
18
|
+
* │ ├─ pickDriver({ adapters, options, id })
|
|
19
|
+
* │ ├─ 构造 Entry 骨架(dataRef = { current: firstValue };authority: null)
|
|
20
|
+
* │ ├─ 若 syncMode='storage-authority' 且 id 存在 → 构造 StorageAuthority:
|
|
21
|
+
* │ │ ├─ host = entry(提供 dataRef / applyRemote / rev / lastAppliedRev / epoch)
|
|
22
|
+
* │ │ ├─ emitSync / emitCommit → fanoutSync / fanoutCommit
|
|
23
|
+
* │ │ └─ registerTeardown(authority.dispose) + 发起 authority.init()
|
|
24
|
+
* │ └─ 把 authority.init() 合成进 dataReadyPromise
|
|
25
|
+
* │
|
|
26
|
+
* ├─ createActions({ entry, options, releaseFromRegistry })
|
|
27
|
+
* ├─ createReadonlyView(entry.dataRef) —— wrapper Proxy;trap 重定向到 dataRef.current
|
|
28
|
+
* └─ 返回:dataReadyPromise === null ? [view, actions] : Promise<[view, actions]>
|
|
29
|
+
*
|
|
30
|
+
* 职责边界:
|
|
31
|
+
* - 参数校验只做"结构层"(类型 / 非空);语义合法性(如 timeout < 0)由下游模块负责
|
|
32
|
+
* - Entry 构造期的部分字段(authority)是"一次性 readonly":仅在 factory 内写入一次,
|
|
33
|
+
* Entry 对外暴露后字段视为 frozen;用 mutable 视图收敛到 factory 闭包内
|
|
34
|
+
* - dataRef.current 在异步 resolve / commit / applyRemote 时由内部重新赋值;
|
|
35
|
+
* wrapper Proxy view 自动看到最新值
|
|
36
|
+
*/
|
|
37
|
+
import { type ResolvedAdapters } from '../adapters/index';
|
|
38
|
+
import type { CommitSource, LockDataMutation, LockDataOptions, LockDataResult, SyncSource } from '../types';
|
|
39
|
+
import { type Entry, type EntryFactory } from './registry';
|
|
40
|
+
/**
|
|
41
|
+
* @internal 仅供测试使用,不通过 index.ts 公开导出
|
|
42
|
+
*
|
|
43
|
+
* 重置进程级 Registry,用于测试间隔离(模拟"新 Tab / 新进程"场景)
|
|
44
|
+
*
|
|
45
|
+
* 注意:不会清理已有 Entry 的 teardown;调用者需自行确保没有活跃的 Entry 引用
|
|
46
|
+
*/
|
|
47
|
+
declare function __resetDefaultRegistry(): void;
|
|
48
|
+
/**
|
|
49
|
+
* 构造期的可变 Entry 视图
|
|
50
|
+
*
|
|
51
|
+
* 在 factory 闭包内把全部字段视为可写,便于 `onStateChange` 写回状态 /
|
|
52
|
+
* authority 构造后回写 authority 引用;返回给调用方后视为 frozen
|
|
53
|
+
*/
|
|
54
|
+
type MutableEntry<T extends object> = {
|
|
55
|
+
-readonly [K in keyof Entry<T>]: Entry<T>[K];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* 注册 authority 自 emit 回调的 teardown 守卫容器
|
|
59
|
+
*
|
|
60
|
+
* 生命周期:Entry 销毁时置 `disposed=true`,fanout 回调即使被滞后触发也直接 no-op
|
|
61
|
+
*/
|
|
62
|
+
interface FanoutGuard {
|
|
63
|
+
disposed: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* emitCommit 事件体;由 StorageAuthority 在 onCommitSuccess 内部构造并透传(已包含 rev)
|
|
67
|
+
*/
|
|
68
|
+
interface AuthorityCommitEvent<T> {
|
|
69
|
+
readonly source: CommitSource;
|
|
70
|
+
readonly token: string;
|
|
71
|
+
readonly rev: number;
|
|
72
|
+
readonly mutations: readonly LockDataMutation[];
|
|
73
|
+
readonly snapshot: T;
|
|
74
|
+
}
|
|
75
|
+
/** emitSync 事件体;由 StorageAuthority 在 applyAuthorityIfNewer 内部构造并透传 */
|
|
76
|
+
interface AuthoritySyncEvent<T> {
|
|
77
|
+
readonly source: SyncSource;
|
|
78
|
+
readonly rev: number;
|
|
79
|
+
readonly snapshot: T;
|
|
80
|
+
}
|
|
81
|
+
declare function buildEmitCommit<T extends object>(entry: Entry<T>, guard: FanoutGuard): (event: AuthorityCommitEvent<T>) => void;
|
|
82
|
+
declare function buildEmitSync<T extends object>(entry: Entry<T>, guard: FanoutGuard): (event: AuthoritySyncEvent<T>) => void;
|
|
83
|
+
/**
|
|
84
|
+
* 构造 StorageAuthority 并写回 entry.authority;返回 init Promise
|
|
85
|
+
*
|
|
86
|
+
* 仅在 `syncMode === 'storage-authority' && id` 时调用;authority 构造失败时
|
|
87
|
+
* 走 logger.warn(RFC「权威副本不可用 → 退化为同进程共享」),返回 null Promise
|
|
88
|
+
*
|
|
89
|
+
* wrapper 方案差异:
|
|
90
|
+
* - 不再注入 `applySnapshot` 钩子(authority 通过 `host.applyRemote(next)` 完成原子覆写)
|
|
91
|
+
* - 不再注入 `clone` 函数(authority 内部走 JSON 拷贝隔离)
|
|
92
|
+
*/
|
|
93
|
+
declare function attachAuthority<T extends object>(mutableEntry: MutableEntry<T>, options: LockDataOptions<T>, adapters: ResolvedAdapters<T>, id: string): Promise<void> | null;
|
|
94
|
+
/**
|
|
95
|
+
* 合成 dataReadyPromise + authority.init() 为统一的就绪 Promise
|
|
96
|
+
*
|
|
97
|
+
* 两个 Promise 至少一个非 null 时返回合成结果;都为 null 时返回 null
|
|
98
|
+
*/
|
|
99
|
+
declare function mergeReadyPromises(dataReady: Promise<void> | null, authorityReady: Promise<void> | null): Promise<void> | null;
|
|
100
|
+
/**
|
|
101
|
+
* lockData 主入口(单参数 + getValue 必传)
|
|
102
|
+
*
|
|
103
|
+
* 返回值类型:
|
|
104
|
+
* - getValue 同步返回 + 未启用 authority → `readonly [T, LockDataActions<T>]`
|
|
105
|
+
* - getValue 返回 Promise 或 syncMode='storage-authority' → `Promise<readonly [T, LockDataActions<T>]>`
|
|
106
|
+
*
|
|
107
|
+
* 初始化失败(getValue reject / getValue 同步抛错)时:
|
|
108
|
+
* - 同步路径:抛 `LockDisposedError`(getValue 同步抛错时 prepareEntryData 直接向上抛)
|
|
109
|
+
* - 异步路径:返回的 Promise reject `LockDisposedError`(cause 携带原始错误)
|
|
110
|
+
*
|
|
111
|
+
* id 冲突:同 id 多次调用 lockData 复用同一份 Entry(dataRef / driver / adapters / authority 共享),
|
|
112
|
+
* 自第二次起 getValue 不被重新执行(首值由首次调用产出);非 listeners 字段冲突走 logger.warn
|
|
113
|
+
*/
|
|
114
|
+
declare function lockData<T extends object>(options: LockDataOptions<T>): LockDataResult<T> | Promise<LockDataResult<T>>;
|
|
115
|
+
/**
|
|
116
|
+
* 无 id 路径:直接执行 factory;teardowns 在 dispose 时运行(Registry 不介入)
|
|
117
|
+
*
|
|
118
|
+
* 入参拆分:
|
|
119
|
+
* - 第一个参数 `'__local__'` 写入 `Entry.id`(展示用占位),用于日志、错误消息等稳定文本输出
|
|
120
|
+
* - 第二个参数 `undefined` 写入 `Entry.lockId`(语义判定用真实 id);
|
|
121
|
+
* 下游 `pickDriver` / `attachAuthority` / driver acquire `name` 都以此识别"无真实 id"分支:
|
|
122
|
+
* - pickDriver 看到 undefined → LocalLockDriver(mode 字段被忽略)
|
|
123
|
+
* - syncMode='storage-authority' 不会启用 authority
|
|
124
|
+
* - driver acquire 的 `name` 走 `${LOCK_PREFIX}:__local__` 占位
|
|
125
|
+
*
|
|
126
|
+
* 详见 `src/shared/lock-data/fixes/standalone-id-leak.md`
|
|
127
|
+
*/
|
|
128
|
+
declare function acquireStandalone<T extends object>(options: LockDataOptions<T>, factory: EntryFactory<T>): {
|
|
129
|
+
entry: Entry<T>;
|
|
130
|
+
releaseFromRegistry: () => void;
|
|
131
|
+
};
|
|
132
|
+
export type { FanoutGuard, MutableEntry };
|
|
133
|
+
export { __resetDefaultRegistry, acquireStandalone, attachAuthority, buildEmitCommit, buildEmitSync, lockData, mergeReadyPromises, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isObject as e,isString as t}from"../../utils/index.js";import{pickDefaultAdapters as r}from"../adapters/index.js";import{createStorageAuthority as o}from"../authority/index.js";import{DEFAULT_SESSION_PROBE_TIMEOUT as n}from"../constants.js";import{pickDriver as s}from"../drivers/index.js";import{cloneByJson as i}from"../utils/json-safe.js";import{createActions as a}from"./actions.js";import{fanoutCommit as l,fanoutSync as d}from"./fanout.js";import{createReadonlyView as u}from"./readonly-view.js";import{createInstanceRegistry as m,prepareEntryData as c}from"./registry.js";let g=null;function y(){g=null}function p(e,t){return r=>{t.disposed||l(e.listenersSet,r,e.adapters.logger)}}function h(e,t){return r=>{t.disposed||d(e.listenersSet,r,e.adapters.logger)}}function f(e,t,r,s){let i="persistent"===t.persistence?"persistent":"session",a=r.getAuthority({id:s}),l=r.getChannel({id:s,channel:"session"}),d=r.getSessionStore({id:s});if(null===a&&null===l&&null===d)return r.logger.warn(`[lockData] syncMode='storage-authority' requested on id=${s} but no authority/channel/sessionStore adapter is available; fallback to in-process sharing only`),null;let u={disposed:!1},m=o({host:e,authority:a,channel:l,sessionStore:d,persistence:i,sessionProbeTimeout:t.sessionProbeTimeout??n,logger:r.logger,emitSync:h(e,u),emitCommit:p(e,u)});return e.authority=m,e.registerTeardown(()=>m.dispose()),e.registerTeardown(()=>{u.disposed=!0}),m.init().then(()=>{},e=>{r.logger.warn(`[lockData] StorageAuthority.init failed on id=${s}`,e)})}function v(e,t){return null!==e&&null!==t?Promise.all([e,t]).then(()=>void 0):e??t}function w(o){var n,l,d,y,p;let h,w,P=function(e){let{id:r}=e;return t(r)&&r.length>0?r:void 0}(o),R=(t,o,n,a)=>{var l,d;let u=r(n.adapters),m=c(t,n),g=s({adapters:u,options:n,id:o}),y=new Set;e(n.listeners)&&y.add(n.listeners);let p={current:m.firstValue},h=e=>{p.current=i(e)},w={id:t,lockId:o,dataRef:p,driver:g,adapters:u,authority:null,listenersSet:y,initOptions:Object.freeze({timeout:n.timeout,mode:n.mode,syncMode:n.syncMode,persistence:n.persistence,sessionProbeTimeout:n.sessionProbeTimeout}),dataReadyPromise:null,registerTeardown:a.registerTeardown,refCount:1,rev:0,lastAppliedRev:0,epoch:null,applyRemote:h},j=(l=h,null===(d=m.dataReadyPromise)?null:d.then(e=>{l(e)})),P="storage-authority"==("storage-authority"===n.syncMode?"storage-authority":"none")&&void 0!==o?f(w,n,u,o):null;return w.dataReadyPromise=v(j,P),w},{entry:S,releaseFromRegistry:b}=void 0===P?j(o,R):(n=P,l=o,d=R,{entry:(h=(null===g&&(g=m()),g)).getOrCreateEntry(n,l,d),releaseFromRegistry:()=>{h.releaseEntry(n,l.listeners)}}),T=a({entry:S,options:o,releaseFromRegistry:b}),k=u(S.dataRef);return y=S,w=[k,p=T],null===y.dataReadyPromise?w:y.dataReadyPromise.then(()=>w,e=>{throw p.dispose(),e})}function j(e,t){let r=[],o={value:!0},n=t("__local__",void 0,e,{registerTeardown:e=>{o.value&&r.push(e)}});return{entry:n,releaseFromRegistry:()=>{if(o.value){o.value=!1;for(let e=r.length-1;e>=0;e--)try{r[e]()}catch(e){n.adapters.logger.warn("[lockData] standalone teardown threw",e)}r.length=0;try{n.driver.destroy()}catch(e){n.adapters.logger.warn("[lockData] standalone driver.destroy threw",e)}}}}}export{y as __resetDefaultRegistry,j as acquireStandalone,f as attachAuthority,p as buildEmitCommit,h as buildEmitSync,w as lockData,v as mergeReadyPromises};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* listeners fanout:把 driver / authority / actions 产生的事件分发给 Entry 的全部 listener
|
|
3
|
+
*
|
|
4
|
+
* 对应 RFC.md L666-667「listeners 不冲突 / listener 异常隔离」契约:
|
|
5
|
+
* - 每个实例的 `listeners` 独立保存在 `Entry.listenersSet` 中
|
|
6
|
+
* - 事件触发时遍历 Set 向每个 listener 的对应 hook 分发
|
|
7
|
+
* - 单个 listener 抛错(同步 throw / 异步 Promise reject)通过 logger.error
|
|
8
|
+
* 统一吞掉 + 记录,继续向剩余 listener 分发,不阻断 actions 状态机
|
|
9
|
+
*
|
|
10
|
+
* 设计边界:
|
|
11
|
+
* - 本模块**不产生**事件:事件对象由上游(actions / authority)构造好传入
|
|
12
|
+
* - 本模块**不管理订阅**:订阅通过 `entry.listenersSet.add/delete` 操作,
|
|
13
|
+
* Registry 的 `releaseEntry` / `getOrCreateEntry` 已负责 Set 的增删
|
|
14
|
+
* - listener 未提供对应 hook 时跳过(不调用 undefined)
|
|
15
|
+
*/
|
|
16
|
+
import type { ResolvedLoggerAdapter } from '../adapters/logger';
|
|
17
|
+
import type { CommitEvent, LockDataListeners, LockStateChangeEvent, RevokeEvent, SyncEvent } from '../types';
|
|
18
|
+
/**
|
|
19
|
+
* fanoutLockStateChange:状态机流转事件(idle → acquiring → holding → ...)
|
|
20
|
+
*
|
|
21
|
+
* 触发时机:Actions 状态机每次状态切换(见 RFC L933「每一步状态流转都通过
|
|
22
|
+
* listenersFanout.onLockStateChange(event) 分发到所有实例的 listeners」)
|
|
23
|
+
*/
|
|
24
|
+
declare function fanoutLockStateChange<T extends object>(listeners: Iterable<LockDataListeners<T>>, event: LockStateChangeEvent, logger: ResolvedLoggerAdapter): void;
|
|
25
|
+
/**
|
|
26
|
+
* fanoutRevoked:持有锁被 driver 驱逐 / timeout / dispose 主动释放时触发
|
|
27
|
+
*/
|
|
28
|
+
declare function fanoutRevoked<T extends object>(listeners: Iterable<LockDataListeners<T>>, event: RevokeEvent, logger: ResolvedLoggerAdapter): void;
|
|
29
|
+
/**
|
|
30
|
+
* fanoutCommit:commit 成功时触发(RFC L1201 onCommitSuccess 写路径)
|
|
31
|
+
*
|
|
32
|
+
* 事件中的 snapshot 必须是**已 clone 的独立副本**,由调用方保证
|
|
33
|
+
* (StorageAuthority.onCommitSuccess 已在调用处 clone)
|
|
34
|
+
*/
|
|
35
|
+
declare function fanoutCommit<T extends object>(listeners: Iterable<LockDataListeners<T>>, event: CommitEvent<T>, logger: ResolvedLoggerAdapter): void;
|
|
36
|
+
/**
|
|
37
|
+
* fanoutSync:authority 拉到新快照时触发(RFC L1214)
|
|
38
|
+
*
|
|
39
|
+
* 来源包括:pull-on-acquire / storage-event / pageshow / visibilitychange
|
|
40
|
+
*/
|
|
41
|
+
declare function fanoutSync<T extends object>(listeners: Iterable<LockDataListeners<T>>, event: SyncEvent<T>, logger: ResolvedLoggerAdapter): void;
|
|
42
|
+
export { fanoutCommit, fanoutLockStateChange, fanoutRevoked, fanoutSync };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isFunction as o,isObject as n}from"../../utils/index.js";function t(t,e,r,c,i){for(let a of t){let t,f=r(a);if(f){try{t=f(c)}catch(o){i.error(`[lockData] listener threw (${e})`,o);continue}n(t)&&"then"in t&&o(t.then)&&Promise.resolve(t).catch(o=>{i.error(`[lockData] listener threw (${e})`,o)})}}}function e(o,n,e){t(o,"onLockStateChange",o=>o.onLockStateChange,n,e)}function r(o,n,e){t(o,"onRevoked",o=>o.onRevoked,n,e)}function c(o,n,e){t(o,"onCommit",o=>o.onCommit,n,e)}function i(o,n,e){t(o,"onSync",o=>o.onSync,n,e)}export{c as fanoutCommit,e as fanoutLockStateChange,r as fanoutRevoked,i as fanoutSync};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 深只读代理:用户从 lockData 拿到的第一个返回值
|
|
3
|
+
*
|
|
4
|
+
* 实现要点(wrapper Proxy 方案,对应 RFC.md「ReadonlyView<T>」+「引用稳定契约」章节):
|
|
5
|
+
*
|
|
6
|
+
* - **wrapper Proxy**:顶层 view 是 `new Proxy(dataRef, ROOT_HANDLER)`,target 为
|
|
7
|
+
* 稳定 `dataRef = { current: T }` 引用;handler 把所有 trap 重定向到 `dataRef.current`
|
|
8
|
+
* - 引用稳定:`dataRef` 引用在 Entry 生命周期内永不变更
|
|
9
|
+
* - 跟随重新赋值:commit / `applyRemote` / 异步 getValue resolve 时 `dataRef.current` 重新赋值,
|
|
10
|
+
* view 上后续读取自动看到最新值,无需重建 Proxy
|
|
11
|
+
*
|
|
12
|
+
* - **嵌套递归**:get 到对象类型的属性时,惰性创建嵌套 readonly Proxy
|
|
13
|
+
* - 嵌套节点直接代理 `T` 类型的子对象(不需要 wrapper 间接层)
|
|
14
|
+
* - WeakMap<object, Proxy> 缓存:同一子对象多次访问拿到同一代理,引用比较有效
|
|
15
|
+
*
|
|
16
|
+
* - **写拦截**:set / deleteProperty / defineProperty / setPrototypeOf 统一抛 `ReadonlyMutationError`
|
|
17
|
+
*
|
|
18
|
+
* - **JSON-safe 契约**:本期 lockData 强制数据为 JSON 安全(getValue / replace 入口由
|
|
19
|
+
* `assertJsonSafe` 校验),故 readonly-view 不再处理 Set / Map / Date / class instance
|
|
20
|
+
* 等非 JSON 类型;仅需处理 plain object / array
|
|
21
|
+
*
|
|
22
|
+
* - **顶层数组禁止**:`dataRef.current` 在类型层(`LockDataValueShape<T>`)+ 运行时
|
|
23
|
+
* (`assertNotTopLevelArray`)双重排除顶层数组,wrapper 方案下 `Object.keys` /
|
|
24
|
+
* `JSON.stringify` 等不变量冲突自然消失
|
|
25
|
+
*
|
|
26
|
+
* - **判型一致性瑕疵**:`Object.isFrozen(view)` 返回 `false`(因为 target 是 wrapper
|
|
27
|
+
* 对象不是 frozen 的),但用户对 view 任何写入操作都会被 trap 拒绝抛 `ReadonlyMutationError`。
|
|
28
|
+
* 这是 wrapper 方案的轻微语义瑕疵,判定只读应通过约定("由 lockData 返回的 view 必只读")
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* wrapper 引用:每个 Entry 持有一个稳定的 `{ current: T }`,view Proxy 以此为 target
|
|
32
|
+
*
|
|
33
|
+
* - `current` 在 commit / `applyRemote` / 异步 getValue resolve 时重新赋值
|
|
34
|
+
* - view Proxy 通过 ROOT_HANDLER 把所有 trap 重定向到 `current`
|
|
35
|
+
*/
|
|
36
|
+
interface DataRef<T extends object> {
|
|
37
|
+
current: T;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 创建顶层 readonly view(wrapper Proxy)
|
|
41
|
+
*
|
|
42
|
+
* @param dataRef 稳定的 `{ current: T }` 引用;commit / applyRemote / 异步 resolve 时
|
|
43
|
+
* 重新赋值 `dataRef.current`,view 自动看到最新值
|
|
44
|
+
*
|
|
45
|
+
* 缓存:同一 `dataRef` 多次创建 view 拿到同一代理(基于 WeakMap<dataRef, Proxy>)
|
|
46
|
+
*/
|
|
47
|
+
declare function createReadonlyView<T extends object>(dataRef: DataRef<T>): T;
|
|
48
|
+
export type { DataRef };
|
|
49
|
+
export { createReadonlyView };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{throwError as e}from"../../throw-error/index.js";import{ERROR_FN_NAME as t}from"../constants.js";import{ReadonlyMutationError as r}from"../errors/index.js";let o=new WeakMap;function n(e){return"object"==typeof e&&null!==e}function f(){e(t,"cannot mutate readonly view",r)}let l={get(e,t,r){let o=Reflect.get(e,t,r);return n(o)?i(o):o},set:f,deleteProperty:f,defineProperty:f,setPrototypeOf:f};function i(e){let t=o.get(e);if(void 0!==t)return t;let r=new Proxy(e,l);return o.set(e,r),r}let c={get(e,t){let r=Reflect.get(e.current,t);return n(r)?i(r):r},set:f,deleteProperty:f,defineProperty:f,setPrototypeOf:f,has:(e,t)=>Reflect.has(e.current,t),ownKeys:e=>Reflect.ownKeys(e.current),getOwnPropertyDescriptor:(e,t)=>{let r=Reflect.getOwnPropertyDescriptor(e.current,t);if(void 0!==r)return{...r,writable:!1,configurable:!0}},getPrototypeOf:e=>Reflect.getPrototypeOf(e.current)};function u(e){let t=o.get(e);if(void 0!==t)return t;let r=new Proxy(e,c);return o.set(e,r),r}export{u as createReadonlyView};
|
|
@@ -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};
|