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,232 @@
1
+ import { WritableAtom, Atom } from 'jotai';
2
+
3
+ type AtomType = 'string' | 'number' | 'boolean' | 'array' | 'object';
4
+ interface AtomConfig<T = unknown> {
5
+ key: string;
6
+ defaultValue: T;
7
+ type?: AtomType;
8
+ /** If true, save immediately without debounce */
9
+ immediate?: boolean;
10
+ }
11
+ type AtomConfigs = readonly AtomConfig[];
12
+ interface PersistedAtomWithLoading<T> {
13
+ atom: WritableAtom<T, [update: T | ((prev: T) => T)], void>;
14
+ loadingAtom: Atom<boolean>;
15
+ }
16
+ type StoreAtoms = Record<string, WritableAtom<any, [any], void>>;
17
+ type StoreLoadingAtoms = Record<string, Atom<boolean>>;
18
+ interface Store {
19
+ entityId: string;
20
+ config: AtomConfigs;
21
+ atoms: StoreAtoms;
22
+ loadingAtoms: StoreLoadingAtoms;
23
+ }
24
+ type UseStoreReturn = Record<string, any> & {
25
+ isLoading: boolean;
26
+ };
27
+ interface StorageOptions {
28
+ debounceMs?: number;
29
+ }
30
+ interface MemoryStorageOptions extends StorageOptions {
31
+ }
32
+ interface BackendConfig {
33
+ backend?: 'mongodb' | 'jsonl';
34
+ mongoUrl?: string;
35
+ mongoDb?: string;
36
+ jsonlDir?: string;
37
+ }
38
+
39
+ interface Storage {
40
+ get<T>(key: string): Promise<T | null>;
41
+ set<T>(key: string, value: T): Promise<void>;
42
+ remove(key: string): Promise<void>;
43
+ /** Optional: get multiple keys at once for batch loading */
44
+ getMultiple?<T>(keys: string[]): Promise<Map<string, T>>;
45
+ /** Optional: set multiple keys at once for batch saving */
46
+ setMultiple?<T>(entries: Map<string, T>): Promise<void>;
47
+ }
48
+ interface StorageAdapter {
49
+ storage: Storage;
50
+ name: string;
51
+ /** Optional: set the entityId for multi-tenant storage */
52
+ setEntityId?(entityId: string): void;
53
+ }
54
+ type StorageFactory<T = unknown> = (options?: T) => StorageAdapter;
55
+ type AsyncStorageFactory<T = unknown> = (options: T) => Promise<StorageAdapter>;
56
+
57
+ /**
58
+ * Creates a store with multiple persisted atoms
59
+ *
60
+ * @param entityId - Unique identifier for this store instance
61
+ * @param config - Array of atom configurations
62
+ * @param options - Store options including storage adapter
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const config = [
67
+ * { key: 'theme', defaultValue: '' },
68
+ * { key: 'characters', defaultValue: [] },
69
+ * ] as const;
70
+ *
71
+ * const adapter = jsonlStorage({ dir: './sessions' });
72
+ *
73
+ * const store = createStore('novel-123', config, {
74
+ * storage: adapter,
75
+ * });
76
+ * ```
77
+ */
78
+ declare function createStore(entityId: string, config: AtomConfigs, options?: {
79
+ storage: StorageAdapter;
80
+ debounceMs?: number;
81
+ }): Store;
82
+
83
+ /**
84
+ * React Hook to use a store
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const store = createStore('user-123', [
89
+ * { key: 'name', defaultValue: '' },
90
+ * { key: 'age', defaultValue: 0 },
91
+ * ], { storage });
92
+ *
93
+ * function UserProfile() {
94
+ * const { name, age, setName, setAge, isLoading } = useStore(store);
95
+ * // ...
96
+ * }
97
+ * ```
98
+ */
99
+ declare function useStore(store: Store): UseStoreReturn;
100
+
101
+ /**
102
+ * Creates a persisted atom with loading state tracking
103
+ */
104
+ declare function createPersistedAtom<T>(config: AtomConfig<T>, entityId: string, storage: Storage, options?: {
105
+ immediate?: boolean;
106
+ debounceMs?: number;
107
+ }): PersistedAtomWithLoading<T>;
108
+
109
+ /**
110
+ * In-memory storage adapter for development and testing
111
+ */
112
+ declare function memoryStorage(_options?: MemoryStorageOptions): StorageAdapter;
113
+ declare const createMemoryStorage: StorageFactory<MemoryStorageOptions>;
114
+
115
+ /**
116
+ * Transport Adapter Interface
117
+ *
118
+ * Abstracts the transport layer for remote storage operations.
119
+ * Can be implemented as HTTP, WebSocket, or other protocols.
120
+ */
121
+ interface TransportAdapter {
122
+ /** GET request */
123
+ get<T>(url: string): Promise<T>;
124
+ /** POST request with JSON body */
125
+ post<T, R>(url: string, body: T): Promise<R>;
126
+ /** DELETE request */
127
+ delete(url: string): Promise<void>;
128
+ }
129
+ /**
130
+ * HTTP Transport Adapter using fetch
131
+ */
132
+ declare class HttpTransport implements TransportAdapter {
133
+ get<T>(url: string): Promise<T>;
134
+ post<T, R>(url: string, body: T): Promise<R>;
135
+ delete(url: string): Promise<void>;
136
+ }
137
+
138
+ declare function createStorageFromTransport(transport: TransportAdapter, baseUrl: string): Storage;
139
+
140
+ /**
141
+ * Storage HTTP Protocol Interface
142
+ *
143
+ * Defines the request/response contract for storage operations over HTTP.
144
+ * This allows the protocol to be implemented differently without changing
145
+ * the storage semantics.
146
+ */
147
+
148
+ interface StorageHttpProtocol {
149
+ /** Build URL for get operation */
150
+ buildGetUrl(key: string): string;
151
+ /** Build URL for set operation */
152
+ buildSetUrl(key: string): string;
153
+ /** Build URL for delete operation */
154
+ buildDeleteUrl(key: string): string;
155
+ /** Build URL for batch get */
156
+ buildBatchGetUrl(): string;
157
+ /** Build URL for batch set */
158
+ buildBatchSetUrl(): string;
159
+ /** Get backend config */
160
+ getBackendConfig(): BackendConfig;
161
+ }
162
+ /**
163
+ * Default protocol implementation using standard REST paths
164
+ */
165
+ declare class RestStorageProtocol implements StorageHttpProtocol {
166
+ private baseUrl;
167
+ private entityId?;
168
+ private backendConfig;
169
+ constructor(baseUrl: string, entityId?: string | {
170
+ current?: string;
171
+ } | undefined, backendConfig?: BackendConfig);
172
+ private getEntityId;
173
+ private appendBackendParams;
174
+ private buildUrl;
175
+ buildGetUrl(key: string): string;
176
+ buildSetUrl(key: string): string;
177
+ buildDeleteUrl(key: string): string;
178
+ buildBatchGetUrl(): string;
179
+ buildBatchSetUrl(): string;
180
+ getBackendConfig(): BackendConfig;
181
+ withEntityId(entityId: string): RestStorageProtocol;
182
+ }
183
+ /**
184
+ * Create a storage that uses a specific protocol
185
+ */
186
+ declare function createStorageWithProtocol(transport: TransportAdapter, protocol: StorageHttpProtocol): {
187
+ get<T>(key: string): Promise<T | null>;
188
+ set<T>(key: string, value: T): Promise<void>;
189
+ remove(key: string): Promise<void>;
190
+ getMultiple<T>(keys: string[]): Promise<Map<string, T>>;
191
+ setMultiple<T>(entries: Map<string, T>): Promise<void>;
192
+ };
193
+
194
+ /**
195
+ * Remote Storage Adapter
196
+ *
197
+ * Client-side adapter that calls the embedded sidecar server.
198
+ * Uses HttpTransport and RestStorageProtocol for HTTP operations.
199
+ */
200
+
201
+ interface RemoteStorageOptions {
202
+ baseUrl?: string;
203
+ entityId?: string;
204
+ transport?: TransportAdapter;
205
+ protocol?: StorageHttpProtocol;
206
+ backend?: 'mongodb' | 'jsonl';
207
+ mongoUrl?: string;
208
+ mongoDb?: string;
209
+ jsonlDir?: string;
210
+ }
211
+ /**
212
+ * Create a remote storage adapter that connects to the embedded sidecar server.
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * import { createStore, remoteStorage } from 'bff-store';
217
+ *
218
+ * // Connect to default server (localhost:3847)
219
+ * const adapter = remoteStorage();
220
+ *
221
+ * // Or with custom server URL
222
+ * const adapter = remoteStorage({ baseUrl: 'http://localhost:3847' });
223
+ *
224
+ * // With entityId
225
+ * const adapter = remoteStorage({ entityId: 'user-123' });
226
+ *
227
+ * const store = createStore('user-123', config, { storage: adapter });
228
+ * ```
229
+ */
230
+ declare function remoteStorage(options?: RemoteStorageOptions): StorageAdapter;
231
+
232
+ 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, createPersistedAtom, remoteStorage as createRemoteStorage, createStorageFromTransport, createStorageWithProtocol, createStore, memoryStorage, remoteStorage, useStore };
@@ -0,0 +1,232 @@
1
+ import { WritableAtom, Atom } from 'jotai';
2
+
3
+ type AtomType = 'string' | 'number' | 'boolean' | 'array' | 'object';
4
+ interface AtomConfig<T = unknown> {
5
+ key: string;
6
+ defaultValue: T;
7
+ type?: AtomType;
8
+ /** If true, save immediately without debounce */
9
+ immediate?: boolean;
10
+ }
11
+ type AtomConfigs = readonly AtomConfig[];
12
+ interface PersistedAtomWithLoading<T> {
13
+ atom: WritableAtom<T, [update: T | ((prev: T) => T)], void>;
14
+ loadingAtom: Atom<boolean>;
15
+ }
16
+ type StoreAtoms = Record<string, WritableAtom<any, [any], void>>;
17
+ type StoreLoadingAtoms = Record<string, Atom<boolean>>;
18
+ interface Store {
19
+ entityId: string;
20
+ config: AtomConfigs;
21
+ atoms: StoreAtoms;
22
+ loadingAtoms: StoreLoadingAtoms;
23
+ }
24
+ type UseStoreReturn = Record<string, any> & {
25
+ isLoading: boolean;
26
+ };
27
+ interface StorageOptions {
28
+ debounceMs?: number;
29
+ }
30
+ interface MemoryStorageOptions extends StorageOptions {
31
+ }
32
+ interface BackendConfig {
33
+ backend?: 'mongodb' | 'jsonl';
34
+ mongoUrl?: string;
35
+ mongoDb?: string;
36
+ jsonlDir?: string;
37
+ }
38
+
39
+ interface Storage {
40
+ get<T>(key: string): Promise<T | null>;
41
+ set<T>(key: string, value: T): Promise<void>;
42
+ remove(key: string): Promise<void>;
43
+ /** Optional: get multiple keys at once for batch loading */
44
+ getMultiple?<T>(keys: string[]): Promise<Map<string, T>>;
45
+ /** Optional: set multiple keys at once for batch saving */
46
+ setMultiple?<T>(entries: Map<string, T>): Promise<void>;
47
+ }
48
+ interface StorageAdapter {
49
+ storage: Storage;
50
+ name: string;
51
+ /** Optional: set the entityId for multi-tenant storage */
52
+ setEntityId?(entityId: string): void;
53
+ }
54
+ type StorageFactory<T = unknown> = (options?: T) => StorageAdapter;
55
+ type AsyncStorageFactory<T = unknown> = (options: T) => Promise<StorageAdapter>;
56
+
57
+ /**
58
+ * Creates a store with multiple persisted atoms
59
+ *
60
+ * @param entityId - Unique identifier for this store instance
61
+ * @param config - Array of atom configurations
62
+ * @param options - Store options including storage adapter
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const config = [
67
+ * { key: 'theme', defaultValue: '' },
68
+ * { key: 'characters', defaultValue: [] },
69
+ * ] as const;
70
+ *
71
+ * const adapter = jsonlStorage({ dir: './sessions' });
72
+ *
73
+ * const store = createStore('novel-123', config, {
74
+ * storage: adapter,
75
+ * });
76
+ * ```
77
+ */
78
+ declare function createStore(entityId: string, config: AtomConfigs, options?: {
79
+ storage: StorageAdapter;
80
+ debounceMs?: number;
81
+ }): Store;
82
+
83
+ /**
84
+ * React Hook to use a store
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const store = createStore('user-123', [
89
+ * { key: 'name', defaultValue: '' },
90
+ * { key: 'age', defaultValue: 0 },
91
+ * ], { storage });
92
+ *
93
+ * function UserProfile() {
94
+ * const { name, age, setName, setAge, isLoading } = useStore(store);
95
+ * // ...
96
+ * }
97
+ * ```
98
+ */
99
+ declare function useStore(store: Store): UseStoreReturn;
100
+
101
+ /**
102
+ * Creates a persisted atom with loading state tracking
103
+ */
104
+ declare function createPersistedAtom<T>(config: AtomConfig<T>, entityId: string, storage: Storage, options?: {
105
+ immediate?: boolean;
106
+ debounceMs?: number;
107
+ }): PersistedAtomWithLoading<T>;
108
+
109
+ /**
110
+ * In-memory storage adapter for development and testing
111
+ */
112
+ declare function memoryStorage(_options?: MemoryStorageOptions): StorageAdapter;
113
+ declare const createMemoryStorage: StorageFactory<MemoryStorageOptions>;
114
+
115
+ /**
116
+ * Transport Adapter Interface
117
+ *
118
+ * Abstracts the transport layer for remote storage operations.
119
+ * Can be implemented as HTTP, WebSocket, or other protocols.
120
+ */
121
+ interface TransportAdapter {
122
+ /** GET request */
123
+ get<T>(url: string): Promise<T>;
124
+ /** POST request with JSON body */
125
+ post<T, R>(url: string, body: T): Promise<R>;
126
+ /** DELETE request */
127
+ delete(url: string): Promise<void>;
128
+ }
129
+ /**
130
+ * HTTP Transport Adapter using fetch
131
+ */
132
+ declare class HttpTransport implements TransportAdapter {
133
+ get<T>(url: string): Promise<T>;
134
+ post<T, R>(url: string, body: T): Promise<R>;
135
+ delete(url: string): Promise<void>;
136
+ }
137
+
138
+ declare function createStorageFromTransport(transport: TransportAdapter, baseUrl: string): Storage;
139
+
140
+ /**
141
+ * Storage HTTP Protocol Interface
142
+ *
143
+ * Defines the request/response contract for storage operations over HTTP.
144
+ * This allows the protocol to be implemented differently without changing
145
+ * the storage semantics.
146
+ */
147
+
148
+ interface StorageHttpProtocol {
149
+ /** Build URL for get operation */
150
+ buildGetUrl(key: string): string;
151
+ /** Build URL for set operation */
152
+ buildSetUrl(key: string): string;
153
+ /** Build URL for delete operation */
154
+ buildDeleteUrl(key: string): string;
155
+ /** Build URL for batch get */
156
+ buildBatchGetUrl(): string;
157
+ /** Build URL for batch set */
158
+ buildBatchSetUrl(): string;
159
+ /** Get backend config */
160
+ getBackendConfig(): BackendConfig;
161
+ }
162
+ /**
163
+ * Default protocol implementation using standard REST paths
164
+ */
165
+ declare class RestStorageProtocol implements StorageHttpProtocol {
166
+ private baseUrl;
167
+ private entityId?;
168
+ private backendConfig;
169
+ constructor(baseUrl: string, entityId?: string | {
170
+ current?: string;
171
+ } | undefined, backendConfig?: BackendConfig);
172
+ private getEntityId;
173
+ private appendBackendParams;
174
+ private buildUrl;
175
+ buildGetUrl(key: string): string;
176
+ buildSetUrl(key: string): string;
177
+ buildDeleteUrl(key: string): string;
178
+ buildBatchGetUrl(): string;
179
+ buildBatchSetUrl(): string;
180
+ getBackendConfig(): BackendConfig;
181
+ withEntityId(entityId: string): RestStorageProtocol;
182
+ }
183
+ /**
184
+ * Create a storage that uses a specific protocol
185
+ */
186
+ declare function createStorageWithProtocol(transport: TransportAdapter, protocol: StorageHttpProtocol): {
187
+ get<T>(key: string): Promise<T | null>;
188
+ set<T>(key: string, value: T): Promise<void>;
189
+ remove(key: string): Promise<void>;
190
+ getMultiple<T>(keys: string[]): Promise<Map<string, T>>;
191
+ setMultiple<T>(entries: Map<string, T>): Promise<void>;
192
+ };
193
+
194
+ /**
195
+ * Remote Storage Adapter
196
+ *
197
+ * Client-side adapter that calls the embedded sidecar server.
198
+ * Uses HttpTransport and RestStorageProtocol for HTTP operations.
199
+ */
200
+
201
+ interface RemoteStorageOptions {
202
+ baseUrl?: string;
203
+ entityId?: string;
204
+ transport?: TransportAdapter;
205
+ protocol?: StorageHttpProtocol;
206
+ backend?: 'mongodb' | 'jsonl';
207
+ mongoUrl?: string;
208
+ mongoDb?: string;
209
+ jsonlDir?: string;
210
+ }
211
+ /**
212
+ * Create a remote storage adapter that connects to the embedded sidecar server.
213
+ *
214
+ * @example
215
+ * ```typescript
216
+ * import { createStore, remoteStorage } from 'bff-store';
217
+ *
218
+ * // Connect to default server (localhost:3847)
219
+ * const adapter = remoteStorage();
220
+ *
221
+ * // Or with custom server URL
222
+ * const adapter = remoteStorage({ baseUrl: 'http://localhost:3847' });
223
+ *
224
+ * // With entityId
225
+ * const adapter = remoteStorage({ entityId: 'user-123' });
226
+ *
227
+ * const store = createStore('user-123', config, { storage: adapter });
228
+ * ```
229
+ */
230
+ declare function remoteStorage(options?: RemoteStorageOptions): StorageAdapter;
231
+
232
+ 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, createPersistedAtom, remoteStorage as createRemoteStorage, createStorageFromTransport, createStorageWithProtocol, createStore, memoryStorage, remoteStorage, useStore };