bff-store 0.1.1 → 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 (45) hide show
  1. package/.claude/settings.local.json +5 -1
  2. package/README.md +172 -68
  3. package/dist/cli.js +11 -9
  4. package/dist/index.d.mts +7 -1
  5. package/dist/index.d.ts +7 -1
  6. package/dist/index.mjs +47 -12
  7. package/dist/package.json +1 -1
  8. package/dist/server/entry.d.mts +1 -1
  9. package/dist/server/entry.d.ts +1 -1
  10. package/dist/server/entry.js +11 -9
  11. package/dist/server/entry.mjs +11 -9
  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 +5 -6
  16. package/dist/storage/mongodb-entry.mjs +5 -6
  17. package/docs/IMPLEMENTATION_OVERVIEW.md +231 -0
  18. package/docs/bugs/01-server-async-startup.high.md +34 -0
  19. package/docs/bugs/03-storage-cache-eviction.md +40 -0
  20. package/docs/bugs/04-http-error-body-loss.md +38 -0
  21. package/docs/bugs/05-debouncer-ms-ignored.md +41 -0
  22. package/docs/bugs/07-jsonl-key-collision.high.md +31 -0
  23. package/docs/bugs/08-dead-code-getStorageForRequest.md +18 -0
  24. package/docs/bugs/09-waitForLoad-infinite.md +37 -0
  25. package/docs/bugs/10-parseBody-silent-failure.md +32 -0
  26. package/docs/bugs/11-mongodb-storage-leak.high.md +35 -0
  27. package/docs/bugs/12-setEntityId-stale-closure.high.md +35 -0
  28. package/docs/bugs/14-dead-code-isServerRunning.md +18 -0
  29. package/docs/bugs/15-unmount-data-loss.md +37 -0
  30. package/docs/bugs/17-unused-parameter.md +26 -0
  31. package/docs/bugs/18-storage-get-silent-error.md +28 -0
  32. package/docs/bugs/README.md +35 -0
  33. package/package.json +1 -4
  34. package/src/atomCreator.ts +10 -2
  35. package/src/createStore.ts +19 -1
  36. package/src/debouncer.ts +5 -2
  37. package/src/index.ts +1 -1
  38. package/src/nodeStore.ts +9 -2
  39. package/src/server/handlers.ts +7 -24
  40. package/src/server/index.ts +0 -4
  41. package/src/storage/adapters/remoteStorage.ts +2 -1
  42. package/src/storage/jsonl.ts +3 -1
  43. package/src/storage/mongodb.ts +6 -6
  44. package/src/storage/transport.ts +24 -3
  45. package/tests/storage/jsonl.test.ts +18 -2
@@ -48,7 +48,11 @@
48
48
  "Bash(echo \"---mongodb status: $\\([ -d node_modules/mongodb ] && echo 'INSTALLED' || echo 'NOT INSTALLED'\\)\")",
49
49
  "Bash(echo \"---mongodb: $\\([ -d node_modules/mongodb ] && echo 'INSTALLED' || echo 'NOT INSTALLED'\\)\")",
50
50
  "Bash(xargs kill:*)",
51
- "Bash(npm start:*)"
51
+ "Bash(npm start:*)",
52
+ "Bash(mv 01-server-async-startup.md 01-server-async-startup.high.md)",
53
+ "Bash(mv 07-jsonl-key-collision.md 07-jsonl-key-collision.high.md)",
54
+ "Bash(mv 11-mongodb-storage-leak.md 11-mongodb-storage-leak.high.md)",
55
+ "Bash(mv 12-setEntityId-stale-closure.md 12-setEntityId-stale-closure.high.md)"
52
56
  ]
53
57
  }
54
58
  }
package/README.md CHANGED
@@ -1,42 +1,77 @@
1
1
  # bff-store
2
2
 
3
- A jotai-based state management library with pluggable storage adapters and built-in BFF (Backend for Frontend) for browser/Next.js environments.
3
+ A jotai-based state management library with pluggable storage adapters and a built-in BFF (Backend for Frontend) for browser/Next.js environments.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Configuration-driven**: Define multiple states via array configuration
8
- - **Pluggable storage**: Use memory, JSONL files, MongoDB, or remote server
8
+ - **Pluggable storage**: Use memory, JSONL files, MongoDB, or a remote server
9
9
  - **Auto-generated hooks**: React hooks auto-generated from config
10
10
  - **Loading states**: Built-in loading state tracking
11
- - **Debounced saves**: Automatic debouncing for non-critical data
11
+ - **Debounced saves**: Automatic debouncing for non-critical data (800ms default)
12
12
  - **Built-in BFF**: Embedded sidecar API server for browser/Next.js environments
13
+ - **Multi-tenant**: Per-entityId isolation via `setEntityId()`
14
+ - **Unmount cleanup**: Pending writes cancelled on component unmount
15
+ - **Embedded Sidecar**: Like a serverless function that auto-starts on first use — no manual server deployment required
13
16
 
14
17
  ## Architecture
15
18
 
16
19
  ```
17
20
  ┌─────────────────────────────────────────────────────────┐
18
- │ Your Application
19
- │ (Next.js/Browser)
20
-
21
- │ useStore() ──► remoteStorage ──► localhost:3847
22
- │ │
23
- │ ▼
24
- │ ┌─────────────────────┐
25
- │ │ BFF (Sidecar)
26
- │ │ bff-store │
27
- │ │
28
- │ │ /storage/get/:key │
29
- │ │ /storage/set/:key
30
- │ │ /storage/delete │ │
31
- └──────────┬──────────┘
32
- │ │
33
-
34
- ┌─────────────────────┐
35
- │ │ JSONL / MongoDB
36
- │ └─────────────────────┘
21
+ │ Your Application
22
+ │ (Next.js / Browser)
23
+
24
+ │ useStore() ──► remoteStorage ──► localhost:3847
25
+ │ │
26
+ │ ▼
27
+ │ ┌─────────────────────┐
28
+ │ │ BFF (Sidecar)
29
+ │ │ │
30
+ │ │ /storage/get/:key
31
+ │ │ /storage/set/:key │
32
+ │ │ /storage/batch-*
33
+ └──────────┬──────────┘
34
+
35
+
36
+ ┌─────────────────────┐
37
+ │ │ JSONL / MongoDB │ │
38
+ │ │ (multi-tenant)
39
+ │ └─────────────────────┘
37
40
  └─────────────────────────────────────────────────────────┘
38
41
  ```
39
42
 
43
+ ### Data Flow
44
+
45
+ 1. **Init**: `createStore()` creates atoms, `remoteStorage` auto-starts the BFF server
46
+ 2. **Read**: `useStore()` returns from jotai store directly — no I/O
47
+ 3. **Write**: Update local jotai atom first (sync UI response), then debounce-persist to storage
48
+ 4. **Unmount**: Automatically cancel pending writes to prevent writes from destroyed components
49
+
50
+ ## Embedded Sidecar Pattern
51
+
52
+ bff-store borrows the **embedded sidecar** pattern from microservices: the BFF server is co-located with your application process, auto-started on first use, with the lifecycle managed transparently.
53
+
54
+ | Aspect | Classic Microservice Sidecar | bff-store Embedded Sidecar |
55
+ |--------|----------------------------|--------------------------|
56
+ | Deployment | Separate container, same pod | In-process, same Node.js process |
57
+ | Startup | Kubernetes orchestrates | `createStore()` triggers `import('./server')` |
58
+ | Singleton | One per pod | One per process (singleton `startServer`) |
59
+ | State | External DB / volume | JSONL / MongoDB |
60
+ | Scaling | Horizontal pod scaling | Multiple store instances share one server |
61
+ | Cold start | Pod scheduling overhead | Module import + server listen (~100ms) |
62
+
63
+ This gives you the **transparency of serverless** (no manual server deployment) with the **control of a long-running process** (full backend, no 100ms timeout, no cold starts after first invocation).
64
+
65
+ ### How it compares to serverless
66
+
67
+ | | Serverless (Lambda) | bff-store Sidecar |
68
+ |--|---------------------|-------------------|
69
+ | Cold start | 100ms–1s (platform) | ~100ms (import + listen) |
70
+ | State | External KV store (S3/Dynamo) | JSONL / MongoDB (local) |
71
+ | Concurrency | One invocation per instance | All stores share one server |
72
+ | Tenant isolation | Separate functions | Separate collections / directories |
73
+ | Failure mode | Invocation fails | Server restarts, pending writes lost (mitigated by unmount cancel) |
74
+
40
75
  ## Installation
41
76
 
42
77
  ```bash
@@ -47,7 +82,7 @@ pnpm add bff-store
47
82
 
48
83
  ## Client Usage (Browser / Next.js)
49
84
 
50
- BFF Server 会自动启动,客户端只需使用 `remoteStorage()` 并指定存储后端:
85
+ The BFF server starts automatically. Use `remoteStorage()` to specify the storage backend:
51
86
 
52
87
  ```typescript
53
88
  import { createStore, useStore, remoteStorage } from 'bff-store';
@@ -55,9 +90,10 @@ import { createStore, useStore, remoteStorage } from 'bff-store';
55
90
  const config = [
56
91
  { key: 'theme', defaultValue: 'dark' },
57
92
  { key: 'characters', defaultValue: [] },
93
+ { key: 'settings', defaultValue: {}, immediate: true }, // persist immediately, no debounce
58
94
  ] as const;
59
95
 
60
- // 使用 MongoDB 存储
96
+ // Using MongoDB
61
97
  const store = createStore('my-app', config, {
62
98
  storage: remoteStorage({
63
99
  backend: 'mongodb',
@@ -66,7 +102,7 @@ const store = createStore('my-app', config, {
66
102
  }),
67
103
  });
68
104
 
69
- // 或使用 JSONL 存储
105
+ // Or using JSONL
70
106
  // const store = createStore('my-app', config, {
71
107
  // storage: remoteStorage({
72
108
  // backend: 'jsonl',
@@ -75,60 +111,75 @@ const store = createStore('my-app', config, {
75
111
  // });
76
112
  ```
77
113
 
78
- ### React 组件中使用
114
+ ### In a React Component
79
115
 
80
116
  ```typescript
81
117
  function App() {
82
- const { theme, setTheme, isLoading } = useStore(store);
118
+ const { theme, setTheme, characters, setCharacters, isLoading } = useStore(store);
83
119
 
84
120
  if (isLoading) return <div>Loading...</div>;
85
121
 
86
- return <input value={theme} onChange={e => setTheme(e.target.value)} />;
122
+ return (
123
+ <input value={theme} onChange={e => setTheme(e.target.value)} />
124
+ );
87
125
  }
88
126
  ```
89
127
 
90
- ### 自定义 BFF 服务器地址
128
+ ### Immediate Persistence (no debounce)
129
+
130
+ For critical data, use `immediate: true`:
91
131
 
92
132
  ```typescript
93
- // 如果 BFF 运行在其他地址
94
- const adapter = remoteStorage({ baseUrl: 'http://localhost:3847' });
95
- const adapter = remoteStorage({ entityId: 'user-123' }); // 多租户支持
133
+ const config = [
134
+ { key: 'autosave', defaultValue: '', immediate: true },
135
+ ] as const;
96
136
  ```
97
137
 
98
- ### Memory Storage (不持久化)
138
+ ### Waiting for the Server
99
139
 
100
- 适用于开发环境或不需要持久化的场景:
140
+ `createStore` is synchronous, but server startup is asynchronous. Optionally wait:
101
141
 
102
142
  ```typescript
103
- import { createStore, useStore, memoryStorage } from 'bff-store';
143
+ import { createStore, waitForServer } from 'bff-store';
104
144
 
105
- const store = createStore('my-app', config, {
106
- storage: memoryStorage(), // 不走 BFF,数据仅存在内存
107
- });
145
+ const store = createStore('my-app', config, { storage: adapter });
146
+ await waitForServer();
108
147
  ```
109
148
 
110
- ## Advanced: 手动启动 BFF Server
149
+ ### Multi-Tenant Switching
111
150
 
112
- 大多数情况不需要手动启动 BFF,它会自动启动。如需手动控制:
151
+ Switch tenants dynamically via `setEntityId`:
113
152
 
114
153
  ```typescript
115
- import { startServer } from 'bff-store/server';
154
+ const adapter = remoteStorage({ entityId: 'tenant-A' });
155
+ const store = createStore('tenant-A', config, { storage: adapter });
116
156
 
117
- await startServer({
118
- port: 3847,
157
+ // Switch tenant
158
+ adapter.setEntityId('tenant-B');
159
+ ```
160
+
161
+ ### Memory Storage (non-persistent)
162
+
163
+ For development or non-persistent use cases:
164
+
165
+ ```typescript
166
+ import { createStore, useStore, memoryStorage } from 'bff-store';
167
+
168
+ const store = createStore('my-app', config, {
169
+ storage: memoryStorage(), // data lives only in memory
119
170
  });
120
171
  ```
121
172
 
122
- ## Node.js 直接使用 Storage Adapters
173
+ ## Node.js Direct Storage Adapter Usage
123
174
 
124
- 如果不通过 BFF,可以直接在 Node.js 中使用存储适配器:
175
+ You can also use storage adapters directly in Node.js without the BFF:
125
176
 
126
177
  ```typescript
127
178
  import { createNodeStore, getDefaultStore } from 'bff-store';
128
179
  import { jsonlStorage } from 'bff-store/jsonl';
129
180
  import { mongodbStorage } from 'bff-store/mongodb';
130
181
 
131
- // 使用 JSONL
182
+ // Using JSONL
132
183
  const store = createNodeStore('entity-123', [
133
184
  { key: 'theme', defaultValue: 'dark' },
134
185
  { key: 'count', defaultValue: 0 },
@@ -136,15 +187,15 @@ const store = createNodeStore('entity-123', [
136
187
  storage: jsonlStorage({ dir: './sessions' }),
137
188
  });
138
189
 
139
- // 等待初始加载完成
190
+ // Wait for initial load (5s timeout)
140
191
  await store.waitForLoad();
141
192
 
142
- // 使用 jotai getDefaultStore() 读写
193
+ // Read/write via jotai getDefaultStore()
143
194
  const jotai = getDefaultStore();
144
- jotai.set(store.atoms.theme, 'light'); // 自动 debounce 持久化
195
+ jotai.set(store.atoms.theme, 'light'); // auto-debounce persists
145
196
  console.log(jotai.get(store.atoms.count));
146
197
 
147
- // 或使用 MongoDB
198
+ // Or using MongoDB
148
199
  const store2 = createNodeStore('entity-456', [
149
200
  { key: 'data', defaultValue: null },
150
201
  ], {
@@ -155,20 +206,32 @@ const store2 = createNodeStore('entity-456', [
155
206
  });
156
207
  ```
157
208
 
158
- ### 环境检测
209
+ ### Environment Detection
159
210
 
160
211
  ```typescript
161
212
  import { isNode, isBrowser } from 'bff-store';
162
213
 
163
- if (isNode()) {
164
- // Node.js 环境
165
- }
166
-
167
- if (isBrowser()) {
168
- // 浏览器环境
169
- }
214
+ if (isNode()) { /* Node.js */ }
215
+ if (isBrowser()) { /* Browser */ }
170
216
  ```
171
217
 
218
+ ## Storage Backends
219
+
220
+ ### JSONL
221
+
222
+ File format: `{dir}/{entityId}/{encodeURIComponent(key)}.jsonl`
223
+
224
+ Each line is a JSON object with timestamp. `get` reads the last line for the latest value.
225
+
226
+ ### MongoDB
227
+
228
+ Collection name: `state_{entityId}`. Each key uses upsert — only the latest value is kept.
229
+
230
+ > For production, create a compound index on `key + entityId`:
231
+ > ```javascript
232
+ > db.state_<entityId>.createIndex({ key: 1, entityId: 1 }, { unique: true })
233
+ > ```
234
+
172
235
  ## API Endpoints (BFF Server)
173
236
 
174
237
  | Method | Endpoint | Body | Description |
@@ -191,9 +254,18 @@ Creates a store with multiple persisted atoms.
191
254
  - `options.storage`: Storage adapter (required)
192
255
  - `options.debounceMs`: Debounce delay in ms (default: 800)
193
256
 
257
+ ### `waitForServer()`
258
+
259
+ Returns a promise that resolves when the auto-started BFF server is ready. Only meaningful in Node.js with `remoteStorage`.
260
+
261
+ ```typescript
262
+ import { waitForServer } from 'bff-store';
263
+ await waitForServer();
264
+ ```
265
+
194
266
  ### `useStore(store)`
195
267
 
196
- React hook to use the store in components.
268
+ React hook to consume the store in components.
197
269
 
198
270
  Returns: `{ ...data, ...setters, isLoading }`
199
271
 
@@ -201,26 +273,39 @@ Setter names are derived by capitalizing the config key:
201
273
  - `theme` → `setTheme`
202
274
  - `userName` → `setUserName`
203
275
 
276
+ ### `createNodeStore(entityId, config, options)`
277
+
278
+ Node.js environment store with `waitForLoad()`.
279
+
280
+ ```typescript
281
+ const store = createNodeStore('entity-123', config, { storage: adapter });
282
+ await store.waitForLoad(); // 5s timeout
283
+ ```
284
+
204
285
  ### `remoteStorage(options?)`
205
286
 
206
287
  Creates a remote storage adapter for connecting to the BFF server.
207
288
 
208
289
  ```typescript
209
- // 基本用法 - BFF 使用默认存储后端
290
+ // Basic
210
291
  const adapter = remoteStorage();
211
292
 
212
- // 指定使用 MongoDB
293
+ // Use MongoDB
213
294
  const adapter = remoteStorage({
214
295
  backend: 'mongodb',
215
296
  mongoUrl: 'mongodb://user:pass@host:27017',
216
297
  mongoDb: 'myapp',
217
298
  });
218
299
 
219
- // 指定使用 JSONL
300
+ // Use JSONL
220
301
  const adapter = remoteStorage({
221
302
  backend: 'jsonl',
222
303
  jsonlDir: '/tmp/my-app-data',
223
304
  });
305
+
306
+ // Multi-tenant
307
+ const adapter = remoteStorage({ entityId: 'user-123' });
308
+ adapter.setEntityId('user-456'); // switch tenant
224
309
  ```
225
310
 
226
311
  - `options.baseUrl`: BFF server URL (default: `http://localhost:3847`)
@@ -228,24 +313,43 @@ const adapter = remoteStorage({
228
313
  - `options.backend`: Storage backend type `'mongodb'` or `'jsonl'`
229
314
  - `options.mongoUrl`: MongoDB connection URL (required if backend is mongodb)
230
315
  - `options.mongoDb`: MongoDB database name (default: `jotai_state_store`)
231
- - `options.jsonlDir`: JSONL storage directory (required if backend is jsonl)
316
+ - `options.jsonlDir`: JSONL storage directory (default: `./data`)
232
317
 
233
318
  ### `startServer(options)`
234
319
 
235
320
  Starts the BFF server (singleton pattern). Import from `bff-store/server`.
236
321
 
237
- - `options.port`: Server port (default: 3847)
322
+ ```typescript
323
+ import { startServer } from 'bff-store/server';
238
324
 
239
- Note: Storage backend is configured by the client via `remoteStorage()` options, not by the server.
325
+ await startServer({
326
+ port: 3847,
327
+ backend: 'jsonl',
328
+ jsonlDir: './data',
329
+ });
330
+ ```
240
331
 
241
332
  ## Package Exports
242
333
 
243
334
  | Export | Description | Environment |
244
335
  |--------|-------------|-------------|
245
- | `bff-store` | Main entry: createStore, useStore, createNodeStore, isNode, isBrowser, memoryStorage, remoteStorage | Browser + Node.js |
336
+ | `bff-store` | Main: createStore, useStore, createNodeStore, waitForServer, isNode, isBrowser, memoryStorage, remoteStorage | Browser + Node.js |
246
337
  | `bff-store/jsonl` | JSONL storage adapter | Node.js only |
247
- | `bff-store/mongodb` | MongoDB storage adapter | Node.js only |
248
- | `bff-store/server` | BFF server (startServer, Router, etc.) | Node.js only |
338
+ | `bff-store/mongodb` | MongoDB storage adapter (async factory) | Node.js only |
339
+ | `bff-store/server` | BFF server: startServer, Router, EntityIdCache | Node.js only |
340
+
341
+ ## Changelog
342
+
343
+ ### v0.1.1 (2026-07-05)
344
+
345
+ - `createStore` with `remote` storage now waits for server startup to complete
346
+ - `waitForServer()` added for explicit server readiness
347
+ - `remoteStorage.setEntityId()` now works correctly (live entityId reference)
348
+ - MongoDB `set` uses upsert mode — no more unbounded document accumulation
349
+ - JSONL key encoding changed to `encodeURIComponent` — prevents `a.b` / `a-b` collision
350
+ - `atom.onMount` unmount cleanup cancels pending debounced writes
351
+ - `waitForLoad()` now has a 5s timeout
352
+ - HTTP error responses now include server-returned error details
249
353
 
250
354
  ## License
251
355
 
package/dist/cli.js CHANGED
@@ -32025,12 +32025,11 @@ async function mongodbStorage(options2) {
32025
32025
  },
32026
32026
  async set(key, value) {
32027
32027
  const collection = getCollection(currentEntityId);
32028
- await collection.insertOne({
32029
- key,
32030
- value,
32031
- timestamp: Date.now(),
32032
- entityId: currentEntityId
32033
- });
32028
+ await collection.updateOne(
32029
+ { key, entityId: currentEntityId },
32030
+ { $set: { value, timestamp: Date.now() } },
32031
+ { upsert: true }
32032
+ );
32034
32033
  },
32035
32034
  async remove(key) {
32036
32035
  const collection = getCollection(currentEntityId);
@@ -32133,7 +32132,7 @@ function jsonlStorage(options2) {
32133
32132
  const baseDir = options2?.dir ?? "./sessions";
32134
32133
  let entityId = "default";
32135
32134
  function getFilePath(eId, key) {
32136
- const safeKey = key.replace(/[^a-zA-Z0-9_]/g, "_");
32135
+ const safeKey = encodeURIComponent(key);
32137
32136
  return path.join(baseDir, eId, `${safeKey}.jsonl`);
32138
32137
  }
32139
32138
  const storage = {
@@ -32281,7 +32280,7 @@ async function getCachedStorage(config, entityId) {
32281
32280
  }
32282
32281
  return entry.adapter.storage;
32283
32282
  }
32284
- if (storageCache.size >= MAX_CACHE_SIZE) {
32283
+ while (storageCache.size >= MAX_CACHE_SIZE && storageCache.size > 0) {
32285
32284
  let oldestKey = null;
32286
32285
  let oldestTime = Infinity;
32287
32286
  for (const [k, entry] of storageCache.entries()) {
@@ -32292,6 +32291,8 @@ async function getCachedStorage(config, entityId) {
32292
32291
  }
32293
32292
  if (oldestKey) {
32294
32293
  storageCache.delete(oldestKey);
32294
+ } else {
32295
+ break;
32295
32296
  }
32296
32297
  }
32297
32298
  let adapter;
@@ -32324,6 +32325,7 @@ function createStorageHandlers(options2) {
32324
32325
  try {
32325
32326
  body = await parseBody(req);
32326
32327
  } catch {
32328
+ console.warn("[bff-store] Failed to parse request body, using URL config only");
32327
32329
  }
32328
32330
  }
32329
32331
  const config = body ? {
@@ -32399,7 +32401,7 @@ function createStorageHandlers(options2) {
32399
32401
  res.writeHead(200);
32400
32402
  res.end(JSON.stringify({ success: true }));
32401
32403
  }
32402
- async function handleHealth(req, res) {
32404
+ async function handleHealth(_req, res) {
32403
32405
  res.setHeader("Content-Type", "application/json");
32404
32406
  res.writeHead(200);
32405
32407
  res.end(JSON.stringify({ status: "ok" }));
package/dist/index.d.mts CHANGED
@@ -79,6 +79,12 @@ declare function createStore(entityId: string, config: AtomConfigs, options?: {
79
79
  storage: StorageAdapter;
80
80
  debounceMs?: number;
81
81
  }): Store;
82
+ /**
83
+ * Wait for the auto-started server to be ready.
84
+ * Only meaningful when using remote storage in Node.js;
85
+ * returns a resolved promise in browser environments.
86
+ */
87
+ declare function waitForServer(): Promise<void> | undefined;
82
88
 
83
89
  /**
84
90
  * React Hook to use a store
@@ -270,4 +276,4 @@ declare function createNodeStore(entityId: string, config: AtomConfigs, options:
270
276
  waitForLoad(): Promise<void>;
271
277
  };
272
278
 
273
- export { type AsyncStorageFactory, type AtomConfig, type AtomConfigs, type AtomType, HttpTransport, type MemoryStorageOptions, RestStorageProtocol, type Storage, type StorageAdapter, type StorageFactory, type StorageHttpProtocol, type StorageOptions, type Store, type TransportAdapter, type UseStoreReturn, createMemoryStorage, createNodeStore, createPersistedAtom, remoteStorage as createRemoteStorage, createStorageFromTransport, createStorageWithProtocol, createStore, isBrowser, isNode, memoryStorage, remoteStorage, useStore };
279
+ export { type AsyncStorageFactory, type AtomConfig, type AtomConfigs, type AtomType, HttpTransport, type MemoryStorageOptions, RestStorageProtocol, type Storage, type StorageAdapter, type StorageFactory, type StorageHttpProtocol, type StorageOptions, type Store, type TransportAdapter, type UseStoreReturn, createMemoryStorage, createNodeStore, createPersistedAtom, remoteStorage as createRemoteStorage, createStorageFromTransport, createStorageWithProtocol, createStore, isBrowser, isNode, memoryStorage, remoteStorage, useStore, waitForServer };
package/dist/index.d.ts CHANGED
@@ -79,6 +79,12 @@ declare function createStore(entityId: string, config: AtomConfigs, options?: {
79
79
  storage: StorageAdapter;
80
80
  debounceMs?: number;
81
81
  }): Store;
82
+ /**
83
+ * Wait for the auto-started server to be ready.
84
+ * Only meaningful when using remote storage in Node.js;
85
+ * returns a resolved promise in browser environments.
86
+ */
87
+ declare function waitForServer(): Promise<void> | undefined;
82
88
 
83
89
  /**
84
90
  * React Hook to use a store
@@ -270,4 +276,4 @@ declare function createNodeStore(entityId: string, config: AtomConfigs, options:
270
276
  waitForLoad(): Promise<void>;
271
277
  };
272
278
 
273
- export { type AsyncStorageFactory, type AtomConfig, type AtomConfigs, type AtomType, HttpTransport, type MemoryStorageOptions, RestStorageProtocol, type Storage, type StorageAdapter, type StorageFactory, type StorageHttpProtocol, type StorageOptions, type Store, type TransportAdapter, type UseStoreReturn, createMemoryStorage, createNodeStore, createPersistedAtom, remoteStorage as createRemoteStorage, createStorageFromTransport, createStorageWithProtocol, createStore, isBrowser, isNode, memoryStorage, remoteStorage, useStore };
279
+ export { type AsyncStorageFactory, type AtomConfig, type AtomConfigs, type AtomType, HttpTransport, type MemoryStorageOptions, RestStorageProtocol, type Storage, type StorageAdapter, type StorageFactory, type StorageHttpProtocol, type StorageOptions, type Store, type TransportAdapter, type UseStoreReturn, createMemoryStorage, createNodeStore, createPersistedAtom, remoteStorage as createRemoteStorage, createStorageFromTransport, createStorageWithProtocol, createStore, isBrowser, isNode, memoryStorage, remoteStorage, useStore, waitForServer };
package/dist/index.mjs CHANGED
@@ -30,8 +30,9 @@ var DebouncerMap = class {
30
30
  }
31
31
  getDebouncer(key, ms) {
32
32
  let debouncer = this.debouncers.get(key);
33
- if (!debouncer) {
34
- debouncer = createDebouncer(ms ?? this.defaultMs);
33
+ const effectiveMs = ms ?? this.defaultMs;
34
+ if (!debouncer || debouncer.ms !== effectiveMs) {
35
+ debouncer = createDebouncer(effectiveMs);
35
36
  this.debouncers.set(key, debouncer);
36
37
  }
37
38
  return debouncer;
@@ -73,12 +74,18 @@ function createPersistedAtom(config, entityId, storage, options) {
73
74
  if (value !== null && value !== void 0) {
74
75
  setValue(value);
75
76
  }
76
- }).catch(console.error).finally(() => {
77
+ }).catch((err) => {
78
+ console.error(`[bff-store] Failed to load atom "${config.key}":`, err);
79
+ }).finally(() => {
77
80
  setTimeout(() => {
78
81
  const store = getDefaultStore();
79
82
  store.set(loadingAtom, false);
80
83
  }, 0);
81
84
  });
85
+ return () => {
86
+ const debounceKey = `${entityId}:${config.key}`;
87
+ debouncerMap.cancel(debounceKey);
88
+ };
82
89
  };
83
90
  const writeAtom = atom(
84
91
  (get) => get(baseAtom),
@@ -104,12 +111,13 @@ function createPersistedAtom(config, entityId, storage, options) {
104
111
  }
105
112
 
106
113
  // src/createStore.ts
114
+ var serverInitPromise = null;
107
115
  function createStore(entityId, config, options) {
108
116
  const adapter = options?.storage;
109
117
  const debounceMs = options?.debounceMs ?? 800;
110
118
  if (adapter?.name === "remote" && typeof window === "undefined" && typeof process !== "undefined") {
111
- import("./server-V7WCW4ZB.mjs").then(({ startServer }) => {
112
- startServer().catch((err) => {
119
+ import("./server-2WMLXQ23.mjs").then(({ startServer }) => {
120
+ serverInitPromise = startServer().then(() => void 0).catch((err) => {
113
121
  console.error("[bff-store] Failed to auto-start server:", err);
114
122
  });
115
123
  });
@@ -138,6 +146,9 @@ function createStore(entityId, config, options) {
138
146
  loadingAtoms
139
147
  };
140
148
  }
149
+ function waitForServer() {
150
+ return serverInitPromise;
151
+ }
141
152
 
142
153
  // src/useStore.ts
143
154
  import { useAtom } from "jotai";
@@ -224,7 +235,13 @@ var HttpTransport = class {
224
235
  async get(url) {
225
236
  const res = await fetch(url);
226
237
  if (!res.ok) {
227
- throw new Error(`GET ${url} failed: ${res.statusText}`);
238
+ let detail = res.statusText;
239
+ try {
240
+ const body = await res.clone().json();
241
+ detail = body?.error ?? body?.message ?? detail;
242
+ } catch {
243
+ }
244
+ throw new Error(`GET ${url} failed: ${detail}`);
228
245
  }
229
246
  return res.json();
230
247
  }
@@ -235,14 +252,26 @@ var HttpTransport = class {
235
252
  body: JSON.stringify(body)
236
253
  });
237
254
  if (!res.ok) {
238
- throw new Error(`POST ${url} failed: ${res.statusText}`);
255
+ let detail = res.statusText;
256
+ try {
257
+ const errBody = await res.clone().json();
258
+ detail = errBody?.error ?? errBody?.message ?? detail;
259
+ } catch {
260
+ }
261
+ throw new Error(`POST ${url} failed: ${detail}`);
239
262
  }
240
263
  return res.json();
241
264
  }
242
265
  async delete(url) {
243
266
  const res = await fetch(url, { method: "DELETE" });
244
267
  if (!res.ok) {
245
- throw new Error(`DELETE ${url} failed: ${res.statusText}`);
268
+ let detail = res.statusText;
269
+ try {
270
+ const errBody = await res.clone().json();
271
+ detail = errBody?.error ?? errBody?.message ?? detail;
272
+ } catch {
273
+ }
274
+ throw new Error(`DELETE ${url} failed: ${detail}`);
246
275
  }
247
276
  }
248
277
  };
@@ -404,7 +433,7 @@ function remoteStorage(options = {}) {
404
433
  mongoDb: options.mongoDb,
405
434
  jsonlDir: options.jsonlDir
406
435
  };
407
- const protocol = options.protocol ?? new RestStorageProtocol(baseUrl, entityId.current, backendConfig);
436
+ const protocol = options.protocol ?? new RestStorageProtocol(baseUrl, entityId, backendConfig);
408
437
  const storage = createStorageWithProtocol(transport, protocol);
409
438
  const adapter = {
410
439
  storage,
@@ -429,21 +458,26 @@ import { getDefaultStore as getDefaultStore3 } from "jotai";
429
458
  function createNodeStore(entityId, config, options) {
430
459
  const store = createStore(entityId, config, options);
431
460
  const jotaiStore = getDefaultStore3();
432
- async function waitForLoad() {
461
+ async function waitForLoad(timeoutMs = 5e3) {
433
462
  for (const atom2 of Object.values(store.atoms)) {
434
463
  jotaiStore.sub(atom2, () => {
435
464
  });
436
465
  }
437
- return new Promise((resolve) => {
466
+ return new Promise((resolve, reject) => {
438
467
  const loadingAtoms = Object.values(store.loadingAtoms);
439
468
  const stillLoading = () => loadingAtoms.some((atom2) => jotaiStore.get(atom2));
440
469
  if (!stillLoading()) {
441
470
  resolve();
442
471
  return;
443
472
  }
473
+ const timeout = setTimeout(() => {
474
+ clearInterval(interval);
475
+ reject(new Error(`waitForLoad timed out after ${timeoutMs}ms`));
476
+ }, timeoutMs);
444
477
  const interval = setInterval(() => {
445
478
  if (!stillLoading()) {
446
479
  clearInterval(interval);
480
+ clearTimeout(timeout);
447
481
  resolve();
448
482
  }
449
483
  }, 10);
@@ -469,5 +503,6 @@ export {
469
503
  isNode,
470
504
  memoryStorage,
471
505
  remoteStorage,
472
- useStore
506
+ useStore,
507
+ waitForServer
473
508
  };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bff-store",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A jotai-based state management library with pluggable storage adapters",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",