bff-store 0.1.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 (65) hide show
  1. package/.claude/settings.local.json +45 -0
  2. package/CONTEXT.md +53 -0
  3. package/README.md +223 -0
  4. package/dist/cli.js +32577 -0
  5. package/dist/index.d.mts +232 -0
  6. package/dist/index.d.ts +232 -0
  7. package/dist/index.mjs +430 -0
  8. package/dist/package.json +62 -0
  9. package/dist/server/entry.d.mts +94 -0
  10. package/dist/server/entry.d.ts +94 -0
  11. package/dist/server/entry.js +573 -0
  12. package/dist/server/entry.mjs +533 -0
  13. package/dist/server-V7WCW4ZB.mjs +530 -0
  14. package/dist/storage/jsonl-entry.d.mts +42 -0
  15. package/dist/storage/jsonl-entry.d.ts +42 -0
  16. package/dist/storage/jsonl-entry.js +112 -0
  17. package/dist/storage/jsonl-entry.mjs +74 -0
  18. package/dist/storage/mongodb-entry.d.mts +40 -0
  19. package/dist/storage/mongodb-entry.d.ts +40 -0
  20. package/dist/storage/mongodb-entry.js +114 -0
  21. package/dist/storage/mongodb-entry.mjs +86 -0
  22. package/docs/BUG_DIAGNOSIS_REMOTE_STORAGE_OPTIONS.md +104 -0
  23. package/docs/BUG_FIX_REMOTE_STORAGE_OPTIONS.md +63 -0
  24. package/docs/BUG_FIX_SESSION_2026-06-03.md +171 -0
  25. package/docs/IMPLEMENTATION.md +333 -0
  26. package/docs/PLAN.md +153 -0
  27. package/docs/REMOTE_STORAGE_CONFIG.md +125 -0
  28. package/docs/SIDECAR_SERVER.md +184 -0
  29. package/package.json +62 -0
  30. package/scripts/adapt-dist-package.js +33 -0
  31. package/src/atomCreator.ts +76 -0
  32. package/src/createStore.ts +77 -0
  33. package/src/debouncer.ts +84 -0
  34. package/src/index.ts +35 -0
  35. package/src/server/cli.ts +62 -0
  36. package/src/server/entityIdCache.ts +57 -0
  37. package/src/server/entry.ts +12 -0
  38. package/src/server/handlers.ts +271 -0
  39. package/src/server/index.ts +182 -0
  40. package/src/server/router.ts +74 -0
  41. package/src/server.ts +5 -0
  42. package/src/storage/adapters/remoteStorage.ts +70 -0
  43. package/src/storage/base.ts +28 -0
  44. package/src/storage/index.ts +9 -0
  45. package/src/storage/jsonl-entry.ts +9 -0
  46. package/src/storage/jsonl.ts +111 -0
  47. package/src/storage/memory.ts +49 -0
  48. package/src/storage/mongodb-entry.ts +9 -0
  49. package/src/storage/mongodb.ts +132 -0
  50. package/src/storage/protocol.ts +170 -0
  51. package/src/storage/transport.ts +95 -0
  52. package/src/types.ts +76 -0
  53. package/src/useStore.ts +83 -0
  54. package/tests/atomCreator.test.ts +153 -0
  55. package/tests/createStore.test.ts +126 -0
  56. package/tests/debouncer.test.ts +125 -0
  57. package/tests/server.test.ts +158 -0
  58. package/tests/storage/jsonl.test.ts +132 -0
  59. package/tests/storage/memory.test.ts +101 -0
  60. package/tests/storage/mongodb.test.ts +40 -0
  61. package/tests/storage/remoteStorage.test.ts +126 -0
  62. package/tests/useStore.test.tsx +147 -0
  63. package/tsconfig.json +18 -0
  64. package/tsup.config.ts +53 -0
  65. package/vitest.config.ts +14 -0
@@ -0,0 +1,171 @@
1
+ # Bug Fix Session — 2026-06-03
2
+
3
+ 本次会话诊断并修复了 4 个 bug,覆盖类型解析、协议层、存储适配器三个层面。
4
+
5
+ ---
6
+
7
+ ## Bug 1 (P0): `'backend' does not exist in type 'RemoteStorageOptions'`
8
+
9
+ ### 现象
10
+
11
+ `tests/next_test` 中 `"bff-store": "file:../../dist"` 安装时,TypeScript 报错:
12
+
13
+ ```
14
+ Object literal may only specify known properties, and 'backend' does not exist in type 'RemoteStorageOptions'.ts(2353)
15
+ ```
16
+
17
+ ### 根因
18
+
19
+ **因素一(主因):dist/package.json 路径错误。** 根目录 `package.json` 的路径字段带有 `./dist/` 前缀:
20
+
21
+ ```json
22
+ "main": "dist/index.js",
23
+ "exports": { ".": { "types": "./dist/index.d.ts", ... } }
24
+ ```
25
+
26
+ 当通过 `"file:../../dist"` 本地安装时,包根目录就是 `dist/` 本身,`./dist/index.d.ts` 解析为 `dist/dist/index.d.ts` —— 文件不存在。
27
+
28
+ | 场景 | 包根目录 | `./dist/index.d.ts` 解析 | 存在? |
29
+ |------|----------|-------------------------|------|
30
+ | npm 发布 | 项目根 | `项目根/dist/index.d.ts` | 是 |
31
+ | `file:../../dist` | `dist/` | `dist/dist/index.d.ts` | 否 |
32
+
33
+ **因素二(助推):全局安装了旧版本。** 本地 symlink 解析失败后,TypeScript 沿目录树向上查找,在 `/Users/Admin/node_modules/bff-store/` 找到旧版 v0.1.1,该版本 `RemoteStorageOptions` 无 `backend` 字段。
34
+
35
+ ### 修复
36
+
37
+ 1. **新增** `scripts/adapt-dist-package.js` — 构建后将 `dist/package.json` 路径改写为相对 dist/:
38
+ - `dist/index.js` → `index.js`
39
+ - `./dist/index.d.ts` → `./index.d.ts`
40
+ - `./dist/storage/...` → `./storage/...`
41
+
42
+ 2. **修改** `package.json` build 脚本:
43
+ ```
44
+ "build": "tsup && cp package.json dist/package.json && node scripts/adapt-dist-package.js && for f in dist/*.d.mts; do cp \"$f\" \"${f%.d.mts}.d.ts\"; done"
45
+ ```
46
+
47
+ ### 涉及文件
48
+
49
+ | 文件 | 操作 |
50
+ |------|------|
51
+ | `scripts/adapt-dist-package.js` | 新增 |
52
+ | `package.json` | 修改 build 脚本 |
53
+
54
+ ---
55
+
56
+ ## Bug 2 (P0): GET/DELETE 请求拿不到 `mongoUrl`
57
+
58
+ ### 现象
59
+
60
+ BFF 模式下,POST (`/storage/set`) 正常,但 GET (`/storage/get`) 返回:
61
+
62
+ ```
63
+ {"error":"Error: mongoUrl is required for mongodb backend"}
64
+ ```
65
+
66
+ ### 根因
67
+
68
+ `src/storage/protocol.ts` 的 `buildUrl` / `buildBatchGetUrl` / `buildBatchSetUrl` 方法只将 `backend` 和 `entityId` 拼入 URL query params,`mongoUrl`、`mongoDb`、`jsonlDir` 仅通过 POST body 传递(`createStorageWithProtocol` 中将 `backendConfig` spread 到 body)。
69
+
70
+ GET/DELETE 无 body → server 端 `resolveStorage` 从 URL query params 解析不到 `mongoUrl` → 尝试 `mongodbStorage({ url: undefined })` → 抛错。
71
+
72
+ ### 修复
73
+
74
+ 抽取 `appendBackendParams()` 方法,将全部 backend 参数拼入 URL query params:
75
+
76
+ ```typescript
77
+ private appendBackendParams(params: URLSearchParams): void {
78
+ if (this.backendConfig.backend) params.set('backend', this.backendConfig.backend);
79
+ if (this.backendConfig.mongoUrl) params.set('mongoUrl', this.backendConfig.mongoUrl);
80
+ if (this.backendConfig.mongoDb) params.set('mongoDb', this.backendConfig.mongoDb);
81
+ if (this.backendConfig.jsonlDir) params.set('jsonlDir', this.backendConfig.jsonlDir);
82
+ }
83
+ ```
84
+
85
+ ### 涉及文件
86
+
87
+ | 文件 | 操作 |
88
+ |------|------|
89
+ | `src/storage/protocol.ts` | 新增 `appendBackendParams()`,`buildUrl` / `buildBatchGetUrl` / `buildBatchSetUrl` 调用之 |
90
+
91
+ ---
92
+
93
+ ## Bug 3 (P2): JSONL adapter 静默失败(entityId 默认 null)
94
+
95
+ ### 现象
96
+
97
+ BFF server 使用 JSONL backend 时,SET 返回 `{"success":true}` 但数据未写入磁盘,GET 返回 `{"value":null}`。
98
+
99
+ ### 根因
100
+
101
+ `src/storage/jsonl.ts` 中 `entityId` 初始值为 `null`,GET 和 SET 操作均有 `if (!entityId) return` 守卫,导致静默跳过。对比 MongoDB adapter 默认 `currentEntityId = 'default'`,行为不一致。
102
+
103
+ ### 修复
104
+
105
+ ```typescript
106
+ // Before
107
+ let entityId: string | null = null;
108
+
109
+ // After
110
+ let entityId: string = 'default';
111
+ ```
112
+
113
+ ### 涉及文件
114
+
115
+ | 文件 | 操作 |
116
+ |------|------|
117
+ | `src/storage/jsonl.ts` | `entityId` 默认值 `null` → `'default'` |
118
+
119
+ ---
120
+
121
+ ## Bug 4 (P2): MongoDB 副本集连接失败
122
+
123
+ ### 现象
124
+
125
+ BFF server 连接 MongoDB 时报:
126
+
127
+ ```
128
+ MongoServerSelectionError: connection <monitor> to 172.16.42.101:27017 closed
129
+ TopologyDescription { type: 'ReplicaSetNoPrimary', setName: 'rs0' }
130
+ ```
131
+
132
+ ### 根因
133
+
134
+ `localhost:27017` 属于副本集 `rs0`,成员地址 `172.16.42.101:27017`。MongoDB driver 自动发现副本集后切换到该地址,但该地址网络不通。
135
+
136
+ ### 修复
137
+
138
+ 在 MongoDB URL 中拼接 `?directConnection=true&authSource=admin` 绕过副本集发现。
139
+
140
+ ```typescript
141
+ const mongoUrl = `mongodb://${user}:${pwd}@${host}/${db}?directConnection=true&authSource=admin`;
142
+ ```
143
+
144
+ ### 涉及文件
145
+
146
+ | 文件 | 操作 |
147
+ |------|------|
148
+ | `tests/next_test/src/app/page.tsx` | 添加 URL 参数 |
149
+
150
+ ---
151
+
152
+ ## 测试覆盖
153
+
154
+ | 文件 | 内容 |
155
+ |------|------|
156
+ | `tests/next_test/test-types.ts` | 原始 bug 用例 — `backend: 'jsonl'` |
157
+ | `tests/next_test/test-similar-root.tsx` | 同上 .tsx 版本 |
158
+ | `tests/next_test/test-regression-remote-storage.ts` | 全参数组合:jsonl / mongodb / 可选字段 / 默认构造 |
159
+ | `tests/next_test/src/app/test-similar.tsx` | .tsx 中 remoteStorage 类型测试 |
160
+ | `tests/next_test/src/app/test-regression-store.tsx` | createStore + remoteStorage 组合测试 |
161
+ | `tests/next_test/src/app/test-mongodb.tsx` | MongoDB 前端组件测试 |
162
+ | `tests/next_test/test-mongodb-direct.ts` | mongodbStorage 直接调用测试脚本 |
163
+ | `tests/next_test/test-mongodb-remote.ts` | remoteStorage → MongoDB 路由测试脚本 |
164
+ | `tests/next_test/src/app/page.tsx` | 浏览器端到端测试(可切换 jsonl/mongodb) |
165
+
166
+ ## 验证结果
167
+
168
+ - `npm run build`(项目根):构建成功
169
+ - `npx tsc --noEmit`(tests/next_test):0 errors
170
+ - `curl` 测试 JSONL SET/GET/DELETE 全链路:通过
171
+ - `curl` 测试 MongoDB SET/GET/DELETE 全链路:通过
@@ -0,0 +1,333 @@
1
+ # 核心实现详解
2
+
3
+ ## 1. Storage 接口
4
+
5
+ ```typescript
6
+ // src/storage/base.ts
7
+ export interface Storage {
8
+ get<T>(key: string): Promise<T | null>;
9
+ set<T>(key: string, value: T): Promise<void>;
10
+ remove(key: string): Promise<void>;
11
+ getMultiple?<T>(keys: string[]): Promise<Map<string, T>>; // 可选:批量读取
12
+ setMultiple?<T>(entries: Map<string, T>): Promise<void>; // 可选:批量写入
13
+ }
14
+ ```
15
+
16
+ 所有存储适配器都实现此接口,实现存储层的可插拔。
17
+
18
+ ### 1.1 JSONL 存储
19
+
20
+ **文件结构**: `{dir}/{entityId}/{key}.jsonl`
21
+
22
+ 每行一条 JSON,方便追溯历史和追加写入:
23
+
24
+ ```jsonl
25
+ {"key":"theme","value":"科幻小说","timestamp":1704067200000}
26
+ {"key":"theme","value":"奇幻小说","timestamp":1704067201000}
27
+ ```
28
+
29
+ 读取时取最后一行(最新值),写入时 append。
30
+
31
+ ```typescript
32
+ // src/storage/jsonl.ts
33
+ export function jsonlStorage(options?: JsonlStorageOptions): JsonlStorageInstance {
34
+ const baseDir = options?.dir ?? './sessions';
35
+
36
+ const storage: Storage = {
37
+ async get<T>(key: string): Promise<T | null> {
38
+ const filePath = getFilePath(entityId, key);
39
+ if (!fs.existsSync(filePath)) return null;
40
+
41
+ const content = fs.readFileSync(filePath, 'utf-8');
42
+ const lines = content.trim().split('\n').filter(Boolean);
43
+ if (lines.length === 0) return null;
44
+
45
+ // 取最后一行
46
+ const lastLine = lines[lines.length - 1];
47
+ const entry: JsonlEntry = JSON.parse(lastLine);
48
+ return entry.value as T;
49
+ },
50
+
51
+ async set<T>(key: string, value: T): Promise<void> {
52
+ const filePath = getFilePath(entityId, key);
53
+ const entry = { key, value, timestamp: Date.now() };
54
+ const line = JSON.stringify(entry) + '\n';
55
+ fs.appendFileSync(filePath, line, 'utf-8');
56
+ },
57
+ };
58
+
59
+ return { storage, name: 'jsonl', setEntityId };
60
+ }
61
+ ```
62
+
63
+ ### 1.2 MongoDB 存储
64
+
65
+ 使用 MongoDB 的 append-only 模式,每次写入插入新文档,读取时取最新:
66
+
67
+ ```typescript
68
+ // src/storage/mongodb.ts
69
+ const storage: Storage = {
70
+ async get<T>(key: string): Promise<T | null> {
71
+ const entry = await collection.findOne(
72
+ { key },
73
+ { sort: { timestamp: -1 } } // 按时间倒序,取第一条
74
+ );
75
+ return entry?.value as T ?? null;
76
+ },
77
+
78
+ async set<T>(key: string, value: T): Promise<void> {
79
+ await collection.insertOne({
80
+ key,
81
+ value,
82
+ timestamp: Date.now(),
83
+ });
84
+ },
85
+ };
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 2. Atom 创建器
91
+
92
+ ### 2.1 核心逻辑
93
+
94
+ ```typescript
95
+ // src/atomCreator.ts
96
+ export function createPersistedAtom<T>(
97
+ config: AtomConfig<T>,
98
+ storage: Storage,
99
+ options?: { immediate?: boolean; debounceMs?: number }
100
+ ): PersistedAtomWithLoading<T> {
101
+ // 1. 创建基础 atom
102
+ const baseAtom = atom<T>(config.defaultValue);
103
+ const loadingAtom = atom<boolean>(true);
104
+
105
+ // 2. onMount 时从 storage 加载初始值
106
+ baseAtom.onMount = (setValue) => {
107
+ storage.get<T>(config.key)
108
+ .then((value) => {
109
+ if (value !== null) setValue(value);
110
+ })
111
+ .finally(() => {
112
+ store.set(loadingAtom, false); // 标记加载完成
113
+ });
114
+ };
115
+
116
+ // 3. 创建写 atom,自动持久化
117
+ const writeAtom = atom(
118
+ (get) => get(baseAtom),
119
+ (get, set, update) => {
120
+ const newValue = typeof update === 'function'
121
+ ? update(get(baseAtom))
122
+ : update;
123
+
124
+ set(baseAtom, newValue); // 先更新本地
125
+
126
+ // 保存到 storage
127
+ if (options?.immediate) {
128
+ storage.set(config.key, newValue); // 立即保存
129
+ } else {
130
+ debouncedSave(config.key, newValue); // 防抖保存
131
+ }
132
+ }
133
+ );
134
+
135
+ return { atom: writeAtom, loadingAtom };
136
+ }
137
+ ```
138
+
139
+ ### 2.2 防抖保存
140
+
141
+ 使用 WeakMap 缓存每个 atom 的 debounced 函数:
142
+
143
+ ```typescript
144
+ const debouncedSaveMap = new WeakMap<object, { timer: Timer; fn: Function }>();
145
+
146
+ function debounce(fn: Function, ms: number): Function {
147
+ let timer: Timer | null = null;
148
+ return (...args) => {
149
+ if (timer) clearTimeout(timer);
150
+ timer = setTimeout(() => {
151
+ fn(...args);
152
+ timer = null;
153
+ }, ms);
154
+ };
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## 3. Store 工厂
161
+
162
+ ### 3.1 createStore
163
+
164
+ 批量创建 atoms:
165
+
166
+ ```typescript
167
+ // src/createStore.ts
168
+ export function createStore(
169
+ entityId: string,
170
+ config: AtomConfigs,
171
+ options?: { storage: Storage; debounceMs?: number }
172
+ ): Store {
173
+ const storage = options.storage;
174
+ const debounceMs = options.debounceMs ?? 800;
175
+
176
+ const atoms = {};
177
+ const loadingAtoms = {};
178
+
179
+ for (const atomConfig of config) {
180
+ const result = createPersistedAtom(atomConfig, storage, {
181
+ immediate: atomConfig.immediate,
182
+ debounceMs,
183
+ });
184
+ atoms[atomConfig.key] = result.atom;
185
+ loadingAtoms[atomConfig.key] = result.loadingAtom;
186
+ }
187
+
188
+ return { entityId, config, atoms, loadingAtoms };
189
+ }
190
+ ```
191
+
192
+ ### 3.2 原子配置
193
+
194
+ ```typescript
195
+ interface AtomConfig<T = unknown> {
196
+ key: string; // 状态键名
197
+ defaultValue: T; // 默认值
198
+ type?: 'string' | 'number' | 'boolean' | 'array' | 'object';
199
+ immediate?: boolean; // 是否立即保存(不用 debounce)
200
+ }
201
+ ```
202
+
203
+ `immediate: true` 用于 critical data(如 chapterOutline),避免 debounce 延迟。
204
+
205
+ ---
206
+
207
+ ## 4. useStore Hook
208
+
209
+ ### 4.1 核心实现
210
+
211
+ ```typescript
212
+ // src/useStore.ts
213
+ export function useStore(store: Store): UseStoreReturn {
214
+ const [isLoading, setIsLoading] = useState(true);
215
+
216
+ // 订阅所有 loading atoms
217
+ useEffect(() => {
218
+ const storeInstance = getDefaultStore();
219
+ const loadingAtoms = Object.values(store.loadingAtoms);
220
+
221
+ const checkLoadingStatus = () => {
222
+ const states = loadingAtoms.map(atom => storeInstance.get(atom));
223
+ setIsLoading(states.some(s => s === true));
224
+ };
225
+
226
+ checkLoadingStatus();
227
+
228
+ // 订阅变化
229
+ const unsubscribers = loadingAtoms.map(atom =>
230
+ storeInstance.sub(atom, checkLoadingStatus)
231
+ );
232
+
233
+ return () => unsubscribers.forEach(unsub => unsub());
234
+ }, [store.loadingAtoms]);
235
+
236
+ // 为每个 atom 调用 useAtom
237
+ const result = {};
238
+ for (const config of store.config) {
239
+ const [value, setter] = useAtom(store.atoms[config.key]);
240
+ result[config.key] = value;
241
+ result[`set${capitalize(config.key)}`] = setter;
242
+ }
243
+
244
+ return { ...result, isLoading };
245
+ }
246
+ ```
247
+
248
+ ### 4.2 使用示例
249
+
250
+ ```typescript
251
+ const config = [
252
+ { key: 'theme', defaultValue: '' },
253
+ { key: 'characters', defaultValue: [] },
254
+ ] as const;
255
+
256
+ const store = createStore('novel-123', config, { storage });
257
+
258
+ function NovelEditor() {
259
+ const { theme, characters, setTheme, setCharacters, isLoading } = useStore(store);
260
+
261
+ if (isLoading) return <Loading />;
262
+
263
+ return (
264
+ <div>
265
+ <input value={theme} onChange={e => setTheme(e.target.value)} />
266
+ <CharacterList characters={characters} onUpdate={setCharacters} />
267
+ </div>
268
+ );
269
+ }
270
+ ```
271
+
272
+ ---
273
+
274
+ ## 5. 类型系统
275
+
276
+ ### 5.1 类型导出
277
+
278
+ ```typescript
279
+ // src/types.ts
280
+ export interface Store {
281
+ entityId: string;
282
+ config: AtomConfigs;
283
+ atoms: StoreAtoms;
284
+ loadingAtoms: StoreLoadingAtoms;
285
+ }
286
+
287
+ export type UseStoreReturn = Record<string, any> & { isLoading: boolean };
288
+ ```
289
+
290
+ ### 5.2 类型安全
291
+
292
+ 虽然使用了 `as const` 和泛型,但由于 JavaScript 动态特性,返回类型使用 `Record<string, any>` 保证灵活性。
293
+
294
+ ---
295
+
296
+ ## 6. 数据流
297
+
298
+ ```
299
+ 用户操作
300
+
301
+
302
+ useStore Hook
303
+
304
+ ├── useAtom(store.atoms.xxx) → 更新 React 组件
305
+
306
+
307
+ writeAtom (jotai)
308
+
309
+ ├── set(baseAtom, newValue) → 立即更新本地状态
310
+
311
+
312
+ debouncedSave / storage.set()
313
+
314
+
315
+ Storage Adapter (JSONL / MongoDB / Memory)
316
+
317
+
318
+ 持久化存储
319
+ ```
320
+
321
+ ---
322
+
323
+ ## 7. 与原系统对比
324
+
325
+ | 特性 | 原系统 (novel-simple-persist.ts) | 新系统 (bff-store) |
326
+ |------|--------------------------------|--------------------------|
327
+ | 配置方式 | 硬编码 ATOM_CONFIGS | 数组配置,通用 |
328
+ | 存储层 | API 调用 (enhancedNovelClientAPI) | 可插拔 Adapter |
329
+ | 单例模式 | NovelAtomsSingleton 强制单例 | 可选,不强制 |
330
+ | MongoDB | 通过 API | 直连 |
331
+ | JSONL | 无 | 原生支持 |
332
+ | 加载状态 | loadingAtoms | loadingAtoms |
333
+ | Debounce | 800ms | 可配置 |
package/docs/PLAN.md ADDED
@@ -0,0 +1,153 @@
1
+ # Plan: 提取 Jotai 状态管理为独立工具包 `bff-store`
2
+
3
+ ## Context
4
+
5
+ 当前项目 `pro_novel` 中有一套基于 jotai 的状态管理系统,分散在 `persist.ts` 和 `novel-simple-persist.ts` 中。存在以下问题:
6
+ - 与业务强耦合,难以跨项目复用
7
+ - 存储层耦合了 API(`enhancedNovelClientAPI`)
8
+ - 单例模式固定,难以按需使用
9
+
10
+ **目标**: 提取为独立 npm 包,配置驱动批量创建 atoms,存储层可插拔(默认 JSONL)。
11
+
12
+ ---
13
+
14
+ ## 核心 API 设计
15
+
16
+ ```typescript
17
+ // 1. 状态配置数组 - 一次定义多个状态
18
+ const config = [
19
+ { key: 'theme', defaultValue: '' },
20
+ { key: 'characters', defaultValue: [] },
21
+ { key: 'chapters', defaultValue: [] },
22
+ ] as const;
23
+
24
+ // 2. 创建 store 实例
25
+ const store = createStore('novel-123', config, {
26
+ storage: jsonlStorage({ dir: './sessions' }), // 默认
27
+ // storage: mongodbStorage({ url: 'mongodb://...' }),
28
+ debounceMs: 800,
29
+ });
30
+
31
+ // 3. React Hook 使用
32
+ function NovelEditor() {
33
+ const { theme, characters, setTheme, setCharacters } = useStore(store);
34
+ }
35
+
36
+ // 4. 非 React 环境直接操作 atoms
37
+ store.atoms.theme.set('new theme');
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 目录结构
43
+
44
+ ```
45
+ bff-store/
46
+ ├── src/
47
+ │ ├── index.ts # 公共 API 导出
48
+ │ ├── types.ts # 类型定义
49
+ │ ├── createStore.ts # 核心工厂函数
50
+ │ ├── useStore.ts # React Hook
51
+ │ ├── atomCreator.ts # 单个 atom 创建逻辑
52
+ │ └── storage/
53
+ │ ├── base.ts # Storage interface
54
+ │ ├── memory.ts # 内存存储(开发用)
55
+ │ ├── jsonl.ts # JSONL 文件存储(默认)
56
+ │ └── mongodb.ts # MongoDB 存储(可选)
57
+ ├── docs/
58
+ │ └── PLAN.md # 本文档
59
+ ├── package.json
60
+ └── README.md
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 实现步骤
66
+
67
+ ### Step 1: 创建基础类型和接口 (`types.ts`, `storage/base.ts`)
68
+
69
+ - 定义 `AtomConfig` - 单个状态配置 `{ key, defaultValue, type?, immediate? }`
70
+ - 定义 `Storage` interface - `get(key)`, `set(key, value)`, `remove(key)`
71
+ - 定义 `StoreOptions` - 存储适配器、debounce 配置
72
+
73
+ ### Step 2: 实现存储适配器 (`storage/`)
74
+
75
+ | 文件 | 职责 |
76
+ |------|------|
77
+ | `memory.ts` | 内存 Map,简单开发/测试用 |
78
+ | `jsonl.ts` | JSONL 文件存储,按 entityId 分文件,`sessions/{entityId}/{key}.jsonl` |
79
+ | `mongodb.ts` | MongoDB 存储,collection per entityId |
80
+
81
+ **JSONL 格式** (每行一条 JSON):
82
+ ```
83
+ {"key":"theme","value":"科幻小说","timestamp":1704067200000}
84
+ {"key":"theme","value":"奇幻小说","timestamp":1704067201000}
85
+ ```
86
+
87
+ ### Step 3: 实现 atom 创建器 (`atomCreator.ts`)
88
+
89
+ - `createPersistedAtom(config, storage, options)` → 返回 `{ atom, loadingAtom }`
90
+ - onMount 时从 storage 加载初始值
91
+ - 写入时 debounce 保存到 storage
92
+
93
+ ### Step 4: 实现 store 工厂 (`createStore.ts`)
94
+
95
+ - 输入: `entityId`, `config[]`, `options`
96
+ - 批量创建 atoms,返回 `{ atoms, loadingAtoms }`
97
+ - 支持分片并发创建(复用现有 `createAtomsInChunks` 逻辑)
98
+
99
+ ### Step 5: 实现 React Hook (`useStore.ts`)
100
+
101
+ - `useStore(store)` - 自动订阅所有 atoms
102
+ - 返回 `{ ...data, ...setters, isLoading }`
103
+ - 优化: useMemo 缓存返回值,避免不必要的 re-render
104
+
105
+ ### Step 6: 导出公共 API (`index.ts`)
106
+
107
+ ```typescript
108
+ export { createStore, useStore };
109
+ export { jsonlStorage, mongodbStorage, memoryStorage };
110
+ export type { AtomConfig, Store, Storage, StoreOptions };
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 关键文件修改
116
+
117
+ | 文件 | 操作 |
118
+ |------|------|
119
+ | `src/app/store/persist.ts` | 保留基础版,工具包独立后可删除 |
120
+ | `src/app/store/novel-simple-persist.ts` | 逐步迁移到工具包 |
121
+
122
+ ---
123
+
124
+ ## 验证方案
125
+
126
+ 1. **单元测试**: `vitest` 测试 atom 创建、存储适配器
127
+ 2. **集成测试**: 创建 mock store,验证 CRUD + Hook 联动
128
+ 3. **手动验证**: 在现有项目中引入工具包,替换 `novel-simple-persist.ts`
129
+
130
+ ---
131
+
132
+ ## 实现状态
133
+
134
+ ✅ 已完成:
135
+ - [x] 项目脚手架 (package.json, tsconfig.json)
136
+ - [x] 类型定义 (types.ts)
137
+ - [x] 存储接口 (storage/base.ts)
138
+ - [x] 存储适配器 (memory.ts, jsonl.ts, mongodb.ts)
139
+ - [x] atom 创建器 (atomCreator.ts)
140
+ - [x] store 工厂 (createStore.ts)
141
+ - [x] React Hook (useStore.ts)
142
+ - [x] 公共 API 导出 (index.ts)
143
+ - [x] 构建配置和输出
144
+ - [x] README.md
145
+
146
+ ---
147
+
148
+ ## 下一步
149
+
150
+ 1. 编写单元测试 (vitest)
151
+ 2. 在 pro_novel 项目中引入工具包
152
+ 3. 改造 `novel-simple-persist.ts` 使用新工具包
153
+ 4. 考虑添加更多存储适配器 (IndexedDB, Redis 等)