@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.
Files changed (90) hide show
  1. package/dist/665.js +1 -1
  2. package/dist/shared/index.d.ts +1 -0
  3. package/dist/shared/index.js +1 -1
  4. package/dist/shared/lock-data/__test__/_helpers/memory-adapters.d.ts +95 -0
  5. package/dist/shared/lock-data/__test__/_helpers/memory-adapters.js +1 -0
  6. package/dist/shared/lock-data/__test__/index.test-d.d.ts +1 -0
  7. package/dist/shared/lock-data/__test__/integration/entry.test-d.d.ts +1 -0
  8. package/dist/shared/lock-data/__test__/playground.d.ts +1 -0
  9. package/dist/shared/lock-data/__test__/playground.js +1 -0
  10. package/dist/shared/lock-data/adapters/authority.d.ts +40 -0
  11. package/dist/shared/lock-data/adapters/authority.js +1 -0
  12. package/dist/shared/lock-data/adapters/channel.d.ts +39 -0
  13. package/dist/shared/lock-data/adapters/channel.js +1 -0
  14. package/dist/shared/lock-data/adapters/index.d.ts +58 -0
  15. package/dist/shared/lock-data/adapters/index.js +1 -0
  16. package/dist/shared/lock-data/adapters/logger.d.ts +56 -0
  17. package/dist/shared/lock-data/adapters/logger.js +1 -0
  18. package/dist/shared/lock-data/adapters/session-store.d.ts +37 -0
  19. package/dist/shared/lock-data/adapters/session-store.js +1 -0
  20. package/dist/shared/lock-data/authority/epoch.d.ts +135 -0
  21. package/dist/shared/lock-data/authority/epoch.js +1 -0
  22. package/dist/shared/lock-data/authority/extract.d.ts +107 -0
  23. package/dist/shared/lock-data/authority/extract.js +1 -0
  24. package/dist/shared/lock-data/authority/index.d.ts +182 -0
  25. package/dist/shared/lock-data/authority/index.js +1 -0
  26. package/dist/shared/lock-data/authority/serialize.d.ts +35 -0
  27. package/dist/shared/lock-data/authority/serialize.js +1 -0
  28. package/dist/shared/lock-data/constants.d.ts +46 -0
  29. package/dist/shared/lock-data/constants.js +1 -0
  30. package/dist/shared/lock-data/core/actions-helpers.d.ts +163 -0
  31. package/dist/shared/lock-data/core/actions-helpers.js +1 -0
  32. package/dist/shared/lock-data/core/actions.d.ts +72 -0
  33. package/dist/shared/lock-data/core/actions.js +1 -0
  34. package/dist/shared/lock-data/core/draft.d.ts +64 -0
  35. package/dist/shared/lock-data/core/draft.js +1 -0
  36. package/dist/shared/lock-data/core/entry.d.ts +133 -0
  37. package/dist/shared/lock-data/core/entry.js +1 -0
  38. package/dist/shared/lock-data/core/fanout.d.ts +42 -0
  39. package/dist/shared/lock-data/core/fanout.js +1 -0
  40. package/dist/shared/lock-data/core/readonly-view.d.ts +49 -0
  41. package/dist/shared/lock-data/core/readonly-view.js +1 -0
  42. package/dist/shared/lock-data/core/registry.d.ts +282 -0
  43. package/dist/shared/lock-data/core/registry.js +1 -0
  44. package/dist/shared/lock-data/core/signal.d.ts +33 -0
  45. package/dist/shared/lock-data/core/signal.js +1 -0
  46. package/dist/shared/lock-data/drivers/broadcast-protocol.d.ts +71 -0
  47. package/dist/shared/lock-data/drivers/broadcast-protocol.js +1 -0
  48. package/dist/shared/lock-data/drivers/broadcast-state.d.ts +125 -0
  49. package/dist/shared/lock-data/drivers/broadcast-state.js +1 -0
  50. package/dist/shared/lock-data/drivers/broadcast.d.ts +36 -0
  51. package/dist/shared/lock-data/drivers/broadcast.js +1 -0
  52. package/dist/shared/lock-data/drivers/custom.d.ts +27 -0
  53. package/dist/shared/lock-data/drivers/custom.js +1 -0
  54. package/dist/shared/lock-data/drivers/index.d.ts +59 -0
  55. package/dist/shared/lock-data/drivers/index.js +1 -0
  56. package/dist/shared/lock-data/drivers/local.d.ts +86 -0
  57. package/dist/shared/lock-data/drivers/local.js +1 -0
  58. package/dist/shared/lock-data/drivers/storage-protocol.d.ts +67 -0
  59. package/dist/shared/lock-data/drivers/storage-protocol.js +1 -0
  60. package/dist/shared/lock-data/drivers/storage-state.d.ts +103 -0
  61. package/dist/shared/lock-data/drivers/storage-state.js +1 -0
  62. package/dist/shared/lock-data/drivers/storage.d.ts +71 -0
  63. package/dist/shared/lock-data/drivers/storage.js +1 -0
  64. package/dist/shared/lock-data/drivers/types.d.ts +73 -0
  65. package/dist/shared/lock-data/drivers/types.js +0 -0
  66. package/dist/shared/lock-data/drivers/web-locks.d.ts +123 -0
  67. package/dist/shared/lock-data/drivers/web-locks.js +1 -0
  68. package/dist/shared/lock-data/errors/index.d.ts +12 -0
  69. package/dist/shared/lock-data/errors/index.js +1 -0
  70. package/dist/shared/lock-data/errors/invalid-options-error.d.ts +11 -0
  71. package/dist/shared/lock-data/errors/invalid-options-error.js +1 -0
  72. package/dist/shared/lock-data/errors/lock-aborted-error.d.ts +10 -0
  73. package/dist/shared/lock-data/errors/lock-aborted-error.js +1 -0
  74. package/dist/shared/lock-data/errors/lock-disposed-error.d.ts +14 -0
  75. package/dist/shared/lock-data/errors/lock-disposed-error.js +1 -0
  76. package/dist/shared/lock-data/errors/lock-revoked-error.d.ts +10 -0
  77. package/dist/shared/lock-data/errors/lock-revoked-error.js +1 -0
  78. package/dist/shared/lock-data/errors/lock-timeout-error.d.ts +9 -0
  79. package/dist/shared/lock-data/errors/lock-timeout-error.js +1 -0
  80. package/dist/shared/lock-data/errors/readonly-mutation-error.d.ts +11 -0
  81. package/dist/shared/lock-data/errors/readonly-mutation-error.js +1 -0
  82. package/dist/shared/lock-data/index.d.ts +57 -0
  83. package/dist/shared/lock-data/index.js +1 -0
  84. package/dist/shared/lock-data/types.d.ts +347 -0
  85. package/dist/shared/lock-data/types.js +0 -0
  86. package/dist/shared/lock-data/utils/json-safe.d.ts +69 -0
  87. package/dist/shared/lock-data/utils/json-safe.js +1 -0
  88. package/dist/shared/throw-error/index.d.ts +10 -3
  89. package/dist/shared/throw-error/index.js +1 -1
  90. package/package.json +2 -2
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 默认 SessionStoreAdapter 实现:基于 sessionStorage
3
+ *
4
+ * 职责:同会话组(同 Tab 及其直系派生 Tab)内存 epoch 快照
5
+ * - read:同步读取当前 raw value
6
+ * - write:写入 raw value;QuotaExceededError 等失败降级 warn
7
+ *
8
+ * 能力探测:sessionStorage 不可用(SSR / 浏览器隐私模式 / 禁用 storage)时
9
+ * 工厂返回 null,由聚合层决定降级路径(session -> persistent)
10
+ *
11
+ * 对应 RFC.md「接口定义」「默认实现」
12
+ */
13
+ import type { LoggerAdapter, SessionStoreAdapter, SessionStoreAdapterContext } from '../types';
14
+ interface SessionStoreFactoryDeps {
15
+ readonly logger: LoggerAdapter;
16
+ }
17
+ /**
18
+ * 能力探测:sessionStorage 是否可实际读写
19
+ *
20
+ * 与 authority 的 localStorage 探测同构:仅判断 `typeof sessionStorage`
21
+ * 不够,Safari 隐私模式下 setItem 会抛 QuotaExceededError;
22
+ * 采用写-删探测法确保真的可用
23
+ */
24
+ declare function hasUsableSessionStorage(): boolean;
25
+ /**
26
+ * 构建 sessionStorage 的完整 key
27
+ *
28
+ * 规范:`${LOCK_PREFIX}:${id}:epoch`
29
+ */
30
+ declare function buildSessionStoreKey(id: string): string;
31
+ /**
32
+ * 创建默认 SessionStoreAdapter
33
+ *
34
+ * @returns SessionStoreAdapter 实例;sessionStorage 不可用时返回 null
35
+ */
36
+ declare function createDefaultSessionStoreAdapter(ctx: SessionStoreAdapterContext, deps: SessionStoreFactoryDeps): SessionStoreAdapter | null;
37
+ export { buildSessionStoreKey, createDefaultSessionStoreAdapter, hasUsableSessionStorage };
@@ -0,0 +1 @@
1
+ import{LOCK_PREFIX as e}from"../constants.js";function t(){try{let t=globalThis.sessionStorage;if(!t)return!1;let r=`${e}:__probe__`;return t.setItem(r,"1"),t.removeItem(r),!0}catch{return!1}}function r(t){return`${e}:${t}:epoch`}function s(e,s){if(!t())return s.logger.warn('sessionStorage is not available; default session store adapter is disabled. persistence="session" will fall back to "persistent".'),null;let o=r(e.id),n=globalThis.sessionStorage;return{read(){try{return n.getItem(o)}catch(e){return s.logger.warn("Failed to read session store from sessionStorage",e),null}},write(e){try{n.setItem(o,e)}catch(e){s.logger.warn("Failed to write session store to sessionStorage; epoch may be reset on next startup.",e)}}}}export{r as buildSessionStoreKey,s as createDefaultSessionStoreAdapter,t as hasUsableSessionStorage};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 会话纪元(epoch)探测与解析
3
+ *
4
+ * 对应 RFC.md「会话级持久化与 epoch 探测」「resolveEpoch 协议」章节。
5
+ *
6
+ * epoch 的核心作用:解决 "localStorage 天然持久化 vs 用户期望的会话级协作" 语义冲突。
7
+ * - `persistence === 'persistent'`:常量 `'persistent'`,跨会话共享同一权威副本
8
+ * - `persistence === 'session'`:每个会话组(同源活跃 Tab 的最大存活期)独立 epoch;
9
+ * 权威副本中 epoch 字段与本地 epoch 不一致时直接丢弃,等价"所有 Tab 关闭即重置"
10
+ *
11
+ * resolveEpoch 六分支协议(RFC L1262):
12
+ *
13
+ * | 分支 | 判定 | epoch 来源 | clearAuthority |
14
+ * | ---- | ---- | ---------- | -------------- |
15
+ * | A | persistence === 'persistent' | 常量 'persistent' | 否 |
16
+ * | B | session + !sessionStore | 降级为 'persistent' | 否 (logger.warn) |
17
+ * | C | sessionStore.read() 有值 | 直接继承(刷新/bfcache) | 否 |
18
+ * | D | 首次 + !channel | 生成新 UUID | 是 (logger.warn) |
19
+ * | E | 首次 + 收到 session-reply | 继承响应方 epoch | 否 |
20
+ * | F | 首次 + 探测超时 | 生成新 UUID | 是 |
21
+ *
22
+ * 响应方(E 分支的对侧):所有 storage-authority + session 的 Tab 在 channel 可用时
23
+ * 常驻订阅 session-probe,若自己已有 epoch 则广播 session-reply;由 StorageAuthority
24
+ * 生命周期管理订阅解绑(refCount === 0 时解绑)
25
+ */
26
+ import type { ChannelAdapter, LoggerAdapter, Persistence, SessionStoreAdapter } from '../types';
27
+ /**
28
+ * session-probe 消息:首次启动的 Tab 广播此消息询问 "当前是否有同会话组的 Tab"
29
+ *
30
+ * `probeId` 防止串扰:同一进程中可能同时有多个 id 的 StorageAuthority 发起 probe,
31
+ * 响应方必须带上原 probeId,发起方用 probeId 过滤避免串扰
32
+ */
33
+ interface SessionProbeMessage {
34
+ readonly type: 'session-probe';
35
+ readonly probeId: string;
36
+ }
37
+ /**
38
+ * session-reply 消息:响应方在收到 probe 时广播自己的 epoch
39
+ */
40
+ interface SessionReplyMessage {
41
+ readonly type: 'session-reply';
42
+ readonly probeId: string;
43
+ readonly epoch: string;
44
+ }
45
+ /**
46
+ * resolveEpoch 的输入上下文
47
+ *
48
+ * 刻意解耦于具体 Entry:Phase 5 的 registry 负责组装此 ctx 后调用 resolveEpoch
49
+ */
50
+ interface ResolveEpochContext {
51
+ readonly persistence: Persistence;
52
+ /** sessionStorage adapter;null 表示能力不可用 */
53
+ readonly sessionStore: SessionStoreAdapter | null;
54
+ /** BroadcastChannel adapter;null 表示能力不可用 */
55
+ readonly channel: ChannelAdapter | null;
56
+ /** 权威副本 adapter;D/F 分支需要 `remove()` 清空残留;null 表示能力不可用 */
57
+ readonly authority: {
58
+ remove: () => void;
59
+ } | null;
60
+ /** 探测窗口(ms);未传用 `DEFAULT_SESSION_PROBE_TIMEOUT`(100ms) */
61
+ readonly sessionProbeTimeout?: number;
62
+ readonly logger: LoggerAdapter;
63
+ }
64
+ /**
65
+ * resolveEpoch 的产物
66
+ *
67
+ * - `epoch`:最终决定的 epoch(常量 `'persistent'` 或 UUID)
68
+ * - `effectivePersistence`:若 B 分支降级,此字段为 `'persistent'`;否则与输入一致
69
+ * - `authorityCleared`:D/F 分支清空了权威副本(true 表示后续 initAuthority 不必再 pull)
70
+ */
71
+ interface ResolveEpochResult {
72
+ readonly epoch: string;
73
+ readonly effectivePersistence: Persistence;
74
+ readonly authorityCleared: boolean;
75
+ }
76
+ /**
77
+ * 生成新 epoch
78
+ *
79
+ * 优先级:`crypto.randomUUID()` → `Math.random().toString(36) + Date.now()` fallback
80
+ *
81
+ * 不使用 `isObject(crypto)` + `isFunction(crypto.randomUUID)` 的完整守卫,
82
+ * 而是 try-catch:`crypto` 变量在部分 SSR 环境是未定义的(读取即 ReferenceError),
83
+ * 跟 navigator 一样必须用 typeof 守卫访问;try-catch 更简洁且能覆盖任何异常路径
84
+ */
85
+ declare function generateUuid(): string;
86
+ /**
87
+ * 构造 session-probe 消息 —— 集中写入保证消息形状一致
88
+ */
89
+ declare function buildProbeMessage(probeId: string): SessionProbeMessage;
90
+ /**
91
+ * 构造 session-reply 消息
92
+ */
93
+ declare function buildReplyMessage(probeId: string, epoch: string): SessionReplyMessage;
94
+ /**
95
+ * 判断消息是否为合法 session-probe
96
+ *
97
+ * 走 verify.ts 语义函数,对齐 `<code_style>` 类型判断规范
98
+ */
99
+ declare function isSessionProbeMessage(message: unknown): message is SessionProbeMessage;
100
+ /**
101
+ * 判断消息是否为合法 session-reply
102
+ */
103
+ declare function isSessionReplyMessage(message: unknown): message is SessionReplyMessage;
104
+ /**
105
+ * resolveEpoch:按六分支协议决定本 Tab 的 epoch
106
+ *
107
+ * 返回 `Promise<ResolveEpochResult>`,因为 E/F 分支需要异步等待 session-reply 或超时
108
+ */
109
+ declare function resolveEpoch(ctx: ResolveEpochContext): Promise<ResolveEpochResult>;
110
+ /**
111
+ * 广播 session-probe 并等待 session-reply
112
+ *
113
+ * @returns 首个收到的 reply.epoch;超时返回 null
114
+ */
115
+ declare function probeForExistingSession(ctx: ResolveEpochContext): Promise<string | null>;
116
+ /**
117
+ * 生成新 epoch(D / F 分支共用)
118
+ *
119
+ * 副作用:
120
+ * - 写入 sessionStore(供本 Tab 后续刷新继承)
121
+ * - 调用 authority.remove()(主动清空上一会话组残留,避免 epoch 不一致的旧数据继续占用配额)
122
+ */
123
+ declare function freshEpoch(ctx: ResolveEpochContext): ResolveEpochResult;
124
+ /**
125
+ * 订阅 session-probe 消息并自动回复 session-reply
126
+ *
127
+ * 由 Phase 4.4 `initAuthority` 在 session 策略下调用;订阅常驻直到 Entry 销毁。
128
+ *
129
+ * @param channel 本 Tab 的 session 通道
130
+ * @param getMyEpoch 返回当前 Tab 的 epoch(null 表示尚未 resolved,此时不回复)
131
+ * @returns 解绑函数;refCount === 0 时由 initAuthority 调用
132
+ */
133
+ declare function subscribeSessionProbe(channel: ChannelAdapter, getMyEpoch: () => string | null): () => void;
134
+ export type { ResolveEpochContext, ResolveEpochResult, SessionProbeMessage, SessionReplyMessage };
135
+ export { buildProbeMessage, buildReplyMessage, freshEpoch, generateUuid, isSessionProbeMessage, isSessionReplyMessage, probeForExistingSession, resolveEpoch, subscribeSessionProbe, };
@@ -0,0 +1 @@
1
+ import{isObject as e,isString as s}from"../../utils/index.js";import{withResolvers as r}from"../../with-resolvers/index.js";import{DEFAULT_SESSION_PROBE_TIMEOUT as t,PERSISTENT_EPOCH as o}from"../constants.js";function i(){try{let e=globalThis.crypto;if(e&&"function"==typeof e.randomUUID)return e.randomUUID()}catch{}return`${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`}function n(e){return{type:"session-probe",probeId:e}}function a(e,s){return{type:"session-reply",probeId:e,epoch:s}}function l(r){return!!e(r)&&"session-probe"===r.type&&s(r.probeId)}function c(r){return!!e(r)&&"session-reply"===r.type&&s(r.probeId)&&s(r.epoch)}async function u(e){if("persistent"===e.persistence)return{epoch:o,effectivePersistence:"persistent",authorityCleared:!1};if(!e.sessionStore)return e.logger.warn('[lockData] sessionStore adapter unavailable, persistence="session" falls back to "persistent"'),{epoch:o,effectivePersistence:"persistent",authorityCleared:!1};let r=e.sessionStore.read();if(s(r)&&r.length>0)return{epoch:r,effectivePersistence:"session",authorityCleared:!1};if(!e.channel)return e.logger.warn("[lockData] channel adapter unavailable, skip session-probe and treat as first tab"),f(e);let t=await p(e);return s(t)?(e.sessionStore.write(t),{epoch:t,effectivePersistence:"session",authorityCleared:!1}):f(e)}function p(e){let{channel:s}=e;if(!s)return Promise.resolve(null);let o=i(),a=e.sessionProbeTimeout||t,l=r(),u=!1,p=s.subscribe(e=>{u||!c(e)||e.probeId===o&&(u=!0,l.resolve(e.epoch))}),f=setTimeout(()=>{u||(u=!0,l.resolve(null))},a);return s.postMessage(n(o)),l.promise.finally(()=>{clearTimeout(f),p()})}function f(e){let s=i();e.sessionStore&&e.sessionStore.write(s);let r=!1;if(e.authority)try{e.authority.remove(),r=!0}catch(s){e.logger.warn("[lockData] authority.remove failed during freshEpoch",s)}return{epoch:s,effectivePersistence:"session",authorityCleared:r}}function h(e,r){return e.subscribe(t=>{if(!l(t))return;let o=r();s(o)&&0!==o.length&&e.postMessage(a(t.probeId,o))})}export{n as buildProbeMessage,a as buildReplyMessage,f as freshEpoch,i as generateUuid,l as isSessionProbeMessage,c as isSessionReplyMessage,p as probeForExistingSession,u as resolveEpoch,h as subscribeSessionProbe};
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 权威副本 Lazy Parse 快路径
3
+ *
4
+ * 对应 RFC.md「Lazy Parse 快路径」章节。
5
+ *
6
+ * 设计动机:
7
+ * - 高频 `storage` 事件(尤其同 Tab 频繁 commit 时)绝大多数命中"rev 未变"快路径
8
+ * - 避免对 MB 级 snapshot 反复 `JSON.parse`
9
+ * - `persistence: 'session'` 下 epoch 不匹配时同样走快路径直接丢弃,不误应用上一会话组数据
10
+ *
11
+ * 两条快路径:
12
+ * 1. `extractRev`:正则锚定开头匹配 `{"rev":<整数>`,失败走全量 parse 兜底
13
+ * 2. `extractEpoch`:正则匹配 `,"epoch":"<string>"`,用于快路径 epoch 过滤
14
+ *
15
+ * 快路径开销恒为 O(首部长度),与 snapshot 总长无关;MB 级 value 下仍稳定在亚微秒
16
+ */
17
+ /**
18
+ * 快路径提取 rev
19
+ *
20
+ * 匹配锚定开头的 `{"rev":<整数>` 字段;失败返回 null(调用方应走全量 parse 兜底)
21
+ *
22
+ * 支持负数 rev(理论上单调递增不会出现,但序列化格式允许)
23
+ */
24
+ declare function extractRev(raw: string): number | null;
25
+ /**
26
+ * 快路径提取 epoch
27
+ *
28
+ * 匹配 `,"epoch":"<string>"`;失败返回 null(调用方应走全量 parse 兜底或按"无 epoch"处理)
29
+ *
30
+ * 正则说明:
31
+ * - 逗号前缀锚定:epoch 必然出现在 rev / ts 之后,不会错匹配 snapshot 内字面量
32
+ * - `[^"\\]*`:epoch 由 UUID 或常量 `'persistent'` 生成,不含引号和反斜杠;
33
+ * 若用户自定义 adapter 注入了含转义的 epoch(理论上不应该),快路径会失配走 JSON.parse 兜底
34
+ */
35
+ declare function extractEpoch(raw: string): string | null;
36
+ /**
37
+ * `readIfNewer` 的最小输入契约
38
+ *
39
+ * 刻意不依赖完整 Entry 结构,只要求 rev 去重基线 + epoch 过滤基线两个字段。
40
+ * Phase 5 的 `core/registry.ts` 中 Entry 天然满足此结构。
41
+ */
42
+ interface ReadIfNewerContext {
43
+ /**
44
+ * 已应用的最大 rev;用于去重判定
45
+ *
46
+ * 首次初始化时为 `0`(首个远端 rev > 0 必定命中)
47
+ */
48
+ readonly lastAppliedRev: number;
49
+ /**
50
+ * 当前 Tab 的会话纪元
51
+ *
52
+ * - `null` 表示"尚未 resolveEpoch 完成" / "不启用 epoch 过滤"(视调用上下文)
53
+ * - `'persistent'` 常量(persistence 策略)
54
+ * - UUID 字符串(session 策略)
55
+ *
56
+ * 非 null 时:快路径提取远端 epoch,若不一致则直接丢弃(不解析 snapshot)
57
+ */
58
+ readonly epoch: string | null;
59
+ }
60
+ /**
61
+ * `readIfNewer` 的产物
62
+ *
63
+ * 仅包含应用所需的最小字段;`ts` / `epoch` 对调用方无意义,不暴露
64
+ */
65
+ interface ReadIfNewerResult {
66
+ readonly rev: number;
67
+ readonly snapshot: unknown;
68
+ }
69
+ /**
70
+ * 权威副本原始 value 的全量形态
71
+ *
72
+ * 仅在快路径失配走 `JSON.parse` 兜底时使用;字段与 `serializeAuthority` 的产物对应
73
+ */
74
+ interface AuthorityFullShape {
75
+ readonly rev: number;
76
+ readonly ts: number;
77
+ readonly epoch: string;
78
+ readonly snapshot: unknown;
79
+ }
80
+ /**
81
+ * 按"是否比本地更新"决定是否返回 snapshot
82
+ *
83
+ * 流程(RFC L1167-1188):
84
+ * 1. `raw` 为 null / 空串 → 返回 null(删除 key / 首次读取)
85
+ * 2. 快路径 `extractRev`:
86
+ * - 失配 → 走 JSON.parse 兜底(旧格式 / 手动写入 / 自定义 adapter 产物)
87
+ * - 命中但 `remoteRev <= lastAppliedRev` → 返回 null(不解析 snapshot,O(1) 丢弃)
88
+ * 3. epoch 快路径过滤:本地 epoch 非 null 时,对比远端 epoch;不一致 → 返回 null
89
+ * 4. 真的要应用时才 `JSON.parse(raw)` 解析 snapshot
90
+ */
91
+ declare function readIfNewer(ctx: ReadIfNewerContext, raw: string | null): ReadIfNewerResult | null;
92
+ /**
93
+ * 安全 JSON.parse + 结构校验
94
+ *
95
+ * 返回 null 的情况:
96
+ * - `JSON.parse` 抛错(非法 JSON)
97
+ * - 结构不符(缺 rev / epoch / snapshot 字段 / rev 非数字 / epoch 非字符串)
98
+ *
99
+ * 刻意不抛错:调用方(`storage` 事件 / 定时 pull)不应因单条脏数据中断
100
+ *
101
+ * `snapshot` 字段用键存在性(`Reflect.has`)判定而非真值判定 —— 因为 `null`、`false`、
102
+ * `0`、`''` 都是合法的 snapshot 值;只要 key 缺失就视为非法结构,与"缺 rev / epoch
103
+ * 返回 null"保持一致的契约
104
+ */
105
+ declare function parseAuthorityRaw(raw: string): AuthorityFullShape | null;
106
+ export type { AuthorityFullShape, ReadIfNewerContext, ReadIfNewerResult };
107
+ export { extractEpoch, extractRev, parseAuthorityRaw, readIfNewer };
@@ -0,0 +1 @@
1
+ import{isNumber as e,isObject as t,isString as r}from"../../utils/index.js";function n(e){let t=/^\{"rev":(?<rev>-?\d+)/u.exec(e);return t?Number(t[1]):null}function l(e){let t=/,"epoch":"(?<epoch>[^"\\]*)"/u.exec(e);return t?t[1]:null}function u(e,t){if(!t)return null;let u=n(t);if(null===u){var p;let n;return p=e,!(n=o(t))||n.rev<=p.lastAppliedRev||r(p.epoch)&&n.epoch!==p.epoch?null:{rev:n.rev,snapshot:n.snapshot}}if(u<=e.lastAppliedRev)return null;if(r(e.epoch)){let n=l(t);if(r(n)&&n!==e.epoch)return null}let s=o(t);return s?{rev:u,snapshot:s.snapshot}:null}function o(n){let l;try{l=JSON.parse(n)}catch{return null}if(!t(l))return null;let u=l;return e(u.rev)&&r(u.epoch)&&Reflect.has(u,"snapshot")?{rev:u.rev,ts:e(u.ts)?u.ts:0,epoch:u.epoch,snapshot:u.snapshot}:null}export{l as extractEpoch,n as extractRev,o as parseAuthorityRaw,u as readIfNewer};
@@ -0,0 +1,182 @@
1
+ /**
2
+ * StorageAuthority 主类:跨进程权威副本的读写 + 推送 + 拉取统一收口
3
+ *
4
+ * 对应 RFC.md「StorageAuthority(localStorage 权威副本)」章节。
5
+ *
6
+ * 职责边界(与 LockDriver 互不干扰):
7
+ * - 权威 snapshot 读写
8
+ * - 跨 Tab 推送(authority.subscribe)/ 激活时主动拉取(pageshow / visibilitychange)
9
+ * - 会话纪元(epoch)生命周期管理(首次 resolveEpoch + 常驻 session-probe 响应)
10
+ * - 与锁调度完全无关(acquire / release / revoke 都不经过此处)
11
+ *
12
+ * 三条读路径共享同一应用流程:
13
+ * | 触发源 | 时机 | source | 数据来源 |
14
+ * | --------------------- | ------------------------------- | ------------------- | ---------------------------- |
15
+ * | acquire 时 pull | driver.acquire 成功、进入 recipe 前 | 'pull-on-acquire' | authority.read() 同步读 |
16
+ * | authority.subscribe | 其他进程写入触发订阅回调 | 'storage-event' | 回调直接传入 newValue |
17
+ * | 激活时主动 pull | pageshow / visibilitychange | 'pageshow' 等 | authority.read() 同步读 |
18
+ *
19
+ * 一条写路径:
20
+ * commit 成功 → rev++ → authority.write(serialize) → 触发 onCommit
21
+ *
22
+ * wrapper 方案下的契约(对应 fixes/api-getvalue-only-redesign.md §14.2 缺口 2):
23
+ * - 不再注入 `applySnapshot` 钩子 —— 远程同步通过 `host.applyRemote(next)` 完成原子覆写,
24
+ * authority 不感知 wrapper / dataRef 实现细节
25
+ * - 不再注入 `clone` 函数 —— 内部用 `cloneByJson` 完成 emit 事件的 snapshot 隔离
26
+ * - 事件触发统一走 `emit` 回调,由调用方接到 listenersFanout
27
+ */
28
+ import type { AuthorityAdapter, ChannelAdapter, CommitSource, LockDataMutation, LoggerAdapter, Persistence, SessionStoreAdapter, SyncSource } from '../types';
29
+ import { type ResolveEpochResult } from './epoch';
30
+ /**
31
+ * StorageAuthority 宿主契约(Entry 的最小子集)
32
+ *
33
+ * 设计动机:authority 不感知 Entry 完整结构,避免循环依赖;同时通过 `applyRemote` 方法
34
+ * 把"如何写入 dataRef.current"的实现细节封装到 Entry 内部,authority 只负责调用该方法
35
+ *
36
+ * 字段语义:
37
+ * - `applyRemote(next)`:远程同步入口;内部走 `cloneByJson(next)` + 赋值 `dataRef.current`,
38
+ * 与 emit 链解耦(emit 由 authority 自己负责)
39
+ * - `rev` / `lastAppliedRev` 读写双向;`epoch` 由 `StorageAuthority` 内部在首次
40
+ * `resolveEpoch` 后回写
41
+ */
42
+ interface StorageAuthorityHost<T extends object> {
43
+ /**
44
+ * 远程同步入口:把 awaited / 远程 snapshot 写入 `dataRef.current`
45
+ *
46
+ * 调用方语义:authority 拿到远端最新 snapshot 后,先 `host.applyRemote(snapshot)` 完成
47
+ * 内部状态切换,再由 authority 自己 `emitSync(...)` 通知 listener。Entry 内部实现
48
+ * 必须保证 `applyRemote` 走 JSON 拷贝隔离(详见 core/entry.ts buildApplyRemote)
49
+ */
50
+ readonly applyRemote: (next: T) => void;
51
+ /** 单调递增版本号;commit 时 `rev++` */
52
+ rev: number;
53
+ /** 已应用的最大 rev;commit 后同步更新;subscribe 回调时用于去重 */
54
+ lastAppliedRev: number;
55
+ /** 当前 Tab 的会话纪元;首次 resolveEpoch 完成后由此类回写 */
56
+ epoch: string | null;
57
+ }
58
+ /**
59
+ * StorageAuthority 的构造依赖集合
60
+ *
61
+ * adapters 三件套允许为 null:authority 或 channel 不可用时降级为
62
+ * 对应功能 no-op,保证 lockData 在任何环境下都能跑
63
+ */
64
+ interface StorageAuthorityDeps<T extends object> {
65
+ readonly host: StorageAuthorityHost<T>;
66
+ readonly authority: AuthorityAdapter | null;
67
+ readonly channel: ChannelAdapter | null;
68
+ readonly sessionStore: SessionStoreAdapter | null;
69
+ readonly persistence: Persistence;
70
+ readonly sessionProbeTimeout?: number;
71
+ readonly logger: LoggerAdapter;
72
+ /** onSync 事件触发回调;上层接到 listenersFanout */
73
+ readonly emitSync: (event: {
74
+ source: SyncSource;
75
+ rev: number;
76
+ snapshot: T;
77
+ }) => void;
78
+ /** onCommit 事件触发回调;上层接到 listenersFanout */
79
+ readonly emitCommit: (event: {
80
+ source: CommitSource;
81
+ token: string;
82
+ rev: number;
83
+ mutations: readonly LockDataMutation[];
84
+ snapshot: T;
85
+ }) => void;
86
+ }
87
+ /**
88
+ * StorageAuthority 对外暴露的 API
89
+ */
90
+ interface StorageAuthority<T extends object> {
91
+ /**
92
+ * 初始化:resolveEpoch + 常驻 session-probe 响应 + 初次 pull + 订阅推送通道
93
+ *
94
+ * 返回 `Promise<ResolveEpochResult>`;调用方(`core/entry.ts`)会把此 Promise 与
95
+ * getValue Promise 合成后挂到 `Entry.dataReadyPromise` 对外暴露
96
+ *
97
+ * 多次调用 init 是非法的:宿主自行保证只调用一次
98
+ */
99
+ init: () => Promise<ResolveEpochResult>;
100
+ /** 手动拉取(acquire 时使用):等价于一次 source='pull-on-acquire' 的 readIfNewer + 应用 */
101
+ pullOnAcquire: () => void;
102
+ /**
103
+ * commit 成功后的写路径:`rev++` → `authority.write` → emit onCommit
104
+ *
105
+ * `mutations` 由 Draft 层(`core/draft.ts`)提供;commit 流程为空 mutations 时也可调用
106
+ * `snapshot` 必须是已隔离的独立副本(调用方走 `cloneByJson`,authority.write 之后
107
+ * 宿主可能继续改 dataRef.current,独立副本保证 listener 看到的是 commit 当时的值)
108
+ */
109
+ onCommitSuccess: (event: {
110
+ source: CommitSource;
111
+ token: string;
112
+ mutations: readonly LockDataMutation[];
113
+ snapshot: T;
114
+ }) => void;
115
+ /**
116
+ * 销毁:解绑所有订阅(authority.subscribe / pageshow / visibilitychange / session-probe)
117
+ * + close channel;幂等
118
+ */
119
+ dispose: () => void;
120
+ }
121
+ /**
122
+ * StorageAuthority 的内部可变状态容器
123
+ *
124
+ * 从 `createStorageAuthority` 中抽离,让生命周期函数(init / pullOnAcquire /
125
+ * onCommitSuccess / dispose)可以作为顶层纯函数独立存在;否则这些函数作为闭包
126
+ * 会让 `createStorageAuthority` 单函数行数超过 biome `noExcessiveLinesPerFunction`
127
+ * 默认阈值(100 行)
128
+ */
129
+ interface AuthorityState {
130
+ readonly unsubscribers: Array<() => void>;
131
+ disposed: boolean;
132
+ initialized: boolean;
133
+ }
134
+ /**
135
+ * 根据远端 raw 应用到 host;三条读路径共享
136
+ *
137
+ * 命中条件 see extract.ts: `readIfNewer`
138
+ *
139
+ * wrapper 方案下:调用 `host.applyRemote(nextSnapshot)` 完成原子覆写,authority 不感知
140
+ * dataRef 实现细节;emitSync 的 snapshot 走 `cloneByJson` 拷贝隔离,避免 listener mutate
141
+ * 影响内部 dataRef.current
142
+ */
143
+ declare function applyAuthorityIfNewer<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>, source: SyncSource, raw: string | null): void;
144
+ /**
145
+ * 订阅激活时 pull:pageshow / visibilitychange → authority.read() + 应用
146
+ *
147
+ * 仅在浏览器环境(`window` / `document` 可用)注册;
148
+ * 非浏览器环境由自定义 AuthorityAdapter.subscribe 在合适时机回调即可
149
+ */
150
+ declare function attachActivationPullSubscription<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>): void;
151
+ /**
152
+ * 执行 init 流程:常驻 probe 响应 → resolveEpoch → 推送/激活订阅 → 初次 pull
153
+ */
154
+ declare function performInit<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>): Promise<ResolveEpochResult>;
155
+ /**
156
+ * 执行 pullOnAcquire 流程:dispose / authority 缺失时 no-op
157
+ */
158
+ declare function performPullOnAcquire<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>): void;
159
+ /**
160
+ * 执行 onCommitSuccess 写路径:rev 自增 → authority.write → emitCommit
161
+ */
162
+ declare function performCommitSuccess<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>, event: {
163
+ source: CommitSource;
164
+ token: string;
165
+ mutations: readonly LockDataMutation[];
166
+ snapshot: T;
167
+ }): void;
168
+ /**
169
+ * 执行 dispose 流程:解绑所有订阅 + channel.close;幂等
170
+ */
171
+ declare function performDispose<T extends object>(state: AuthorityState, deps: StorageAuthorityDeps<T>): void;
172
+ /**
173
+ * 创建 StorageAuthority 实例
174
+ *
175
+ * 仅负责:state 初始化 + 绑定 deps 闭包 + 返回 API 表面
176
+ * 具体生命周期逻辑由顶层 `perform*` / `attach*` 纯函数承担
177
+ *
178
+ * 立即执行:不做初始化(resolveEpoch / 订阅);这些在 `init()` 中异步触发
179
+ */
180
+ declare function createStorageAuthority<T extends object>(deps: StorageAuthorityDeps<T>): StorageAuthority<T>;
181
+ export type { AuthorityState, StorageAuthority, StorageAuthorityDeps, StorageAuthorityHost };
182
+ export { applyAuthorityIfNewer, attachActivationPullSubscription, createStorageAuthority, performCommitSuccess, performDispose, performInit, performPullOnAcquire, };
@@ -0,0 +1 @@
1
+ import{isObject as e,isString as t}from"../../utils/index.js";import{cloneByJson as r}from"../utils/json-safe.js";import{resolveEpoch as i,subscribeSessionProbe as o}from"./epoch.js";import{readIfNewer as s}from"./extract.js";import{serializeAuthority as n}from"./serialize.js";function a(t,i,o,n){if(t.disposed)return;let{host:a,logger:c,emitSync:u}=i,l=s({lastAppliedRev:a.lastAppliedRev,epoch:a.epoch},n);if(!l)return;if(!e(l.snapshot))return void c.warn(`[lockData] authority snapshot is not an object (source=${o}), skip apply`);let p=l.snapshot;try{a.applyRemote(p)}catch(e){c.error(`[lockData] host.applyRemote failed (source=${o}, rev=${l.rev})`,e);return}a.rev=l.rev,a.lastAppliedRev=l.rev;try{u({source:o,rev:l.rev,snapshot:r(p)})}catch(e){c.error(`[lockData] emitSync listener threw (source=${o})`,e)}}function c(e,t){let{authority:r}=t;if(!r||void 0===globalThis.window||"u"<typeof document)return;let i=i=>{i.persisted&&a(e,t,"pageshow",r.read())},o=()=>{"visible"===document.visibilityState&&a(e,t,"visibilitychange",r.read())};window.addEventListener("pageshow",i),document.addEventListener("visibilitychange",o),e.unsubscribers.push(()=>{window.removeEventListener("pageshow",i),document.removeEventListener("visibilitychange",o)})}async function u(e,t){let{host:r,authority:s,channel:n,sessionStore:u,persistence:l,sessionProbeTimeout:p,logger:h}=t;if(e.initialized)return h.warn("[lockData] StorageAuthority.init called twice, ignore the second call"),{epoch:r.epoch||"persistent",effectivePersistence:l,authorityCleared:!1};e.initialized=!0,function(e,t){if("session"!==t.persistence||!t.channel)return;let r=o(t.channel,()=>t.host.epoch);e.unsubscribers.push(r)}(e,t);let d=await i({persistence:l,sessionStore:u,channel:n,authority:s?{remove:()=>s.remove()}:null,sessionProbeTimeout:p,logger:h});return e.disposed||(r.epoch=d.epoch,!function(e,t){let{authority:r}=t;if(!r)return;let i=r.subscribe(r=>{a(e,t,"storage-event",r)});e.unsubscribers.push(i)}(e,t),c(e,t),s&&!d.authorityCleared&&a(e,t,"pull-on-acquire",s.read())),d}function l(e,t){let{authority:r}=t;!e.disposed&&r&&a(e,t,"pull-on-acquire",r.read())}function p(e,r,i){if(e.disposed)return;let{host:o,authority:s,logger:a,emitCommit:c}=r;if(o.rev++,o.lastAppliedRev=o.rev,s&&t(o.epoch)){let e=n(o.rev,Date.now(),o.epoch,i.snapshot);s.write(e)}try{c({source:i.source,token:i.token,rev:o.rev,mutations:i.mutations,snapshot:i.snapshot})}catch(e){a.error(`[lockData] emitCommit listener threw (source=${i.source})`,e)}}function h(e,t){if(e.disposed)return;e.disposed=!0;let{channel:r,logger:i}=t;for(let t=0;t<e.unsubscribers.length;t++)try{e.unsubscribers[t]()}catch(e){i.warn("[lockData] StorageAuthority dispose: unsubscriber threw",e)}if(e.unsubscribers.length=0,r)try{r.close()}catch(e){i.warn("[lockData] StorageAuthority dispose: channel.close threw",e)}}function d(e){let t={unsubscribers:[],disposed:!1,initialized:!1};return{init:()=>u(t,e),pullOnAcquire:()=>l(t,e),onCommitSuccess:r=>p(t,e,r),dispose:()=>h(t,e)}}export{a as applyAuthorityIfNewer,c as attachActivationPullSubscription,d as createStorageAuthority,p as performCommitSuccess,h as performDispose,u as performInit,l as performPullOnAcquire};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 权威副本存储格式序列化
3
+ *
4
+ * 对应 RFC.md「存储格式(固化契约)」章节。
5
+ *
6
+ * 固化字段顺序:`rev → ts → epoch → snapshot`
7
+ *
8
+ * 固化理由:
9
+ * 1. `JSON.stringify({ rev, ts, epoch, snapshot })` 在 JS 规范上不保证字段顺序
10
+ * (V8 / SpiderMonkey 实测按插入顺序,但不作为契约),手动拼接避免任何引擎差异
11
+ * 2. `rev` 固定首位:`extractRev` 用锚定开头的正则即可安全提取,不被 snapshot 内容干扰
12
+ * 3. `epoch` 固定在 snapshot 之前:`extractEpoch` 在小范围内匹配,快路径开销与 value 总长无关
13
+ * 4. `snapshot` 固定尾部:用户数据可能包含 `"rev"` / `"epoch"` 等字面量,放在尾部避免正则锚定出错
14
+ *
15
+ * snapshot 的序列化统一走 `JSON.stringify`,不做字段顺序保证(snapshot 内部由用户控制)
16
+ */
17
+ interface AuthoritySerializedParts {
18
+ readonly rev: number;
19
+ readonly ts: number;
20
+ readonly epoch: string;
21
+ readonly snapshot: unknown;
22
+ }
23
+ /**
24
+ * 序列化权威副本 value
25
+ *
26
+ * 产物形如:`{"rev":42,"ts":1714198800123,"epoch":"ab12...","snapshot":{...}}`
27
+ *
28
+ * @param rev 单调递增版本号
29
+ * @param ts 写入时间戳(ms,来自 `Date.now()`)
30
+ * @param epoch 会话纪元;`persistence === 'persistent'` 时固定为 `'persistent'`,否则为 UUID 字符串
31
+ * @param snapshot 用户数据快照;由调用方保证已通过 `cloneByJson` 深克隆(JSON 拷贝隔离)
32
+ */
33
+ declare function serializeAuthority(rev: number, ts: number, epoch: string, snapshot: unknown): string;
34
+ export type { AuthoritySerializedParts };
35
+ export { serializeAuthority };
@@ -0,0 +1 @@
1
+ function t(t,i,r,e){return`{"rev":${t},"ts":${i},"epoch":${JSON.stringify(r)},"snapshot":${JSON.stringify(e)}}`}export{t as serializeAuthority};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * lock-data 模块的全局常量
3
+ *
4
+ * 所有 key / 默认值统一收敛到此文件,避免散落在各模块中导致跨 Tab 契约漂移。
5
+ * 详见 RFC.md「默认值总览」「StorageAuthority / 存储格式」章节。
6
+ */
7
+ /**
8
+ * 跨 Tab 存储 key 的统一前缀
9
+ *
10
+ * 为什么不用 `lingshu:lock-data`:包含作者 scope 可避免与其他未来集成到页面的
11
+ * 第三方锁库(同样用 lock-data 命名)发生 localStorage key 冲突
12
+ */
13
+ declare const LOCK_PREFIX = "@cmtlyt/lingshu-toolkit:lockData";
14
+ /**
15
+ * "永不超时"标记
16
+ *
17
+ * 使用 unique symbol(而非 Infinity / -1 / 0)是为了:
18
+ * 1. 在 TypeScript 层完整区别于"未设置"和任意数值
19
+ * 2. 避免 setTimeout 对非法数值的静默降级
20
+ * 3. 让业务侧必须显式 import 才能使用,降低误用概率
21
+ */
22
+ declare const NEVER_TIMEOUT: unique symbol;
23
+ /**
24
+ * 默认抢锁超时(毫秒)
25
+ *
26
+ * 5000ms 是协作类场景的经验值:
27
+ * - 大于人类主动操作的响应延迟阈值(2s)
28
+ * - 小于用户感知到"卡住"的耐心上限(10s)
29
+ */
30
+ declare const DEFAULT_TIMEOUT = 5000;
31
+ /**
32
+ * session-probe 探测响应的等待窗口(毫秒)
33
+ *
34
+ * 仅首次启动(sessionStorage 无 epoch 时)阻塞;刷新 / bfcache 恢复走快路径跳过探测。
35
+ * 100ms 已覆盖同源 Tab 间 BroadcastChannel 的典型回响延迟(<30ms)。
36
+ */
37
+ declare const DEFAULT_SESSION_PROBE_TIMEOUT = 100;
38
+ /**
39
+ * persistent 策略下固定使用的 epoch 值
40
+ *
41
+ * 使用常量字符串而非随机 uuid,保证跨会话重开仍能匹配同一权威副本。
42
+ */
43
+ declare const PERSISTENT_EPOCH = "persistent";
44
+ /** 统一用于 throwError 第一参数的函数名,保证错误消息前缀一致 */
45
+ declare const ERROR_FN_NAME = "lockData";
46
+ export { DEFAULT_SESSION_PROBE_TIMEOUT, DEFAULT_TIMEOUT, ERROR_FN_NAME, LOCK_PREFIX, NEVER_TIMEOUT, PERSISTENT_EPOCH };
@@ -0,0 +1 @@
1
+ let E="@cmtlyt/lingshu-toolkit:lockData",t=Symbol("@cmtlyt/lingshu-toolkit:lockData#NEVER_TIMEOUT"),T=5e3,l=100,_="persistent",o="lockData";export{l as DEFAULT_SESSION_PROBE_TIMEOUT,T as DEFAULT_TIMEOUT,o as ERROR_FN_NAME,E as LOCK_PREFIX,t as NEVER_TIMEOUT,_ as PERSISTENT_EPOCH};