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