@effuse/store 1.0.2 → 1.0.4

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/src/core/store.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * SOFTWARE.
23
23
  */
24
24
 
25
- import { Effect, Option, pipe, Predicate } from 'effect';
25
+ import { Effect, Option, pipe } from 'effect';
26
26
  import { signal, type Signal } from '@effuse/core';
27
27
  import type {
28
28
  Store,
@@ -43,41 +43,23 @@ import {
43
43
  import {
44
44
  createCancellationScope,
45
45
  createCancellationToken,
46
- type CancellationScope,
47
- type CancellationToken,
48
46
  } from '../actions/cancellation.js';
47
+ import {
48
+ setValue,
49
+ resetState,
50
+ batchUpdates,
51
+ updateState,
52
+ addSubscriber,
53
+ addKeySubscriber,
54
+ getSnapshot,
55
+ type StoreInternals,
56
+ type StoreHandlerDeps,
57
+ } from '../handlers/index.js';
49
58
 
50
- interface StoreInternals {
51
- signalMap: Map<string, Signal<unknown>>;
52
- initialState: Record<string, unknown>;
53
- actions: Record<string, (...args: unknown[]) => unknown>;
54
- subscribers: Set<() => void>;
55
- keySubscribers: Map<string, Set<(value: unknown) => void>>;
56
- computedSelectors: Map<
57
- (s: Record<string, unknown>) => unknown,
58
- Signal<unknown>
59
- >;
60
- isBatching: boolean;
61
- cancellationScope: CancellationScope;
62
- pendingActions: Map<string, CancellationToken>;
63
- }
64
-
65
- const getSnapshot = (
66
- signalMap: Map<string, Signal<unknown>>
67
- ): Record<string, unknown> => {
68
- const snapshot: Record<string, unknown> = {};
69
- for (const [key, sig] of signalMap) {
70
- snapshot[key] = sig.value;
71
- }
72
- return snapshot;
73
- };
74
-
75
- // Store configuration options
76
59
  export interface CreateStoreOptions extends StoreOptions {
77
60
  storage?: StorageAdapter;
78
61
  }
79
62
 
80
- // Initialize reactive store
81
63
  export const createStore = <T extends object>(
82
64
  name: string,
83
65
  definition: StoreDefinition<T>,
@@ -106,6 +88,7 @@ export const createStore = <T extends object>(
106
88
  );
107
89
 
108
90
  if (config.debug) {
91
+ // eslint-disable-next-line no-console
109
92
  console.log(`[store] Creating: ${name}`);
110
93
  }
111
94
 
@@ -134,6 +117,19 @@ export const createStore = <T extends object>(
134
117
 
135
118
  const atomicState = createAtomicState({ ...internals.initialState });
136
119
 
120
+ const handlerDeps: StoreHandlerDeps = {
121
+ internals,
122
+ atomicState,
123
+ middlewareManager,
124
+ config: {
125
+ name,
126
+ shouldPersist,
127
+ storageKey,
128
+ enableDevtools,
129
+ adapter,
130
+ },
131
+ };
132
+
137
133
  if (shouldPersist) {
138
134
  pipe(
139
135
  runAdapter.getItem(adapter, storageKey),
@@ -158,31 +154,6 @@ export const createStore = <T extends object>(
158
154
  );
159
155
  }
160
156
 
161
- const notifySubscribers = (): void => {
162
- if (internals.isBatching) return;
163
- for (const callback of internals.subscribers) callback();
164
- };
165
-
166
- const notifyKeySubscribers = (key: string, value: unknown): void => {
167
- if (internals.isBatching) return;
168
- const subs = internals.keySubscribers.get(key);
169
- if (subs) for (const cb of subs) cb(value);
170
- };
171
-
172
- const persistState = (): void => {
173
- if (!shouldPersist) return;
174
- const snapshot = getSnapshot(internals.signalMap);
175
- runAdapter.setItem(adapter, storageKey, JSON.stringify(snapshot));
176
- };
177
-
178
- const updateComputed = (): void => {
179
- const snapshot = getSnapshot(internals.signalMap);
180
- for (const [selector, sig] of internals.computedSelectors) {
181
- const newValue = selector(snapshot);
182
- if (sig.value !== newValue) sig.value = newValue;
183
- }
184
- };
185
-
186
157
  const stateProxy = new Proxy({} as Record<string, unknown>, {
187
158
  get(_, prop: string) {
188
159
  const sig = internals.signalMap.get(prop);
@@ -192,48 +163,7 @@ export const createStore = <T extends object>(
192
163
  return undefined;
193
164
  },
194
165
  set(_, prop: string, value: unknown) {
195
- if (!internals.signalMap.has(prop)) return false;
196
-
197
- const sig = internals.signalMap.get(prop);
198
- if (!sig) return false;
199
- const newState = middlewareManager.execute(
200
- { ...atomicState.get(), [prop]: value },
201
- `set:${prop}`,
202
- [value]
203
- );
204
-
205
- sig.value = newState[prop];
206
- atomicState.update((s) => ({ ...s, [prop]: newState[prop] }));
207
-
208
- if (enableDevtools) {
209
- const time = new Date().toLocaleTimeString();
210
- console.groupCollapsed(
211
- `%caction %c${name}/set:${prop} %c@ ${time}`,
212
- 'color: gray; font-weight: lighter;',
213
- 'color: inherit; font-weight: bold;',
214
- 'color: gray; font-weight: lighter;'
215
- );
216
- console.log(
217
- '%cprev state',
218
- 'color: #9E9E9E; font-weight: bold;',
219
- atomicState.get()
220
- );
221
- console.log('%caction', 'color: #03A9F4; font-weight: bold;', {
222
- type: `set:${prop}`,
223
- payload: value,
224
- });
225
- console.log('%cnext state', 'color: #4CAF50; font-weight: bold;', {
226
- ...atomicState.get(),
227
- [prop]: newState[prop],
228
- });
229
- console.groupEnd();
230
- }
231
-
232
- notifySubscribers();
233
- notifyKeySubscribers(prop, newState[prop]);
234
- persistState();
235
- updateComputed();
236
- return true;
166
+ return setValue(handlerDeps, { prop, value });
237
167
  },
238
168
  });
239
169
 
@@ -264,30 +194,35 @@ export const createStore = <T extends object>(
264
194
 
265
195
  if (enableDevtools) {
266
196
  const time = new Date().toLocaleTimeString();
197
+ // eslint-disable-next-line no-console
267
198
  console.groupCollapsed(
268
199
  `%caction %c${name}/${key} (async) %c@ ${time}`,
269
200
  'color: gray; font-weight: lighter;',
270
201
  'color: inherit; font-weight: bold;',
271
202
  'color: gray; font-weight: lighter;'
272
203
  );
204
+ // eslint-disable-next-line no-console
273
205
  console.log(
274
206
  '%cprev state',
275
207
  'color: #9E9E9E; font-weight: bold;',
276
208
  prevState
277
209
  );
210
+ // eslint-disable-next-line no-console
278
211
  console.log('%caction', 'color: #03A9F4; font-weight: bold;', {
279
212
  type: `${name}/${key}`,
280
213
  payload: args,
281
214
  });
215
+ // eslint-disable-next-line no-console
282
216
  console.log(
283
217
  '%cnext state',
284
218
  'color: #4CAF50; font-weight: bold;',
285
219
  currentState
286
220
  );
221
+ // eslint-disable-next-line no-console
287
222
  console.groupEnd();
288
223
  }
289
224
 
290
- notifySubscribers();
225
+ for (const callback of internals.subscribers) callback();
291
226
  }
292
227
  return value;
293
228
  })
@@ -302,30 +237,35 @@ export const createStore = <T extends object>(
302
237
 
303
238
  if (enableDevtools) {
304
239
  const time = new Date().toLocaleTimeString();
240
+ // eslint-disable-next-line no-console
305
241
  console.groupCollapsed(
306
242
  `%caction %c${name}/${key} %c@ ${time}`,
307
243
  'color: gray; font-weight: lighter;',
308
244
  'color: inherit; font-weight: bold;',
309
245
  'color: gray; font-weight: lighter;'
310
246
  );
247
+ // eslint-disable-next-line no-console
311
248
  console.log(
312
249
  '%cprev state',
313
250
  'color: #9E9E9E; font-weight: bold;',
314
251
  prevState
315
252
  );
253
+ // eslint-disable-next-line no-console
316
254
  console.log('%caction', 'color: #03A9F4; font-weight: bold;', {
317
255
  type: `${name}/${key}`,
318
256
  payload: args,
319
257
  });
258
+ // eslint-disable-next-line no-console
320
259
  console.log(
321
260
  '%cnext state',
322
261
  'color: #4CAF50; font-weight: bold;',
323
262
  currentState
324
263
  );
264
+ // eslint-disable-next-line no-console
325
265
  console.groupEnd();
326
266
  }
327
267
 
328
- notifySubscribers();
268
+ for (const callback of internals.subscribers) callback();
329
269
 
330
270
  return result;
331
271
  };
@@ -340,28 +280,15 @@ export const createStore = <T extends object>(
340
280
  name,
341
281
  state: storeState as StoreState<T>,
342
282
 
343
- subscribe: (callback) => {
344
- internals.subscribers.add(callback);
345
- return () => {
346
- internals.subscribers.delete(callback);
347
- };
348
- },
283
+ subscribe: (callback) => addSubscriber(internals, { callback }),
349
284
 
350
285
  subscribeToKey: (key, callback) => {
351
286
  const keyStr = String(key);
352
- let subs = internals.keySubscribers.get(keyStr);
353
- if (!subs) {
354
- subs = new Set();
355
- internals.keySubscribers.set(keyStr, subs);
356
- }
357
287
  const typedCallback = callback as (value: unknown) => void;
358
- subs.add(typedCallback);
359
- return () => {
360
- const subsSet = internals.keySubscribers.get(keyStr);
361
- if (Predicate.isNotNullable(subsSet)) {
362
- subsSet.delete(typedCallback);
363
- }
364
- };
288
+ return addKeySubscriber(internals, {
289
+ key: keyStr,
290
+ callback: typedCallback,
291
+ });
365
292
  },
366
293
 
367
294
  getSnapshot: () =>
@@ -381,23 +308,11 @@ export const createStore = <T extends object>(
381
308
  },
382
309
 
383
310
  batch: (updates) => {
384
- internals.isBatching = true;
385
- updates();
386
- internals.isBatching = false;
387
- notifySubscribers();
388
- persistState();
389
- updateComputed();
311
+ batchUpdates(handlerDeps, updates);
390
312
  },
391
313
 
392
314
  reset: () => {
393
- for (const [key, value] of Object.entries(internals.initialState)) {
394
- const sig = internals.signalMap.get(key);
395
- if (sig) sig.value = value;
396
- }
397
- atomicState.set({ ...internals.initialState });
398
- notifySubscribers();
399
- persistState();
400
- updateComputed();
315
+ resetState(handlerDeps);
401
316
  },
402
317
 
403
318
  use: (middleware: Middleware<Record<string, unknown>>) =>
@@ -407,27 +322,9 @@ export const createStore = <T extends object>(
407
322
  getSnapshot(internals.signalMap) as ReturnType<Store<T>['getSnapshot']>,
408
323
 
409
324
  update: (updater) => {
410
- const draft = { ...getSnapshot(internals.signalMap) } as {
411
- [K in keyof T]: T[K] extends (...args: unknown[]) => unknown
412
- ? never
413
- : T[K];
414
- };
415
-
416
- updater(draft);
417
-
418
- internals.isBatching = true;
419
- for (const [key, val] of Object.entries(draft)) {
420
- const sig = internals.signalMap.get(key);
421
- if (sig && sig.value !== val) {
422
- sig.value = val;
423
- atomicState.update((s) => ({ ...s, [key]: val }));
424
- }
425
- }
426
- internals.isBatching = false;
427
-
428
- notifySubscribers();
429
- persistState();
430
- updateComputed();
325
+ updateState(handlerDeps, {
326
+ updater: updater as (d: Record<string, unknown>) => void,
327
+ });
431
328
  },
432
329
 
433
330
  select: <R>(
@@ -460,6 +357,13 @@ export const createStore = <T extends object>(
460
357
  if (propStr in boundActions) return boundActions[propStr];
461
358
  return undefined;
462
359
  },
360
+ set(target, prop: string | symbol, value: unknown): boolean {
361
+ const propStr = String(prop);
362
+ if (propStr in target) {
363
+ return false;
364
+ }
365
+ return setValue(handlerDeps, { prop: propStr, value });
366
+ },
463
367
  }) as Store<T> & StoreState<T>;
464
368
 
465
369
  registerStore(name, storeProxy);
package/src/core/types.ts CHANGED
@@ -31,41 +31,34 @@ export const StoreOptionsSchema = Schema.Struct({
31
31
  devtools: Schema.optional(Schema.Boolean),
32
32
  });
33
33
 
34
- // Store configuration schema
35
34
  export type StoreOptions = Schema.Schema.Type<typeof StoreOptionsSchema>;
36
35
 
37
- // Reactive store state
38
36
  export type StoreState<T> = {
39
37
  [K in keyof T]: T[K] extends (...args: infer A) => infer R
40
38
  ? (...args: A) => R
41
39
  : Signal<T[K]>;
42
40
  };
43
41
 
44
- // Internal store context
45
42
  export type StoreContext<T> = {
46
43
  [K in keyof T]: T[K] extends (...args: unknown[]) => unknown
47
44
  ? T[K]
48
45
  : Signal<T[K]>;
49
46
  };
50
47
 
51
- // Store definition structure
52
48
  export type StoreDefinition<T> = {
53
49
  [K in keyof T]: T[K] extends (...args: infer A) => infer R
54
50
  ? (this: StoreContext<T>, ...args: A) => R
55
51
  : T[K];
56
52
  };
57
53
 
58
- // Action execution context
59
54
  export type ActionContext<T> = StoreContext<T>;
60
55
 
61
- // Global state middleware
62
56
  export type Middleware<T> = (
63
57
  state: T,
64
58
  action: string,
65
59
  args: unknown[]
66
60
  ) => T | undefined;
67
61
 
68
- // Reactive store instance
69
62
  export interface Store<T> {
70
63
  readonly name: string;
71
64
  readonly state: StoreState<T>;
@@ -96,7 +89,6 @@ export interface Store<T> {
96
89
  select: <R>(selector: (snapshot: Record<string, unknown>) => R) => Signal<R>;
97
90
  }
98
91
 
99
- // Extract state type from store
100
92
  export type InferStoreState<S> =
101
93
  S extends Store<infer T>
102
94
  ? {
@@ -35,7 +35,6 @@ interface DevToolsConnection {
35
35
  subscribe: (listener: (message: { type: string }) => void) => () => void;
36
36
  }
37
37
 
38
- // Detect Redux DevTools extension
39
38
  export const hasDevTools = (): boolean => {
40
39
  if (typeof globalThis === 'undefined') return false;
41
40
  const w = globalThis as unknown as {
@@ -46,7 +45,6 @@ export const hasDevTools = (): boolean => {
46
45
 
47
46
  const connections = new Map<string, DevToolsConnection>();
48
47
 
49
- // Connect store to DevTools
50
48
  export const connectDevTools = <T>(
51
49
  store: Store<T>,
52
50
  options?: { name?: string }
@@ -86,7 +84,6 @@ export const connectDevTools = <T>(
86
84
  };
87
85
  };
88
86
 
89
- // DevTools reporting middleware
90
87
  export const devToolsMiddleware = <T>(storeName: string) => {
91
88
  return (state: T, action: string, args: unknown[]): T | undefined => {
92
89
  const connection = connections.get(storeName);
@@ -100,16 +97,13 @@ export const devToolsMiddleware = <T>(storeName: string) => {
100
97
  };
101
98
  };
102
99
 
103
- // Build DevTools middleware
104
100
  export const createDevToolsMiddleware = <T>(storeName: string) =>
105
101
  devToolsMiddleware<T>(storeName);
106
102
 
107
- // Disconnect store from DevTools
108
103
  export const disconnectDevTools = (storeName: string): void => {
109
104
  connections.delete(storeName);
110
105
  };
111
106
 
112
- // Disconnect all DevTools connections
113
107
  export const disconnectAllDevTools = (): void => {
114
108
  connections.clear();
115
109
  };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2025 Chris M. Perez
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ export type {
26
+ StoreInternals,
27
+ StoreConfig,
28
+ StoreHandlerDeps,
29
+ SetValueInput,
30
+ } from './types.js';
31
+
32
+ export {
33
+ setValue,
34
+ resetState,
35
+ batchUpdates,
36
+ updateState,
37
+ getSnapshot,
38
+ } from './operations.js';
39
+
40
+ export { addSubscriber, addKeySubscriber } from './subscriptions.js';
41
+
42
+ export type {
43
+ StorageHandlerDeps,
44
+ StorageGetInput,
45
+ StorageSetInput,
46
+ StorageRemoveInput,
47
+ } from './persistence.js';
48
+
49
+ export {
50
+ getItem,
51
+ setItem,
52
+ removeItem,
53
+ hasItem,
54
+ clearStorage,
55
+ getStorageKeys,
56
+ getStorageSize,
57
+ } from './persistence.js';