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.
- package/.claude/settings.local.json +5 -1
- package/README.md +172 -68
- package/dist/cli.js +11 -9
- package/dist/index.d.mts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.mjs +47 -12
- package/dist/package.json +1 -1
- package/dist/server/entry.d.mts +1 -1
- package/dist/server/entry.d.ts +1 -1
- package/dist/server/entry.js +11 -9
- package/dist/server/entry.mjs +11 -9
- package/dist/{server-V7WCW4ZB.mjs → server-2WMLXQ23.mjs} +11 -9
- package/dist/storage/jsonl-entry.js +1 -1
- package/dist/storage/jsonl-entry.mjs +1 -1
- package/dist/storage/mongodb-entry.js +5 -6
- package/dist/storage/mongodb-entry.mjs +5 -6
- package/docs/IMPLEMENTATION_OVERVIEW.md +231 -0
- package/docs/bugs/01-server-async-startup.high.md +34 -0
- package/docs/bugs/03-storage-cache-eviction.md +40 -0
- package/docs/bugs/04-http-error-body-loss.md +38 -0
- package/docs/bugs/05-debouncer-ms-ignored.md +41 -0
- package/docs/bugs/07-jsonl-key-collision.high.md +31 -0
- package/docs/bugs/08-dead-code-getStorageForRequest.md +18 -0
- package/docs/bugs/09-waitForLoad-infinite.md +37 -0
- package/docs/bugs/10-parseBody-silent-failure.md +32 -0
- package/docs/bugs/11-mongodb-storage-leak.high.md +35 -0
- package/docs/bugs/12-setEntityId-stale-closure.high.md +35 -0
- package/docs/bugs/14-dead-code-isServerRunning.md +18 -0
- package/docs/bugs/15-unmount-data-loss.md +37 -0
- package/docs/bugs/17-unused-parameter.md +26 -0
- package/docs/bugs/18-storage-get-silent-error.md +28 -0
- package/docs/bugs/README.md +35 -0
- package/package.json +1 -4
- package/src/atomCreator.ts +10 -2
- package/src/createStore.ts +19 -1
- package/src/debouncer.ts +5 -2
- package/src/index.ts +1 -1
- package/src/nodeStore.ts +9 -2
- package/src/server/handlers.ts +7 -24
- package/src/server/index.ts +0 -4
- package/src/storage/adapters/remoteStorage.ts +2 -1
- package/src/storage/jsonl.ts +3 -1
- package/src/storage/mongodb.ts +6 -6
- package/src/storage/transport.ts +24 -3
- 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
|
-
│ │
|
|
26
|
-
│ │
|
|
27
|
-
│ │
|
|
28
|
-
│ │ /storage/
|
|
29
|
-
│ │ /storage/
|
|
30
|
-
│ │
|
|
31
|
-
│
|
|
32
|
-
│ │
|
|
33
|
-
│
|
|
34
|
-
│
|
|
35
|
-
│ │
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
###
|
|
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
|
|
122
|
+
return (
|
|
123
|
+
<input value={theme} onChange={e => setTheme(e.target.value)} />
|
|
124
|
+
);
|
|
87
125
|
}
|
|
88
126
|
```
|
|
89
127
|
|
|
90
|
-
###
|
|
128
|
+
### Immediate Persistence (no debounce)
|
|
129
|
+
|
|
130
|
+
For critical data, use `immediate: true`:
|
|
91
131
|
|
|
92
132
|
```typescript
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
133
|
+
const config = [
|
|
134
|
+
{ key: 'autosave', defaultValue: '', immediate: true },
|
|
135
|
+
] as const;
|
|
96
136
|
```
|
|
97
137
|
|
|
98
|
-
###
|
|
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,
|
|
143
|
+
import { createStore, waitForServer } from 'bff-store';
|
|
104
144
|
|
|
105
|
-
const store = createStore('my-app', config, {
|
|
106
|
-
|
|
107
|
-
});
|
|
145
|
+
const store = createStore('my-app', config, { storage: adapter });
|
|
146
|
+
await waitForServer();
|
|
108
147
|
```
|
|
109
148
|
|
|
110
|
-
|
|
149
|
+
### Multi-Tenant Switching
|
|
111
150
|
|
|
112
|
-
|
|
151
|
+
Switch tenants dynamically via `setEntityId`:
|
|
113
152
|
|
|
114
153
|
```typescript
|
|
115
|
-
|
|
154
|
+
const adapter = remoteStorage({ entityId: 'tenant-A' });
|
|
155
|
+
const store = createStore('tenant-A', config, { storage: adapter });
|
|
116
156
|
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
173
|
+
## Node.js Direct Storage Adapter Usage
|
|
123
174
|
|
|
124
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
193
|
+
// Read/write via jotai getDefaultStore()
|
|
143
194
|
const jotai = getDefaultStore();
|
|
144
|
-
jotai.set(store.atoms.theme, 'light'); //
|
|
195
|
+
jotai.set(store.atoms.theme, 'light'); // auto-debounce persists
|
|
145
196
|
console.log(jotai.get(store.atoms.count));
|
|
146
197
|
|
|
147
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
290
|
+
// Basic
|
|
210
291
|
const adapter = remoteStorage();
|
|
211
292
|
|
|
212
|
-
//
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
|
|
322
|
+
```typescript
|
|
323
|
+
import { startServer } from 'bff-store/server';
|
|
238
324
|
|
|
239
|
-
|
|
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
|
|
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
|
|
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.
|
|
32029
|
-
key,
|
|
32030
|
-
value,
|
|
32031
|
-
|
|
32032
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
34
|
-
|
|
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(
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
};
|