@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,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测试专用:全内存的 Authority / Channel / SessionStore 适配器工厂
|
|
3
|
+
*
|
|
4
|
+
* 设计目的:
|
|
5
|
+
* - 在 Node 环境下模拟"多 Tab 共享浏览器存储 + BroadcastChannel"的完整链路
|
|
6
|
+
* - 7.2 集成测试(`__test__/integration/memory-adapters.node.test.ts`)复用此工厂验证 lockData 全链路
|
|
7
|
+
* - 7.3 合规性测试套件(`__test__/adapters/memory-integration.node.test.ts`)复用此工厂作为"参考实现"
|
|
8
|
+
*
|
|
9
|
+
* 共享语义对齐(与默认 localStorage / BroadcastChannel / sessionStorage 实现完全一致):
|
|
10
|
+
* - `storage`:跨 Tab 共享的 key-value 存储(模拟 localStorage)
|
|
11
|
+
* - `bus`:跨 Tab 共享的消息总线(模拟 BroadcastChannel,按 channel name 分桶)
|
|
12
|
+
* - `sessionScope`:每个 Tab 独立的 session 存储(模拟 sessionStorage,不跨 Tab)
|
|
13
|
+
*
|
|
14
|
+
* 关键契约对齐(对齐 `src/shared/lock-data/adapters/*.ts` 的真实实现):
|
|
15
|
+
* - `AuthorityAdapter.subscribe`:仅响应"跨 Tab"的 write 通知;本 Tab 自己 write 不触发自己的 subscribe 回调
|
|
16
|
+
* (对齐原生 storage 事件:发起 write 的那个 document 不会收到自己的 storage 事件)
|
|
17
|
+
* - `ChannelAdapter.postMessage`:发送方不会收到自己 postMessage 的消息
|
|
18
|
+
* (对齐原生 BroadcastChannel 规范)
|
|
19
|
+
* - 订阅回调异常走 `logger.error` 隔离,不会传播到发送方,也不会污染其他订阅者
|
|
20
|
+
*
|
|
21
|
+
* 参考:RFC.md「适配器合规测试套件」章节
|
|
22
|
+
*/
|
|
23
|
+
import type { AuthorityAdapter, AuthorityAdapterContext, ChannelAdapter, ChannelAdapterContext, LoggerAdapter, SessionStoreAdapter, SessionStoreAdapterContext } from '../../types';
|
|
24
|
+
/**
|
|
25
|
+
* storage 订阅者记录:每个订阅者同时携带其所属 Tab 标识 + 订阅者自己的 logger,
|
|
26
|
+
* 用于:
|
|
27
|
+
* 1. 模拟「本 Tab write 不触发本 Tab subscribe 回调」的原生 storage 事件语义
|
|
28
|
+
* 2. 订阅者回调异常时用**订阅者自己**注入的 logger.error 记录(异常属于订阅者代码的责任)
|
|
29
|
+
*/
|
|
30
|
+
interface StorageSubscriber {
|
|
31
|
+
readonly tabId: symbol;
|
|
32
|
+
readonly logger: LoggerAdapter | undefined;
|
|
33
|
+
readonly onExternalUpdate: (newValue: string | null) => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 跨 Tab 共享的"浏览器环境":authority storage + channel bus
|
|
37
|
+
*
|
|
38
|
+
* 注意:每个 Tab 的 sessionScope 独立(sessionStorage 语义),故由 `createMemoryAdapters`
|
|
39
|
+
* 内部维护,不在 env 里共享
|
|
40
|
+
*/
|
|
41
|
+
interface SharedMemoryEnv {
|
|
42
|
+
/** 跨 Tab 共享的 key-value 存储(模拟 localStorage) */
|
|
43
|
+
readonly storage: Map<string, string>;
|
|
44
|
+
/** 跨 Tab 共享的消息总线;key 为 channel name,value 为该 channel 上所有订阅者 */
|
|
45
|
+
readonly bus: Map<string, Set<ChannelSubscriber>>;
|
|
46
|
+
/** storage 订阅者按 key 分桶(AuthorityAdapter.subscribe 语义:仅订阅指定 key) */
|
|
47
|
+
readonly storageSubscribers: Map<string, Set<StorageSubscriber>>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* channel 订阅者记录:同样携带 Tab 标识用于模拟「postMessage 发送方不收自己消息」,
|
|
51
|
+
* 并携带订阅者自己的 logger 以便异常日志归属正确
|
|
52
|
+
*/
|
|
53
|
+
interface ChannelSubscriber {
|
|
54
|
+
readonly tabId: symbol;
|
|
55
|
+
readonly logger: LoggerAdapter | undefined;
|
|
56
|
+
readonly onMessage: (message: unknown) => void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 创建一个"多 Tab 共享浏览器环境"的空白容器;随后调用 `createMemoryAdapters(env)`
|
|
60
|
+
* 创建每个 Tab 独立的 adapter 工厂集合即可
|
|
61
|
+
*/
|
|
62
|
+
declare function createSharedMemoryEnv(): SharedMemoryEnv;
|
|
63
|
+
/**
|
|
64
|
+
* 单 Tab 的三件套 adapter 工厂集合
|
|
65
|
+
*
|
|
66
|
+
* 与 `LockDataAdapters<T>` 的 `getAuthority` / `getChannel` / `getSessionStore` 字段一一对应,
|
|
67
|
+
* 可直接作为 `lockData({ adapters: { ...memoryAdapters, logger } })` 注入
|
|
68
|
+
*/
|
|
69
|
+
interface MemoryAdapters {
|
|
70
|
+
readonly getAuthority: (ctx: AuthorityAdapterContext) => AuthorityAdapter;
|
|
71
|
+
readonly getChannel: (ctx: ChannelAdapterContext) => ChannelAdapter;
|
|
72
|
+
readonly getSessionStore: (ctx: SessionStoreAdapterContext) => SessionStoreAdapter;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 可选的 logger 注入:用于验证「回调异常走 logger.error 隔离」的合规测试
|
|
76
|
+
*
|
|
77
|
+
* 未提供时内部 silently swallow 异常(保持接口契约不向上抛)
|
|
78
|
+
*/
|
|
79
|
+
interface CreateMemoryAdaptersOptions {
|
|
80
|
+
readonly logger?: LoggerAdapter;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 创建"某一个 Tab"的内存 adapter 工厂集合
|
|
84
|
+
*
|
|
85
|
+
* 同一 env 下多次调用 `createMemoryAdapters(env)` 即模拟多 Tab:
|
|
86
|
+
* - 各 Tab 的 authority / channel 自然通过 env 共享
|
|
87
|
+
* - 各 Tab 的 sessionScope 由工厂闭包内部维护,互不可见
|
|
88
|
+
*
|
|
89
|
+
* 每次调用会生成一个新的 `tabId` symbol 作为订阅者归属标识,用于:
|
|
90
|
+
* - authority.subscribe:过滤"本 Tab write 触发的自通知"
|
|
91
|
+
* - channel.postMessage:过滤"发送方自己订阅的回调"
|
|
92
|
+
*/
|
|
93
|
+
declare function createMemoryAdapters(env: SharedMemoryEnv, options?: CreateMemoryAdaptersOptions): MemoryAdapters;
|
|
94
|
+
export type { CreateMemoryAdaptersOptions, MemoryAdapters, SharedMemoryEnv };
|
|
95
|
+
export { createMemoryAdapters, createSharedMemoryEnv };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(){return{storage:new Map,bus:new Map,storageSubscribers:new Map}}function r(e,a={}){let s=Symbol("lock-data.memory-adapters.tab"),o=new Map,{logger:n}=a;return{getAuthority:r=>{var a,o,l,u,i;let b;return a=e,o=s,l=r,u=n,b=(i=l.id,`authority:${i}`),{read:()=>a.storage.get(b)??null,write(e){a.storage.set(b,e),t(a,b,e,o)},remove(){a.storage.delete(b),t(a,b,null,o)},subscribe(e){let r={tabId:o,logger:u,onExternalUpdate:e},t=a.storageSubscribers.get(b)??new Set;return t.add(r),a.storageSubscribers.set(b,t),()=>{t.delete(r)}}}},getChannel:r=>{var t,a,o,l,u,i;let b,d,g;return t=e,a=s,o=r,l=n,b=(u=o.id,i=o.channel,`${u}:${i}`),d=new Set,g=!1,{postMessage(e){if(g)return;let r=t.bus.get(b);if(r){for(let t of Array.from(r))if(t.tabId!==a)try{t.onMessage(e)}catch(e){t.logger?.error("[memory-adapters] channel subscriber threw",e)}}},subscribe(e){if(g)return()=>{};let r={tabId:a,logger:l,onMessage:e},s=t.bus.get(b)??new Set;return s.add(r),t.bus.set(b,s),d.add(r),()=>{s.delete(r),d.delete(r)}},close(){if(g)return;g=!0;let e=t.bus.get(b);if(e)for(let r of d)e.delete(r);d.clear()}}},getSessionStore:e=>{var r,t;let a;return r=o,a=(t=e.id,`session:${t}`),{read:()=>r.get(a)??null,write(e){r.set(a,e)}}}}}function t(e,r,t,a){let s=e.storageSubscribers.get(r);if(s){for(let e of Array.from(s))if(e.tabId!==a)try{e.onExternalUpdate(t)}catch(r){e.logger?.error("[memory-adapters] authority subscriber threw",r)}}}export{r as createMemoryAdapters,e as createSharedMemoryEnv};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LockDisposedError as e,NEVER_TIMEOUT as t,ReadonlyMutationError as n,lockData as o}from"../index.js";function a(e,t="info"){let n=document.getElementById("log"),o=new Date().toLocaleTimeString("zh-CN",{hour12:!1}),r=document.createElement("div");r.className="log-entry",r.innerHTML=`<span class="log-time">[${o}]</span> <span class="log-${t}">${e}</span>`,n.prepend(r)}function r(e){document.getElementById("last-error").textContent=e}let[s,c]=o({getValue:()=>({count:0,label:"init"}),listeners:{onCommit:e=>{a(`onCommit <span class="tag tag-rev">rev ${e.rev}</span> source=${e.source} snapshot=${JSON.stringify(e.snapshot)}`,"event"),i()},onLockStateChange:e=>{a(`onLockStateChange <span class="tag tag-phase">${e.phase}</span>`,"event"),u()},onRevoked:e=>{a(`onRevoked reason=${e.reason}`,"event")}}});function i(){document.getElementById("view-count").textContent=String(s.count),document.getElementById("view-label").textContent=s.label}function u(){document.getElementById("is-holding").textContent=String(c.isHolding)}async function d(){await c.update(e=>{e.count+=1}),i(),a(`count +1 → ${s.count}`,"success")}async function l(){await c.update(e=>{e.count-=1}),i(),a(`count -1 → ${s.count}`,"success")}async function g(){await c.replace({count:100,label:"replaced"}),i(),a(`replace → count=${s.count}, label=${s.label}`,"success")}i(),a("基础实例已创建(同步初始化)","success");let b=null,m=null;async function p(){let[e,t]=o({id:"playground-shared",getValue:()=>({counter:0})});return{view:e,actions:t}}let f=null,y=null,h=0;function $(){f&&(document.getElementById("cross-tab-value").textContent=String(f.value),document.getElementById("cross-tab-rev").textContent=String(h)),document.getElementById("cross-tab-holding").textContent=y?String(y.isHolding):"-"}let v=null,T=null,E=null;Object.assign(globalThis,{doIncrement:d,doDecrement:l,doReplace:g,doSnapshot:function(){let e=c.snapshot();a(`snapshot → ${JSON.stringify(e)}`,"info")},doWriteView:function(){try{s.count=999,a("未抛错(不应该到这里)","error")}catch(o){let e=o instanceof n,t=o instanceof Error?o.message:String(o);a(`捕获 ReadonlyMutationError: ${t} (instanceof=${e})`,e?"success":"error"),r(`ReadonlyMutationError: ${t}`)}},doDeleteView:function(){try{delete s.count,a("未抛错(不应该到这里)","error")}catch(o){let e=o instanceof n,t=o instanceof Error?o.message:String(o);a(`捕获 ReadonlyMutationError (delete): ${t}`,e?"success":"error"),r(`ReadonlyMutationError: ${t}`)}},doUseAfterDispose:async function t(){let[,t]=o({getValue:()=>({x:1})});await t.dispose();try{await t.update(e=>{e.x=2}),a("未抛错(不应该到这里)","error")}catch(o){let t=o instanceof e,n=o instanceof Error?o.message:String(o);a(`捕获 LockDisposedError: ${n} (instanceof=${t})`,t?"success":"error"),r(`LockDisposedError: ${n}`)}},doGetLock:async function e(){await c.getLock({holdTimeout:t}),u(),a(`getLock (holdTimeout=NEVER_TIMEOUT) → isHolding=${c.isHolding}`,"success")},doUpdateWhileHolding:async function e(){await c.update(e=>{e.count+=10}),i(),a(`持锁 update → count=${s.count}`,"success")},doRelease:function(){c.release(),u(),a(`release → isHolding=${c.isHolding}`,"success")},doCreateShared:async function e(){let e=await p();m=e.view,b=e.actions,document.getElementById("shared-counter").textContent=String(m.counter),a(`shared 实例已创建 id=playground-shared counter=${m.counter}`,"success")},doSharedUpdate:async function e(){b&&m?(await b.update(e=>{e.counter+=5}),document.getElementById("shared-counter").textContent=String(m.counter),a(`shared update → counter=${m.counter}`,"success")):a("请先创建 shared 实例","warn")},doSharedRead:function(){m?(document.getElementById("shared-counter").textContent=String(m.counter),a(`shared read → counter=${m.counter}`,"info")):a("请先创建 shared 实例","warn")},doSharedDispose:async function e(){b?(await b.dispose(),a("shared 实例已 dispose","success"),b=null,m=null,document.getElementById("shared-counter").textContent="-"):a("请先创建 shared 实例","warn")},doInitCrossTab:async function e(){try{let[e,t]=await o({id:"playground-cross-tab",syncMode:"storage-authority",getValue:()=>({value:0}),listeners:{onSync:e=>{h=e.rev,a(`[跨Tab] onSync rev=${e.rev} source=${e.source} snapshot=${JSON.stringify(e.snapshot)}`,"event"),$()},onCommit:e=>{h=e.rev,a(`[跨Tab] onCommit rev=${e.rev}`,"event"),$()}}});f=e,y=t,$(),a("跨 Tab 实例已初始化(打开多个 Tab 试试!)","success")}catch(t){let e=t instanceof Error?t.message:String(t);a(`跨 Tab 初始化失败: ${e}`,"error")}},doCrossTabUpdate:async function e(){y&&f?(await y.update(e=>{e.value+=1}),$(),a(`跨 Tab update → value=${f.value}`,"success")):a("请先初始化跨 Tab 实例","warn")},doCrossTabRead:function(){f?($(),a(`跨 Tab read → value=${f.value}`,"info")):a("请先初始化跨 Tab 实例","warn")},doCrossTabGetLock:async function e(){if(!y)return void a("[跨Tab] 请先初始化跨 Tab 实例","warn");try{await y.getLock({holdTimeout:t}),$(),a(`[跨Tab] getLock → isHolding=${y.isHolding}(其他 Tab 的 update 会排队等待)`,"success")}catch(t){let e=t instanceof Error?t.message:String(t);a(`[跨Tab] getLock 失败: ${e}`,"error")}},doCrossTabUpdateWhileHolding:async function e(){if(!y)return void a("[跨Tab] 请先初始化跨 Tab 实例","warn");try{await y.update(e=>{e.value+=10}),$(),a(`[跨Tab] 持锁 update → value=${f?.value}`,"success")}catch(t){let e=t instanceof Error?t.message:String(t);a(`[跨Tab] 持锁 update 失败: ${e}`,"error")}},doCrossTabRelease:function(){y?(y.release(),$(),a(`[跨Tab] release → isHolding=${y.isHolding}`,"success")):a("[跨Tab] 请先初始化跨 Tab 实例","warn")},doCrossTabDispose:async function e(){y?(await y.dispose(),a("跨 Tab 实例已 dispose","success"),f=null,y=null,document.getElementById("cross-tab-value").textContent="-",document.getElementById("cross-tab-rev").textContent="-",document.getElementById("cross-tab-holding").textContent="-"):a("请先初始化跨 Tab 实例","warn")},doCreateAbortable:function(){let[e,t]=o({getValue:()=>({count:0}),signal:(v=new AbortController).signal});T=e,E=t,document.getElementById("abortable-count").textContent=String(T.count),a("可取消实例已创建","success")},doAbortableUpdate:async function e(){if(!(E&&T))return void a("请先创建可取消实例","warn");try{await E.update(e=>{e.count+=1}),document.getElementById("abortable-count").textContent=String(T.count),a(`abortable update → count=${T.count}`,"success")}catch(t){let e=t instanceof Error?t.message:String(t);a(`abortable update 失败: ${e}`,"error")}},doAbort:function(){v?(v.abort(),a("已调用 abort(),实例等价 dispose","success")):a("请先创建可取消实例","warn")},doAbortableUpdateAfterAbort:async function t(){if(!E)return void a("请先创建可取消实例","warn");try{await E.update(e=>{e.count+=1}),a("未抛错(不应该到这里)","error")}catch(o){let t=o instanceof e,n=o instanceof Error?o.message:String(o);a(`abort 后 update 捕获 LockDisposedError: ${n} (instanceof=${t})`,t?"success":"error"),r(`LockDisposedError: ${n}`)}},clearLog:function(){document.getElementById("log").innerHTML=""}});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认 AuthorityAdapter 实现:基于 localStorage
|
|
3
|
+
*
|
|
4
|
+
* 职责:作为跨 Tab 权威副本的存储与订阅通道
|
|
5
|
+
* - read:同步读取当前 raw value
|
|
6
|
+
* - write:写入 raw value,捕获 QuotaExceededError 降级为 warn(允许继续运行,
|
|
7
|
+
* 下次 commit 再次尝试;lock-data 的后续 authority 层会依 rev 去重)
|
|
8
|
+
* - remove:删除当前 key
|
|
9
|
+
* - subscribe:订阅跨 Tab 的 `storage` 事件,仅响应同 key 且 storageArea === localStorage 的变更
|
|
10
|
+
*
|
|
11
|
+
* 能力探测:localStorage 不可用(SSR / 浏览器隐私模式 / 禁用三方 cookie)时工厂返回 null,
|
|
12
|
+
* 由聚合层(pickDefaultAdapters)决定是否降级或抛 InvalidOptionsError
|
|
13
|
+
*
|
|
14
|
+
* 对应 RFC.md「接口定义」「默认实现」
|
|
15
|
+
*/
|
|
16
|
+
import type { AuthorityAdapter, AuthorityAdapterContext, LoggerAdapter } from '../types';
|
|
17
|
+
interface AuthorityFactoryDeps {
|
|
18
|
+
/** 由聚合层传入;用于降级 / 异常路径的统一日志 */
|
|
19
|
+
readonly logger: LoggerAdapter;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 能力探测:localStorage 是否可实际读写
|
|
23
|
+
*
|
|
24
|
+
* 仅判断 `typeof localStorage === 'object'` 不够,Safari 隐私模式下 localStorage 存在
|
|
25
|
+
* 但写入会抛 QuotaExceededError;此处用写-删探测法确保真的可用
|
|
26
|
+
*/
|
|
27
|
+
declare function hasUsableLocalStorage(): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* 构建 localStorage 权威副本的完整 key
|
|
30
|
+
*
|
|
31
|
+
* 规范:`${LOCK_PREFIX}:${id}:latest`
|
|
32
|
+
*/
|
|
33
|
+
declare function buildAuthorityKey(id: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* 创建默认 AuthorityAdapter
|
|
36
|
+
*
|
|
37
|
+
* @returns AuthorityAdapter 实例;localStorage 不可用时返回 null
|
|
38
|
+
*/
|
|
39
|
+
declare function createDefaultAuthorityAdapter(ctx: AuthorityAdapterContext, deps: AuthorityFactoryDeps): AuthorityAdapter | null;
|
|
40
|
+
export { buildAuthorityKey, createDefaultAuthorityAdapter, hasUsableLocalStorage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LOCK_PREFIX as e}from"../constants.js";function t(){try{let t=globalThis.localStorage;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}:latest`}function o(e,o){if(!t())return o.logger.warn('localStorage is not available; default authority adapter is disabled. syncMode="storage-authority" will fall back to local-only semantics.'),null;let a=r(e.id),l=globalThis.localStorage;return{read(){try{return l.getItem(a)}catch(e){return o.logger.warn("Failed to read authority snapshot from localStorage",e),null}},write(e){try{l.setItem(a,e)}catch(e){o.logger.warn("Failed to write authority snapshot to localStorage (likely QuotaExceededError); remote Tabs will not see this commit until next successful write.",e)}},remove(){try{l.removeItem(a)}catch(e){o.logger.warn("Failed to remove authority snapshot from localStorage",e)}},subscribe(e){let t=t=>{if(t.storageArea===l&&t.key===a)try{e(t.newValue)}catch(e){o.logger.error("Authority subscribe callback threw",e)}},r=globalThis;return"function"!=typeof r.addEventListener?(o.logger.warn("globalThis.addEventListener is not available; authority subscribe is noop."),()=>void 0):(r.addEventListener("storage",t),()=>{r.removeEventListener?.("storage",t)})}}}export{r as buildAuthorityKey,o as createDefaultAuthorityAdapter,t as hasUsableLocalStorage};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认 ChannelAdapter 实现:基于原生 BroadcastChannel
|
|
3
|
+
*
|
|
4
|
+
* 职责:跨同源 Tab / Worker 的消息广播通道
|
|
5
|
+
* - postMessage:发送消息;已关闭时降级为 warn,不抛错
|
|
6
|
+
* - subscribe:订阅 message 事件;回调异常走 logger.error 隔离
|
|
7
|
+
* - close:幂等关闭,后续操作全部降级 noop
|
|
8
|
+
*
|
|
9
|
+
* 能力探测:`BroadcastChannel` 不可用(SSR / 老浏览器)时工厂返回 null,
|
|
10
|
+
* 由聚合层决定降级路径
|
|
11
|
+
*
|
|
12
|
+
* 对应 RFC.md「接口定义」「默认实现」
|
|
13
|
+
*/
|
|
14
|
+
import type { ChannelAdapter, ChannelAdapterContext, LoggerAdapter } from '../types';
|
|
15
|
+
interface ChannelFactoryDeps {
|
|
16
|
+
readonly logger: LoggerAdapter;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 能力探测:BroadcastChannel 是否可实例化
|
|
20
|
+
*
|
|
21
|
+
* 仅判断构造器存在还不够,某些环境(如部分 Electron 版本 / 早期 Safari)
|
|
22
|
+
* 构造器存在但实例化会抛错;此处用 try-catch 做真实构造探测
|
|
23
|
+
*/
|
|
24
|
+
declare function hasUsableBroadcastChannel(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* 构建 BroadcastChannel 名称
|
|
27
|
+
*
|
|
28
|
+
* 规范:`${LOCK_PREFIX}:${id}:${channel}`
|
|
29
|
+
* - channel === 'session':session-probe / session-reply 协议通道
|
|
30
|
+
* - channel === 'custom':Phase 3 广播驱动等其他业务通道
|
|
31
|
+
*/
|
|
32
|
+
declare function buildChannelName(id: string, channel: ChannelAdapterContext['channel']): string;
|
|
33
|
+
/**
|
|
34
|
+
* 创建默认 ChannelAdapter
|
|
35
|
+
*
|
|
36
|
+
* @returns ChannelAdapter 实例;BroadcastChannel 不可用时返回 null
|
|
37
|
+
*/
|
|
38
|
+
declare function createDefaultChannelAdapter(ctx: ChannelAdapterContext, deps: ChannelFactoryDeps): ChannelAdapter | null;
|
|
39
|
+
export { buildChannelName, createDefaultChannelAdapter, hasUsableBroadcastChannel };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LOCK_PREFIX as e}from"../constants.js";function r(){let r=globalThis.BroadcastChannel;if("function"!=typeof r)return!1;try{return new r(`${e}:__probe__`).close(),!0}catch{return!1}}function a(r,a){return`${e}:${r}:${a}`}function n(e,n){if(!r())return n.logger.warn("BroadcastChannel is not available; default channel adapter is disabled. Cross-tab sync features may fall back to degraded mode."),null;let t=new globalThis.BroadcastChannel(a(e.id,e.channel)),l=!1;return{postMessage(e){if(l||null===t)return void n.logger.warn("postMessage on closed ChannelAdapter is ignored.");try{t.postMessage(e)}catch(e){n.logger.warn("BroadcastChannel.postMessage failed",e)}},subscribe(e){if(l||null===t)return n.logger.warn("subscribe on closed ChannelAdapter is ignored; returning noop unsubscriber."),()=>void 0;let r=r=>{try{e(r.data)}catch(e){n.logger.error("Channel subscribe callback threw",e)}};return t.addEventListener("message",r),()=>{try{t?.removeEventListener("message",r)}catch{}}},close(){if(!l){l=!0;try{t?.close()}catch(e){n.logger.warn("BroadcastChannel.close failed",e)}finally{t=null}}}}}export{a as buildChannelName,n as createDefaultChannelAdapter,r as hasUsableBroadcastChannel};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters 聚合入口:pickDefaultAdapters
|
|
3
|
+
*
|
|
4
|
+
* 职责:把 `options.adapters` 的用户自定义项与默认实现合并,产出"已解析"形态
|
|
5
|
+
* 供 Phase 3+ 消费。合并策略:用户提供 > 默认实现 > null。
|
|
6
|
+
*
|
|
7
|
+
* 输入形态(用户向):工厂函数(getXxx(ctx) => Adapter | null)+ 实例(logger)
|
|
8
|
+
* 输出形态(内部向):工厂保留工厂形态(延迟到使用时调用),但工厂内部自动做"用户工厂返回
|
|
9
|
+
* null 时降级到默认工厂"的合并;实例直接用解析好的值
|
|
10
|
+
*
|
|
11
|
+
* 设计要点:
|
|
12
|
+
* 1. logger 无 id 作用域 —— 直接解析为实例,内部 adapter 构造时可复用同一个 logger,
|
|
13
|
+
* 保证所有降级日志走用户注入的 logger
|
|
14
|
+
* 2. getAuthority / getChannel / getSessionStore 保留工厂形态,因为 id / channel 语义
|
|
15
|
+
* 要在调用点才确定
|
|
16
|
+
* 3. getLock 直接透传(由 Phase 3 drivers 层解释)
|
|
17
|
+
*
|
|
18
|
+
* wrapper 方案契约(与旧 clone 适配器的根本差异):
|
|
19
|
+
* - 历史版本曾通过 `adapters.clone`(structuredClone / 用户自定义)做深拷贝;
|
|
20
|
+
* wrapper 方案下统一改为 `JSON.parse(JSON.stringify(...))`(详见 utils/json-safe.ts),
|
|
21
|
+
* 配合 JSON-only 数据契约(顶层数组禁止 + 非 JSON-safe 类型 fail-fast)确保隔离正确性
|
|
22
|
+
* - 不再支持自定义 clone:用户若需保留 Set / Map / Date 等,应在业务层自行序列化为 JSON 形态
|
|
23
|
+
*
|
|
24
|
+
* 对应 RFC.md「设计原则」:用户提供 > 默认实现 > null
|
|
25
|
+
*/
|
|
26
|
+
import type { AuthorityAdapter, AuthorityAdapterContext, ChannelAdapter, ChannelAdapterContext, LockDataAdapters, SessionStoreAdapter, SessionStoreAdapterContext } from '../types';
|
|
27
|
+
import { type ResolvedLoggerAdapter } from './logger';
|
|
28
|
+
/**
|
|
29
|
+
* 解析后的适配器集合
|
|
30
|
+
*
|
|
31
|
+
* 所有字段均为"可直接调用"形态:
|
|
32
|
+
* - logger:实例
|
|
33
|
+
* - getAuthority / getChannel / getSessionStore:工厂(返回 null 时表示能力不可用)
|
|
34
|
+
* - getLock:原样透传;Phase 3 的 pickDriver 决定是否使用
|
|
35
|
+
*/
|
|
36
|
+
interface ResolvedAdapters<T> {
|
|
37
|
+
readonly logger: ResolvedLoggerAdapter;
|
|
38
|
+
readonly getAuthority: (ctx: AuthorityAdapterContext) => AuthorityAdapter | null;
|
|
39
|
+
readonly getChannel: (ctx: ChannelAdapterContext) => ChannelAdapter | null;
|
|
40
|
+
readonly getSessionStore: (ctx: SessionStoreAdapterContext) => SessionStoreAdapter | null;
|
|
41
|
+
readonly getLock: LockDataAdapters<T>['getLock'];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 合并用户自定义 adapters 与默认实现
|
|
45
|
+
*
|
|
46
|
+
* 合并语义:
|
|
47
|
+
* - logger:`userAdapters.logger` 优先;未提供时用默认 logger
|
|
48
|
+
* - getAuthority:用户工厂存在 → 先调用;返回非 null 直接用;返回 null 时 fallback 到默认工厂
|
|
49
|
+
* 用户未提供 → 直接用默认工厂
|
|
50
|
+
* 默认工厂也返回 null 时表示能力不可用
|
|
51
|
+
* - getChannel / getSessionStore:同 getAuthority
|
|
52
|
+
* - getLock:透传(聚合层不关心其行为)
|
|
53
|
+
*
|
|
54
|
+
* @param userAdapters 用户传入的 `options.adapters`;未传视为空
|
|
55
|
+
*/
|
|
56
|
+
declare function pickDefaultAdapters<T>(userAdapters?: LockDataAdapters<T>): ResolvedAdapters<T>;
|
|
57
|
+
export type { ResolvedAdapters };
|
|
58
|
+
export { pickDefaultAdapters };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createDefaultAuthorityAdapter as t}from"./authority.js";import{createDefaultChannelAdapter as e}from"./channel.js";import{resolveLoggerAdapter as r}from"./logger.js";import{createDefaultSessionStoreAdapter as o}from"./session-store.js";function n(n){let i=n||{},l=r(i.logger),u=i.getAuthority,s=i.getChannel,f=i.getSessionStore;return{logger:l,getAuthority:e=>{if(u){let t=u(e);if(null!==t)return t}return t(e,{logger:l})},getChannel:t=>{if(s){let e=s(t);if(null!==e)return e}return e(t,{logger:l})},getSessionStore:t=>{if(f){let e=f(t);if(null!==e)return e}return o(t,{logger:l})},getLock:i.getLock}}export{n as pickDefaultAdapters};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认 LoggerAdapter 实现 + 用户 logger 兜底合并
|
|
3
|
+
*
|
|
4
|
+
* 委托到 `@/shared/logger` 的全局 logger(console Proxy),
|
|
5
|
+
* 负责把 `LoggerAdapter` 契约的 `(message, ...extras)` 形态
|
|
6
|
+
* 适配为 `shared/logger` 的 `(fnName, ...args)` 形态。
|
|
7
|
+
*
|
|
8
|
+
* - fnName 固定为 `ERROR_FN_NAME`(`lockData`),保证错误消息前缀一致
|
|
9
|
+
* - `globalThis.$lingshu$.disableLogger` 可全局关闭日志输出,无需在此额外处理
|
|
10
|
+
*
|
|
11
|
+
* 对应 RFC.md「默认实现」:默认 LoggerAdapter 委托到 shared/logger
|
|
12
|
+
*
|
|
13
|
+
* **logger 混合兜底契约(Phase 2 决策)**:
|
|
14
|
+
* `LoggerAdapter` 的 `warn` / `error` 为必选、`debug` 为可选。为避免下游链路
|
|
15
|
+
* 在调用 `logger.debug` 时需要反复 guard、且保证"用户覆盖 + 默认补全"的
|
|
16
|
+
* 无缝混合,`resolveLoggerAdapter` 对用户 logger 做缺失方法补全:
|
|
17
|
+
* - 用户 logger 存在且已实现的方法 → 原样使用(保持用户日志目的地)
|
|
18
|
+
* - 用户 logger 未实现的方法 → 走默认 logger(仍带 `[lockData]` 前缀)
|
|
19
|
+
* 输出永远是"三方法齐全"的 logger,`debug` 在下游可直接调用无需 optional chain
|
|
20
|
+
*/
|
|
21
|
+
import type { LoggerAdapter } from '../types';
|
|
22
|
+
/**
|
|
23
|
+
* 必选方法齐全的 logger 内部形态
|
|
24
|
+
*
|
|
25
|
+
* 与 `LoggerAdapter` 的差异:`debug` 由可选变为必选,`resolveLoggerAdapter`
|
|
26
|
+
* 的产物一律满足此形态,供下游(clone / authority / channel / ...)直接调用
|
|
27
|
+
*/
|
|
28
|
+
interface ResolvedLoggerAdapter extends LoggerAdapter {
|
|
29
|
+
debug: NonNullable<LoggerAdapter['debug']>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 创建默认的 LoggerAdapter(三方法齐全)
|
|
33
|
+
*
|
|
34
|
+
* 设计要点:
|
|
35
|
+
* 1. 始终返回一个可用实例,不需要能力探测(console 在所有运行环境存在)
|
|
36
|
+
* 2. `warn` / `error` / `debug` 均委托到全局 logger,统一加 `[@cmtlyt/lingshu-toolkit#lockData]` 前缀
|
|
37
|
+
*/
|
|
38
|
+
declare function createDefaultLogger(): ResolvedLoggerAdapter;
|
|
39
|
+
/**
|
|
40
|
+
* 把"用户 logger(可能部分缺失)"与"默认 logger"混合为"三方法齐全"的 logger
|
|
41
|
+
*
|
|
42
|
+
* 合并规则(字段级):
|
|
43
|
+
* - `warn` / `error` / `debug` 每个方法独立判定:
|
|
44
|
+
* - 用户 logger 的该方法是 `function` → 用用户版本(保持日志目的地)
|
|
45
|
+
* - 否则 → 用默认 logger 的该方法(走 shared/logger 通道,带统一前缀)
|
|
46
|
+
*
|
|
47
|
+
* 为什么不直接 `{ ...defaultLogger, ...userLogger }`:
|
|
48
|
+
* - 用户可能显式传 `debug: undefined`(例:从对象里 pick 字段),这会把默认
|
|
49
|
+
* 的 `debug` 覆盖为 `undefined`;严格按方法类型判定才能避免此类陷阱
|
|
50
|
+
*
|
|
51
|
+
* @param userLogger 用户传入的 logger(未传或为 undefined 等价于"全部走默认")
|
|
52
|
+
* @returns 三方法齐全的 logger;下游可直接调用 `logger.debug(...)` 无需 guard
|
|
53
|
+
*/
|
|
54
|
+
declare function resolveLoggerAdapter(userLogger?: LoggerAdapter): ResolvedLoggerAdapter;
|
|
55
|
+
export type { ResolvedLoggerAdapter };
|
|
56
|
+
export { createDefaultLogger, resolveLoggerAdapter };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{logger as r}from"../../logger/index.js";import{ERROR_FN_NAME as e}from"../constants.js";function n(){return{warn(n,...o){r.warn(e,n,...o)},error(n,...o){r.error(e,n,...o)},debug(n,...o){r.debug(e,n,...o)}}}function o(r){let e=n(),o=r||{};return{warn:"function"==typeof o.warn?o.warn.bind(o):e.warn,error:"function"==typeof o.error?o.error.bind(o):e.error,debug:"function"==typeof o.debug?o.debug.bind(o):e.debug}}export{n as createDefaultLogger,o as resolveLoggerAdapter};
|
|
@@ -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};
|