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.
- package/.claude/settings.local.json +14 -1
- package/README.md +196 -63
- package/dist/cli.js +11 -9
- package/dist/index.d.mts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.mjs +88 -10
- package/dist/package.json +3 -6
- package/dist/server/entry.d.mts +1 -1
- package/dist/server/entry.d.ts +1 -1
- package/dist/server/entry.js +31992 -23
- package/dist/server/entry.mjs +32016 -13
- 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 +31986 -10
- package/dist/storage/mongodb-entry.mjs +32008 -8
- package/docs/BUG_FIX_MONGODB_CHILD_PROCESS.md +129 -0
- package/docs/FRONTEND_BACKEND_ISOLATION.md +150 -0
- package/docs/IMPLEMENTATION_OVERVIEW.md +231 -0
- package/docs/LOCAL_LINK_STRATEGIES.md +117 -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 +3 -6
- package/src/atomCreator.ts +10 -2
- package/src/createStore.ts +19 -1
- package/src/debouncer.ts +5 -2
- package/src/environment.ts +11 -0
- package/src/index.ts +8 -1
- package/src/nodeStore.ts +86 -0
- 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
- package/tsconfig.json +1 -0
- package/docs/BUG_DIAGNOSIS_REMOTE_STORAGE_OPTIONS.md +0 -104
- package/docs/BUG_FIX_REMOTE_STORAGE_OPTIONS.md +0 -63
- package/docs/BUG_FIX_SESSION_2026-06-03.md +0 -171
- /package/docs/{SIDECAR_SERVER.md → BFF_SIDECAR_SERVER.md} +0 -0
|
@@ -39,7 +39,20 @@
|
|
|
39
39
|
"Bash(npm publish:*)",
|
|
40
40
|
"Bash(rg -rn \"jotai-state-store\" --type-list)",
|
|
41
41
|
"Bash(rg -rn \"jotai-state-store\" .)",
|
|
42
|
-
"Bash(xargs sed:*)"
|
|
42
|
+
"Bash(xargs sed:*)",
|
|
43
|
+
"Bash(npx tsx:*)",
|
|
44
|
+
"Bash(npm ls:*)",
|
|
45
|
+
"Bash(perl -e 'alarm 5; exec @ARGV' -- node debug-simple.js)",
|
|
46
|
+
"Bash(echo \"exit: $?\")",
|
|
47
|
+
"Bash([ -d node_modules/mongodb ])",
|
|
48
|
+
"Bash(echo \"---mongodb status: $\\([ -d node_modules/mongodb ] && echo 'INSTALLED' || echo 'NOT INSTALLED'\\)\")",
|
|
49
|
+
"Bash(echo \"---mongodb: $\\([ -d node_modules/mongodb ] && echo 'INSTALLED' || echo 'NOT INSTALLED'\\)\")",
|
|
50
|
+
"Bash(xargs kill:*)",
|
|
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)"
|
|
43
56
|
]
|
|
44
57
|
}
|
|
45
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,71 +111,127 @@ 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
|
-
import {
|
|
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
|
-
//
|
|
132
|
-
const
|
|
182
|
+
// Using JSONL
|
|
183
|
+
const store = createNodeStore('entity-123', [
|
|
184
|
+
{ key: 'theme', defaultValue: 'dark' },
|
|
185
|
+
{ key: 'count', defaultValue: 0 },
|
|
186
|
+
], {
|
|
187
|
+
storage: jsonlStorage({ dir: './sessions' }),
|
|
188
|
+
});
|
|
133
189
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
190
|
+
// Wait for initial load (5s timeout)
|
|
191
|
+
await store.waitForLoad();
|
|
192
|
+
|
|
193
|
+
// Read/write via jotai getDefaultStore()
|
|
194
|
+
const jotai = getDefaultStore();
|
|
195
|
+
jotai.set(store.atoms.theme, 'light'); // auto-debounce persists
|
|
196
|
+
console.log(jotai.get(store.atoms.count));
|
|
197
|
+
|
|
198
|
+
// Or using MongoDB
|
|
199
|
+
const store2 = createNodeStore('entity-456', [
|
|
200
|
+
{ key: 'data', defaultValue: null },
|
|
201
|
+
], {
|
|
202
|
+
storage: await mongodbStorage({
|
|
203
|
+
url: 'mongodb://localhost:27017',
|
|
204
|
+
database: 'myapp',
|
|
205
|
+
}),
|
|
138
206
|
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Environment Detection
|
|
139
210
|
|
|
140
|
-
|
|
211
|
+
```typescript
|
|
212
|
+
import { isNode, isBrowser } from 'bff-store';
|
|
213
|
+
|
|
214
|
+
if (isNode()) { /* Node.js */ }
|
|
215
|
+
if (isBrowser()) { /* Browser */ }
|
|
141
216
|
```
|
|
142
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
|
+
|
|
143
235
|
## API Endpoints (BFF Server)
|
|
144
236
|
|
|
145
237
|
| Method | Endpoint | Body | Description |
|
|
@@ -162,9 +254,18 @@ Creates a store with multiple persisted atoms.
|
|
|
162
254
|
- `options.storage`: Storage adapter (required)
|
|
163
255
|
- `options.debounceMs`: Debounce delay in ms (default: 800)
|
|
164
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
|
+
|
|
165
266
|
### `useStore(store)`
|
|
166
267
|
|
|
167
|
-
React hook to
|
|
268
|
+
React hook to consume the store in components.
|
|
168
269
|
|
|
169
270
|
Returns: `{ ...data, ...setters, isLoading }`
|
|
170
271
|
|
|
@@ -172,26 +273,39 @@ Setter names are derived by capitalizing the config key:
|
|
|
172
273
|
- `theme` → `setTheme`
|
|
173
274
|
- `userName` → `setUserName`
|
|
174
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
|
+
|
|
175
285
|
### `remoteStorage(options?)`
|
|
176
286
|
|
|
177
287
|
Creates a remote storage adapter for connecting to the BFF server.
|
|
178
288
|
|
|
179
289
|
```typescript
|
|
180
|
-
//
|
|
290
|
+
// Basic
|
|
181
291
|
const adapter = remoteStorage();
|
|
182
292
|
|
|
183
|
-
//
|
|
293
|
+
// Use MongoDB
|
|
184
294
|
const adapter = remoteStorage({
|
|
185
295
|
backend: 'mongodb',
|
|
186
296
|
mongoUrl: 'mongodb://user:pass@host:27017',
|
|
187
297
|
mongoDb: 'myapp',
|
|
188
298
|
});
|
|
189
299
|
|
|
190
|
-
//
|
|
300
|
+
// Use JSONL
|
|
191
301
|
const adapter = remoteStorage({
|
|
192
302
|
backend: 'jsonl',
|
|
193
303
|
jsonlDir: '/tmp/my-app-data',
|
|
194
304
|
});
|
|
305
|
+
|
|
306
|
+
// Multi-tenant
|
|
307
|
+
const adapter = remoteStorage({ entityId: 'user-123' });
|
|
308
|
+
adapter.setEntityId('user-456'); // switch tenant
|
|
195
309
|
```
|
|
196
310
|
|
|
197
311
|
- `options.baseUrl`: BFF server URL (default: `http://localhost:3847`)
|
|
@@ -199,24 +313,43 @@ const adapter = remoteStorage({
|
|
|
199
313
|
- `options.backend`: Storage backend type `'mongodb'` or `'jsonl'`
|
|
200
314
|
- `options.mongoUrl`: MongoDB connection URL (required if backend is mongodb)
|
|
201
315
|
- `options.mongoDb`: MongoDB database name (default: `jotai_state_store`)
|
|
202
|
-
- `options.jsonlDir`: JSONL storage directory (
|
|
316
|
+
- `options.jsonlDir`: JSONL storage directory (default: `./data`)
|
|
203
317
|
|
|
204
318
|
### `startServer(options)`
|
|
205
319
|
|
|
206
320
|
Starts the BFF server (singleton pattern). Import from `bff-store/server`.
|
|
207
321
|
|
|
208
|
-
|
|
322
|
+
```typescript
|
|
323
|
+
import { startServer } from 'bff-store/server';
|
|
209
324
|
|
|
210
|
-
|
|
325
|
+
await startServer({
|
|
326
|
+
port: 3847,
|
|
327
|
+
backend: 'jsonl',
|
|
328
|
+
jsonlDir: './data',
|
|
329
|
+
});
|
|
330
|
+
```
|
|
211
331
|
|
|
212
332
|
## Package Exports
|
|
213
333
|
|
|
214
334
|
| Export | Description | Environment |
|
|
215
335
|
|--------|-------------|-------------|
|
|
216
|
-
| `bff-store` | Main
|
|
336
|
+
| `bff-store` | Main: createStore, useStore, createNodeStore, waitForServer, isNode, isBrowser, memoryStorage, remoteStorage | Browser + Node.js |
|
|
217
337
|
| `bff-store/jsonl` | JSONL storage adapter | Node.js only |
|
|
218
|
-
| `bff-store/mongodb` | MongoDB storage adapter | Node.js only |
|
|
219
|
-
| `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
|
|
220
353
|
|
|
221
354
|
## License
|
|
222
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
|
|
@@ -229,4 +235,45 @@ interface RemoteStorageOptions {
|
|
|
229
235
|
*/
|
|
230
236
|
declare function remoteStorage(options?: RemoteStorageOptions): StorageAdapter;
|
|
231
237
|
|
|
232
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Environment detection utilities
|
|
240
|
+
*/
|
|
241
|
+
declare function isNode(): boolean;
|
|
242
|
+
declare function isBrowser(): boolean;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Create a store for Node.js environments.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* import { jsonlStorage } from 'bff-store/jsonl';
|
|
250
|
+
* import { createNodeStore } from 'bff-store';
|
|
251
|
+
*
|
|
252
|
+
* const store = createNodeStore('entity-123', [
|
|
253
|
+
* { key: 'theme', defaultValue: 'dark' },
|
|
254
|
+
* ], {
|
|
255
|
+
* storage: jsonlStorage({ dir: './sessions' }),
|
|
256
|
+
* });
|
|
257
|
+
*
|
|
258
|
+
* await store.waitForLoad();
|
|
259
|
+
*
|
|
260
|
+
* const jotai = getDefaultStore();
|
|
261
|
+
* jotai.set(store.atoms.theme, 'light');
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
declare function createNodeStore(entityId: string, config: AtomConfigs, options: {
|
|
265
|
+
storage: StorageAdapter;
|
|
266
|
+
debounceMs?: number;
|
|
267
|
+
}): {
|
|
268
|
+
/** Atoms to use with getDefaultStore().get/set */
|
|
269
|
+
atoms: StoreAtoms;
|
|
270
|
+
/** Loading state atoms */
|
|
271
|
+
loadingAtoms: StoreLoadingAtoms;
|
|
272
|
+
/**
|
|
273
|
+
* Wait for all atoms to finish loading from storage.
|
|
274
|
+
* Call this after creating the store to ensure data is ready.
|
|
275
|
+
*/
|
|
276
|
+
waitForLoad(): Promise<void>;
|
|
277
|
+
};
|
|
278
|
+
|
|
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
|
|
@@ -229,4 +235,45 @@ interface RemoteStorageOptions {
|
|
|
229
235
|
*/
|
|
230
236
|
declare function remoteStorage(options?: RemoteStorageOptions): StorageAdapter;
|
|
231
237
|
|
|
232
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Environment detection utilities
|
|
240
|
+
*/
|
|
241
|
+
declare function isNode(): boolean;
|
|
242
|
+
declare function isBrowser(): boolean;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Create a store for Node.js environments.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* import { jsonlStorage } from 'bff-store/jsonl';
|
|
250
|
+
* import { createNodeStore } from 'bff-store';
|
|
251
|
+
*
|
|
252
|
+
* const store = createNodeStore('entity-123', [
|
|
253
|
+
* { key: 'theme', defaultValue: 'dark' },
|
|
254
|
+
* ], {
|
|
255
|
+
* storage: jsonlStorage({ dir: './sessions' }),
|
|
256
|
+
* });
|
|
257
|
+
*
|
|
258
|
+
* await store.waitForLoad();
|
|
259
|
+
*
|
|
260
|
+
* const jotai = getDefaultStore();
|
|
261
|
+
* jotai.set(store.atoms.theme, 'light');
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
declare function createNodeStore(entityId: string, config: AtomConfigs, options: {
|
|
265
|
+
storage: StorageAdapter;
|
|
266
|
+
debounceMs?: number;
|
|
267
|
+
}): {
|
|
268
|
+
/** Atoms to use with getDefaultStore().get/set */
|
|
269
|
+
atoms: StoreAtoms;
|
|
270
|
+
/** Loading state atoms */
|
|
271
|
+
loadingAtoms: StoreLoadingAtoms;
|
|
272
|
+
/**
|
|
273
|
+
* Wait for all atoms to finish loading from storage.
|
|
274
|
+
* Call this after creating the store to ensure data is ready.
|
|
275
|
+
*/
|
|
276
|
+
waitForLoad(): Promise<void>;
|
|
277
|
+
};
|
|
278
|
+
|
|
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 };
|