lockData
package version >0.6.0
shadcn any version
author: cmtlyt
update time: 2026/05/11 13:46:00
install
npm
npm i @cmtlyt/lingshu-toolkit
shadcn
npx shadcn@latest add https://cmtlyt.github.io/lingshu-toolkit/r/sharedLockData.json
usage
import { lockData } from '@cmtlyt/lingshu-toolkit/shared'
// or
import { lockData } from '@cmtlyt/lingshu-toolkit/shared/lock-data'
特性
- 事务式写入:通过
actions.update 传入 recipe 函数,recipe 内部可任意改写草稿,提交时整体成功或整体回滚
- 深只读视图:返回的
view 是深只读代理,任何直接写入都会抛 ReadonlyMutationError,保证读写边界清晰
- 跨模块共享:同一
id 的多次调用复用同一份数据,天然适合模块边界解耦
- 跨 Tab 同步:开启
syncMode: 'storage-authority' 后自动在同源 Tab 间同步,无需手动编排
- 声明式会话语义:
persistence: 'session' 下所有 Tab 关闭即重置;'persistent' 跨浏览器重启保留
- 可插拔适配器:锁驱动、权威副本、通道、会话存储、日志全部可注入,便于测试和定制
- 严格超时控制:抢锁 / 持锁超时独立控制,
NEVER_TIMEOUT 显式表达"永不超时"意图
- 类型安全:同步 / 异步分支在类型层明确区分,无
any,无隐式 Promise
基础用法
同步初始化
getValue 同步返回且 syncMode 为 'none'(默认)时,lockData 直接返回元组:
import { lockData } from '@cmtlyt/lingshu-toolkit/shared';
interface Counter {
count: number;
label: string;
}
const [view, actions] = lockData({ getValue: () => ({ count: 0, label: 'init' }) });
// view 是深只读视图
console.log(view.count); // 0
console.log(view.label); // 'init'
// 事务式写入
await actions.update((draft) => {
draft.count = 42;
});
console.log(view.count); // 42
// 用完主动销毁
await actions.dispose();
直接写 view 会抛错
import { lockData, ReadonlyMutationError } from '@cmtlyt/lingshu-toolkit/shared';
const [view, actions] = lockData({ getValue: () => ({ count: 0 }) });
// ❌ 运行时抛 ReadonlyMutationError
try {
(view as { count: number }).count = 999;
} catch (error) {
console.log(error instanceof ReadonlyMutationError); // true
}
// ✅ 必须通过 actions
await actions.update((draft) => {
draft.count = 999;
});
await actions.dispose();
整体替换
const [view, actions] = lockData({ getValue: () => ({ count: 0, label: 'init' }) });
// replace 相当于一次隐式事务:整体替换成新对象
await actions.replace({ count: 100, label: 'reset' });
console.log(view.count); // 100
console.log(view.label); // 'reset'
await actions.dispose();
异步初始化
getValue 返回 Promise 时,lockData 整体返回 Promise<元组>:
interface User {
id: string;
name: string;
}
const [view, actions] = await lockData({
getValue: () => fetch('/api/user').then((res) => res.json()) as Promise<User>,
});
console.log(view.name); // 从 /api/user 拉到的值
await actions.dispose();
跨模块共享(同 id 复用)
传入 id 后,同一进程内多次 lockData 会复用同一份数据:
// moduleA.ts
const [viewA, actionsA] = lockData({ id: 'shared-counter', getValue: () => ({ counter: 0 }) });
await actionsA.update((draft) => {
draft.counter = 5;
});
// moduleB.ts(同一进程)
const [viewB, actionsB] = lockData({ id: 'shared-counter', getValue: () => ({ counter: 0 }) });
// 注意:第二次调用的 getValue 不会被执行,viewB 读到的是已有数据
console.log(viewB.counter); // 5
// 两边都 dispose 后,底层数据才真正释放
await actionsA.dispose();
await actionsB.dispose();
高级用法
跨 Tab 同步(storage-authority)
开启 syncMode: 'storage-authority' 后,lockData 返回 Promise<元组>,同源 Tab 间自动同步:
const [view, actions] = await lockData({
id: 'cross-tab-counter',
syncMode: 'storage-authority',
getValue: () => ({ count: 0 }),
});
// Tab A 写入
await actions.update((draft) => {
draft.count += 1;
});
// Tab B(同 id + 同 syncMode)会通过 listeners.onSync 收到新值
监听数据变更
const [view, actions] = lockData({
getValue: () => ({ count: 0 }),
listeners: {
onCommit: (event) => {
// event.source: 'update' | 'replace'
// event.rev: 本次提交后的版本号(单调递增)
// event.snapshot: 提交成功时的最新快照
console.log(`commit #${event.rev} by ${event.source}`);
},
onRevoked: (event) => {
// 锁被撤销(抢占 / 超时 / dispose)时触发
console.log(`revoked: ${event.reason}`);
},
},
});
await actions.update((draft) => {
draft.count = 1;
}); // onCommit rev=1
await actions.update((draft) => {
draft.count = 2;
}); // onCommit rev=2
await actions.dispose();
手动持锁(多步事务)
getLock + 多次 update + release 可以把多个写操作合并到同一把锁内:
const [view, actions] = lockData({ getValue: () => ({ balance: 100, pending: 0 }) });
// 手动抢锁;获取后 isHolding === true
await actions.getLock();
// 持锁期间的多次 update 不会各自再抢锁
await actions.update((draft) => {
draft.balance -= 50;
});
await actions.update((draft) => {
draft.pending += 50;
});
// 手动释放
actions.release();
await actions.dispose();
超时控制
import { lockData, NEVER_TIMEOUT } from '@cmtlyt/lingshu-toolkit/shared';
const [view, actions] = lockData({
getValue: () => ({ count: 0 }),
timeout: 3000, // 默认 5000ms;本实例默认改为 3000ms
});
// 单次调用覆盖
await actions.update(
(draft) => {
draft.count = 1;
},
{ acquireTimeout: 1000, holdTimeout: 2000 },
);
// 永不超时(业务自控)
await actions.update(
(draft) => {
draft.count = 2;
},
{ acquireTimeout: NEVER_TIMEOUT },
);
await actions.dispose();
AbortSignal 控制
const controller = new AbortController();
const [view, actions] = lockData({ getValue: () => ({ count: 0 }), signal: controller.signal });
// abort 等价于自动 dispose
controller.abort();
// 之后调用 update / replace 会抛 LockDisposedError
强制抢占
// actions.update 的 callOptions.force 可以强制抢占当前持有者
await actions.update(
(draft) => {
draft.count = 999;
},
{ force: true },
);
错误处理
import {
lockData,
LockAbortedError,
LockDisposedError,
LockRevokedError,
LockTimeoutError,
ReadonlyMutationError,
InvalidOptionsError,
} from '@cmtlyt/lingshu-toolkit/shared';
const [view, actions] = lockData({ getValue: () => ({ count: 0 }) });
try {
await actions.update(
(draft) => {
draft.count = 1;
},
{ acquireTimeout: 100 },
);
} catch (error) {
if (error instanceof LockTimeoutError) {
// 抢锁超时 / 持锁超时
} else if (error instanceof LockAbortedError) {
// callOptions.signal 被 abort
} else if (error instanceof LockRevokedError) {
// 被更高优先级的 force 调用抢占
} else if (error instanceof LockDisposedError) {
// dispose 之后的操作
}
}
await actions.dispose();
API
lockData
单参数 API,返回类型由 options 的组合自动推断(条件类型):
function lockData<const O extends LockDataOptions<unknown>>(options: O): LockDataResolveReturn<O>
返回类型判定规则:
syncMode === 'storage-authority'(需配 id: string)→ Promise<LockDataTuple<T>>
getValue 返回 Promise<T> → Promise<LockDataTuple<T>>
- 其他 →
LockDataTuple<T>(同步返回)
LockDataOptions
返回值
返回深只读视图与 actions 的元组 [ReadonlyView<T>, LockDataActions<T>]:
getValue 同步返回且 syncMode 为 'none' → 同步返回元组
getValue 返回 Promise 或 syncMode: 'storage-authority' → 返回 Promise<元组>
TypeScript 条件类型会从 options 自动推断返回类型,调用方无需显式传任何泛型或手动断言。
初始化失败时:同步路径直接抛 LockDisposedError;异步路径返回的 Promise reject LockDisposedError(cause 字段携带原始错误)。
LockDataActions
interface LockDataActions<T extends object> {
readonly isHolding: boolean;
readonly token: string;
update: (recipe: (draft: T) => void | Promise<void>, callOptions?: ActionCallOptions) => void | Promise<void>;
replace: (next: T, callOptions?: ActionCallOptions) => void | Promise<void>;
snapshot: () => T;
getLock: (callOptions?: ActionCallOptions) => void | Promise<void>;
release: () => void;
dispose: () => Promise<void> | void;
}
ActionCallOptions
LockDataListeners
错误类
所有错误类都继承 Error,可用 instanceof 精准判别:
常量
注意事项
⚠️ 直接写 view 会抛错
view 是深只读代理,任何直接赋值(view.count = 1、delete view.count)都会抛 ReadonlyMutationError。写入必须经 actions.update 或 actions.replace。
⚠️ 同 id 的后续 getValue 不会被执行
相同 id 的第二次及之后调用 lockData 会复用已有数据,新传入的 getValue 不会被调用。如需重置数据,请先让所有持有者 dispose,或主动 replace。
⚠️ 条件类型决定同步 / 异步返回
getValue 同步返回且 syncMode 为 'none'(默认)→ 同步返回元组
getValue 返回 Promise 或 syncMode: 'storage-authority' → 返回 Promise<元组>
TypeScript 条件类型会从 options 自动推断返回类型,无需手动断言。
⚠️ recipe 内避免同步抛错
actions.update(recipe) 的 recipe 抛错时,所有 draft 上的改动会被整体回滚,update 返回的 Promise 会 reject。业务侧请在 recipe 外层统一 try/catch。
⚠️ dispose 后的 actions 不可复用
dispose 是终态:之后调用 update / replace / getLock / snapshot 都会抛 LockDisposedError。如需重用,请重新 lockData。
⚠️ storage-authority 的会话语义
syncMode: 'storage-authority' 配合 persistence: 'session'(默认)时,"所有同源 Tab 全部关闭"会触发数据重置;如需跨浏览器重启保留,请显式传 persistence: 'persistent'。
🔧 能力检测与降级
syncMode: 'storage-authority' 在不支持 localStorage 的环境(SSR / 隐私模式)下会降级为本地共享语义并触发一条 warn 日志
persistence: 'session' 在不支持 sessionStorage 的环境下会自动降级为 'persistent'
- 锁驱动按
web-locks → broadcast → storage 的顺序降级;强制指定 mode 时,若对应能力不可用则抛错