bff-store 0.1.0
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 +45 -0
- package/CONTEXT.md +53 -0
- package/README.md +223 -0
- package/dist/cli.js +32577 -0
- package/dist/index.d.mts +232 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.mjs +430 -0
- package/dist/package.json +62 -0
- package/dist/server/entry.d.mts +94 -0
- package/dist/server/entry.d.ts +94 -0
- package/dist/server/entry.js +573 -0
- package/dist/server/entry.mjs +533 -0
- package/dist/server-V7WCW4ZB.mjs +530 -0
- package/dist/storage/jsonl-entry.d.mts +42 -0
- package/dist/storage/jsonl-entry.d.ts +42 -0
- package/dist/storage/jsonl-entry.js +112 -0
- package/dist/storage/jsonl-entry.mjs +74 -0
- package/dist/storage/mongodb-entry.d.mts +40 -0
- package/dist/storage/mongodb-entry.d.ts +40 -0
- package/dist/storage/mongodb-entry.js +114 -0
- package/dist/storage/mongodb-entry.mjs +86 -0
- package/docs/BUG_DIAGNOSIS_REMOTE_STORAGE_OPTIONS.md +104 -0
- package/docs/BUG_FIX_REMOTE_STORAGE_OPTIONS.md +63 -0
- package/docs/BUG_FIX_SESSION_2026-06-03.md +171 -0
- package/docs/IMPLEMENTATION.md +333 -0
- package/docs/PLAN.md +153 -0
- package/docs/REMOTE_STORAGE_CONFIG.md +125 -0
- package/docs/SIDECAR_SERVER.md +184 -0
- package/package.json +62 -0
- package/scripts/adapt-dist-package.js +33 -0
- package/src/atomCreator.ts +76 -0
- package/src/createStore.ts +77 -0
- package/src/debouncer.ts +84 -0
- package/src/index.ts +35 -0
- package/src/server/cli.ts +62 -0
- package/src/server/entityIdCache.ts +57 -0
- package/src/server/entry.ts +12 -0
- package/src/server/handlers.ts +271 -0
- package/src/server/index.ts +182 -0
- package/src/server/router.ts +74 -0
- package/src/server.ts +5 -0
- package/src/storage/adapters/remoteStorage.ts +70 -0
- package/src/storage/base.ts +28 -0
- package/src/storage/index.ts +9 -0
- package/src/storage/jsonl-entry.ts +9 -0
- package/src/storage/jsonl.ts +111 -0
- package/src/storage/memory.ts +49 -0
- package/src/storage/mongodb-entry.ts +9 -0
- package/src/storage/mongodb.ts +132 -0
- package/src/storage/protocol.ts +170 -0
- package/src/storage/transport.ts +95 -0
- package/src/types.ts +76 -0
- package/src/useStore.ts +83 -0
- package/tests/atomCreator.test.ts +153 -0
- package/tests/createStore.test.ts +126 -0
- package/tests/debouncer.test.ts +125 -0
- package/tests/server.test.ts +158 -0
- package/tests/storage/jsonl.test.ts +132 -0
- package/tests/storage/memory.test.ts +101 -0
- package/tests/storage/mongodb.test.ts +40 -0
- package/tests/storage/remoteStorage.test.ts +126 -0
- package/tests/useStore.test.tsx +147 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +53 -0
- package/vitest.config.ts +14 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// src/atomCreator.ts
|
|
2
|
+
import { atom, getDefaultStore } from "jotai";
|
|
3
|
+
|
|
4
|
+
// src/debouncer.ts
|
|
5
|
+
function createDebouncer(ms) {
|
|
6
|
+
let timer = null;
|
|
7
|
+
return {
|
|
8
|
+
get ms() {
|
|
9
|
+
return ms;
|
|
10
|
+
},
|
|
11
|
+
run(fn) {
|
|
12
|
+
if (timer) clearTimeout(timer);
|
|
13
|
+
timer = setTimeout(() => {
|
|
14
|
+
fn();
|
|
15
|
+
timer = null;
|
|
16
|
+
}, ms);
|
|
17
|
+
},
|
|
18
|
+
cancel() {
|
|
19
|
+
if (timer) {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
timer = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var DebouncerMap = class {
|
|
27
|
+
constructor(defaultMs = 800) {
|
|
28
|
+
this.debouncers = /* @__PURE__ */ new Map();
|
|
29
|
+
this.defaultMs = defaultMs;
|
|
30
|
+
}
|
|
31
|
+
getDebouncer(key, ms) {
|
|
32
|
+
let debouncer = this.debouncers.get(key);
|
|
33
|
+
if (!debouncer) {
|
|
34
|
+
debouncer = createDebouncer(ms ?? this.defaultMs);
|
|
35
|
+
this.debouncers.set(key, debouncer);
|
|
36
|
+
}
|
|
37
|
+
return debouncer;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Execute a function with debounce for a given key.
|
|
41
|
+
* Subsequent calls for the same key reset the timer.
|
|
42
|
+
*/
|
|
43
|
+
debounce(key, fn, ms) {
|
|
44
|
+
const debouncer = this.getDebouncer(key, ms);
|
|
45
|
+
debouncer.run(fn);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Cancel pending debounced call for a key
|
|
49
|
+
*/
|
|
50
|
+
cancel(key) {
|
|
51
|
+
const debouncer = this.debouncers.get(key);
|
|
52
|
+
if (debouncer) {
|
|
53
|
+
debouncer.cancel();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Cancel all pending debounced calls
|
|
58
|
+
*/
|
|
59
|
+
cancelAll() {
|
|
60
|
+
for (const debouncer of this.debouncers.values()) {
|
|
61
|
+
debouncer.cancel();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/atomCreator.ts
|
|
67
|
+
var debouncerMap = new DebouncerMap();
|
|
68
|
+
function createPersistedAtom(config, entityId, storage, options) {
|
|
69
|
+
const baseAtom = atom(config.defaultValue);
|
|
70
|
+
const loadingAtom = atom(true);
|
|
71
|
+
baseAtom.onMount = (setValue) => {
|
|
72
|
+
storage.get(config.key).then((value) => {
|
|
73
|
+
if (value !== null && value !== void 0) {
|
|
74
|
+
setValue(value);
|
|
75
|
+
}
|
|
76
|
+
}).catch(console.error).finally(() => {
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
const store = getDefaultStore();
|
|
79
|
+
store.set(loadingAtom, false);
|
|
80
|
+
}, 0);
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
const writeAtom = atom(
|
|
84
|
+
(get) => get(baseAtom),
|
|
85
|
+
(get, set, update) => {
|
|
86
|
+
const newValue = typeof update === "function" ? update(get(baseAtom)) : update;
|
|
87
|
+
set(baseAtom, newValue);
|
|
88
|
+
if (options?.immediate) {
|
|
89
|
+
storage.set(config.key, newValue).catch(console.error);
|
|
90
|
+
} else {
|
|
91
|
+
const debounceKey = `${entityId}:${config.key}`;
|
|
92
|
+
const debounceMs = options?.debounceMs ?? 800;
|
|
93
|
+
const saveFn = () => {
|
|
94
|
+
storage.set(config.key, newValue).catch(console.error);
|
|
95
|
+
};
|
|
96
|
+
debouncerMap.debounce(debounceKey, saveFn, debounceMs);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
return {
|
|
101
|
+
atom: writeAtom,
|
|
102
|
+
loadingAtom
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/createStore.ts
|
|
107
|
+
function createStore(entityId, config, options) {
|
|
108
|
+
const adapter = options?.storage;
|
|
109
|
+
const debounceMs = options?.debounceMs ?? 800;
|
|
110
|
+
if (adapter?.name === "remote" && typeof window === "undefined" && typeof process !== "undefined") {
|
|
111
|
+
import("./server-V7WCW4ZB.mjs").then(({ startServer }) => {
|
|
112
|
+
startServer().catch((err) => {
|
|
113
|
+
console.error("[bff-store] Failed to auto-start server:", err);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (!adapter) {
|
|
118
|
+
throw new Error("Storage adapter is required");
|
|
119
|
+
}
|
|
120
|
+
if ("setEntityId" in adapter && typeof adapter.setEntityId === "function") {
|
|
121
|
+
adapter.setEntityId(entityId);
|
|
122
|
+
}
|
|
123
|
+
const storage = adapter.storage;
|
|
124
|
+
const atoms = {};
|
|
125
|
+
const loadingAtoms = {};
|
|
126
|
+
for (const atomConfig of config) {
|
|
127
|
+
const result = createPersistedAtom(atomConfig, entityId, storage, {
|
|
128
|
+
immediate: atomConfig.immediate,
|
|
129
|
+
debounceMs
|
|
130
|
+
});
|
|
131
|
+
atoms[atomConfig.key] = result.atom;
|
|
132
|
+
loadingAtoms[atomConfig.key] = result.loadingAtom;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
entityId,
|
|
136
|
+
config,
|
|
137
|
+
atoms,
|
|
138
|
+
loadingAtoms
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/useStore.ts
|
|
143
|
+
import { useAtom } from "jotai";
|
|
144
|
+
import { useEffect, useState } from "react";
|
|
145
|
+
import { getDefaultStore as getDefaultStore2 } from "jotai";
|
|
146
|
+
function useStore(store) {
|
|
147
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const storeInstance = getDefaultStore2();
|
|
150
|
+
const loadingAtoms = Object.values(store.loadingAtoms);
|
|
151
|
+
if (loadingAtoms.length === 0) {
|
|
152
|
+
setIsLoading(false);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const checkLoadingStatus = () => {
|
|
156
|
+
const loadingStates = loadingAtoms.map((atom2) => storeInstance.get(atom2));
|
|
157
|
+
const anyLoading = loadingStates.some((loading) => loading === true);
|
|
158
|
+
setIsLoading(anyLoading);
|
|
159
|
+
};
|
|
160
|
+
checkLoadingStatus();
|
|
161
|
+
const unsubscribers = loadingAtoms.map(
|
|
162
|
+
(atom2) => storeInstance.sub(atom2, checkLoadingStatus)
|
|
163
|
+
);
|
|
164
|
+
return () => {
|
|
165
|
+
unsubscribers.forEach((unsub) => unsub());
|
|
166
|
+
};
|
|
167
|
+
}, [store.loadingAtoms]);
|
|
168
|
+
const result = buildStoreResult(store, isLoading);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
function buildStoreResult(store, isLoading) {
|
|
172
|
+
const result = { isLoading };
|
|
173
|
+
for (let i = 0; i < store.config.length; i++) {
|
|
174
|
+
const config = store.config[i];
|
|
175
|
+
const atom2 = store.atoms[config.key];
|
|
176
|
+
const [value, setter] = useAtom(atom2);
|
|
177
|
+
result[config.key] = value;
|
|
178
|
+
result[`set${capitalize(config.key)}`] = setter;
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
function capitalize(str) {
|
|
183
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/storage/memory.ts
|
|
187
|
+
function memoryStorage(_options) {
|
|
188
|
+
const store = /* @__PURE__ */ new Map();
|
|
189
|
+
const storage = {
|
|
190
|
+
async get(key) {
|
|
191
|
+
return store.get(key) ?? null;
|
|
192
|
+
},
|
|
193
|
+
async set(key, value) {
|
|
194
|
+
store.set(key, value);
|
|
195
|
+
},
|
|
196
|
+
async remove(key) {
|
|
197
|
+
store.delete(key);
|
|
198
|
+
},
|
|
199
|
+
async getMultiple(keys) {
|
|
200
|
+
const result = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const key of keys) {
|
|
202
|
+
const value = store.get(key);
|
|
203
|
+
if (value !== void 0) {
|
|
204
|
+
result.set(key, value);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
},
|
|
209
|
+
async setMultiple(entries) {
|
|
210
|
+
entries.forEach((value, key) => {
|
|
211
|
+
store.set(key, value);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
storage,
|
|
217
|
+
name: "memory"
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
var createMemoryStorage = memoryStorage;
|
|
221
|
+
|
|
222
|
+
// src/storage/transport.ts
|
|
223
|
+
var HttpTransport = class {
|
|
224
|
+
async get(url) {
|
|
225
|
+
const res = await fetch(url);
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
throw new Error(`GET ${url} failed: ${res.statusText}`);
|
|
228
|
+
}
|
|
229
|
+
return res.json();
|
|
230
|
+
}
|
|
231
|
+
async post(url, body) {
|
|
232
|
+
const res = await fetch(url, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: { "Content-Type": "application/json" },
|
|
235
|
+
body: JSON.stringify(body)
|
|
236
|
+
});
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
throw new Error(`POST ${url} failed: ${res.statusText}`);
|
|
239
|
+
}
|
|
240
|
+
return res.json();
|
|
241
|
+
}
|
|
242
|
+
async delete(url) {
|
|
243
|
+
const res = await fetch(url, { method: "DELETE" });
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
throw new Error(`DELETE ${url} failed: ${res.statusText}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
function createStorageFromTransport(transport, baseUrl) {
|
|
250
|
+
return {
|
|
251
|
+
async get(key) {
|
|
252
|
+
try {
|
|
253
|
+
const res = await transport.get(`${baseUrl}/storage/get/${encodeURIComponent(key)}`);
|
|
254
|
+
return res.value;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (err.message.includes("failed")) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
async set(key, value) {
|
|
263
|
+
await transport.post(`${baseUrl}/storage/set/${encodeURIComponent(key)}`, { value });
|
|
264
|
+
},
|
|
265
|
+
async remove(key) {
|
|
266
|
+
await transport.delete(`${baseUrl}/storage/delete/${encodeURIComponent(key)}`);
|
|
267
|
+
},
|
|
268
|
+
async getMultiple(keys) {
|
|
269
|
+
const res = await transport.post(
|
|
270
|
+
`${baseUrl}/storage/batch-get`,
|
|
271
|
+
{ keys }
|
|
272
|
+
);
|
|
273
|
+
return new Map(Object.entries(res.entries));
|
|
274
|
+
},
|
|
275
|
+
async setMultiple(entries) {
|
|
276
|
+
const obj = {};
|
|
277
|
+
entries.forEach((value, key) => {
|
|
278
|
+
obj[key] = value;
|
|
279
|
+
});
|
|
280
|
+
await transport.post(`${baseUrl}/storage/batch-set`, { entries: obj });
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/storage/protocol.ts
|
|
286
|
+
var RestStorageProtocol = class _RestStorageProtocol {
|
|
287
|
+
constructor(baseUrl, entityId, backendConfig = {}) {
|
|
288
|
+
this.baseUrl = baseUrl;
|
|
289
|
+
this.entityId = entityId;
|
|
290
|
+
this.backendConfig = backendConfig;
|
|
291
|
+
}
|
|
292
|
+
getEntityId() {
|
|
293
|
+
return typeof this.entityId === "object" ? this.entityId.current : this.entityId;
|
|
294
|
+
}
|
|
295
|
+
appendBackendParams(params) {
|
|
296
|
+
if (this.backendConfig.backend) {
|
|
297
|
+
params.set("backend", this.backendConfig.backend);
|
|
298
|
+
}
|
|
299
|
+
if (this.backendConfig.mongoUrl) {
|
|
300
|
+
params.set("mongoUrl", this.backendConfig.mongoUrl);
|
|
301
|
+
}
|
|
302
|
+
if (this.backendConfig.mongoDb) {
|
|
303
|
+
params.set("mongoDb", this.backendConfig.mongoDb);
|
|
304
|
+
}
|
|
305
|
+
if (this.backendConfig.jsonlDir) {
|
|
306
|
+
params.set("jsonlDir", this.backendConfig.jsonlDir);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
buildUrl(path, key) {
|
|
310
|
+
const url = `${this.baseUrl}${path}${encodeURIComponent(key)}`;
|
|
311
|
+
const params = new URLSearchParams();
|
|
312
|
+
const eid = this.getEntityId();
|
|
313
|
+
if (eid) {
|
|
314
|
+
params.set("entityId", eid);
|
|
315
|
+
}
|
|
316
|
+
this.appendBackendParams(params);
|
|
317
|
+
const queryString = params.toString();
|
|
318
|
+
return queryString ? `${url}?${queryString}` : url;
|
|
319
|
+
}
|
|
320
|
+
buildGetUrl(key) {
|
|
321
|
+
return this.buildUrl("/storage/get/", key);
|
|
322
|
+
}
|
|
323
|
+
buildSetUrl(key) {
|
|
324
|
+
return this.buildUrl("/storage/set/", key);
|
|
325
|
+
}
|
|
326
|
+
buildDeleteUrl(key) {
|
|
327
|
+
return this.buildUrl("/storage/delete/", key);
|
|
328
|
+
}
|
|
329
|
+
buildBatchGetUrl() {
|
|
330
|
+
const url = `${this.baseUrl}/storage/batch-get`;
|
|
331
|
+
const params = new URLSearchParams();
|
|
332
|
+
const eid = this.getEntityId();
|
|
333
|
+
if (eid) {
|
|
334
|
+
params.set("entityId", eid);
|
|
335
|
+
}
|
|
336
|
+
this.appendBackendParams(params);
|
|
337
|
+
const queryString = params.toString();
|
|
338
|
+
return queryString ? `${url}?${queryString}` : url;
|
|
339
|
+
}
|
|
340
|
+
buildBatchSetUrl() {
|
|
341
|
+
const url = `${this.baseUrl}/storage/batch-set`;
|
|
342
|
+
const params = new URLSearchParams();
|
|
343
|
+
const eid = this.getEntityId();
|
|
344
|
+
if (eid) {
|
|
345
|
+
params.set("entityId", eid);
|
|
346
|
+
}
|
|
347
|
+
this.appendBackendParams(params);
|
|
348
|
+
const queryString = params.toString();
|
|
349
|
+
return queryString ? `${url}?${queryString}` : url;
|
|
350
|
+
}
|
|
351
|
+
getBackendConfig() {
|
|
352
|
+
return this.backendConfig;
|
|
353
|
+
}
|
|
354
|
+
withEntityId(entityId) {
|
|
355
|
+
return new _RestStorageProtocol(this.baseUrl, entityId, this.backendConfig);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
function createStorageWithProtocol(transport, protocol) {
|
|
359
|
+
const backendConfig = "getBackendConfig" in protocol ? protocol.getBackendConfig() : {};
|
|
360
|
+
return {
|
|
361
|
+
async get(key) {
|
|
362
|
+
try {
|
|
363
|
+
const res = await transport.get(protocol.buildGetUrl(key));
|
|
364
|
+
return res.value;
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (err.message.includes("failed")) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
async set(key, value) {
|
|
373
|
+
const body = { value, ...backendConfig };
|
|
374
|
+
await transport.post(protocol.buildSetUrl(key), body);
|
|
375
|
+
},
|
|
376
|
+
async remove(key) {
|
|
377
|
+
await transport.delete(protocol.buildDeleteUrl(key));
|
|
378
|
+
},
|
|
379
|
+
async getMultiple(keys) {
|
|
380
|
+
const res = await transport.post(
|
|
381
|
+
protocol.buildBatchGetUrl(),
|
|
382
|
+
{ keys, ...backendConfig }
|
|
383
|
+
);
|
|
384
|
+
return new Map(Object.entries(res.entries));
|
|
385
|
+
},
|
|
386
|
+
async setMultiple(entries) {
|
|
387
|
+
const obj = {};
|
|
388
|
+
entries.forEach((value, key) => {
|
|
389
|
+
obj[key] = value;
|
|
390
|
+
});
|
|
391
|
+
await transport.post(protocol.buildBatchSetUrl(), { entries: obj, ...backendConfig });
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/storage/adapters/remoteStorage.ts
|
|
397
|
+
function remoteStorage(options = {}) {
|
|
398
|
+
const baseUrl = options.baseUrl ?? "http://localhost:3847";
|
|
399
|
+
const transport = options.transport ?? new HttpTransport();
|
|
400
|
+
const entityId = { current: options.entityId };
|
|
401
|
+
const backendConfig = {
|
|
402
|
+
backend: options.backend,
|
|
403
|
+
mongoUrl: options.mongoUrl,
|
|
404
|
+
mongoDb: options.mongoDb,
|
|
405
|
+
jsonlDir: options.jsonlDir
|
|
406
|
+
};
|
|
407
|
+
const protocol = options.protocol ?? new RestStorageProtocol(baseUrl, entityId.current, backendConfig);
|
|
408
|
+
const storage = createStorageWithProtocol(transport, protocol);
|
|
409
|
+
const adapter = {
|
|
410
|
+
storage,
|
|
411
|
+
name: "remote",
|
|
412
|
+
setEntityId(id) {
|
|
413
|
+
entityId.current = id;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
return adapter;
|
|
417
|
+
}
|
|
418
|
+
export {
|
|
419
|
+
HttpTransport,
|
|
420
|
+
RestStorageProtocol,
|
|
421
|
+
createMemoryStorage,
|
|
422
|
+
createPersistedAtom,
|
|
423
|
+
remoteStorage as createRemoteStorage,
|
|
424
|
+
createStorageFromTransport,
|
|
425
|
+
createStorageWithProtocol,
|
|
426
|
+
createStore,
|
|
427
|
+
memoryStorage,
|
|
428
|
+
remoteStorage,
|
|
429
|
+
useStore
|
|
430
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bff-store",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A jotai-based state management library with pluggable storage adapters",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.mjs",
|
|
12
|
+
"require": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"./jsonl": {
|
|
15
|
+
"types": "./storage/jsonl-entry.d.ts",
|
|
16
|
+
"import": "./storage/jsonl-entry.mjs",
|
|
17
|
+
"require": "./storage/jsonl-entry.js"
|
|
18
|
+
},
|
|
19
|
+
"./mongodb": {
|
|
20
|
+
"types": "./storage/mongodb-entry.d.ts",
|
|
21
|
+
"import": "./storage/mongodb-entry.mjs",
|
|
22
|
+
"require": "./storage/mongodb-entry.js"
|
|
23
|
+
},
|
|
24
|
+
"./server": {
|
|
25
|
+
"types": "./server/entry.d.ts",
|
|
26
|
+
"import": "./server/entry.mjs",
|
|
27
|
+
"require": "./server/entry.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"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",
|
|
32
|
+
"dev": "tsup --watch",
|
|
33
|
+
"test": "vitest",
|
|
34
|
+
"start": "node dist/server/cli.js"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"jotai",
|
|
38
|
+
"state-management",
|
|
39
|
+
"react",
|
|
40
|
+
"storage"
|
|
41
|
+
],
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"jotai": ">=2.0.0",
|
|
44
|
+
"react": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@testing-library/dom": "^10.4.1",
|
|
48
|
+
"@testing-library/react": "^16.3.2",
|
|
49
|
+
"@types/node": "^20.0.0",
|
|
50
|
+
"@types/react": "^19.2.16",
|
|
51
|
+
"jotai": "^2.6.0",
|
|
52
|
+
"jsdom": "^29.1.1",
|
|
53
|
+
"react": "^18.2.0",
|
|
54
|
+
"react-dom": "^18.2.0",
|
|
55
|
+
"tsup": "^8.0.0",
|
|
56
|
+
"typescript": "^5.0.0",
|
|
57
|
+
"vitest": "^1.0.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"mongodb": "^6.21.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTTP Router for Storage Server
|
|
6
|
+
*
|
|
7
|
+
* Simple pattern-based router for HTTP requests.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type RequestHandler = (req: IncomingMessage, res: ServerResponse, params?: Record<string, string>) => Promise<void>;
|
|
11
|
+
declare class Router {
|
|
12
|
+
private routes;
|
|
13
|
+
addRoute(method: string, path: string, handler: RequestHandler): void;
|
|
14
|
+
get(path: string, handler: RequestHandler): void;
|
|
15
|
+
post(path: string, handler: RequestHandler): void;
|
|
16
|
+
delete(path: string, handler: RequestHandler): void;
|
|
17
|
+
handle(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Storage {
|
|
21
|
+
get<T>(key: string): Promise<T | null>;
|
|
22
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
23
|
+
remove(key: string): Promise<void>;
|
|
24
|
+
/** Optional: get multiple keys at once for batch loading */
|
|
25
|
+
getMultiple?<T>(keys: string[]): Promise<Map<string, T>>;
|
|
26
|
+
/** Optional: set multiple keys at once for batch saving */
|
|
27
|
+
setMultiple?<T>(entries: Map<string, T>): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* EntityId Cache for Multi-Tenant Storage
|
|
32
|
+
*
|
|
33
|
+
* Caches storage instances per entityId to avoid recreating them.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
interface EntityIdCacheOptions {
|
|
37
|
+
dir: string;
|
|
38
|
+
}
|
|
39
|
+
declare class EntityIdCache {
|
|
40
|
+
private cache;
|
|
41
|
+
private defaultStorage;
|
|
42
|
+
private dir;
|
|
43
|
+
constructor(options: EntityIdCacheOptions);
|
|
44
|
+
getStorage(entityId?: string): Storage;
|
|
45
|
+
getEntityIds(): string[];
|
|
46
|
+
clear(): void;
|
|
47
|
+
size(): number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Storage Handlers
|
|
52
|
+
*
|
|
53
|
+
* Request handlers for storage operations with caching for dynamic storage adapters.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
interface StorageHandlersOptions {
|
|
57
|
+
getStorage: (entityId?: string) => Storage;
|
|
58
|
+
}
|
|
59
|
+
declare function createStorageHandlers(options: StorageHandlersOptions): {
|
|
60
|
+
handleGet: (req: IncomingMessage, res: ServerResponse, params?: Record<string, string>) => Promise<void>;
|
|
61
|
+
handleSet: (req: IncomingMessage, res: ServerResponse, params?: Record<string, string>) => Promise<void>;
|
|
62
|
+
handleDelete: (req: IncomingMessage, res: ServerResponse, params?: Record<string, string>) => Promise<void>;
|
|
63
|
+
handleBatchGet: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
64
|
+
handleBatchSet: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
65
|
+
handleHealth: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Embedded Sidecar API Server
|
|
70
|
+
*
|
|
71
|
+
* Lightweight HTTP server that proxies storage operations to JSONL or MongoDB.
|
|
72
|
+
* Auto-shuts down when parent process exits.
|
|
73
|
+
*
|
|
74
|
+
* Supports singleton mode: call startServer() multiple times, only one server starts.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
interface ServerOptions {
|
|
78
|
+
port?: number;
|
|
79
|
+
host?: string;
|
|
80
|
+
backend: 'jsonl' | 'mongodb';
|
|
81
|
+
jsonlDir?: string;
|
|
82
|
+
mongoUrl?: string;
|
|
83
|
+
mongoDb?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Start the embedded API server (singleton).
|
|
87
|
+
*
|
|
88
|
+
* First call starts the server. Subsequent calls return the existing instance.
|
|
89
|
+
* If the existing server has been closed externally, a new one will be started.
|
|
90
|
+
* Use options to customize on first call only.
|
|
91
|
+
*/
|
|
92
|
+
declare function startServer(options?: Partial<ServerOptions>): Promise<http.Server>;
|
|
93
|
+
|
|
94
|
+
export { EntityIdCache, Router, type ServerOptions, createStorageHandlers, startServer };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HTTP Router for Storage Server
|
|
6
|
+
*
|
|
7
|
+
* Simple pattern-based router for HTTP requests.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type RequestHandler = (req: IncomingMessage, res: ServerResponse, params?: Record<string, string>) => Promise<void>;
|
|
11
|
+
declare class Router {
|
|
12
|
+
private routes;
|
|
13
|
+
addRoute(method: string, path: string, handler: RequestHandler): void;
|
|
14
|
+
get(path: string, handler: RequestHandler): void;
|
|
15
|
+
post(path: string, handler: RequestHandler): void;
|
|
16
|
+
delete(path: string, handler: RequestHandler): void;
|
|
17
|
+
handle(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Storage {
|
|
21
|
+
get<T>(key: string): Promise<T | null>;
|
|
22
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
23
|
+
remove(key: string): Promise<void>;
|
|
24
|
+
/** Optional: get multiple keys at once for batch loading */
|
|
25
|
+
getMultiple?<T>(keys: string[]): Promise<Map<string, T>>;
|
|
26
|
+
/** Optional: set multiple keys at once for batch saving */
|
|
27
|
+
setMultiple?<T>(entries: Map<string, T>): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* EntityId Cache for Multi-Tenant Storage
|
|
32
|
+
*
|
|
33
|
+
* Caches storage instances per entityId to avoid recreating them.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
interface EntityIdCacheOptions {
|
|
37
|
+
dir: string;
|
|
38
|
+
}
|
|
39
|
+
declare class EntityIdCache {
|
|
40
|
+
private cache;
|
|
41
|
+
private defaultStorage;
|
|
42
|
+
private dir;
|
|
43
|
+
constructor(options: EntityIdCacheOptions);
|
|
44
|
+
getStorage(entityId?: string): Storage;
|
|
45
|
+
getEntityIds(): string[];
|
|
46
|
+
clear(): void;
|
|
47
|
+
size(): number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Storage Handlers
|
|
52
|
+
*
|
|
53
|
+
* Request handlers for storage operations with caching for dynamic storage adapters.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
interface StorageHandlersOptions {
|
|
57
|
+
getStorage: (entityId?: string) => Storage;
|
|
58
|
+
}
|
|
59
|
+
declare function createStorageHandlers(options: StorageHandlersOptions): {
|
|
60
|
+
handleGet: (req: IncomingMessage, res: ServerResponse, params?: Record<string, string>) => Promise<void>;
|
|
61
|
+
handleSet: (req: IncomingMessage, res: ServerResponse, params?: Record<string, string>) => Promise<void>;
|
|
62
|
+
handleDelete: (req: IncomingMessage, res: ServerResponse, params?: Record<string, string>) => Promise<void>;
|
|
63
|
+
handleBatchGet: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
64
|
+
handleBatchSet: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
65
|
+
handleHealth: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Embedded Sidecar API Server
|
|
70
|
+
*
|
|
71
|
+
* Lightweight HTTP server that proxies storage operations to JSONL or MongoDB.
|
|
72
|
+
* Auto-shuts down when parent process exits.
|
|
73
|
+
*
|
|
74
|
+
* Supports singleton mode: call startServer() multiple times, only one server starts.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
interface ServerOptions {
|
|
78
|
+
port?: number;
|
|
79
|
+
host?: string;
|
|
80
|
+
backend: 'jsonl' | 'mongodb';
|
|
81
|
+
jsonlDir?: string;
|
|
82
|
+
mongoUrl?: string;
|
|
83
|
+
mongoDb?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Start the embedded API server (singleton).
|
|
87
|
+
*
|
|
88
|
+
* First call starts the server. Subsequent calls return the existing instance.
|
|
89
|
+
* If the existing server has been closed externally, a new one will be started.
|
|
90
|
+
* Use options to customize on first call only.
|
|
91
|
+
*/
|
|
92
|
+
declare function startServer(options?: Partial<ServerOptions>): Promise<http.Server>;
|
|
93
|
+
|
|
94
|
+
export { EntityIdCache, Router, type ServerOptions, createStorageHandlers, startServer };
|