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
@@ -81,8 +81,8 @@ async function getCachedStorage(config: BackendConfig, entityId?: string): Promi
81
81
  return entry.adapter.storage;
82
82
  }
83
83
 
84
- // Evict oldest if cache is full
85
- if (storageCache.size >= MAX_CACHE_SIZE) {
84
+ // Evict oldest entries until there's space for a new one
85
+ while (storageCache.size >= MAX_CACHE_SIZE && storageCache.size > 0) {
86
86
  let oldestKey: string | null = null;
87
87
  let oldestTime = Infinity;
88
88
  for (const [k, entry] of storageCache.entries()) {
@@ -93,6 +93,8 @@ async function getCachedStorage(config: BackendConfig, entityId?: string): Promi
93
93
  }
94
94
  if (oldestKey) {
95
95
  storageCache.delete(oldestKey);
96
+ } else {
97
+ break; // should not happen, but guard against infinite loop
96
98
  }
97
99
  }
98
100
 
@@ -123,26 +125,6 @@ async function getCachedStorage(config: BackendConfig, entityId?: string): Promi
123
125
  return adapter.storage;
124
126
  }
125
127
 
126
- async function getStorageForRequest(
127
- req: IncomingMessage,
128
- entityId?: string
129
- ): Promise<Storage> {
130
- const urlConfig = getBackendConfig(req);
131
- const body = await parseBody<BackendConfig>(req);
132
-
133
- const config: BackendConfig = {
134
- backend: body.backend ?? urlConfig.backend,
135
- mongoUrl: body.mongoUrl ?? urlConfig.mongoUrl,
136
- mongoDb: body.mongoDb ?? urlConfig.mongoDb,
137
- jsonlDir: body.jsonlDir ?? urlConfig.jsonlDir,
138
- };
139
-
140
- // Periodic cleanup of expired cache entries
141
- cleanExpiredCache();
142
-
143
- return getCachedStorage(config, entityId);
144
- }
145
-
146
128
  export function createStorageHandlers(options: StorageHandlersOptions) {
147
129
  const { getStorage } = options;
148
130
 
@@ -157,7 +139,8 @@ export function createStorageHandlers(options: StorageHandlersOptions) {
157
139
  try {
158
140
  body = await parseBody<Record<string, unknown>>(req);
159
141
  } catch {
160
- // Ignore parse errors
142
+ // Ignore parse errors - fall back to urlConfig only
143
+ console.warn('[bff-store] Failed to parse request body, using URL config only');
161
144
  }
162
145
  }
163
146
 
@@ -254,7 +237,7 @@ export function createStorageHandlers(options: StorageHandlersOptions) {
254
237
  res.end(JSON.stringify({ success: true }));
255
238
  }
256
239
 
257
- async function handleHealth(req: IncomingMessage, res: ServerResponse): Promise<void> {
240
+ async function handleHealth(_req: IncomingMessage, res: ServerResponse): Promise<void> {
258
241
  res.setHeader('Content-Type', 'application/json');
259
242
  res.writeHead(200);
260
243
  res.end(JSON.stringify({ status: 'ok' }));
@@ -38,10 +38,6 @@ const defaultOptions: ServerOptions = {
38
38
  jsonlDir: './data',
39
39
  };
40
40
 
41
- function isServerRunning(): boolean {
42
- return serverInstance !== null;
43
- }
44
-
45
41
  /**
46
42
  * Start the embedded API server (singleton).
47
43
  *
@@ -53,7 +53,8 @@ export function remoteStorage(options: RemoteStorageOptions = {}): StorageAdapte
53
53
  jsonlDir: options.jsonlDir,
54
54
  };
55
55
 
56
- const protocol = options.protocol ?? new RestStorageProtocol(baseUrl, entityId.current, backendConfig);
56
+ // Pass the entityId object ref so getEntityId() reads the live value on each request
57
+ const protocol = options.protocol ?? new RestStorageProtocol(baseUrl, entityId, backendConfig);
57
58
  const storage = createStorageWithProtocol(transport, protocol);
58
59
 
59
60
  const adapter: StorageAdapter = {
@@ -29,7 +29,9 @@ export function jsonlStorage(options?: JsonlStorageOptions): JsonlStorageInstanc
29
29
  let entityId: string = 'default';
30
30
 
31
31
  function getFilePath(eId: string, key: string): string {
32
- const safeKey = key.replace(/[^a-zA-Z0-9_]/g, '_');
32
+ // Use encodeURIComponent to avoid key collision:
33
+ // "user.name", "user-name", "user%2Ename" all produce distinct filenames
34
+ const safeKey = encodeURIComponent(key);
33
35
  return path.join(baseDir, eId, `${safeKey}.jsonl`);
34
36
  }
35
37
 
@@ -60,12 +60,12 @@ export async function mongodbStorage(
60
60
  async set<T>(key: string, value: T): Promise<void> {
61
61
  const collection = getCollection(currentEntityId);
62
62
 
63
- await collection.insertOne({
64
- key,
65
- value,
66
- timestamp: Date.now(),
67
- entityId: currentEntityId,
68
- });
63
+ // Use upsert to avoid accumulating duplicate entries over time
64
+ await collection.updateOne(
65
+ { key, entityId: currentEntityId },
66
+ { $set: { value, timestamp: Date.now() } },
67
+ { upsert: true }
68
+ );
69
69
  },
70
70
 
71
71
  async remove(key: string): Promise<void> {
@@ -21,7 +21,14 @@ export class HttpTransport implements TransportAdapter {
21
21
  async get<T>(url: string): Promise<T> {
22
22
  const res = await fetch(url);
23
23
  if (!res.ok) {
24
- throw new Error(`GET ${url} failed: ${res.statusText}`);
24
+ let detail = res.statusText;
25
+ try {
26
+ const body = await res.clone().json();
27
+ detail = body?.error ?? body?.message ?? detail;
28
+ } catch {
29
+ // body is not JSON or unreadable, use statusText only
30
+ }
31
+ throw new Error(`GET ${url} failed: ${detail}`);
25
32
  }
26
33
  return res.json();
27
34
  }
@@ -33,7 +40,14 @@ export class HttpTransport implements TransportAdapter {
33
40
  body: JSON.stringify(body),
34
41
  });
35
42
  if (!res.ok) {
36
- throw new Error(`POST ${url} failed: ${res.statusText}`);
43
+ let detail = res.statusText;
44
+ try {
45
+ const errBody = await res.clone().json();
46
+ detail = errBody?.error ?? errBody?.message ?? detail;
47
+ } catch {
48
+ // body is not JSON or unreadable, use statusText only
49
+ }
50
+ throw new Error(`POST ${url} failed: ${detail}`);
37
51
  }
38
52
  return res.json();
39
53
  }
@@ -41,7 +55,14 @@ export class HttpTransport implements TransportAdapter {
41
55
  async delete(url: string): Promise<void> {
42
56
  const res = await fetch(url, { method: 'DELETE' });
43
57
  if (!res.ok) {
44
- throw new Error(`DELETE ${url} failed: ${res.statusText}`);
58
+ let detail = res.statusText;
59
+ try {
60
+ const errBody = await res.clone().json();
61
+ detail = errBody?.error ?? errBody?.message ?? detail;
62
+ } catch {
63
+ // body is not JSON or unreadable, use statusText only
64
+ }
65
+ throw new Error(`DELETE ${url} failed: ${detail}`);
45
66
  }
46
67
  }
47
68
  }
@@ -79,16 +79,32 @@ describe('jsonlStorage', () => {
79
79
  expect(await adapter2.storage.get('key')).toBe('value2');
80
80
  });
81
81
 
82
- it('should sanitize key in filename', async () => {
82
+ it('should encode key in filename to avoid collision', async () => {
83
83
  const adapter = jsonlStorage({ dir: testDir });
84
84
  adapter.setEntityId('entity1');
85
85
 
86
86
  await adapter.storage.set('key with spaces', 'value');
87
87
 
88
- const filePath = path.join(testDir, 'entity1', 'key_with_spaces.jsonl');
88
+ // encodeURIComponent converts " " → "%20", producing a collision-safe filename
89
+ const filePath = path.join(testDir, 'entity1', 'key%20with%20spaces.jsonl');
89
90
  expect(fs.existsSync(filePath)).toBe(true);
90
91
  });
91
92
 
93
+ it('should not collide on keys with dots and hyphens', async () => {
94
+ const adapter = jsonlStorage({ dir: testDir });
95
+ adapter.setEntityId('entity1');
96
+
97
+ await adapter.storage.set('user.name', 'value1');
98
+ await adapter.storage.set('user-name', 'value2');
99
+
100
+ const filePath1 = path.join(testDir, 'entity1', 'user.name.jsonl');
101
+ const filePath2 = path.join(testDir, 'entity1', 'user-name.jsonl');
102
+ expect(fs.existsSync(filePath1)).toBe(true);
103
+ expect(fs.existsSync(filePath2)).toBe(true);
104
+ expect(await adapter.storage.get('user.name')).toBe('value1');
105
+ expect(await adapter.storage.get('user-name')).toBe('value2');
106
+ });
107
+
92
108
  it('should handle multiple types', async () => {
93
109
  const adapter = jsonlStorage({ dir: testDir });
94
110
  adapter.setEntityId('entity1');
package/tsconfig.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "target": "ES2020",
4
4
  "module": "ESNext",
5
5
  "lib": ["ES2020", "DOM"],
6
+ "types": ["node"],
6
7
  "moduleResolution": "bundler",
7
8
  "strict": true,
8
9
  "esModuleInterop": true,
@@ -1,104 +0,0 @@
1
- # Bug 诊断:`'backend' does not exist in type 'RemoteStorageOptions'`
2
-
3
- ## 现象
4
-
5
- 在 `tests/next_test` 中使用 `bff-store`(通过 `"file:../../dist"` 安装)时,TypeScript 报错:
6
-
7
- ```
8
- Object literal may only specify known properties, and 'backend' does not exist in type 'RemoteStorageOptions'.ts(2353)
9
- ```
10
-
11
- 触发代码:
12
-
13
- ```typescript
14
- import { remoteStorage } from 'bff-store';
15
-
16
- const adapter = remoteStorage({
17
- backend: 'jsonl',
18
- jsonlDir: '/tmp/test',
19
- });
20
- ```
21
-
22
- ## 根因分析
23
-
24
- 问题由**两个因素叠加**导致:
25
-
26
- ### 因素一(主因):`dist/package.json` 路径错误
27
-
28
- 构建脚本 `npm run build` 直接将项目根目录的 `package.json` 复制到 `dist/`。根目录的 `package.json` 中,`exports`、`main`、`module`、`types` 字段的路径都带有 `./dist/` 前缀:
29
-
30
- ```json
31
- {
32
- "main": "dist/index.js",
33
- "module": "dist/index.mjs",
34
- "types": "dist/index.d.ts",
35
- "exports": {
36
- ".": {
37
- "types": "./dist/index.d.ts",
38
- "import": "./dist/index.mjs",
39
- "require": "./dist/index.js"
40
- }
41
- }
42
- }
43
- ```
44
-
45
- 这些路径在 npm 发布时是正确的(因为 `dist/` 是项目根目录的子目录)。但当通过 `"file:../../dist"` 本地安装时,**包根目录就是 `dist/` 本身**,`./dist/index.d.ts` 就会解析为 `dist/dist/index.d.ts` —— 该文件不存在。
46
-
47
- | 场景 | 包根目录 | `./dist/index.d.ts` 解析结果 | 存在? |
48
- |------|----------|---------------------------|------|
49
- | npm 发布 | 项目根目录 | `项目根/dist/index.d.ts` | 是 |
50
- | `file:../../dist` | `dist/` | `dist/dist/index.d.ts` | **否** |
51
-
52
- 因此 TypeScript 无法通过本地 symlink 解析到正确的类型声明文件。
53
-
54
- ### 因素二(助推):全局安装了旧版本
55
-
56
- 由于本地 symlink 解析失败,TypeScript 沿目录树向上查找,最终在 `/Users/Admin/node_modules/bff-store/` 找到了全局安装的 **v0.1.1**(当前项目版本为 v0.1.3)。
57
-
58
- 该旧版本的 `RemoteStorageOptions` 接口**没有** `backend` 等新增字段:
59
-
60
- - v0.1.1(旧):
61
- ```typescript
62
- interface RemoteStorageOptions {
63
- baseUrl?: string;
64
- entityId?: string;
65
- transport?: TransportAdapter;
66
- protocol?: StorageHttpProtocol;
67
- }
68
- ```
69
-
70
- - v0.1.3(当前源码/dist):
71
- ```typescript
72
- interface RemoteStorageOptions {
73
- baseUrl?: string;
74
- entityId?: string;
75
- transport?: TransportAdapter;
76
- protocol?: StorageHttpProtocol;
77
- backend?: 'mongodb' | 'jsonl'; // 新增
78
- mongoUrl?: string; // 新增
79
- mongoDb?: string; // 新增
80
- jsonlDir?: string; // 新增
81
- }
82
- ```
83
-
84
- ## 修复方案
85
-
86
- 新增 `scripts/adapt-dist-package.js`,在构建时将 `dist/package.json` 中的路径从"相对于项目根目录"改写为"相对于 dist/":
87
-
88
- - `./dist/index.d.ts` → `./index.d.ts`
89
- - `./dist/index.mjs` → `./index.mjs`
90
- - `./dist/index.js` → `./index.js`
91
- - `./dist/storage/...` → `./storage/...`
92
- - `./dist/server/...` → `./server/...`
93
-
94
- 同时更新 `package.json` 的 `build` 脚本,在 `tsup` 和 `cp` 之间插入该脚本。
95
-
96
- ## 修复文件
97
-
98
- - **新增**: `scripts/adapt-dist-package.js` — 路径改写脚本
99
- - **修改**: `package.json` — build 脚本中调用 `node scripts/adapt-dist-package.js`
100
-
101
- ## 验证
102
-
103
- - `npx tsc --noEmit` in `tests/next_test`:**0 errors**(修复前 4 errors)
104
- - `npx vitest run`:68/69 通过(1 个 pre-existing failure,与本 bug 无关)
@@ -1,63 +0,0 @@
1
- # Bug Fix: `'backend' does not exist in type 'RemoteStorageOptions'`
2
-
3
- ## 现象
4
-
5
- `tests/next_test` 中使用 `"bff-store": "file:../../dist"` 时,TypeScript 报错:
6
-
7
- ```
8
- Object literal may only specify known properties, and 'backend' does not exist in type 'RemoteStorageOptions'.ts(2353)
9
- ```
10
-
11
- ## 根因
12
-
13
- ### 因素一(主因):dist/package.json 路径错误
14
-
15
- 根目录 `package.json` 的 `main`/`module`/`types`/`exports` 字段路径都带有 `./dist/` 前缀:
16
-
17
- ```json
18
- "main": "dist/index.js",
19
- "exports": { ".": { "types": "./dist/index.d.ts", ... } }
20
- ```
21
-
22
- 当通过 `"file:../../dist"` 本地安装时,包根目录就是 `dist/` 本身,`./dist/index.d.ts` 解析为 `dist/dist/index.d.ts` —— 该文件不存在。
23
-
24
- | 场景 | 包根目录 | `./dist/index.d.ts` 解析结果 | 存在? |
25
- |------|----------|---------------------------|------|
26
- | npm 发布 | 项目根 | `项目根/dist/index.d.ts` | 是 |
27
- | `file:../../dist` | `dist/` | `dist/dist/index.d.ts` | 否 |
28
-
29
- ### 因素二(助推):全局安装了旧版本
30
-
31
- 本地 symlink 解析失败后,TypeScript 沿目录树向上查找,在 `/Users/Admin/node_modules/bff-store/` 找到全局安装的旧版本(v0.1.1),该版本 `RemoteStorageOptions` 没有 `backend` 等新字段。
32
-
33
- ## 修复方案
34
-
35
- 1. **新增** `scripts/adapt-dist-package.js` — 构建后将 `dist/package.json` 中的路径从"相对于项目根"改写为"相对于 dist/":
36
- - `dist/index.js` → `index.js`
37
- - `./dist/index.d.ts` → `./index.d.ts`
38
- - `./dist/storage/...` → `./storage/...`
39
- - `./dist/server/...` → `./server/...`
40
-
41
- 2. **修改** `package.json` 的 `build` 脚本:
42
- ```
43
- "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"
44
- ```
45
- 完整流程:
46
- - `tsup` — 构建到 dist/
47
- - `cp package.json dist/package.json` — 复制配置到 dist
48
- - `node scripts/adapt-dist-package.js` — 改写路径
49
- - shell 循环 — 复制 `.d.mts` → `.d.ts`(主入口 tsup 只生成 `.d.mts`)
50
-
51
- ## 修复文件
52
-
53
- | 文件 | 操作 |
54
- |------|------|
55
- | `scripts/adapt-dist-package.js` | 新增 — 路径改写脚本 |
56
- | `package.json` | 修改 — build 脚本 |
57
- | `tests/next_test/test-regression-remote-storage.ts` | 新增 — jsonl/mongodb/默认三种场景类型回归测试 |
58
- | `tests/next_test/src/app/test-regression-store.tsx` | 新增 — createStore + storage 的 .tsx 回归测试 |
59
-
60
- ## 验证
61
-
62
- - `npm run build`:构建成功,dist/package.json 路径正确
63
- - `npx tsc --noEmit` in `tests/next_test`:0 errors
@@ -1,171 +0,0 @@
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 全链路:通过