@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
package/src/store.ts DELETED
@@ -1,250 +0,0 @@
1
- import { Store, StoreDefinition, Listener, StoreOptions, StoreState, StoreActions } from './types'
2
- import { createStateProxy } from './proxy'
3
- import { produce } from 'immer'
4
- import { isBatching, subscribeToBatch } from './batch'
5
- import { getExtensions } from './registry'
6
-
7
- /**
8
- * Creates a reactive store with auto-tracking features via Proxies.
9
- * Any mutations to the state via setState or directly to deep objects will notify subscribers.
10
- * Extensions (async, computed) register when their modules are imported and extend the store.
11
- *
12
- * @param definition - The initial state object including optional actions.
13
- * @param options - Configuration options for the store (e.g., immer).
14
- * @returns A generic store instance with getState, setState, subscribe, batch, and actions.
15
- */
16
- export function createStore<D extends object>(
17
- definition: StoreDefinition<D>,
18
- options: StoreOptions = {}
19
- ): Store<D> {
20
- const { actions: rawActions = {}, ...initialData } =
21
- definition as D & { actions?: Record<string, (...args: unknown[]) => unknown> }
22
-
23
- let workingData: Record<string, unknown> = { ...initialData }
24
- const allAsyncInits: Array<{ key: string; init: (onUpdate: (state: unknown) => void) => unknown }> = []
25
- const readOnlyKeys = new Set<string>()
26
- const onStateChangedCallbacks: Array<(ctx: {
27
- changedKeys: Set<string>;
28
- getState: () => Record<string, unknown>;
29
- setComputed: (key: string, value: unknown) => void;
30
- store: Store<D>;
31
- }) => void> = []
32
-
33
- // Run extension pipeline
34
- for (const ext of getExtensions()) {
35
- if (ext.processDefinition) {
36
- const result = ext.processDefinition(workingData)
37
- workingData = { ...workingData, ...result.state }
38
- if (result.asyncInits) allAsyncInits.push(...result.asyncInits)
39
- if (result.readOnlyKeys) result.readOnlyKeys.forEach((k) => readOnlyKeys.add(k))
40
- if (result.onStateChanged) onStateChangedCallbacks.push(result.onStateChanged)
41
- }
42
- }
43
-
44
- // Run async inits — use ref so callback can call setState before store is assigned
45
- const setStateRef: { current: ((p: Partial<StoreState<D>>) => void) | null } = { current: null }
46
- const engines = new Map<string, unknown>()
47
- for (const { key, init } of allAsyncInits) {
48
- const engine = init((nodeState) => {
49
- setStateRef.current?.({ [key]: nodeState } as Partial<StoreState<D>>)
50
- })
51
- engines.set(key, engine)
52
- workingData[key] = (engine as { getState: () => unknown }).getState()
53
- }
54
-
55
- const initialState = workingData as StoreState<D>
56
- const listeners = new Set<Listener<StoreState<D>>>()
57
- let currentState = initialState as StoreState<D>
58
- let batchCount = 0
59
- let batchDirty = false
60
- let unsubscribeBatch: (() => void) | null = null
61
- let pendingChangedKeys = new Set<string>()
62
- let lastSnapshot: StoreState<D> | null = null
63
- let lastSnapshotState: StoreState<D> | null = null
64
-
65
- const notify = () => {
66
- if (batchCount > 0 || isBatching()) {
67
- batchDirty = true
68
- return
69
- }
70
- batchDirty = false
71
- listeners.forEach((listener) => listener(currentState))
72
- }
73
-
74
- const runOnStateChanged = (changedKeys: Set<string>) => {
75
- const setComputed = (key: string, value: unknown) => {
76
- (currentState as Record<string, unknown>)[key] = value
77
- }
78
- for (const cb of onStateChangedCallbacks) {
79
- cb({
80
- changedKeys,
81
- getState: () => currentState as Record<string, unknown>,
82
- setComputed,
83
- store,
84
- })
85
- }
86
- }
87
-
88
- const proxyState = createStateProxy(initialState, notify)
89
-
90
- const setState = (
91
- updater:
92
- | Partial<StoreState<D>>
93
- | ((s: StoreState<D>) => Partial<StoreState<D>>)
94
- | ((draft: StoreState<D>) => void)
95
- ) => {
96
- let nextState: StoreState<D>
97
-
98
- if (typeof updater === 'function') {
99
- if (options.immer) {
100
- nextState = produce(currentState, updater as (draft: StoreState<D>) => void) as StoreState<D>
101
- } else {
102
- nextState = {
103
- ...currentState,
104
- ...(updater as (s: StoreState<D>) => Partial<StoreState<D>>)(currentState),
105
- }
106
- }
107
- } else {
108
- nextState = { ...currentState, ...updater }
109
- }
110
-
111
- if (nextState === currentState) return
112
-
113
- const writableNext = { ...nextState } as Record<string, unknown>
114
- readOnlyKeys.forEach((k) => delete writableNext[k])
115
- const prevState = currentState
116
- const updatedKeys = new Set(
117
- Object.keys(writableNext).filter(
118
- (k) => (prevState as Record<string, unknown>)[k] !== writableNext[k]
119
- )
120
- )
121
- // Notify even if no keys changed, because signals or other extensions might care about
122
- // derived changes or reference-based equality in transforms.
123
- // if (updatedKeys.size === 0) return
124
-
125
- currentState = { ...currentState, ...writableNext } as StoreState<D>
126
-
127
- if (batchCount > 0 || isBatching()) {
128
- updatedKeys.forEach((k) => pendingChangedKeys.add(k))
129
- batchDirty = true
130
- } else {
131
- runOnStateChanged(updatedKeys)
132
- }
133
-
134
- lastSnapshot = null
135
- lastSnapshotState = null
136
-
137
- batchCount++
138
- try {
139
- for (const key in currentState) {
140
- if (
141
- Object.prototype.hasOwnProperty.call(currentState, key) &&
142
- (currentState as Record<string, unknown>)[key] !==
143
- (prevState as Record<string, unknown>)[key]
144
- ) {
145
- (proxyState as Record<string, unknown>)[key] =
146
- currentState[key as keyof StoreState<D>]
147
- }
148
- }
149
- } finally {
150
- batchCount--
151
- }
152
-
153
- if (batchCount > 0) {
154
- batchDirty = true
155
- } else {
156
- notify()
157
- }
158
- }
159
-
160
- setStateRef.current = setState
161
-
162
- const store = {
163
- getState: () => {
164
- if (lastSnapshot !== null && lastSnapshotState === currentState) {
165
- return lastSnapshot
166
- }
167
- const snapshot = { ...currentState } as StoreState<D>
168
- lastSnapshot = snapshot
169
- lastSnapshotState = currentState
170
- return snapshot
171
- },
172
-
173
- setState,
174
-
175
- subscribe: (listener: Listener<StoreState<D>>) => {
176
- listeners.add(listener)
177
- if (listeners.size === 1) {
178
- unsubscribeBatch = subscribeToBatch(() => {
179
- if (batchDirty) {
180
- batchDirty = false
181
- runOnStateChanged(pendingChangedKeys)
182
- pendingChangedKeys = new Set()
183
- notify()
184
- }
185
- })
186
- if (batchDirty) {
187
- batchDirty = false
188
- runOnStateChanged(pendingChangedKeys)
189
- pendingChangedKeys = new Set()
190
- notify()
191
- }
192
- }
193
- return () => {
194
- listeners.delete(listener)
195
- if (listeners.size === 0) {
196
- unsubscribeBatch?.()
197
- unsubscribeBatch = null
198
- }
199
- }
200
- },
201
-
202
- batch: (fn: () => void) => {
203
- batchCount++
204
- try {
205
- fn()
206
- } finally {
207
- batchCount--
208
- if (batchCount === 0 && batchDirty) {
209
- batchDirty = false
210
- runOnStateChanged(pendingChangedKeys)
211
- pendingChangedKeys = new Set()
212
- notify()
213
- }
214
- }
215
- },
216
-
217
- actions: {} as StoreActions<D>,
218
-
219
- // Default async stubs — overwritten by async extension when engines exist
220
- fetch: async (key: string) => {
221
- throw new Error(`Storve: no async key "${key}" found in store. Import "storve/async" to use createAsync.`);
222
- },
223
- refetch: async () => {},
224
- invalidate: () => {},
225
- invalidateAll: () => {},
226
- getAsyncState: () => undefined,
227
- } as Store<D>
228
-
229
- // Add methods from extensions (async overwrites stubs when engines exist)
230
- for (const ext of getExtensions()) {
231
- if (ext.extendStore) {
232
- const methods = ext.extendStore({ engines, store: store as unknown as Store<object>, definition: definition as object })
233
- Object.defineProperties(store, Object.getOwnPropertyDescriptors(methods))
234
- }
235
- }
236
-
237
- // Trigger initial onStateChanged for extensions (e.g. devtools init)
238
- runOnStateChanged(new Set(Object.keys(currentState)))
239
-
240
- type RawActionsType = Record<string, (...args: unknown[]) => unknown>
241
- const boundActions = {} as StoreActions<D>
242
- Object.keys(rawActions).forEach((key) => {
243
- (boundActions as RawActionsType)[key] = (...args: unknown[]) =>
244
- (rawActions as RawActionsType)[key](...args)
245
- })
246
- Object.assign(store, boundActions)
247
- store.actions = boundActions
248
-
249
- return store
250
- }
@@ -1,15 +0,0 @@
1
- /**
2
- * Opens a BroadcastChannel if available in the current environment.
3
- * Gracefully returns null in SSR or older browsers.
4
- * @internal
5
- */
6
- export function openChannel(name: string): BroadcastChannel | null {
7
- if (typeof window === 'undefined') return null;
8
- if (typeof BroadcastChannel === 'undefined') return null;
9
- try {
10
- return new BroadcastChannel(name);
11
- } catch {
12
- /* v8 ignore next 2 */
13
- return null;
14
- }
15
- }
package/src/sync/index.ts DELETED
@@ -1,3 +0,0 @@
1
- /* v8 ignore next 10 */
2
- export { withSync } from './withSync';
3
- export type { SyncOptions } from './withSync';
@@ -1,18 +0,0 @@
1
- /**
2
- * Random tab ID generated once per tab session.
3
- */
4
- export const tabId = (function () {
5
- if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
6
- return crypto.randomUUID();
7
- }
8
- return Math.random().toString(36).substring(2, 11);
9
- })();
10
-
11
- /**
12
- * Message types for cross-tab synchronization.
13
- * @template S - The state type
14
- */
15
- export type SyncMessage<S> =
16
- | { type: 'STATE_UPDATE'; payload: Partial<S>; tabId: string }
17
- | { type: 'REQUEST_STATE'; tabId: string }
18
- | { type: 'PROVIDE_STATE'; payload: S; targetTabId: string; tabId: string };
@@ -1,147 +0,0 @@
1
- import { registerExtension } from '../registry';
2
- import { openChannel } from './channel';
3
- import { tabId, SyncMessage } from './protocol';
4
- import type { Store, StoreState } from '../types';
5
-
6
- /**
7
- * Options for configuring cross-tab synchronization.
8
- */
9
- export interface SyncOptions {
10
- /** Unique name for the BroadcastChannel */
11
- channel: string;
12
- /** Optional list of keys to sync. If omitted, all keys are synced. */
13
- keys?: string[];
14
- /** Whether sync is enabled (default true) */
15
- enabled?: boolean;
16
- }
17
-
18
- /** @internal Symbol marker to store sync options on the definition object */
19
- const SYNC_OPTIONS = Symbol('storve_sync_options');
20
-
21
- /**
22
- * Wraps a store definition with cross-tab synchronization.
23
- * Updates to the store will be broadcast to other tabs via BroadcastChannel.
24
- */
25
- export function withSync<D extends object>(
26
- definition: D,
27
- options: SyncOptions
28
- ): D {
29
- Object.defineProperty(definition, SYNC_OPTIONS, {
30
- value: options,
31
- enumerable: false,
32
- configurable: true
33
- });
34
- return definition;
35
- }
36
-
37
- registerExtension({
38
- key: 'sync',
39
- extendStore: (context) => {
40
- const { store, definition } = context as { store: Store<object>; definition: object };
41
- const options = (definition as Record<symbol, unknown>)[SYNC_OPTIONS] as SyncOptions | undefined;
42
-
43
- if (!options || options.enabled === false) return {};
44
-
45
- const channel = openChannel(options.channel);
46
- if (!channel) return {};
47
-
48
- let isSyncUpdate = false;
49
- let rehydrated = false;
50
-
51
- // 1. Function to build payload based on selective keys and changes
52
- const buildPayload = (nextState: Record<string, unknown>, prevState: Record<string, unknown>) => {
53
- const keysToSync = options.keys || Object.keys(nextState);
54
- const payload: Record<string, unknown> = {};
55
- let hasChanges = false;
56
-
57
- for (const key of keysToSync) {
58
- // Skip internal symbols or non-enumerable props that might have leaked
59
- if (typeof key === 'symbol') continue;
60
-
61
- if (nextState[key] !== prevState[key]) {
62
- payload[key] = nextState[key];
63
- hasChanges = true;
64
- }
65
- }
66
- return hasChanges ? payload : null;
67
- };
68
-
69
- // 2. Wrap setState to broadcast local changes
70
- const originalSetState = store.setState.bind(store);
71
- store.setState = (updater) => {
72
- const prevState = { ...store.getState() as Record<string, unknown> };
73
- originalSetState(updater);
74
- if (!isSyncUpdate) {
75
- const nextState = store.getState() as Record<string, unknown>;
76
- const payload = buildPayload(nextState, prevState);
77
- if (payload) {
78
- channel.postMessage({
79
- type: 'STATE_UPDATE',
80
- payload,
81
- tabId
82
- });
83
- }
84
- }
85
- };
86
-
87
- // 3. Handle incoming messages
88
- channel.onmessage = (event) => {
89
- const data = event.data as SyncMessage<Record<string, unknown>>;
90
-
91
- // Ignore messages from self
92
- if (data.tabId === tabId) return;
93
-
94
- switch (data.type) {
95
- case 'STATE_UPDATE': {
96
- isSyncUpdate = true;
97
- store.setState(data.payload as Partial<StoreState<object>>);
98
- isSyncUpdate = false;
99
- break;
100
- }
101
- case 'REQUEST_STATE': {
102
- // Provide current state to the requesting tab, filtered by sync keys
103
- const currentState = store.getState() as Record<string, unknown>;
104
- const payload: Record<string, unknown> = {};
105
- const keysToSync = options.keys || Object.keys(currentState);
106
-
107
- for (const key of keysToSync) {
108
- if (typeof key === 'symbol') continue;
109
- if (key in currentState) {
110
- payload[key] = currentState[key];
111
- }
112
- }
113
-
114
- channel.postMessage({
115
- type: 'PROVIDE_STATE',
116
- payload,
117
- targetTabId: data.tabId,
118
- tabId
119
- });
120
- break;
121
- }
122
- case 'PROVIDE_STATE': {
123
- // Only apply if it's targeted at us and we haven't rehydrated yet
124
- if (data.targetTabId === tabId && !rehydrated) {
125
- rehydrated = true;
126
- isSyncUpdate = true;
127
- store.setState(data.payload as Partial<StoreState<object>>);
128
- isSyncUpdate = false;
129
- }
130
- break;
131
- }
132
- }
133
- };
134
-
135
- // 4. Trigger rehydration request
136
- channel.postMessage({ type: 'REQUEST_STATE', tabId });
137
-
138
- // 5. Cleanup on channel if possible (BroadcastChannel doesn't have a direct destroy hook in our store,
139
- // but it's good practice to close it if the store were to be destroyed).
140
- // Since Storve stores are usually singletons per-tab, we rely on browser cleanup,
141
- // but we can expose a close method.
142
-
143
- return {
144
- __sync_channel: channel, // for testing
145
- };
146
- }
147
- });
package/src/types.ts DELETED
@@ -1,159 +0,0 @@
1
- import type { ComputedValue } from './computed';
2
-
3
- /** @internal */
4
- export const ASYNC_VALUE_MARKER = '__rf_async';
5
-
6
- /** Re-export for consumers. Defined in computed.ts. */
7
- export type { ComputedValue } from './computed';
8
-
9
- /**
10
- * Status of an async operation.
11
- */
12
- export type AsyncStatus = 'idle' | 'loading' | 'success' | 'error';
13
-
14
- /**
15
- * The shape of an async state value in the store.
16
- */
17
- export interface AsyncState<T> {
18
- /** The resolved data or null */
19
- data: T | null;
20
- /** Error message if the operation failed */
21
- error: string | null;
22
- /** Current status of the operation */
23
- status: AsyncStatus;
24
- /** Derived from status === 'loading' */
25
- loading: boolean;
26
- /** Re-runs the async function with the last used arguments */
27
- refetch: () => Promise<void>;
28
- }
29
-
30
- /**
31
- * Configuration options for an async value.
32
- */
33
- export interface AsyncOptions {
34
- /** Cache TTL in milliseconds. Default: 0 (no cache) */
35
- ttl?: number;
36
- /** Enable stale-while-revalidate behavior. Default: false */
37
- staleWhileRevalidate?: boolean;
38
- }
39
-
40
- /**
41
- * Internal interface for the async engine.
42
- */
43
- export interface IAsyncEngine<T> {
44
- getState: () => AsyncState<T>;
45
- fetch: (...args: unknown[]) => Promise<void>;
46
- refetch: () => Promise<void>;
47
- invalidate: () => void;
48
- }
49
-
50
- /**
51
- * Internal marker for values created via createAsync().
52
- * Use this to distinguish async definitions from sync state.
53
- */
54
- export interface AsyncValue<T> {
55
- readonly [ASYNC_VALUE_MARKER]: true;
56
- init: (onUpdate: (s: AsyncState<T>) => void) => IAsyncEngine<T>;
57
- }
58
-
59
- /** Keys of the definition that are computed values (read-only in setState). */
60
- export type ComputedKeys<D> = {
61
- [K in keyof Omit<D, 'actions'>]: Omit<D, 'actions'>[K] extends ComputedValue<unknown> ? K : never;
62
- }[keyof Omit<D, 'actions'>];
63
-
64
- /**
65
- * State shape with computed keys omitted. Use for setState payloads so TS flags setting computed keys.
66
- */
67
- export type WritableStoreState<D> = Omit<StoreState<D>, ComputedKeys<D>>;
68
-
69
- /**
70
- * Utility to extract state, omitting the actions key and unwrapping AsyncValues and ComputedValues.
71
- */
72
- export type StoreState<D> = {
73
- [K in keyof Omit<D, 'actions'>]: Omit<D, 'actions'>[K] extends AsyncValue<infer T>
74
- ? AsyncState<T>
75
- : Omit<D, 'actions'>[K] extends ComputedValue<infer T>
76
- ? T
77
- : Omit<D, 'actions'>[K];
78
- };
79
-
80
- /**
81
- * Utility to extract actions from the definition.
82
- */
83
- export type StoreActions<D> = D extends { actions: infer A } ? A : Record<string, never>;
84
-
85
- /**
86
- * The shape of initial state passed to createStore.
87
- * D represents the full definition including optional actions.
88
- */
89
- export type StoreDefinition<D extends object> = D;
90
-
91
- /**
92
- * Configuration options for the store.
93
- */
94
- export interface StoreOptions {
95
- /**
96
- * Enable Immer for mutation-style state updates.
97
- * @default false
98
- */
99
- immer?: boolean;
100
- }
101
-
102
- // Listener callback — called on every state change
103
- export type Listener<T> = (state: T) => void;
104
-
105
- // Returned by subscribe() — call to stop listening
106
- export type Unsubscribe = () => void;
107
-
108
- /**
109
- * The store instance returned by createStore().
110
- */
111
- export type Store<D extends object> = {
112
- /**
113
- * Returns the current state snapshot.
114
- */
115
- getState: () => StoreState<D>;
116
- /**
117
- * Updates the state. Supports partial objects, updaters, and Immer mutators.
118
- * Computed keys are read-only; passing them in the payload is a type error and is ignored at runtime.
119
- */
120
- setState: (
121
- updater:
122
- | Partial<WritableStoreState<D>>
123
- | ((state: StoreState<D>) => Partial<WritableStoreState<D>>)
124
- | ((draft: StoreState<D>) => void)
125
- ) => void;
126
- /**
127
- * Subscribes to state changes.
128
- */
129
- subscribe: (listener: Listener<StoreState<D>>) => Unsubscribe;
130
- /**
131
- * Batches multiple state updates to trigger only one notification.
132
- */
133
- batch: (fn: () => void) => void;
134
- /**
135
- * Stable reference to the store's actions.
136
- */
137
- actions: StoreActions<D>;
138
-
139
- /**
140
- * Triggers an async fetch for a specific key.
141
- */
142
- fetch: (key: keyof StoreState<D>, ...args: unknown[]) => Promise<void>;
143
- /**
144
- * Re-runs the async operation for a key with the last used arguments.
145
- */
146
- refetch: (key: keyof StoreState<D>) => Promise<void>;
147
- /**
148
- * Invalidates the cache for a specific async key.
149
- */
150
- invalidate: (key: keyof StoreState<D>) => void;
151
- /**
152
- * Invalidates all async caches in the store.
153
- */
154
- invalidateAll: () => void;
155
- /**
156
- * Gets the raw async state for a key (internal use).
157
- */
158
- getAsyncState: (key: keyof StoreState<D>) => AsyncState<unknown> | undefined;
159
- } & StoreActions<D>;