bff-store 0.1.0 → 0.1.2

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 (54) hide show
  1. package/.claude/settings.local.json +14 -1
  2. package/README.md +196 -63
  3. package/dist/cli.js +11 -9
  4. package/dist/index.d.mts +48 -1
  5. package/dist/index.d.ts +48 -1
  6. package/dist/index.mjs +88 -10
  7. package/dist/package.json +3 -6
  8. package/dist/server/entry.d.mts +1 -1
  9. package/dist/server/entry.d.ts +1 -1
  10. package/dist/server/entry.js +31992 -23
  11. package/dist/server/entry.mjs +32016 -13
  12. package/dist/{server-V7WCW4ZB.mjs → server-2WMLXQ23.mjs} +11 -9
  13. package/dist/storage/jsonl-entry.js +1 -1
  14. package/dist/storage/jsonl-entry.mjs +1 -1
  15. package/dist/storage/mongodb-entry.js +31986 -10
  16. package/dist/storage/mongodb-entry.mjs +32008 -8
  17. package/docs/BUG_FIX_MONGODB_CHILD_PROCESS.md +129 -0
  18. package/docs/FRONTEND_BACKEND_ISOLATION.md +150 -0
  19. package/docs/IMPLEMENTATION_OVERVIEW.md +231 -0
  20. package/docs/LOCAL_LINK_STRATEGIES.md +117 -0
  21. package/docs/bugs/01-server-async-startup.high.md +34 -0
  22. package/docs/bugs/03-storage-cache-eviction.md +40 -0
  23. package/docs/bugs/04-http-error-body-loss.md +38 -0
  24. package/docs/bugs/05-debouncer-ms-ignored.md +41 -0
  25. package/docs/bugs/07-jsonl-key-collision.high.md +31 -0
  26. package/docs/bugs/08-dead-code-getStorageForRequest.md +18 -0
  27. package/docs/bugs/09-waitForLoad-infinite.md +37 -0
  28. package/docs/bugs/10-parseBody-silent-failure.md +32 -0
  29. package/docs/bugs/11-mongodb-storage-leak.high.md +35 -0
  30. package/docs/bugs/12-setEntityId-stale-closure.high.md +35 -0
  31. package/docs/bugs/14-dead-code-isServerRunning.md +18 -0
  32. package/docs/bugs/15-unmount-data-loss.md +37 -0
  33. package/docs/bugs/17-unused-parameter.md +26 -0
  34. package/docs/bugs/18-storage-get-silent-error.md +28 -0
  35. package/docs/bugs/README.md +35 -0
  36. package/package.json +3 -6
  37. package/src/atomCreator.ts +10 -2
  38. package/src/createStore.ts +19 -1
  39. package/src/debouncer.ts +5 -2
  40. package/src/environment.ts +11 -0
  41. package/src/index.ts +8 -1
  42. package/src/nodeStore.ts +86 -0
  43. package/src/server/handlers.ts +7 -24
  44. package/src/server/index.ts +0 -4
  45. package/src/storage/adapters/remoteStorage.ts +2 -1
  46. package/src/storage/jsonl.ts +3 -1
  47. package/src/storage/mongodb.ts +6 -6
  48. package/src/storage/transport.ts +24 -3
  49. package/tests/storage/jsonl.test.ts +18 -2
  50. package/tsconfig.json +1 -0
  51. package/docs/BUG_DIAGNOSIS_REMOTE_STORAGE_OPTIONS.md +0 -104
  52. package/docs/BUG_FIX_REMOTE_STORAGE_OPTIONS.md +0 -63
  53. package/docs/BUG_FIX_SESSION_2026-06-03.md +0 -171
  54. /package/docs/{SIDECAR_SERVER.md → BFF_SIDECAR_SERVER.md} +0 -0
@@ -0,0 +1,129 @@
1
+ # BUG: child_process / mongodb 错误
2
+
3
+ **日期**: 2026-06-03
4
+
5
+ ## 问题现象
6
+
7
+ Next.js 构建时报错:
8
+ ```
9
+ Module not found: Can't resolve 'child_process'
10
+ ./tests/next_test/node_modules/mongodb/lib/client-side-encryption/mongocryptd_manager.js:38
11
+ const { spawn } = require('child_process');
12
+ ```
13
+
14
+ ## 根因分析
15
+
16
+ **不是本次改动引起的,是之前就存在的依赖架构问题。**
17
+
18
+ ### 1. bff-store 的 `package.json` 把 mongodb 放在 `dependencies`
19
+
20
+ ```json
21
+ // bff-store/package.json
22
+ {
23
+ "dependencies": {
24
+ "mongodb": "^6.21.0"
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### 2. next_test 通过 `file:` 路径安装 bff-store
30
+
31
+ ```json
32
+ // next_test/package.json
33
+ {
34
+ "bff-store": "file:../../dist"
35
+ }
36
+ ```
37
+
38
+ ### 3. npm install 时把 mongodb 也装进了 next_test/node_modules
39
+
40
+ ```
41
+ next_test/node_modules/mongodb/ ← 被安装了
42
+ ```
43
+
44
+ ### 4. Next.js/Webpack 在静态分析时解析到了 mongodb
45
+
46
+ 即使 `dist/index.mjs` 实际不引用 mongodb(`tsup external: ['mongodb']`),但 Next.js 的 bundler 在分析依赖时看到了 `node_modules/mongodb`,尝试解析它,然后触发了 `child_process` 的引用。
47
+
48
+ ## 验证
49
+
50
+ - `dist/index.mjs` 实际 bundle:干净(0 个 mongodb 引用)
51
+ - tsup 的 external 配置:正确(mongodb 被 external)
52
+ - 清理 next_test 的 `node_modules` 和 `package-lock.json` 后重装,mongodb 没再被安装,构建成功
53
+
54
+ ## 结论
55
+
56
+ | 环节 | 状态 |
57
+ |------|------|
58
+ | 本次新增代码(environment.ts、nodeStore.ts)| 无问题 |
59
+ | `dist/index.mjs` 实际 bundle | 干净(0 个 mongodb 引用)|
60
+ | tsup 的 external 配置 | 正确(mongodb 被 external)|
61
+ | **根本原因** | `package.json` 的 `dependencies` 里有 mongodb |
62
+
63
+ 这是之前就存在的问题,只是以前 next_test 可能没走到触发这个解析的代码路径。
64
+
65
+ ## 修复(2026-06-03)
66
+
67
+ 已将 `mongodb` 从 `package.json` 的 `dependencies` 移到 `devDependencies`。
68
+
69
+ **原因**:
70
+ - `tsup` 构建 `mongodb-entry.ts` 时会将 mongodb 打包进 `dist/storage/mongodb-entry.js`
71
+ - 根 `package.json` 的 `dependencies` 不需要 mongodb
72
+ - npm install 时不会为消费者安装 devDependencies,所以 next_test 不再被强制安装 mongodb
73
+
74
+ **修复后验证**:
75
+ - next_test 重新 `npm install` 后,`node_modules/mongodb` 不存在
76
+ - Next.js 构建成功,无 `child_process` 错误
77
+
78
+ ---
79
+
80
+ ## 问题二:Can't resolve 'bff-store'
81
+
82
+ **日期**: 2026-06-03
83
+
84
+ ### 问题现象
85
+
86
+ Next.js dev 时报错:
87
+ ```
88
+ Module not found: Can't resolve 'bff-store'
89
+ import { createStore, useStore, remoteStorage } from 'bff-store';
90
+ ```
91
+
92
+ ### 根因分析
93
+
94
+ `package.json` exports 的 `require` 字段指向不存在的文件:
95
+
96
+ ```json
97
+ "exports": {
98
+ ".": {
99
+ "types": "./dist/index.d.ts",
100
+ "import": "./dist/index.mjs",
101
+ "require": "./dist/index.js" // ← 这个文件不存在!
102
+ }
103
+ }
104
+ ```
105
+
106
+ tsup 只生成 ESM(`.mjs`),没有生成 CJS(`.js`)。但 exports 里写了 `require` 字段指向 `index.js`,Node.js CJS resolver 尝试解析时找不到文件就报错了。
107
+
108
+ ### 修复
109
+
110
+ 移除 exports 中的 `require` 字段(ESM-only 库):
111
+
112
+ ```json
113
+ "exports": {
114
+ ".": {
115
+ "types": "./dist/index.d.ts",
116
+ "import": "./dist/index.mjs"
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### ESM-only 的影响
122
+
123
+ | 方式 | 支持 |
124
+ |------|------|
125
+ | `import from 'bff-store'` | ✅ 正常 |
126
+ | `node --input-type=module` + import | ✅ 正常 |
127
+ | `const x = require('bff-store')` | ❌ 报错 |
128
+
129
+ bff-store 主打 React + 新项目,用 ESM 语法不受影响。老项目用 `require` 的场景暂不支持。
@@ -0,0 +1,150 @@
1
+ # 前端/后端隔离机制
2
+
3
+ ## 概述
4
+
5
+ bff-store 通过构建配置和 package.json exports 条件导出实现前端/后端隔离,确保 Node.js 特有模块(fs、mongodb)不会进入前端 bundle。
6
+
7
+ ---
8
+
9
+ ## 1. 构建层面隔离(tsup)
10
+
11
+ tsup.config.ts 定义了 5 个独立的构建入口:
12
+
13
+ | 入口 | 输出 | external |
14
+ |------|------|----------|
15
+ | `src/index.ts` | `dist/index.mjs` | `react, jotai, mongodb` |
16
+ | `src/storage/jsonl-entry.ts` | `dist/storage/jsonl-entry.mjs` | `react, jotai` |
17
+ | `src/storage/mongodb-entry.ts` | `dist/storage/mongodb-entry.mjs` | `react, jotai` |
18
+ | `src/server/entry.ts` | `dist/server/entry.mjs` | `react, jotai` |
19
+ | `src/server/cli.ts` | `dist/cli.js` | `react, jotai` + noExternal: mongodb |
20
+
21
+ **关键**:主入口的 tsup external 里标了 `mongodb`,所以 `dist/index.mjs` 不会打包 mongodb。fs 同理。
22
+
23
+ ---
24
+
25
+ ## 2. package.json 条件导出
26
+
27
+ ```json
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.mjs",
32
+ "require": "./dist/index.js"
33
+ },
34
+ "./jsonl": {
35
+ "types": "./dist/storage/jsonl-entry.d.ts",
36
+ "import": "./dist/storage/jsonl-entry.mjs",
37
+ "require": "./dist/storage/jsonl-entry.js"
38
+ },
39
+ "./mongodb": {
40
+ "types": "./dist/storage/mongodb-entry.d.ts",
41
+ "import": "./dist/storage/mongodb-entry.mjs",
42
+ "require": "./dist/storage/mongodb-entry.js"
43
+ },
44
+ "./server": {
45
+ "types": "./dist/server/entry.d.ts",
46
+ "import": "./dist/server/entry.mjs",
47
+ "require": "./dist/server/entry.js"
48
+ }
49
+ }
50
+ ```
51
+
52
+ 用户不主动 `import from 'bff-store/jsonl'` 或 `bff-store/mongodb'`,就拿不到包含 fs/mongodb 的模块。
53
+
54
+ ---
55
+
56
+ ## 3. 主入口 index.ts 干净
57
+
58
+ `src/index.ts` 只导出前端安全的模块:
59
+
60
+ - `createStore`, `useStore`, `createPersistedAtom`
61
+ - `memoryStorage`, `remoteStorage`
62
+ - `isNode`, `isBrowser`, `createNodeStore`
63
+
64
+ **没有**引用 `jsonl.ts` 或 `mongodb.ts`。
65
+
66
+ ---
67
+
68
+ ## 4. 引用 fs/mongodb 的文件
69
+
70
+ | 文件 | 引用 |
71
+ |------|------|
72
+ | `src/storage/jsonl.ts` | `import * as fs from 'fs'` |
73
+ | `src/storage/mongodb.ts` | `import { MongoClient } from 'mongodb'` |
74
+ | `src/server/index.ts` | `import { mongodbStorage } from '../storage/mongodb'` |
75
+
76
+ 这些文件**只会被** `jsonl-entry.ts`、`mongodb-entry.ts`、`server/entry.ts` 引用,不会被主入口 `index.ts` 引用。
77
+
78
+ ### 为什么 index.ts 不引用 fs/mongodb 很重要?
79
+
80
+ **不是"空间换时间",是避免运行时错误。**
81
+
82
+ tsup 的 `external` 配置是保险,但 `import` 语句本身就会触发模块解析:
83
+
84
+ ```typescript
85
+ // index.ts 如果这样写
86
+ import * as fs from 'fs'; // ← 加了这行
87
+ export { createStore } from './createStore';
88
+ ```
89
+
90
+ 1. tsup external 会让 fs 代码不打包进 bundle
91
+ 2. 但 bundler(webpack/rollup)看到 `import` 语句仍会尝试解析
92
+ 3. Next.js 环境有 node_modules/fs → 解析成功,但运行时会出问题
93
+ 4. 纯浏览器环境没有 fs → 运行时直接报错
94
+
95
+ **不让源头存在,比靠 external 兜底更安全。**
96
+
97
+ ---
98
+
99
+ ## 5. createNodeStore 的安全性
100
+
101
+ `createNodeStore` 在 `src/nodeStore.ts` 中实现,它的 import:
102
+
103
+ ```typescript
104
+ import { getDefaultStore } from 'jotai';
105
+ import { createStore } from './createStore';
106
+ import type { AtomConfigs, StoreAtoms, StoreLoadingAtoms } from './types';
107
+ import type { StorageAdapter } from './storage/base';
108
+ ```
109
+
110
+ **没有**引用 fs 或 mongodb,所以即使在 index.ts 中导出,前端也不会被打包进去。
111
+
112
+ ---
113
+
114
+ ## 6. 之前 mongodb 进 next_test 的原因
115
+
116
+ 不是代码问题,是 **package.json dependencies** 的问题。
117
+
118
+ ```json
119
+ // 原先的 package.json
120
+ {
121
+ "dependencies": {
122
+ "mongodb": "^6.21.0" // ← 问题在这里
123
+ }
124
+ }
125
+ ```
126
+
127
+ next_test 通过 `file:../../dist` 安装 bff-store 时,npm install 会安装 `dependencies` 中的包,导致 mongodb 被装进 next_test/node_modules,即使用不到也会被 Next.js bundler 解析到。
128
+
129
+ **修复**:将 mongodb 移到 `devDependencies`,因为 tsup 构建时会把它打包进 `dist/storage/mongodb-entry.js`,不需要作为独立依赖安装。
130
+
131
+ ---
132
+
133
+ ## 验证方法
134
+
135
+ ### 检查主入口是否干净
136
+ ```bash
137
+ grep -c "mongodb" dist/index.mjs # 应为 0
138
+ grep -c "fs" dist/index.mjs # 应为 0
139
+ ```
140
+
141
+ ### 检查子入口是否包含目标模块
142
+ ```bash
143
+ grep -c "mongodb" dist/storage/mongodb-entry.mjs # 应 > 0
144
+ grep -c "fs" dist/storage/jsonl-entry.mjs # 应 > 0
145
+ ```
146
+
147
+ ### 检查 next_test 是否被安装 mongodb
148
+ ```bash
149
+ ls node_modules/mongodb # 应不存在
150
+ ```
@@ -0,0 +1,231 @@
1
+ # 实现大纲 / Implementation Overview
2
+
3
+ > 本文档反映 2026-07-05 post-mortem 修复后的实现状态。
4
+
5
+ ---
6
+
7
+ ## 架构概览
8
+
9
+ ```
10
+ ┌─────────────────────────────────────────────────────────┐
11
+ │ Frontend / React │
12
+ │ createStore() → useStore() → atoms (jotai) │
13
+ └────────────────────────┬────────────────────────────────┘
14
+ │ storage API (HTTP or direct)
15
+ ┌────────────────────────▼────────────────────────────────┐
16
+ │ BFF Server (Node.js sidecar) │
17
+ │ Router → Handlers → Storage Cache │
18
+ └────────────────────────┬────────────────────────────────┘
19
+ │ storage adapter
20
+ ┌────────────────────────▼────────────────────────────────┐
21
+ │ Storage Backend │
22
+ │ jsonl / mongodb / memory │
23
+ └─────────────────────────────────────────────────────────┘
24
+ ```
25
+
26
+ 三层分离:
27
+ - **前端层**:React 组件通过 `useStore` 消费 jotai atom
28
+ - **BFF 层**:Node.js sidecar server,代理存储请求,支持多租户
29
+ - **存储层**:可插拔后端(JSONL 文件、MongoDB、内存)
30
+
31
+ ---
32
+
33
+ ## 模块映射表
34
+
35
+ | 模块 | 文件 | 职责 |
36
+ |------|------|------|
37
+ | `createStore` | `src/createStore.ts` | 创建多 atom 持久化 store |
38
+ | `waitForServer` | `src/createStore.ts` | 等待 auto-start server 就绪 |
39
+ | `createPersistedAtom` | `src/atomCreator.ts` | 单个 atom + loading 状态 |
40
+ | `useStore` | `src/useStore.ts` | React hook,消费 store |
41
+ | `debouncer` | `src/debouncer.ts` | 写冲突防抖(800ms default,ms 不一致时替换实例) |
42
+ | `remoteStorage` | `src/storage/adapters/remoteStorage.ts` | 客户端 HTTP 适配器 |
43
+ | `HttpTransport` | `src/storage/transport.ts` | HTTP GET/POST/DELETE 原语(含错误详情提取) |
44
+ | `RestStorageProtocol` | `src/storage/protocol.ts` | REST 风格协议封装(支持 live entityId 引用) |
45
+ | `startServer` | `src/server/index.ts` | BFF 服务端单例启动 |
46
+ | `createStorageHandlers` | `src/server/handlers.ts` | 请求 → storage 分发 + 缓存 + 过期清理 |
47
+ | `Router` | `src/server/router.ts` | 路径 → handler 匹配(:key 参数) |
48
+ | `EntityIdCache` | `src/server/entityIdCache.ts` | JSONL 多租户隔离 |
49
+ | `jsonlStorage` | `src/storage/jsonl.ts` | 文件落盘 `{dir}/{entityId}/{encodeURIComponent(key)}.jsonl` |
50
+ | `mongodbStorage` | `src/storage/mongodb.ts` | MongoDB 落盘 `state_{entityId}` collection,upsert 模式 |
51
+ | `memoryStorage` | `src/storage/memory.ts` | 内存存储(测试/开发) |
52
+ | `createNodeStore` | `src/nodeStore.ts` | Node.js 环境专用(非 React),含 5s 超时 waitForLoad |
53
+
54
+ ---
55
+
56
+ ## 数据流详解
57
+
58
+ ### 1. 初始化流程
59
+
60
+ ```typescript
61
+ const store = createStore('entity-A', config, { storage: adapter });
62
+
63
+ // 内部执行
64
+ adapter.setEntityId('entity-A') // 设置租户 ID
65
+ for (atomConfig of config) {
66
+ createPersistedAtom(atomConfig, entityId, storage, { debounceMs })
67
+ // → 创建 baseAtom(初始值 defaultValue)
68
+ // → 创建 loadingAtom(初始值 true)
69
+ // → baseAtom.onMount = (setValue, unmount) => {
70
+ // storage.get(key).then(setValue).catch(console.error)
71
+ // return () => debouncerMap.cancel(`${entityId}:${key}`) // unmount cleanup
72
+ // }
73
+ }
74
+
75
+ // remote 模式下 auto-start server(fire-and-forget)
76
+ import('./server').then(({ startServer }) => {
77
+ serverInitPromise = startServer().then(() => void 0);
78
+ });
79
+ ```
80
+
81
+ ### 2. 读取流程(React 组件)
82
+
83
+ ```
84
+ useStore(store)
85
+ → buildStoreResult(store)
86
+ → useAtom(atom) for each config[key]
87
+ → baseAtom.get() // 同步返回 jotai store 中的当前值
88
+ ```
89
+
90
+ **注意**:`storage.get()` 只在 `onMount` 时调用一次,结果写入 baseAtom。后续读取直接走 jotai store 内部状态,不涉及 storage。
91
+
92
+ ### 3. 写入流程(带防抖 + unmount 取消)
93
+
94
+ ```
95
+ setTheme('dark')
96
+ → writeAtom.write(dark)
97
+ → baseAtom.set(dark) // 本地立即更新,UI 同步响应
98
+ → debouncer.debounce(key, saveFn, 800ms)
99
+ → storage.set(key, dark) // 延迟写入后端
100
+ // unmount 时:debouncerMap.cancel(`${entityId}:${key}`) 取消 pending 写入
101
+ ```
102
+
103
+ - `immediate: true` 的 atom 跳过防抖,直接 `storage.set()`
104
+ - 防抖 key 格式:`${entityId}:${config.key}`,避免跨 store 干扰
105
+ - **ms 不一致时**:替换已有 debouncer 实例,确保延迟时间正确
106
+
107
+ ### 4. 远程写入(remoteStorage 路径)
108
+
109
+ ```
110
+ setTheme('dark')
111
+ → writeAtom.write(dark)
112
+ → baseAtom.set(dark)
113
+ → debouncer.debounce(...)
114
+ → storage.set(key, value)
115
+ → HttpTransport.post('/storage/set/:key', { value, ...backendConfig })
116
+ → BFF server: POST /storage/set/:key
117
+ → handlers.handleSet()
118
+ → getCachedStorage(config, entityId)
119
+ → storage.set(key, value)
120
+ ```
121
+
122
+ ### 5. Node.js 服务端(BFF)
123
+
124
+ ```
125
+ http.createServer()
126
+ → Router.handle(req, res)
127
+ → matched route: /storage/get/:key
128
+ → handlers.handleGet(req, res, { key })
129
+ → resolveStorage(req) // 解析 body + url config
130
+ → storage.get(key)
131
+ → res.json({ value })
132
+ ```
133
+
134
+ ---
135
+
136
+ ## 多租户隔离策略
137
+
138
+ ### Server 端
139
+
140
+ - **JSONL**:每个 `entityId` 对应独立目录 `{jsonlDir}/{entityId}/`
141
+ - **MongoDB**:每个 `entityId` 对应独立 collection `state_{entityId}`
142
+ - **Storage Cache**:`Map<cacheKey, { adapter, lastUsed }>`,TTL 5min,**full LRU 驱逐(满时全部驱逐直到有空间)**
143
+
144
+ ### Client 端
145
+
146
+ - `remoteStorage.setEntityId(id)` 更新 `entityId.current`(live 引用,protocol 层每次请求实时读取)
147
+ - 后续所有请求在 protocol 层拼入 query param `?entityId=xxx`
148
+
149
+ ---
150
+
151
+ ## 关键设计决策
152
+
153
+ ### 为什么 atom 初始值用 defaultValue 而非从 storage 加载?
154
+
155
+ jotai 要求 atom 必须有同步初始值。`createPersistedAtom` 创建时传入 `defaultValue`,然后通过 `onMount` 异步从 storage 覆盖。
156
+
157
+ ```typescript
158
+ const baseAtom = atom(config.defaultValue); // 同步默认值
159
+ baseAtom.onMount = (setValue) => {
160
+ storage.get(key)
161
+ .then((value) => { if (value != null) setValue(value); })
162
+ .catch((err) => { console.error(`[bff-store] Failed to load atom "${key}":`, err); })
163
+ .finally(() => { /* loadingAtom = false */ });
164
+ };
165
+ ```
166
+
167
+ ### 为什么需要 loadingAtom?
168
+
169
+ `onMount` 是异步的,React 组件可能在数据加载完成前就开始 render。`loadingAtom` 跟踪所有 atom 的加载状态,合并为 `useStore` 返回的 `isLoading` 布尔值。
170
+
171
+ ### 防抖 DebouncerMap 为什么用 module-level 单例?
172
+
173
+ 所有 atom 共享同一个 `DebouncerMap` 实例,确保同一 `entityId:key` 的多次写入只触发一次延迟保存。ms 不一致时替换已有实例。
174
+
175
+ ### unmount 时为什么要 cancel pending writes?
176
+
177
+ 组件卸载后,写入回调可能仍在计时器队列中,若不取消会导致:
178
+ 1. 写入操作在组件销毁后执行
179
+ 2. `getDefaultStore()` 在已销毁的组件上下文中被调用
180
+
181
+ ---
182
+
183
+ ## 路由表(服务端)
184
+
185
+ | Method | Path | Handler |
186
+ |--------|------|---------|
187
+ | GET | `/storage/get/:key` | `handleGet` |
188
+ | POST | `/storage/set/:key` | `handleSet` |
189
+ | DELETE | `/storage/delete/:key` | `handleDelete` |
190
+ | POST | `/storage/batch-get` | `handleBatchGet` |
191
+ | POST | `/storage/batch-set` | `handleBatchSet` |
192
+ | GET | `/health` | `handleHealth` |
193
+
194
+ ---
195
+
196
+ ## 已修复 Bug 清单(Post-Mortem 2026-07-05)
197
+
198
+ | # | Severity | Location | Bug | Fix |
199
+ |---|----------|----------|-----|-----|
200
+ | 1 | High | `createStore.ts` | server 启动 fire-and-forget | 引入 `serverInitPromise` + `waitForServer()` |
201
+ | 3 | Medium | `handlers.ts` | 缓存满时只驱逐 1 个 entry | 改为 `while` 循环直到有空间 |
202
+ | 4 | Medium | `transport.ts` | HTTP 错误 body 丢失 | `res.clone().json()` 读取错误详情 |
203
+ | 5 | Low | `debouncer.ts` | ms 不一致时忽略新值 | ms 不同时替换已有 debouncer |
204
+ | 7 | High | `jsonl.ts` | `.` `-` 等分隔符替换为 `_` 导致 key 碰撞 | 改用 `encodeURIComponent(key)` |
205
+ | 8 | Medium | `handlers.ts` | `getStorageForRequest` 死代码 | 已删除 |
206
+ | 9 | Low | `nodeStore.ts` | `waitForLoad` 无限等待 | 添加 5s 超时 reject |
207
+ | 10 | Low | `handlers.ts` | `parseBody` 失败静默忽略 | 改为 `console.warn` |
208
+ | 11 | High | `mongodb.ts` | `insertOne` 无限积累旧版本 | 改用 `updateOne` + `upsert: true` |
209
+ | 12 | High | `remoteStorage.ts` | `setEntityId` 不生效(闭包捕获快照) | 传入对象引用 `{ current }` |
210
+ | 14 | Low | `server/index.ts` | `isServerRunning()` 未使用 | 已删除 |
211
+ | 15 | Medium | `atomCreator.ts` | unmount 时 pending writes 丢失 | `onMount` 返回 cleanup 调用 `cancel()` |
212
+ | 17 | Low | `handlers.ts` | `handleHealth` 参数未使用 | `req` → `_req` |
213
+ | 18 | Low | `atomCreator.ts` | storage.get 失败静默忽略 | 添加 `console.error` 记录 |
214
+
215
+ ### 未修复(设计选择)
216
+
217
+ | # | Reason |
218
+ |---|--------|
219
+ | BUG-6 | module-level `debouncerMap` 通过 `entityId:key` 隔离键名,实际无跨 store 干扰 |
220
+ | BUG-13 | `createStorageFromTransport.get` 所有错误返回 null — 符合 storage graceful degradation 哲学 |
221
+
222
+ ---
223
+
224
+ ## 配置文件
225
+
226
+ | 文件 | 用途 |
227
+ |------|------|
228
+ | `tsup.config.ts` | 多入口打包(index, jsonl-entry, mongodb-entry, server/entry, cli) |
229
+ | `tsconfig.json` | TypeScript 配置 |
230
+ | `vitest.config.ts` | 测试配置 |
231
+ | `package.json` | 导出字段:`import` → `dist/index.mjs` |
@@ -0,0 +1,117 @@
1
+ # 本地包链接策略
2
+
3
+ ## 背景
4
+
5
+ 开发 bff-store 时,需要在项目中测试自己的包。常见的链接方式有三种。
6
+
7
+ ---
8
+
9
+ ## 方式一:`file:` 路径(当前使用)
10
+
11
+ ### 配置
12
+
13
+ ```json
14
+ // tests/next_test/package.json
15
+ {
16
+ "dependencies": {
17
+ "bff-store": "file:../../dist"
18
+ }
19
+ }
20
+ ```
21
+
22
+ npm install 后会创建 symlink:
23
+
24
+ ```
25
+ tests/next_test/node_modules/bff-store -> bff-store/dist
26
+ ```
27
+
28
+ ### 优点
29
+ - 简单直接,改完 `npm run build` 就生效
30
+ - 无需额外命令
31
+
32
+ ### 缺点
33
+ - 不是真实 npm 安装行为,可能有边界情况
34
+ - 路径固定,不够灵活
35
+
36
+ ---
37
+
38
+ ## 方式二:`npm link`(推荐本地开发)
39
+
40
+ ### 配置
41
+
42
+ ```bash
43
+ # 1. 在 bff-store 目录注册全局链接
44
+ cd bff-store
45
+ npm link
46
+
47
+ # 2. 在目标项目链接全局包
48
+ cd tests/next_test
49
+ npm link bff-store
50
+ ```
51
+
52
+ ### 原理
53
+
54
+ ```
55
+ bff-store/ (全局注册的包)
56
+
57
+ tests/next_test/node_modules/bff-store -> 全局包的 symlink
58
+ ```
59
+
60
+ ### 优点
61
+ - 更接近真实 npm 安装行为
62
+ - 全局一份,多个项目共用
63
+ - 可同时开发多个相互依赖的包
64
+
65
+ ### 缺点
66
+ - 需要两条命令
67
+ - 全局状态管理可能混乱
68
+
69
+ ### 清理
70
+
71
+ ```bash
72
+ cd tests/next_test
73
+ npm unlink bff-store
74
+
75
+ cd bff-store
76
+ npm link remove bff-store
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 方式三:发布到 npm(正式发布)
82
+
83
+ ### 配置
84
+
85
+ ```bash
86
+ # 1. 登录 npm
87
+ npm login
88
+
89
+ # 2. 发布(需要 package.json 的 name 是唯一的)
90
+ npm publish
91
+
92
+ # 3. 在项目中使用
93
+ npm install bff-store
94
+ ```
95
+
96
+ ### 优点
97
+ - 真实用户安装方式
98
+ - 版本管理规范
99
+
100
+ ### 缺点
101
+ - 需要 npm 账号
102
+ - 版本管理,每次改动都要发版
103
+ - 发布后不能即时生效
104
+
105
+ ---
106
+
107
+ ## 总结
108
+
109
+ | 方式 | 即时生效 | 配置复杂度 | 接近真实安装 |
110
+ |------|---------|-----------|------------|
111
+ | `file:` | ✅ | 简单 | 一般 |
112
+ | `npm link` | ✅ | 中等 | ✅ |
113
+ | `npm publish` | ❌ | 需要版本管理 | ✅ |
114
+
115
+ **本地开发推荐**:`npm link` 或保持 `file:`
116
+
117
+ **正式发布**:用 `npm publish`
@@ -0,0 +1,34 @@
1
+ # BUG-01: server 异步启动未等待
2
+
3
+ **Severity**: High
4
+ **Location**: `src/createStore.ts:47-53`
5
+ **Type**: Async Fire-and-Forget
6
+
7
+ ## 问题
8
+
9
+ `createStore` 中对 `remote` 存储类型自动启动 server 时,使用 `import().then()` 的 fire-and-forget 模式:
10
+
11
+ ```typescript
12
+ import('./server').then(({ startServer }) => {
13
+ serverInitPromise = startServer().then(() => void 0).catch(...);
14
+ });
15
+ ```
16
+
17
+ `createStore` 是同步函数,立即返回,但 server 可能尚未启动完成,导致后续 remote storage 操作立即失败。
18
+
19
+ ## 修复
20
+
21
+ 引入 module-level `serverInitPromise` 追踪启动状态,并导出 `waitForServer()` 供调用者等待:
22
+
23
+ ```typescript
24
+ let serverInitPromise: Promise<unknown> | null = null;
25
+
26
+ export function waitForServer(): Promise<void> | undefined {
27
+ return serverInitPromise as Promise<void> | undefined;
28
+ }
29
+ ```
30
+
31
+ ## 验证
32
+
33
+ - Build: ✅
34
+ - Tests: ✅ (69 passed)
@@ -0,0 +1,40 @@
1
+ # BUG-03: storage cache 驱逐不充分
2
+
3
+ **Severity**: Medium
4
+ **Location**: `src/server/handlers.ts:85-96`
5
+ **Type**: Cache Eviction
6
+
7
+ ## 问题
8
+
9
+ 缓存满时(`MAX_CACHE_SIZE=10`)只驱逐 1 个最旧 entry,但新 adapter 创建后会再次超过限制:
10
+
11
+ ```typescript
12
+ if (storageCache.size >= MAX_CACHE_SIZE) {
13
+ storageCache.delete(oldestKey); // 只删 1 个
14
+ }
15
+ // 再次 check: size 可能仍 >= MAX_CACHE_SIZE
16
+ ```
17
+
18
+ ## 修复
19
+
20
+ 改为 `while` 循环,直到有空间:
21
+
22
+ ```typescript
23
+ while (storageCache.size >= MAX_CACHE_SIZE && storageCache.size > 0) {
24
+ let oldestKey: string | null = null;
25
+ let oldestTime = Infinity;
26
+ for (const [k, entry] of storageCache.entries()) {
27
+ if (entry.lastUsed < oldestTime) {
28
+ oldestTime = entry.lastUsed;
29
+ oldestKey = k;
30
+ }
31
+ }
32
+ if (oldestKey) storageCache.delete(oldestKey);
33
+ else break; // 防止无限循环
34
+ }
35
+ ```
36
+
37
+ ## 验证
38
+
39
+ - Build: ✅
40
+ - Tests: ✅ (69 passed)