@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.
- package/LICENSE +21 -0
- package/README.md +993 -26
- package/dist/adapters/indexedDB.cjs +0 -1
- package/dist/adapters/indexedDB.mjs +0 -1
- package/dist/adapters/localStorage.cjs +0 -1
- package/dist/adapters/localStorage.mjs +0 -1
- package/dist/adapters/memory.cjs +0 -1
- package/dist/adapters/memory.mjs +0 -1
- package/dist/adapters/sessionStorage.cjs +0 -1
- package/dist/adapters/sessionStorage.mjs +0 -1
- package/dist/async-entry.d.ts +0 -1
- package/dist/async.cjs +0 -1
- package/dist/async.d.ts +0 -1
- package/dist/async.mjs +0 -1
- package/dist/batch.d.ts +0 -1
- package/dist/compose.d.ts +0 -1
- package/dist/computed-entry.d.ts +0 -1
- package/dist/computed.cjs +0 -1
- package/dist/computed.d.ts +0 -1
- package/dist/computed.mjs +0 -1
- package/dist/devtools/history.d.ts +0 -1
- package/dist/devtools/index.d.ts +0 -1
- package/dist/devtools/redux-bridge.d.ts +0 -1
- package/dist/devtools/snapshots.d.ts +0 -1
- package/dist/devtools/withDevtools.d.ts +0 -1
- package/dist/devtools.cjs +0 -1
- package/dist/devtools.mjs +0 -1
- package/dist/extensions/noop.d.ts +0 -1
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +0 -1
- package/dist/persist/adapters/indexedDB.d.ts +0 -1
- package/dist/persist/adapters/localStorage.d.ts +0 -1
- package/dist/persist/adapters/memory.d.ts +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts +0 -1
- package/dist/persist/debounce.d.ts +0 -1
- package/dist/persist/hydrate.d.ts +0 -1
- package/dist/persist/index.d.ts +0 -1
- package/dist/persist/serialize.d.ts +0 -1
- package/dist/persist.cjs +0 -1
- package/dist/persist.mjs +0 -1
- package/dist/proxy.d.ts +0 -1
- package/dist/registry-qtr1UpFU.js +0 -1
- package/dist/registry-zaKZ1P-s.js +0 -1
- package/dist/registry.d.ts +0 -1
- package/dist/signals/createSignal.d.ts +0 -1
- package/dist/signals/index.d.ts +0 -1
- package/dist/signals/useSignal.d.ts +0 -1
- package/dist/signals.cjs +0 -1
- package/dist/signals.mjs +0 -1
- package/dist/store.d.ts +0 -1
- package/dist/sync/channel.d.ts +0 -1
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/protocol.d.ts +0 -1
- package/dist/sync/withSync.d.ts +0 -1
- package/dist/sync.cjs +0 -1
- package/dist/sync.mjs +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +9 -3
- package/CHANGELOG.md +0 -151
- package/benchmarks/run.ts +0 -102
- package/benchmarks/week2.md +0 -9
- package/benchmarks/week2.ts +0 -64
- package/benchmarks/week4.md +0 -13
- package/benchmarks/week4.ts +0 -178
- package/benchmarks/week5.md +0 -15
- package/benchmarks/week5.ts +0 -184
- package/coverage/coverage-summary.json +0 -31
- package/dist/adapters/indexedDB.cjs.map +0 -1
- package/dist/adapters/indexedDB.mjs.map +0 -1
- package/dist/adapters/localStorage.cjs.map +0 -1
- package/dist/adapters/localStorage.mjs.map +0 -1
- package/dist/adapters/memory.cjs.map +0 -1
- package/dist/adapters/memory.mjs.map +0 -1
- package/dist/adapters/sessionStorage.cjs.map +0 -1
- package/dist/adapters/sessionStorage.mjs.map +0 -1
- package/dist/async-entry.d.ts.map +0 -1
- package/dist/async.cjs.map +0 -1
- package/dist/async.d.ts.map +0 -1
- package/dist/async.mjs.map +0 -1
- package/dist/batch.d.ts.map +0 -1
- package/dist/compose.d.ts.map +0 -1
- package/dist/computed-entry.d.ts.map +0 -1
- package/dist/computed.cjs.map +0 -1
- package/dist/computed.d.ts.map +0 -1
- package/dist/computed.mjs.map +0 -1
- package/dist/devtools/history.d.ts.map +0 -1
- package/dist/devtools/index.d.ts.map +0 -1
- package/dist/devtools/redux-bridge.d.ts.map +0 -1
- package/dist/devtools/snapshots.d.ts.map +0 -1
- package/dist/devtools/withDevtools.d.ts.map +0 -1
- package/dist/devtools.cjs.map +0 -1
- package/dist/devtools.mjs.map +0 -1
- package/dist/extensions/noop.d.ts.map +0 -1
- package/dist/index.cjs.js +0 -118
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.esm.js +0 -116
- package/dist/index.esm.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
- package/dist/persist/adapters/localStorage.d.ts.map +0 -1
- package/dist/persist/adapters/memory.d.ts.map +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
- package/dist/persist/debounce.d.ts.map +0 -1
- package/dist/persist/hydrate.d.ts.map +0 -1
- package/dist/persist/index.d.ts.map +0 -1
- package/dist/persist/serialize.d.ts.map +0 -1
- package/dist/persist.cjs.map +0 -1
- package/dist/persist.mjs.map +0 -1
- package/dist/proxy.d.ts.map +0 -1
- package/dist/registry-D3X0HSbl.js +0 -26
- package/dist/registry-D3X0HSbl.js.map +0 -1
- package/dist/registry-RDjbeJdx.js +0 -29
- package/dist/registry-RDjbeJdx.js.map +0 -1
- package/dist/registry-qtr1UpFU.js.map +0 -1
- package/dist/registry-zaKZ1P-s.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/signals/createSignal.d.ts.map +0 -1
- package/dist/signals/index.d.ts.map +0 -1
- package/dist/signals/useSignal.d.ts.map +0 -1
- package/dist/signals.cjs.map +0 -1
- package/dist/signals.mjs.map +0 -1
- package/dist/stats.html +0 -4949
- package/dist/store.d.ts.map +0 -1
- package/dist/sync/channel.d.ts.map +0 -1
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/protocol.d.ts.map +0 -1
- package/dist/sync/withSync.d.ts.map +0 -1
- package/dist/sync.cjs.map +0 -1
- package/dist/sync.mjs.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/rollup.config.mjs +0 -44
- package/src/async-entry.ts +0 -6
- package/src/async.ts +0 -240
- package/src/batch.ts +0 -33
- package/src/compose.ts +0 -50
- package/src/computed-entry.ts +0 -6
- package/src/computed.ts +0 -187
- package/src/devtools/history.ts +0 -103
- package/src/devtools/index.ts +0 -5
- package/src/devtools/redux-bridge.ts +0 -70
- package/src/devtools/snapshots.ts +0 -54
- package/src/devtools/withDevtools.ts +0 -196
- package/src/extensions/noop.ts +0 -12
- package/src/index.ts +0 -4
- package/src/persist/adapters/indexedDB.ts +0 -114
- package/src/persist/adapters/localStorage.ts +0 -28
- package/src/persist/adapters/memory.ts +0 -26
- package/src/persist/adapters/sessionStorage.ts +0 -28
- package/src/persist/debounce.ts +0 -28
- package/src/persist/hydrate.ts +0 -60
- package/src/persist/index.ts +0 -141
- package/src/persist/serialize.ts +0 -60
- package/src/proxy.ts +0 -87
- package/src/registry.ts +0 -67
- package/src/signals/createSignal.ts +0 -81
- package/src/signals/index.ts +0 -20
- package/src/signals/useSignal.ts +0 -18
- package/src/store.ts +0 -250
- package/src/sync/channel.ts +0 -15
- package/src/sync/index.ts +0 -3
- package/src/sync/protocol.ts +0 -18
- package/src/sync/withSync.ts +0 -147
- package/src/types.ts +0 -159
- package/tests/async.test.ts +0 -1100
- package/tests/batch.test.ts +0 -41
- package/tests/compose.test.ts +0 -209
- package/tests/computed.test.ts +0 -867
- package/tests/devtools.test.ts +0 -1039
- package/tests/integration/persist.integration.test.ts +0 -258
- package/tests/integration/signals.integration.test.ts +0 -309
- package/tests/integration.test.ts +0 -278
- package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
- package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
- package/tests/persist/adapters/memory.adapter.test.ts +0 -112
- package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
- package/tests/persist/debounce.test.ts +0 -121
- package/tests/persist/hydrate.test.ts +0 -120
- package/tests/persist/migrate.test.ts +0 -208
- package/tests/persist/persist.test.ts +0 -357
- package/tests/persist/serialize.test.ts +0 -128
- package/tests/proxy.test.ts +0 -473
- package/tests/registry.test.ts +0 -67
- package/tests/signals/derived.test.ts +0 -244
- package/tests/signals/inference.test.ts +0 -108
- package/tests/signals/signal.test.ts +0 -348
- package/tests/signals/useSignal.test.tsx +0 -275
- package/tests/store.test.ts +0 -482
- package/tests/stress.test.ts +0 -268
- package/tests/sync.test.ts +0 -576
- package/tests/types.test.ts +0 -32
- package/tests/v0.3.test.ts +0 -813
- package/tree-shake-test.js +0 -1
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -22
- 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
|
-
}
|
package/src/sync/channel.ts
DELETED
|
@@ -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
package/src/sync/protocol.ts
DELETED
|
@@ -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 };
|
package/src/sync/withSync.ts
DELETED
|
@@ -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>;
|