@storve/core 1.0.1 → 1.0.3

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +993 -26
  3. package/dist/adapters/indexedDB.cjs +0 -1
  4. package/dist/adapters/indexedDB.mjs +0 -1
  5. package/dist/adapters/localStorage.cjs +0 -1
  6. package/dist/adapters/localStorage.mjs +0 -1
  7. package/dist/adapters/memory.cjs +0 -1
  8. package/dist/adapters/memory.mjs +0 -1
  9. package/dist/adapters/sessionStorage.cjs +0 -1
  10. package/dist/adapters/sessionStorage.mjs +0 -1
  11. package/dist/async-entry.d.ts +0 -1
  12. package/dist/async.cjs +0 -1
  13. package/dist/async.d.ts +0 -1
  14. package/dist/async.mjs +0 -1
  15. package/dist/batch.d.ts +0 -1
  16. package/dist/compose.d.ts +0 -1
  17. package/dist/computed-entry.d.ts +0 -1
  18. package/dist/computed.cjs +0 -1
  19. package/dist/computed.d.ts +0 -1
  20. package/dist/computed.mjs +0 -1
  21. package/dist/devtools/history.d.ts +0 -1
  22. package/dist/devtools/index.d.ts +0 -1
  23. package/dist/devtools/redux-bridge.d.ts +0 -1
  24. package/dist/devtools/snapshots.d.ts +0 -1
  25. package/dist/devtools/withDevtools.d.ts +0 -1
  26. package/dist/devtools.cjs +0 -1
  27. package/dist/devtools.mjs +0 -1
  28. package/dist/extensions/noop.d.ts +0 -1
  29. package/dist/index.cjs +0 -1
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.mjs +0 -1
  32. package/dist/persist/adapters/indexedDB.d.ts +0 -1
  33. package/dist/persist/adapters/localStorage.d.ts +0 -1
  34. package/dist/persist/adapters/memory.d.ts +0 -1
  35. package/dist/persist/adapters/sessionStorage.d.ts +0 -1
  36. package/dist/persist/debounce.d.ts +0 -1
  37. package/dist/persist/hydrate.d.ts +0 -1
  38. package/dist/persist/index.d.ts +0 -1
  39. package/dist/persist/serialize.d.ts +0 -1
  40. package/dist/persist.cjs +0 -1
  41. package/dist/persist.mjs +0 -1
  42. package/dist/proxy.d.ts +0 -1
  43. package/dist/registry-qtr1UpFU.js +0 -1
  44. package/dist/registry-zaKZ1P-s.js +0 -1
  45. package/dist/registry.d.ts +0 -1
  46. package/dist/signals/createSignal.d.ts +0 -1
  47. package/dist/signals/index.d.ts +0 -1
  48. package/dist/signals/useSignal.d.ts +0 -1
  49. package/dist/signals.cjs +0 -1
  50. package/dist/signals.mjs +0 -1
  51. package/dist/store.d.ts +0 -1
  52. package/dist/sync/channel.d.ts +0 -1
  53. package/dist/sync/index.d.ts +0 -1
  54. package/dist/sync/protocol.d.ts +0 -1
  55. package/dist/sync/withSync.d.ts +0 -1
  56. package/dist/sync.cjs +0 -1
  57. package/dist/sync.mjs +0 -1
  58. package/dist/types.d.ts +0 -1
  59. package/package.json +9 -3
  60. package/CHANGELOG.md +0 -151
  61. package/benchmarks/run.ts +0 -102
  62. package/benchmarks/week2.md +0 -9
  63. package/benchmarks/week2.ts +0 -64
  64. package/benchmarks/week4.md +0 -13
  65. package/benchmarks/week4.ts +0 -178
  66. package/benchmarks/week5.md +0 -15
  67. package/benchmarks/week5.ts +0 -184
  68. package/coverage/coverage-summary.json +0 -31
  69. package/dist/adapters/indexedDB.cjs.map +0 -1
  70. package/dist/adapters/indexedDB.mjs.map +0 -1
  71. package/dist/adapters/localStorage.cjs.map +0 -1
  72. package/dist/adapters/localStorage.mjs.map +0 -1
  73. package/dist/adapters/memory.cjs.map +0 -1
  74. package/dist/adapters/memory.mjs.map +0 -1
  75. package/dist/adapters/sessionStorage.cjs.map +0 -1
  76. package/dist/adapters/sessionStorage.mjs.map +0 -1
  77. package/dist/async-entry.d.ts.map +0 -1
  78. package/dist/async.cjs.map +0 -1
  79. package/dist/async.d.ts.map +0 -1
  80. package/dist/async.mjs.map +0 -1
  81. package/dist/batch.d.ts.map +0 -1
  82. package/dist/compose.d.ts.map +0 -1
  83. package/dist/computed-entry.d.ts.map +0 -1
  84. package/dist/computed.cjs.map +0 -1
  85. package/dist/computed.d.ts.map +0 -1
  86. package/dist/computed.mjs.map +0 -1
  87. package/dist/devtools/history.d.ts.map +0 -1
  88. package/dist/devtools/index.d.ts.map +0 -1
  89. package/dist/devtools/redux-bridge.d.ts.map +0 -1
  90. package/dist/devtools/snapshots.d.ts.map +0 -1
  91. package/dist/devtools/withDevtools.d.ts.map +0 -1
  92. package/dist/devtools.cjs.map +0 -1
  93. package/dist/devtools.mjs.map +0 -1
  94. package/dist/extensions/noop.d.ts.map +0 -1
  95. package/dist/index.cjs.js +0 -118
  96. package/dist/index.cjs.js.map +0 -1
  97. package/dist/index.cjs.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.esm.js +0 -116
  100. package/dist/index.esm.js.map +0 -1
  101. package/dist/index.mjs.map +0 -1
  102. package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
  103. package/dist/persist/adapters/localStorage.d.ts.map +0 -1
  104. package/dist/persist/adapters/memory.d.ts.map +0 -1
  105. package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
  106. package/dist/persist/debounce.d.ts.map +0 -1
  107. package/dist/persist/hydrate.d.ts.map +0 -1
  108. package/dist/persist/index.d.ts.map +0 -1
  109. package/dist/persist/serialize.d.ts.map +0 -1
  110. package/dist/persist.cjs.map +0 -1
  111. package/dist/persist.mjs.map +0 -1
  112. package/dist/proxy.d.ts.map +0 -1
  113. package/dist/registry-D3X0HSbl.js +0 -26
  114. package/dist/registry-D3X0HSbl.js.map +0 -1
  115. package/dist/registry-RDjbeJdx.js +0 -29
  116. package/dist/registry-RDjbeJdx.js.map +0 -1
  117. package/dist/registry-qtr1UpFU.js.map +0 -1
  118. package/dist/registry-zaKZ1P-s.js.map +0 -1
  119. package/dist/registry.d.ts.map +0 -1
  120. package/dist/signals/createSignal.d.ts.map +0 -1
  121. package/dist/signals/index.d.ts.map +0 -1
  122. package/dist/signals/useSignal.d.ts.map +0 -1
  123. package/dist/signals.cjs.map +0 -1
  124. package/dist/signals.mjs.map +0 -1
  125. package/dist/stats.html +0 -4949
  126. package/dist/store.d.ts.map +0 -1
  127. package/dist/sync/channel.d.ts.map +0 -1
  128. package/dist/sync/index.d.ts.map +0 -1
  129. package/dist/sync/protocol.d.ts.map +0 -1
  130. package/dist/sync/withSync.d.ts.map +0 -1
  131. package/dist/sync.cjs.map +0 -1
  132. package/dist/sync.mjs.map +0 -1
  133. package/dist/types.d.ts.map +0 -1
  134. package/rollup.config.mjs +0 -44
  135. package/src/async-entry.ts +0 -6
  136. package/src/async.ts +0 -240
  137. package/src/batch.ts +0 -33
  138. package/src/compose.ts +0 -50
  139. package/src/computed-entry.ts +0 -6
  140. package/src/computed.ts +0 -187
  141. package/src/devtools/history.ts +0 -103
  142. package/src/devtools/index.ts +0 -5
  143. package/src/devtools/redux-bridge.ts +0 -70
  144. package/src/devtools/snapshots.ts +0 -54
  145. package/src/devtools/withDevtools.ts +0 -196
  146. package/src/extensions/noop.ts +0 -12
  147. package/src/index.ts +0 -4
  148. package/src/persist/adapters/indexedDB.ts +0 -114
  149. package/src/persist/adapters/localStorage.ts +0 -28
  150. package/src/persist/adapters/memory.ts +0 -26
  151. package/src/persist/adapters/sessionStorage.ts +0 -28
  152. package/src/persist/debounce.ts +0 -28
  153. package/src/persist/hydrate.ts +0 -60
  154. package/src/persist/index.ts +0 -141
  155. package/src/persist/serialize.ts +0 -60
  156. package/src/proxy.ts +0 -87
  157. package/src/registry.ts +0 -67
  158. package/src/signals/createSignal.ts +0 -81
  159. package/src/signals/index.ts +0 -20
  160. package/src/signals/useSignal.ts +0 -18
  161. package/src/store.ts +0 -250
  162. package/src/sync/channel.ts +0 -15
  163. package/src/sync/index.ts +0 -3
  164. package/src/sync/protocol.ts +0 -18
  165. package/src/sync/withSync.ts +0 -147
  166. package/src/types.ts +0 -159
  167. package/tests/async.test.ts +0 -1100
  168. package/tests/batch.test.ts +0 -41
  169. package/tests/compose.test.ts +0 -209
  170. package/tests/computed.test.ts +0 -867
  171. package/tests/devtools.test.ts +0 -1039
  172. package/tests/integration/persist.integration.test.ts +0 -258
  173. package/tests/integration/signals.integration.test.ts +0 -309
  174. package/tests/integration.test.ts +0 -278
  175. package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
  176. package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
  177. package/tests/persist/adapters/memory.adapter.test.ts +0 -112
  178. package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
  179. package/tests/persist/debounce.test.ts +0 -121
  180. package/tests/persist/hydrate.test.ts +0 -120
  181. package/tests/persist/migrate.test.ts +0 -208
  182. package/tests/persist/persist.test.ts +0 -357
  183. package/tests/persist/serialize.test.ts +0 -128
  184. package/tests/proxy.test.ts +0 -473
  185. package/tests/registry.test.ts +0 -67
  186. package/tests/signals/derived.test.ts +0 -244
  187. package/tests/signals/inference.test.ts +0 -108
  188. package/tests/signals/signal.test.ts +0 -348
  189. package/tests/signals/useSignal.test.tsx +0 -275
  190. package/tests/store.test.ts +0 -482
  191. package/tests/stress.test.ts +0 -268
  192. package/tests/sync.test.ts +0 -576
  193. package/tests/types.test.ts +0 -32
  194. package/tests/v0.3.test.ts +0 -813
  195. package/tree-shake-test.js +0 -1
  196. package/tsconfig.json +0 -15
  197. package/vitest.config.ts +0 -22
  198. package/vitest_play.ts +0 -7
@@ -1,60 +0,0 @@
1
- import type { PersistAdapter } from './index.js'
2
- import { fromJSON } from './serialize.js'
3
-
4
- type PersistedWrapper<T> = Partial<T> & { __version?: number }
5
-
6
- /**
7
- * Hydrates state from a persistence adapter.
8
- * Handles reading from the adapter, JSON parsing, version checking, and migration.
9
- *
10
- * @template T - The state object type.
11
- * @param {PersistAdapter} adapter - The persistence adapter to read from.
12
- * @param {string} key - The unique namespace/key for the store in the adapter.
13
- * @param {T} currentState - The current store state.
14
- * @param {number} version - The expected state version.
15
- * @param {(persisted: Partial<T>, version: number) => Partial<T>} [migrate] - Optional migration function.
16
- * @returns {Promise<Partial<T>>} A promise that resolves to the hydrated partial state (or an empty object).
17
- */
18
- export async function hydrate<T extends object>(
19
- adapter: PersistAdapter,
20
- key: string,
21
- currentState: T,
22
- version: number,
23
- migrate?: (persisted: Partial<T>, version: number) => Partial<T>
24
- ): Promise<Partial<T>> {
25
- const raw = await adapter.getItem(key)
26
- if (!raw) {
27
- return {}
28
- }
29
-
30
- let parsed: PersistedWrapper<T>
31
- try {
32
- parsed = fromJSON<PersistedWrapper<T>>(raw)
33
- } catch (err) {
34
- console.warn(`[storve] Hydration failed for key "${key}":`, err)
35
- return {}
36
- }
37
-
38
- const persistedVersion = parsed.__version !== undefined ? parsed.__version : 0
39
-
40
- let finalState: Partial<T>
41
-
42
- if (persistedVersion !== version) {
43
- if (migrate !== undefined) {
44
- finalState = migrate(parsed, persistedVersion)
45
- } else {
46
- console.warn(
47
- `Storve: persisted state version mismatch (stored: ${persistedVersion}, expected: ${version}). No migrate function provided — falling back to default state.`
48
- )
49
- return {} // stale data, no migration path
50
- }
51
- } else {
52
- finalState = parsed
53
- }
54
-
55
- // Strip __version from the final state to be merged
56
- const cleaned: PersistedWrapper<T> = { ...finalState }
57
- delete cleaned.__version
58
-
59
- return cleaned
60
- }
@@ -1,141 +0,0 @@
1
- import type { Store, StoreState } from '../types.js'
2
- import { pick, toJSON } from './serialize.js'
3
- import { createDebounce } from './debounce.js'
4
- import { hydrate } from './hydrate.js'
5
-
6
- /**
7
- * Core interface for Storve persistence adapters.
8
- * All adapters must implement these three methods to be compatible.
9
- * Depending on the underlying storage, methods can be sync or async.
10
- */
11
- export interface PersistAdapter {
12
- getItem(key: string): string | null | Promise<string | null>
13
- setItem(key: string, value: string): void | Promise<void>
14
- removeItem(key: string): void | Promise<void>
15
- }
16
-
17
- /**
18
- * Options for configuring persistence.
19
- * @template T - The state type of the store.
20
- */
21
- export interface PersistOptions<T> {
22
- key: string
23
- adapter: PersistAdapter
24
- pick?: Array<keyof T>
25
- version?: number
26
- migrate?: (persisted: Partial<T>, version: number) => Partial<T>
27
- debounce?: number
28
- }
29
-
30
- // Internal type guard to distinguish options from store while preserving D
31
- function isPersistOptions<D extends object>(
32
- obj: Store<D> | PersistOptions<StoreState<D>>
33
- ): obj is PersistOptions<StoreState<D>> {
34
- return obj !== null && typeof obj === 'object' && 'adapter' in obj && 'key' in obj
35
- }
36
-
37
- // Internal helper for withPersist to avoid signature overload complexities
38
- function createEnhancedStore<D extends object>(
39
- store: Store<D>,
40
- options: PersistOptions<StoreState<D>>
41
- ): Store<D> & { hydrated: Promise<void> } {
42
- let resolveHydrated!: () => void
43
- const hydrated = new Promise<void>((resolve) => {
44
- resolveHydrated = resolve
45
- })
46
-
47
- const version = options.version !== undefined ? options.version : 1
48
- const debounceMs = options.debounce !== undefined ? options.debounce : 100
49
-
50
- // 1. Kick off hydration immediately
51
- hydrate<StoreState<D>>(
52
- options.adapter,
53
- options.key,
54
- store.getState(),
55
- version,
56
- options.migrate
57
- ).then((hydratedState) => {
58
- // Merge result into store via setState
59
- store.setState(hydratedState)
60
- resolveHydrated()
61
- }).catch(
62
- /* v8 ignore next 4 */
63
- (err: unknown) => {
64
- console.warn(`[storve] withPersist hydrate error for key "${options.key}":`, err)
65
- resolveHydrated()
66
- }
67
- )
68
-
69
- // 2. Setup debounced exact writes
70
- const debouncedWrite = createDebounce((serialized: string) => {
71
- const result = options.adapter.setItem(options.key, serialized)
72
-
73
- if (result && typeof result.catch === 'function') {
74
- /* v8 ignore next 4 */
75
- result.catch((e: unknown) => {
76
- console.warn(`[storve] Failed to persist state for key "${options.key}":`, e)
77
- })
78
- }
79
- }, debounceMs)
80
-
81
- // Keep a reference to the last persisted picked state
82
- const initialPicked = options.pick && options.pick.length > 0
83
- ? pick(store.getState(), options.pick)
84
- : { ...store.getState() }
85
- let lastPersistedSnapshot: string | null = toJSON({ ...initialPicked, __version: version })
86
-
87
- // 3. Subscribe to store changes to trigger writes
88
- store.subscribe((newState) => {
89
- // 1. Extract only the picked keys (or full state if no pick option)
90
- const picked = options.pick && options.pick.length > 0
91
- ? pick(newState, options.pick)
92
- : { ...newState }
93
-
94
- // 2. Serialize to compare
95
- const serialized = toJSON({ ...picked, __version: version })
96
-
97
- // 3. Skip write if nothing changed in the picked portion
98
- if (serialized === lastPersistedSnapshot) return
99
-
100
- // 4. Update snapshot reference and write
101
- lastPersistedSnapshot = serialized
102
- debouncedWrite(serialized)
103
- })
104
-
105
- return {
106
- ...store,
107
- hydrated
108
- }
109
- }
110
-
111
- /**
112
- * Enhances a Storve store with continuous automatic persistence.
113
- * Can be called directly or curried for use with compose().
114
- */
115
- export function withPersist<D extends object>(
116
- store: Store<D>,
117
- options: PersistOptions<StoreState<D>>
118
- ): Store<D> & { hydrated: Promise<void> }
119
-
120
- export function withPersist<D extends object>(
121
- options: PersistOptions<StoreState<D>>
122
- ): (store: Store<D>) => Store<D> & { hydrated: Promise<void> }
123
-
124
- export function withPersist<D extends object>(
125
- storeOrOptions: Store<D> | PersistOptions<StoreState<D>>,
126
- options?: PersistOptions<StoreState<D>>
127
- ): (Store<D> & { hydrated: Promise<void> }) | ((store: Store<D>) => Store<D> & { hydrated: Promise<void> }) {
128
- if (options !== undefined) {
129
- if (!isPersistOptions(storeOrOptions)) {
130
- return createEnhancedStore(storeOrOptions, options)
131
- }
132
- }
133
-
134
- if (isPersistOptions(storeOrOptions)) {
135
- return (store: Store<D>) => createEnhancedStore(store, storeOrOptions)
136
- }
137
-
138
- /* v8 ignore next 2 */
139
- throw new Error('[storve] Invalid withPersist arguments')
140
- }
141
-
@@ -1,60 +0,0 @@
1
- /**
2
- * Returns a new object with only the specified keys from state.
3
- * If keys is undefine or empty, returns the full state.
4
- *
5
- * @template T - The state object type.
6
- * @param {T} state - The complete state object.
7
- * @param {Array<keyof T>} [keys] - The keys to pick.
8
- * @returns {Partial<T> | T} A new object with picked keys, or the original state.
9
- */
10
- export function pick<T extends object>(state: T, keys?: Array<keyof T>): Partial<T> | T {
11
- if (keys === undefined || keys.length === 0) {
12
- return { ...state }
13
- }
14
-
15
- const result: Partial<T> = {}
16
-
17
- for (let i = 0; i < keys.length; i++) {
18
- const key = keys[i]
19
- if (key in state) {
20
- result[key] = state[key]
21
- }
22
- }
23
-
24
- return result
25
- }
26
-
27
- /**
28
- * Safely stringifies a value to JSON, throwing a clear error if it fails.
29
- *
30
- * @param {unknown} value - The object or value to serialize.
31
- * @returns {string} The JSON string representation.
32
- * @throws {Error} Throws a detailed error if JSON.stringify fails.
33
- */
34
- export function toJSON(value: unknown): string {
35
- try {
36
- return JSON.stringify(value)
37
- } catch (err) {
38
- throw new Error(`[storve] Failed to serialize state to JSON: ${err instanceof Error ? err.message : String(err)}`)
39
- }
40
- }
41
-
42
- /**
43
- * Safely parses a JSON string, throwing a clear error if parsing fails or if the string is empty/null.
44
- *
45
- * @template T - The expected output type.
46
- * @param {string} raw - The string to parse.
47
- * @returns {T} The typed parsed JSON object.
48
- * @throws {Error} Throws if 'raw' is empty or if JSON.parse fails.
49
- */
50
- export function fromJSON<T>(raw: string): T {
51
- if (!raw) {
52
- throw new Error('[storve] Cannot parse empty or null/undefined JSON string.')
53
- }
54
- try {
55
- const parsed: T = JSON.parse(raw)
56
- return parsed
57
- } catch (err) {
58
- throw new Error(`[storve] Failed to parse JSON state: ${err instanceof Error ? err.message : String(err)}`)
59
- }
60
- }
package/src/proxy.ts DELETED
@@ -1,87 +0,0 @@
1
- const proxyMap = new WeakMap<object, object>();
2
- const rawMap = new WeakMap<object, object>();
3
-
4
- let isBatching = false;
5
-
6
- function isPlainObjectOrArray(value: unknown): value is object {
7
- if (value === null || typeof value !== 'object') return false;
8
- const proto = Object.getPrototypeOf(value);
9
- return proto === Object.prototype || Array.isArray(value);
10
- }
11
-
12
- export function createStateProxy<T extends object>(state: T, onChange: () => void): T {
13
- if (!isPlainObjectOrArray(state)) {
14
- return state;
15
- }
16
-
17
- if (proxyMap.has(state)) {
18
- return proxyMap.get(state) as T;
19
- }
20
-
21
- if (rawMap.has(state)) {
22
- return state; // It's already a proxy
23
- }
24
-
25
- const handler: ProxyHandler<T> = {
26
- get(target, prop, receiver) {
27
- if (Array.isArray(target) && typeof prop === 'string' && ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].includes(prop)) {
28
- return (...args: unknown[]) => {
29
- const prevBatching = isBatching;
30
- isBatching = true;
31
-
32
- const method = Reflect.get(target, prop, receiver) as (...a: unknown[]) => unknown;
33
- const result = Reflect.apply(method, receiver, args);
34
-
35
- isBatching = prevBatching;
36
- if (!isBatching) {
37
- onChange();
38
- }
39
- return result;
40
- };
41
- }
42
-
43
- const value = Reflect.get(target, prop, receiver);
44
- if (isPlainObjectOrArray(value) && proxyMap.has(value)) {
45
- return proxyMap.get(value);
46
- }
47
- return value;
48
- },
49
- set(target, prop, value, receiver) {
50
- // Unpack if value is a proxy itself
51
- const rawValue = rawMap.has(value as object) ? rawMap.get(value as object) : value;
52
-
53
- // Immediately wrap new nested objects
54
- if (isPlainObjectOrArray(rawValue)) {
55
- createStateProxy(rawValue as object, onChange);
56
- }
57
-
58
- const result = Reflect.set(target, prop, rawValue, receiver);
59
-
60
- // Trigger listeners on write
61
- if (!isBatching) onChange();
62
-
63
- return result;
64
- },
65
- deleteProperty(target, prop) {
66
- const result = Reflect.deleteProperty(target, prop);
67
- if (!isBatching) onChange();
68
- return result;
69
- }
70
- };
71
-
72
- const proxy = new Proxy(state, handler);
73
- proxyMap.set(state, proxy);
74
- rawMap.set(proxy, state);
75
-
76
- // Recursively proxy existing nested objects upfront
77
- for (const key in state) {
78
- if (Object.prototype.hasOwnProperty.call(state, key)) {
79
- const val = state[(key as keyof typeof state)];
80
- if (isPlainObjectOrArray(val)) {
81
- createStateProxy(val as object, onChange);
82
- }
83
- }
84
- }
85
-
86
- return proxy;
87
- }
package/src/registry.ts DELETED
@@ -1,67 +0,0 @@
1
- import type { Store } from './types';
2
-
3
- /**
4
- * Extension registry for store plugins.
5
- * Features register when imported; createStore applies all registered extensions.
6
- * @internal
7
- */
8
-
9
- export interface ProcessDefinitionResult {
10
- state: Record<string, unknown>;
11
- engines?: Map<string, unknown>;
12
- /** Async keys to init after store has setState. Each init receives (onUpdate) => engine. */
13
- asyncInits?: Array<{ key: string; init: (onUpdate: (state: unknown) => void) => unknown }>;
14
- readOnlyKeys?: Set<string>;
15
- /** Called when state changes. Extensions can recompute derived values via setComputed. */
16
- onStateChanged?: (ctx: {
17
- changedKeys: Set<string>;
18
- getState: () => Record<string, unknown>;
19
- setComputed: (key: string, value: unknown) => void;
20
- store: unknown;
21
- }) => void;
22
- }
23
-
24
- export interface ExtensionContext {
25
- engines: Map<string, unknown>;
26
- store: Store<object>;
27
- definition: object;
28
- }
29
-
30
-
31
-
32
- export interface StoreExtension {
33
- /** Unique key to avoid double registration */
34
- key: string;
35
- /** Process definition values before store init. Return modified state + optional metadata. */
36
- processDefinition?: (definition: Record<string, unknown>) => ProcessDefinitionResult;
37
- /** Add methods to the store. Called after store is created. */
38
- extendStore?: (context: ExtensionContext) => Record<string, unknown>;
39
- }
40
-
41
- const extensions: StoreExtension[] = [];
42
-
43
- /**
44
- * Register an extension. Called by feature modules on import (side-effect).
45
- * @internal
46
- */
47
- export function registerExtension(ext: StoreExtension & { order?: number }): void {
48
- if (extensions.some((e) => e.key === ext.key)) return;
49
- extensions.push(ext);
50
- extensions.sort((a, b) => ((a as { order?: number }).order ?? 99) - ((b as { order?: number }).order ?? 99));
51
- }
52
-
53
- /**
54
- * Get all registered extensions. Used by createStore.
55
- * @internal
56
- */
57
- export function getExtensions(): readonly StoreExtension[] {
58
- return extensions;
59
- }
60
-
61
- /**
62
- * Clear all extensions. For testing only.
63
- * @internal
64
- */
65
- export function __testingOnlyClearExtensions(): void {
66
- extensions.length = 0
67
- }
@@ -1,81 +0,0 @@
1
- import type { Store, StoreState } from '../types';
2
- import type { Signal } from './index';
3
-
4
- /**
5
- * Creates a Signal that subscribes to a specific key in a Storve store.
6
- * Signals provide fine-grained reactivity by only notifying listeners when
7
- * the specific key's value changes.
8
- *
9
- * @example
10
- * const countSignal = signal(store, 'count');
11
- */
12
- export function signal<D extends object, K extends keyof StoreState<D>>(
13
- store: Store<D>,
14
- key: K
15
- ): Signal<StoreState<D>[K]>;
16
-
17
- /**
18
- * Creates a derived read-only Signal that transforms a value from the store.
19
- *
20
- * @example
21
- * const doubleSignal = signal(store, 'count', v => v * 2);
22
- */
23
- export function signal<D extends object, K extends keyof StoreState<D>, R>(
24
- store: Store<D>,
25
- key: K,
26
- transform: (value: StoreState<D>[K]) => R
27
- ): Signal<R>;
28
-
29
- export function signal<D extends object, K extends keyof StoreState<D>, R>(
30
- store: Store<D>,
31
- key: K,
32
- transform?: (value: StoreState<D>[K]) => R
33
- ): Signal<R | StoreState<D>[K]> {
34
- const isDerived = !!transform;
35
-
36
- const get = () => {
37
- const value = store.getState()[key];
38
- return transform ? transform(value) : value;
39
- };
40
-
41
- const signalInstance = {
42
- get,
43
- set(value: StoreState<D>[K] | ((prev: StoreState<D>[K]) => StoreState<D>[K])) {
44
- if (isDerived) {
45
- throw new Error(
46
- 'Storve: cannot call set() on a derived signal. Derived signals are read-only.'
47
- );
48
- }
49
- const next =
50
- typeof value === 'function'
51
- ? (value as (prev: StoreState<D>[K]) => StoreState<D>[K])(store.getState()[key])
52
- : value;
53
- store.setState({ [key]: next } as Partial<StoreState<D>>);
54
- },
55
- subscribe(listener: (value: R | StoreState<D>[K]) => void) {
56
- let prev = transform
57
- ? transform(store.getState()[key])
58
- : (store.getState()[key] as R | StoreState<D>[K]);
59
-
60
- return store.subscribe(() => {
61
- const next = transform
62
- ? transform(store.getState()[key])
63
- : (store.getState()[key] as R | StoreState<D>[K]);
64
-
65
- if (Object.is(prev, next)) return;
66
- prev = next;
67
- listener(next);
68
- });
69
- },
70
- _derived: isDerived,
71
- };
72
-
73
- return new Proxy(signalInstance, {
74
- set(target, prop, value) {
75
- if (prop === '_derived') {
76
- return true; // Silently ignore writes to _derived
77
- }
78
- return Reflect.set(target, prop, value);
79
- },
80
- }) as Signal<R | StoreState<D>[K]>;
81
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * A subscribable reference to a single key in a Storve store.
3
- * Read-only signals (derived) throw a clear error when set() is called.
4
- */
5
- export interface Signal<T> {
6
- /** Returns the current value of this signal */
7
- get(): T;
8
- /**
9
- * Sets a new value. Throws if called on a derived (read-only) signal.
10
- * Writes back to the store — the store remains the single source of truth.
11
- */
12
- set(value: T | ((prev: T) => T)): void;
13
- /** Subscribe to value changes. Returns an unsubscribe function. */
14
- subscribe(listener: (value: T) => void): () => void;
15
- /** Internal flag — true if this is a derived read-only signal */
16
- readonly _derived: boolean;
17
- }
18
-
19
- export { signal } from './createSignal';
20
- export { useSignal } from './useSignal';
@@ -1,18 +0,0 @@
1
- import { useSyncExternalStore } from 'react';
2
- import type { Signal } from './index';
3
-
4
- /**
5
- * React hook that subscribes to a Signal and returns its current value.
6
- * The component re-renders ONLY when this signal's value changes.
7
- * Unrelated store key changes are completely ignored.
8
- *
9
- * @example
10
- * const count = useSignal(countSignal) // re-renders only when count changes
11
- */
12
- export function useSignal<T>(signal: Signal<T>): T {
13
- return useSyncExternalStore(
14
- (onStoreChange: () => void) => signal.subscribe(onStoreChange),
15
- () => signal.get(),
16
- () => signal.get()
17
- );
18
- }