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.
Files changed (65) hide show
  1. package/.claude/settings.local.json +45 -0
  2. package/CONTEXT.md +53 -0
  3. package/README.md +223 -0
  4. package/dist/cli.js +32577 -0
  5. package/dist/index.d.mts +232 -0
  6. package/dist/index.d.ts +232 -0
  7. package/dist/index.mjs +430 -0
  8. package/dist/package.json +62 -0
  9. package/dist/server/entry.d.mts +94 -0
  10. package/dist/server/entry.d.ts +94 -0
  11. package/dist/server/entry.js +573 -0
  12. package/dist/server/entry.mjs +533 -0
  13. package/dist/server-V7WCW4ZB.mjs +530 -0
  14. package/dist/storage/jsonl-entry.d.mts +42 -0
  15. package/dist/storage/jsonl-entry.d.ts +42 -0
  16. package/dist/storage/jsonl-entry.js +112 -0
  17. package/dist/storage/jsonl-entry.mjs +74 -0
  18. package/dist/storage/mongodb-entry.d.mts +40 -0
  19. package/dist/storage/mongodb-entry.d.ts +40 -0
  20. package/dist/storage/mongodb-entry.js +114 -0
  21. package/dist/storage/mongodb-entry.mjs +86 -0
  22. package/docs/BUG_DIAGNOSIS_REMOTE_STORAGE_OPTIONS.md +104 -0
  23. package/docs/BUG_FIX_REMOTE_STORAGE_OPTIONS.md +63 -0
  24. package/docs/BUG_FIX_SESSION_2026-06-03.md +171 -0
  25. package/docs/IMPLEMENTATION.md +333 -0
  26. package/docs/PLAN.md +153 -0
  27. package/docs/REMOTE_STORAGE_CONFIG.md +125 -0
  28. package/docs/SIDECAR_SERVER.md +184 -0
  29. package/package.json +62 -0
  30. package/scripts/adapt-dist-package.js +33 -0
  31. package/src/atomCreator.ts +76 -0
  32. package/src/createStore.ts +77 -0
  33. package/src/debouncer.ts +84 -0
  34. package/src/index.ts +35 -0
  35. package/src/server/cli.ts +62 -0
  36. package/src/server/entityIdCache.ts +57 -0
  37. package/src/server/entry.ts +12 -0
  38. package/src/server/handlers.ts +271 -0
  39. package/src/server/index.ts +182 -0
  40. package/src/server/router.ts +74 -0
  41. package/src/server.ts +5 -0
  42. package/src/storage/adapters/remoteStorage.ts +70 -0
  43. package/src/storage/base.ts +28 -0
  44. package/src/storage/index.ts +9 -0
  45. package/src/storage/jsonl-entry.ts +9 -0
  46. package/src/storage/jsonl.ts +111 -0
  47. package/src/storage/memory.ts +49 -0
  48. package/src/storage/mongodb-entry.ts +9 -0
  49. package/src/storage/mongodb.ts +132 -0
  50. package/src/storage/protocol.ts +170 -0
  51. package/src/storage/transport.ts +95 -0
  52. package/src/types.ts +76 -0
  53. package/src/useStore.ts +83 -0
  54. package/tests/atomCreator.test.ts +153 -0
  55. package/tests/createStore.test.ts +126 -0
  56. package/tests/debouncer.test.ts +125 -0
  57. package/tests/server.test.ts +158 -0
  58. package/tests/storage/jsonl.test.ts +132 -0
  59. package/tests/storage/memory.test.ts +101 -0
  60. package/tests/storage/mongodb.test.ts +40 -0
  61. package/tests/storage/remoteStorage.test.ts +126 -0
  62. package/tests/useStore.test.tsx +147 -0
  63. package/tsconfig.json +18 -0
  64. package/tsup.config.ts +53 -0
  65. package/vitest.config.ts +14 -0
@@ -0,0 +1,125 @@
1
+ # Remote Storage 客户端配置存储后端
2
+
3
+ ## 背景
4
+
5
+ 用户希望在客户端通过 `remoteStorage()` 配置使用 jsonl 还是 mongodb,以及各自的参数。
6
+
7
+ ## 设计
8
+
9
+ ### 客户端 API
10
+
11
+ ```typescript
12
+ // 使用 MongoDB
13
+ const adapter = remoteStorage({
14
+ backend: 'mongodb',
15
+ mongoUrl: 'mongodb://user:pass@host:27017',
16
+ database: 'myapp'
17
+ })
18
+
19
+ // 使用 JSONL
20
+ const adapter = remoteStorage({
21
+ backend: 'jsonl',
22
+ jsonlDir: '/tmp/my-app-data'
23
+ })
24
+
25
+ // 默认 (不指定 backend,走 BFF 默认配置)
26
+ const adapter = remoteStorage()
27
+ ```
28
+
29
+ ### 请求流程
30
+
31
+ 1. 客户端发起请求,带 `backend` 类型和配置
32
+ 2. BFF 根据 `backend` 路由到 mongodb 或 jsonl adapter
33
+ 3. 每次请求都传配置参数
34
+
35
+ ### BFF 行为
36
+
37
+ BFF 根据客户端请求中的 `backend` 类型,动态调用对应 adapter。BFF 不预建连接。
38
+
39
+ ## 实现
40
+
41
+ ### 1. `src/storage/adapters/remoteStorage.ts`
42
+
43
+ 更新 `RemoteStorageOptions` 类型:
44
+
45
+ ```typescript
46
+ export interface RemoteStorageOptions {
47
+ baseUrl?: string;
48
+ entityId?: string;
49
+ // 新增: 存储后端配置
50
+ backend?: 'mongodb' | 'jsonl';
51
+ mongoUrl?: string;
52
+ mongoDb?: string;
53
+ jsonlDir?: string;
54
+ }
55
+ ```
56
+
57
+ 更新 `remoteStorage()` 函数,将 backend 配置放入请求中。
58
+
59
+ ### 2. `src/storage/protocol.ts`
60
+
61
+ 更新 `RestStorageProtocol`,在请求中附带 backend 配置:
62
+
63
+ ```typescript
64
+ buildGetUrl(key: string): string {
65
+ const url = `${this.baseUrl}/storage/get/${encodeURIComponent(key)}`;
66
+ // 附带 backend 配置
67
+ const params = new URLSearchParams();
68
+ if (this.entityId) params.set('entityId', this.entityId);
69
+ if (this.backend) params.set('backend', this.backend);
70
+ if (this.backend === 'mongodb' && this.mongoUrl) {
71
+ params.set('mongoUrl', this.mongoUrl);
72
+ params.set('mongoDb', this.mongoDb || 'jotai_state_store');
73
+ }
74
+ if (this.backend === 'jsonl' && this.jsonlDir) {
75
+ params.set('jsonlDir', this.jsonlDir);
76
+ }
77
+ // ...
78
+ }
79
+ ```
80
+
81
+ ### 3. `src/server/handlers.ts`
82
+
83
+ 更新 handlers,从请求中解析 backend 配置,动态创建对应 adapter:
84
+
85
+ ```typescript
86
+ async handleSet(req, res) {
87
+ const { key } = req.params;
88
+ const { value, backend, mongoUrl, mongoDb, jsonlDir } = await parseBody(req);
89
+
90
+ let storage;
91
+ if (backend === 'mongodb') {
92
+ const adapter = await mongodbStorage({ url: mongoUrl, database: mongoDb });
93
+ storage = adapter.storage;
94
+ } else if (backend === 'jsonl') {
95
+ const adapter = jsonlStorage({ dir: jsonlDir });
96
+ storage = adapter.storage;
97
+ } else {
98
+ // 使用默认 storage
99
+ storage = getDefaultStorage();
100
+ }
101
+
102
+ await storage.set(key, value);
103
+ // ...
104
+ }
105
+ ```
106
+
107
+ ### 4. `src/server/router.ts`
108
+
109
+ 更新 router 传递 backend 相关参数。
110
+
111
+ ## 文件修改清单
112
+
113
+ | 文件 | 修改内容 |
114
+ |------|----------|
115
+ | `src/storage/adapters/remoteStorage.ts` | 添加 backend 配置字段 |
116
+ | `src/storage/protocol.ts` | 附带 backend 配置到请求 URL |
117
+ | `src/server/handlers.ts` | 解析 backend 并动态路由 |
118
+ | `src/server/router.ts` | 传递 backend 参数 |
119
+
120
+ ## 验证
121
+
122
+ 1. 构建后检查主 bundle 不含 mongodb/jsonl 代码
123
+ 2. 测试 `remoteStorage({ backend: 'mongodb', ... })` 能正常工作
124
+ 3. 测试 `remoteStorage({ backend: 'jsonl', ... })` 能正常工作
125
+ 4. 测试默认 `remoteStorage()` 仍能正常工作
@@ -0,0 +1,184 @@
1
+ # Embedded Sidecar API Server (BFF)
2
+
3
+ ## Overview
4
+
5
+ Provides a lightweight HTTP server that acts as a BFF (Backend for Frontend) proxy for storage backends (JSONL/MongoDB). Designed for use in browser/Next.js environments where direct file system or database access isn't available.
6
+
7
+ ## Architecture
8
+
9
+ ```
10
+ ┌─────────────────────────────────────────────────────────┐
11
+ │ Main Application │
12
+ │ (Next.js/Node.js) │
13
+ │ │
14
+ │ useStore() ───► remoteStorageAdapter ──► localhost:3847 │
15
+ │ (HTTP) │ │
16
+ └──────────────────────────────────────────┼───────────────┘
17
+
18
+
19
+ ┌────────────────────────┐
20
+ │ Embedded API Server │
21
+ │ (BFF Layer) │
22
+ │ │
23
+ │ /storage/get/:key │
24
+ │ /storage/set/:key │
25
+ │ /storage/delete/:key │
26
+ │ /storage/batch-get │
27
+ │ /storage/batch-set │
28
+ └────────────────────────┘
29
+ ```
30
+
31
+ ## Auto-Start Behavior
32
+
33
+ The server starts **automatically** when using `remoteStorage()`:
34
+
35
+ ```typescript
36
+ import { createStore, useStore, remoteStorage } from 'bff-store';
37
+
38
+ // Server auto-starts on first createStore call
39
+ const store = createStore('user-1', config, {
40
+ storage: remoteStorage()
41
+ });
42
+ ```
43
+
44
+ **Singleton pattern**: Only one server instance runs. Subsequent `createStore` calls reuse the existing server.
45
+
46
+ ## API Endpoints
47
+
48
+ | Method | Endpoint | Body | Response |
49
+ |--------|----------|------|----------|
50
+ | GET | `/storage/get/:key?entityId=x` | - | `{ value: T }` |
51
+ | POST | `/storage/set/:key?entityId=x` | `{ value: T }` | `{ success: true }` |
52
+ | DELETE | `/storage/delete/:key?entityId=x` | - | `{ success: true }` |
53
+ | POST | `/storage/batch-get?entityId=x` | `{ keys: string[] }` | `{ entries: {...} }` |
54
+ | POST | `/storage/batch-set?entityId=x` | `{ entries: {...} }` | `{ success: true }` |
55
+ | GET | `/health` | - | `{ status: 'ok' }` |
56
+
57
+ ## Server Options
58
+
59
+ ```typescript
60
+ interface ServerOptions {
61
+ port?: number; // Default: 3847
62
+ host?: string; // Default: localhost
63
+ backend: 'jsonl' | 'mongodb';
64
+ jsonlDir?: string; // For JSONL backend (default: ./data)
65
+ mongoUrl?: string; // For MongoDB backend (required)
66
+ mongoDb?: string; // For MongoDB backend (default: jotai_state_store)
67
+ }
68
+ ```
69
+
70
+ ## Programmatic Usage
71
+
72
+ ### Auto-Start (Recommended)
73
+
74
+ ```typescript
75
+ import { createStore, remoteStorage } from 'bff-store';
76
+
77
+ // Server auto-starts with defaults (JSONL, port 3847, ./data dir)
78
+ const store = createStore('user-1', config, {
79
+ storage: remoteStorage()
80
+ });
81
+ ```
82
+
83
+ ### Customize Server Before First Use
84
+
85
+ ```typescript
86
+ import { startServer, createStore, remoteStorage } from 'bff-store';
87
+
88
+ // Start with custom settings
89
+ await startServer({
90
+ backend: 'mongodb',
91
+ mongoUrl: 'mongodb://localhost:27017',
92
+ port: 5000,
93
+ });
94
+
95
+ // Now all remoteStorage() calls use this server
96
+ const store = createStore('user-1', config, {
97
+ storage: remoteStorage()
98
+ });
99
+ ```
100
+
101
+ ### Using startServer Directly
102
+
103
+ ```typescript
104
+ import { startServer } from 'bff-store';
105
+
106
+ const server = await startServer({
107
+ backend: 'jsonl',
108
+ port: 3847,
109
+ jsonlDir: './data',
110
+ });
111
+
112
+ // Server runs until Ctrl+C
113
+ ```
114
+
115
+ ## Remote Storage Adapter
116
+
117
+ ```typescript
118
+ import { remoteStorage } from 'bff-store';
119
+
120
+ // Default: http://localhost:3847
121
+ const adapter = remoteStorage();
122
+
123
+ // Custom server URL
124
+ const adapter = remoteStorage({ baseUrl: 'http://custom:9999' });
125
+
126
+ // With default entityId
127
+ const adapter = remoteStorage({ entityId: 'user-123' });
128
+ ```
129
+
130
+ ## Graceful Shutdown
131
+
132
+ The server handles `SIGINT` and `SIGTERM` signals:
133
+ - Stops accepting new connections
134
+ - Closes existing connections
135
+ - Exits cleanly after 5 seconds max
136
+
137
+ ## File Structure
138
+
139
+ ```
140
+ src/server/
141
+ ├── index.ts # Main entry, singleton manager, startServer()
142
+ ├── router.ts # HTTP pattern-based router
143
+ ├── handlers.ts # Storage operation handlers
144
+ └── entityIdCache.ts # Multi-tenant storage cache
145
+ ```
146
+
147
+ ### Router (`router.ts`)
148
+
149
+ Pattern-based HTTP router with named parameter extraction.
150
+
151
+ ```typescript
152
+ router.get('/storage/get/:key', handler); // :key is a named param
153
+ router.post('/storage/set/:key', handler);
154
+ router.delete('/storage/delete/:key', handler);
155
+ router.post('/storage/batch-get', handler);
156
+ router.post('/storage/batch-set', handler);
157
+ ```
158
+
159
+ ### EntityIdCache (`entityIdCache.ts`)
160
+
161
+ Caches JSONL storage adapters per `entityId` to avoid recreation:
162
+
163
+ ```typescript
164
+ const cache = new EntityIdCache({ dir: './data' });
165
+ const storage = cache.getStorage('user-123'); // Gets or creates
166
+ ```
167
+
168
+ ### Handlers (`handlers.ts`)
169
+
170
+ Request handlers for storage operations. Each handler reads `entityId` from URL query params:
171
+
172
+ ```typescript
173
+ const handlers = createStorageHandlers({
174
+ getStorage: (entityId?: string) => entityIdCache.getStorage(entityId),
175
+ });
176
+ ```
177
+
178
+ ## Testing
179
+
180
+ ```bash
181
+ npm test -- tests/server.test.ts
182
+ ```
183
+
184
+ Server tests create a temporary directory for JSONL storage, start the server on port 3849, and verify all endpoints work correctly.
package/package.json ADDED
@@ -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": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./jsonl": {
15
+ "types": "./dist/storage/jsonl-entry.d.ts",
16
+ "import": "./dist/storage/jsonl-entry.mjs",
17
+ "require": "./dist/storage/jsonl-entry.js"
18
+ },
19
+ "./mongodb": {
20
+ "types": "./dist/storage/mongodb-entry.d.ts",
21
+ "import": "./dist/storage/mongodb-entry.mjs",
22
+ "require": "./dist/storage/mongodb-entry.js"
23
+ },
24
+ "./server": {
25
+ "types": "./dist/server/entry.d.ts",
26
+ "import": "./dist/server/entry.mjs",
27
+ "require": "./dist/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,33 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const distPkgPath = path.resolve(__dirname, '..', 'dist', 'package.json');
5
+
6
+ if (!fs.existsSync(distPkgPath)) {
7
+ console.error('dist/package.json not found — skipping path adaptation');
8
+ process.exit(1);
9
+ }
10
+
11
+ const pkg = JSON.parse(fs.readFileSync(distPkgPath, 'utf-8'));
12
+
13
+ // Strip ./dist/ prefix from top-level entry fields
14
+ for (const field of ['main', 'module', 'types']) {
15
+ if (typeof pkg[field] === 'string') {
16
+ pkg[field] = pkg[field].replace(/^dist\//, '');
17
+ }
18
+ }
19
+
20
+ // Strip ./dist/ prefix from exports map
21
+ if (pkg.exports) {
22
+ for (const key of Object.keys(pkg.exports)) {
23
+ const entry = pkg.exports[key];
24
+ for (const cond of ['types', 'import', 'require', 'default']) {
25
+ if (typeof entry[cond] === 'string') {
26
+ entry[cond] = entry[cond].replace(/^\.\/dist\//, './');
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ fs.writeFileSync(distPkgPath, JSON.stringify(pkg, null, 2) + '\n');
33
+ console.log('dist/package.json paths adapted for local resolution');
@@ -0,0 +1,76 @@
1
+ import { atom, getDefaultStore } from 'jotai';
2
+ import type { AtomConfig, PersistedAtomWithLoading } from './types';
3
+ import type { Storage } from './storage/base';
4
+ import { DebouncerMap } from './debouncer';
5
+
6
+ /**
7
+ * Module-level debouncer map for all persisted atoms.
8
+ * Each unique key (atom config key) gets its own debouncer.
9
+ */
10
+ const debouncerMap = new DebouncerMap();
11
+
12
+ /**
13
+ * Creates a persisted atom with loading state tracking
14
+ */
15
+ export function createPersistedAtom<T>(
16
+ config: AtomConfig<T>,
17
+ entityId: string,
18
+ storage: Storage,
19
+ options?: { immediate?: boolean; debounceMs?: number }
20
+ ): PersistedAtomWithLoading<T> {
21
+ const baseAtom = atom<T>(config.defaultValue);
22
+ const loadingAtom = atom<boolean>(true);
23
+
24
+ // Load initial value on mount
25
+ baseAtom.onMount = (setValue) => {
26
+ storage
27
+ .get<T>(config.key)
28
+ .then((value) => {
29
+ if (value !== null && value !== undefined) {
30
+ setValue(value);
31
+ }
32
+ })
33
+ .catch(console.error)
34
+ .finally(() => {
35
+ // Mark as loaded
36
+ setTimeout(() => {
37
+ const store = getDefaultStore();
38
+ store.set(loadingAtom, false);
39
+ }, 0);
40
+ });
41
+ };
42
+
43
+ // Create write atom with persistence
44
+ const writeAtom = atom(
45
+ (get) => get(baseAtom),
46
+ (get, set, update: T | ((prev: T) => T)) => {
47
+ const newValue =
48
+ typeof update === 'function'
49
+ ? (update as (prev: T) => T)(get(baseAtom))
50
+ : update;
51
+
52
+ // Update local state first
53
+ set(baseAtom, newValue);
54
+
55
+ // Save to storage
56
+ if (options?.immediate) {
57
+ // Immediate save for critical data
58
+ storage.set(config.key, newValue).catch(console.error);
59
+ } else {
60
+ // Debounced save for normal data
61
+ // Use entityId:key as the debounce key to avoid cross-store interference
62
+ const debounceKey = `${entityId}:${config.key}`;
63
+ const debounceMs = options?.debounceMs ?? 800;
64
+ const saveFn = () => {
65
+ storage.set(config.key, newValue).catch(console.error);
66
+ };
67
+ debouncerMap.debounce(debounceKey, saveFn, debounceMs);
68
+ }
69
+ }
70
+ );
71
+
72
+ return {
73
+ atom: writeAtom,
74
+ loadingAtom,
75
+ };
76
+ }
@@ -0,0 +1,77 @@
1
+ import type { AtomConfigs, Store, StoreAtoms, StoreLoadingAtoms } from './types';
2
+ import type { StorageAdapter } from './storage/base';
3
+ import { createPersistedAtom } from './atomCreator';
4
+
5
+ /**
6
+ * Creates a store with multiple persisted atoms
7
+ *
8
+ * @param entityId - Unique identifier for this store instance
9
+ * @param config - Array of atom configurations
10
+ * @param options - Store options including storage adapter
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const config = [
15
+ * { key: 'theme', defaultValue: '' },
16
+ * { key: 'characters', defaultValue: [] },
17
+ * ] as const;
18
+ *
19
+ * const adapter = jsonlStorage({ dir: './sessions' });
20
+ *
21
+ * const store = createStore('novel-123', config, {
22
+ * storage: adapter,
23
+ * });
24
+ * ```
25
+ */
26
+ export function createStore(
27
+ entityId: string,
28
+ config: AtomConfigs,
29
+ options?: {
30
+ storage: StorageAdapter;
31
+ debounceMs?: number;
32
+ }
33
+ ): Store {
34
+ const adapter = options?.storage;
35
+ const debounceMs = options?.debounceMs ?? 800;
36
+
37
+ // Auto-start embedded server when using remote storage (Node.js only)
38
+ // In browser/Next.js environments, remoteStorage connects to an already-running BFF server
39
+ if (adapter?.name === 'remote' && typeof window === 'undefined' && typeof process !== 'undefined') {
40
+ import('./server').then(({ startServer }) => {
41
+ startServer().catch((err) => {
42
+ console.error('[bff-store] Failed to auto-start server:', err);
43
+ });
44
+ });
45
+ }
46
+
47
+ if (!adapter) {
48
+ throw new Error('Storage adapter is required');
49
+ }
50
+
51
+ // Initialize adapter with entityId if it supports it
52
+ if ('setEntityId' in adapter && typeof adapter.setEntityId === 'function') {
53
+ adapter.setEntityId(entityId);
54
+ }
55
+
56
+ const storage = adapter.storage;
57
+
58
+ const atoms: StoreAtoms = {};
59
+ const loadingAtoms: StoreLoadingAtoms = {};
60
+
61
+ // Create atoms synchronously for immediate availability
62
+ for (const atomConfig of config) {
63
+ const result = createPersistedAtom(atomConfig, entityId, storage, {
64
+ immediate: atomConfig.immediate,
65
+ debounceMs,
66
+ });
67
+ atoms[atomConfig.key] = result.atom;
68
+ loadingAtoms[atomConfig.key] = result.loadingAtom;
69
+ }
70
+
71
+ return {
72
+ entityId,
73
+ config,
74
+ atoms,
75
+ loadingAtoms,
76
+ };
77
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Debouncer - manages debounced function execution
3
+ *
4
+ * Encapsulates timer state for delayed function calls.
5
+ * Each Debouncer instance manages one debounce pipeline.
6
+ */
7
+ export interface Debouncer {
8
+ readonly ms: number;
9
+ run(fn: () => void): void;
10
+ cancel(): void;
11
+ }
12
+
13
+ export function createDebouncer(ms: number): Debouncer {
14
+ let timer: ReturnType<typeof setTimeout> | null = null;
15
+
16
+ return {
17
+ get ms() {
18
+ return ms;
19
+ },
20
+ run(fn) {
21
+ if (timer) clearTimeout(timer);
22
+ timer = setTimeout(() => {
23
+ fn();
24
+ timer = null;
25
+ }, ms);
26
+ },
27
+ cancel() {
28
+ if (timer) {
29
+ clearTimeout(timer);
30
+ timer = null;
31
+ }
32
+ },
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Factory for creating debounced save functions per key.
38
+ * Maintains a map of debouncers, one per unique key.
39
+ */
40
+ export class DebouncerMap {
41
+ private debouncers = new Map<string, Debouncer>();
42
+ private readonly defaultMs: number;
43
+
44
+ constructor(defaultMs: number = 800) {
45
+ this.defaultMs = defaultMs;
46
+ }
47
+
48
+ getDebouncer(key: string, ms?: number): Debouncer {
49
+ let debouncer = this.debouncers.get(key);
50
+ if (!debouncer) {
51
+ debouncer = createDebouncer(ms ?? this.defaultMs);
52
+ this.debouncers.set(key, debouncer);
53
+ }
54
+ return debouncer;
55
+ }
56
+
57
+ /**
58
+ * Execute a function with debounce for a given key.
59
+ * Subsequent calls for the same key reset the timer.
60
+ */
61
+ debounce(key: string, fn: () => void, ms?: number): void {
62
+ const debouncer = this.getDebouncer(key, ms);
63
+ debouncer.run(fn);
64
+ }
65
+
66
+ /**
67
+ * Cancel pending debounced call for a key
68
+ */
69
+ cancel(key: string): void {
70
+ const debouncer = this.debouncers.get(key);
71
+ if (debouncer) {
72
+ debouncer.cancel();
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Cancel all pending debounced calls
78
+ */
79
+ cancelAll(): void {
80
+ for (const debouncer of this.debouncers.values()) {
81
+ debouncer.cancel();
82
+ }
83
+ }
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // ========================================
2
+ // Main Export
3
+ // ========================================
4
+
5
+ export { createStore } from './createStore';
6
+ export { useStore } from './useStore';
7
+ export { createPersistedAtom } from './atomCreator';
8
+
9
+ // ========================================
10
+ // Types
11
+ // ========================================
12
+
13
+ export type {
14
+ AtomConfig,
15
+ AtomConfigs,
16
+ AtomType,
17
+ Store,
18
+ StorageOptions,
19
+ MemoryStorageOptions,
20
+ UseStoreReturn,
21
+ } from './types';
22
+
23
+ // ========================================
24
+ // Storage Adapters
25
+ // ========================================
26
+
27
+ export { memoryStorage, createMemoryStorage } from './storage/memory';
28
+ export { remoteStorage, createRemoteStorage } from './storage/adapters/remoteStorage';
29
+ export type { Storage, StorageAdapter, StorageFactory, AsyncStorageFactory } from './storage/base';
30
+
31
+ // Transport & Protocol
32
+ export { HttpTransport, createStorageFromTransport } from './storage/transport';
33
+ export { RestStorageProtocol, createStorageWithProtocol } from './storage/protocol';
34
+ export type { TransportAdapter } from './storage/transport';
35
+ export type { StorageHttpProtocol } from './storage/protocol';