@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/persist/hydrate.ts
DELETED
|
@@ -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
|
-
}
|
package/src/persist/index.ts
DELETED
|
@@ -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
|
-
|
package/src/persist/serialize.ts
DELETED
|
@@ -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
|
-
}
|
package/src/signals/index.ts
DELETED
|
@@ -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';
|
package/src/signals/useSignal.ts
DELETED
|
@@ -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
|
-
}
|