@hanzogui/use-store 2.0.0
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 +91 -0
- package/dist/cjs/comparators.cjs +37 -0
- package/dist/cjs/comparators.native.js +46 -0
- package/dist/cjs/comparators.native.js.map +1 -0
- package/dist/cjs/configureUseStore.cjs +30 -0
- package/dist/cjs/configureUseStore.native.js +33 -0
- package/dist/cjs/configureUseStore.native.js.map +1 -0
- package/dist/cjs/constants.cjs +31 -0
- package/dist/cjs/constants.native.js +34 -0
- package/dist/cjs/constants.native.js.map +1 -0
- package/dist/cjs/decorators.cjs +30 -0
- package/dist/cjs/decorators.native.js +33 -0
- package/dist/cjs/decorators.native.js.map +1 -0
- package/dist/cjs/helpers.cjs +55 -0
- package/dist/cjs/helpers.native.js +65 -0
- package/dist/cjs/helpers.native.js.map +1 -0
- package/dist/cjs/index.cjs +40 -0
- package/dist/cjs/index.native.js +54 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/cjs/interfaces.cjs +16 -0
- package/dist/cjs/interfaces.native.js +19 -0
- package/dist/cjs/interfaces.native.js.map +1 -0
- package/dist/cjs/observe.cjs +120 -0
- package/dist/cjs/observe.native.js +186 -0
- package/dist/cjs/observe.native.js.map +1 -0
- package/dist/cjs/useStore.cjs +390 -0
- package/dist/cjs/useStore.native.js +544 -0
- package/dist/cjs/useStore.native.js.map +1 -0
- package/dist/cjs/useStoreDebug.cjs +56 -0
- package/dist/cjs/useStoreDebug.native.js +65 -0
- package/dist/cjs/useStoreDebug.native.js.map +1 -0
- package/dist/esm/comparators.mjs +14 -0
- package/dist/esm/comparators.mjs.map +1 -0
- package/dist/esm/comparators.native.js +20 -0
- package/dist/esm/comparators.native.js.map +1 -0
- package/dist/esm/configureUseStore.mjs +6 -0
- package/dist/esm/configureUseStore.mjs.map +1 -0
- package/dist/esm/configureUseStore.native.js +6 -0
- package/dist/esm/configureUseStore.native.js.map +1 -0
- package/dist/esm/constants.mjs +7 -0
- package/dist/esm/constants.mjs.map +1 -0
- package/dist/esm/constants.native.js +7 -0
- package/dist/esm/constants.native.js.map +1 -0
- package/dist/esm/decorators.mjs +7 -0
- package/dist/esm/decorators.mjs.map +1 -0
- package/dist/esm/decorators.native.js +7 -0
- package/dist/esm/decorators.native.js.map +1 -0
- package/dist/esm/helpers.mjs +26 -0
- package/dist/esm/helpers.mjs.map +1 -0
- package/dist/esm/helpers.native.js +33 -0
- package/dist/esm/helpers.native.js.map +1 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.mjs +14 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +25 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/esm/interfaces.mjs +2 -0
- package/dist/esm/interfaces.mjs.map +1 -0
- package/dist/esm/interfaces.native.js +2 -0
- package/dist/esm/interfaces.native.js.map +1 -0
- package/dist/esm/observe.mjs +85 -0
- package/dist/esm/observe.mjs.map +1 -0
- package/dist/esm/observe.native.js +148 -0
- package/dist/esm/observe.native.js.map +1 -0
- package/dist/esm/useStore.mjs +342 -0
- package/dist/esm/useStore.mjs.map +1 -0
- package/dist/esm/useStore.native.js +493 -0
- package/dist/esm/useStore.native.js.map +1 -0
- package/dist/esm/useStoreDebug.mjs +18 -0
- package/dist/esm/useStoreDebug.mjs.map +1 -0
- package/dist/esm/useStoreDebug.native.js +24 -0
- package/dist/esm/useStoreDebug.native.js.map +1 -0
- package/package.json +47 -0
- package/src/comparators.tsx +18 -0
- package/src/configureUseStore.tsx +7 -0
- package/src/constants.tsx +6 -0
- package/src/decorators.tsx +8 -0
- package/src/helpers.tsx +56 -0
- package/src/index.ts +12 -0
- package/src/interfaces.tsx +46 -0
- package/src/observe.tsx +160 -0
- package/src/useStore.tsx +705 -0
- package/src/useStoreDebug.tsx +41 -0
package/src/useStore.tsx
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { isEqualSubsetShallow } from './comparators'
|
|
4
|
+
import { configureOpts } from './configureUseStore'
|
|
5
|
+
import { UNWRAP_PROXY, defaultOptions } from './constants'
|
|
6
|
+
import {
|
|
7
|
+
UNWRAP_STORE_INFO,
|
|
8
|
+
cache,
|
|
9
|
+
getStoreDescriptors,
|
|
10
|
+
getStoreUid,
|
|
11
|
+
simpleStr,
|
|
12
|
+
} from './helpers'
|
|
13
|
+
import type { Selector, Store, StoreInfo, UseStoreOptions } from './interfaces'
|
|
14
|
+
import { DebugStores, shouldDebug, useCurrentComponent } from './useStoreDebug'
|
|
15
|
+
|
|
16
|
+
const idFn = (_) => _
|
|
17
|
+
|
|
18
|
+
// no singleton, just react
|
|
19
|
+
export function useStore<A, B extends object>(
|
|
20
|
+
StoreKlass: (new (props: B) => A) | (new () => A) | null | undefined,
|
|
21
|
+
props?: B | null,
|
|
22
|
+
options: UseStoreOptions<A, any> = defaultOptions
|
|
23
|
+
): A {
|
|
24
|
+
const info = getOrCreateStoreInfo(StoreKlass, props, options)
|
|
25
|
+
return useStoreFromInfo(info, options.selector, options)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useStoreDebug<A, B extends object>(
|
|
29
|
+
StoreKlass: (new (props: B) => A) | (new () => A),
|
|
30
|
+
props?: B
|
|
31
|
+
): A {
|
|
32
|
+
return useStore(StoreKlass, props, { debug: true })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// singleton
|
|
36
|
+
export function createStore<A, B extends object>(
|
|
37
|
+
StoreKlass: new (props: B) => A | (new () => A) | null | undefined,
|
|
38
|
+
props?: B,
|
|
39
|
+
options?: UseStoreOptions<A, any>
|
|
40
|
+
): A {
|
|
41
|
+
return getOrCreateStoreInfo(StoreKlass, props, options)?.store as any
|
|
42
|
+
}
|
|
43
|
+
// use singleton with react
|
|
44
|
+
// TODO selector support with types...
|
|
45
|
+
|
|
46
|
+
export function useGlobalStore<A, B extends object>(instance: A, debug?: boolean): A {
|
|
47
|
+
const store = instance[UNWRAP_PROXY]
|
|
48
|
+
const uid = getStoreUid(store.constructor, store.props)
|
|
49
|
+
const info = cache.get(uid)
|
|
50
|
+
if (!info) {
|
|
51
|
+
throw new Error(`This store not created using createStore()`)
|
|
52
|
+
}
|
|
53
|
+
return useStoreFromInfo(info, undefined, { debug })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useGlobalStoreSelector<A, Selector extends (store: A) => any>(
|
|
57
|
+
instance: A,
|
|
58
|
+
selector: Selector,
|
|
59
|
+
debug?: boolean
|
|
60
|
+
): Selector extends (a: A) => infer C ? C : unknown {
|
|
61
|
+
const store = instance[UNWRAP_PROXY]
|
|
62
|
+
const uid = getStoreUid(store.constructor, store.props)
|
|
63
|
+
const info = cache.get(uid)
|
|
64
|
+
if (!info) {
|
|
65
|
+
throw new Error(`This store not created using createStore()`)
|
|
66
|
+
}
|
|
67
|
+
return useStoreFromInfo(info, selector, { debug })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// for creating a usable store hook
|
|
71
|
+
export function createUseStore<Props, Store>(
|
|
72
|
+
StoreKlass: (new (props: Props) => Store) | (new () => Store)
|
|
73
|
+
) {
|
|
74
|
+
return <Res, C extends Selector<Store, Res>, Props extends object>(
|
|
75
|
+
props?: Props,
|
|
76
|
+
options?: UseStoreOptions
|
|
77
|
+
): C extends Selector<any, infer B> ? (B extends object ? B : Store) : Store =>
|
|
78
|
+
useStore(StoreKlass as any, props, options)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// for creating a usable selector hook
|
|
82
|
+
export function createUseStoreSelector<
|
|
83
|
+
A extends Store<Props>,
|
|
84
|
+
Props extends object,
|
|
85
|
+
Selected,
|
|
86
|
+
>(
|
|
87
|
+
StoreKlass: (new (props: Props) => A) | (new () => A),
|
|
88
|
+
selector: Selector<A, Selected>
|
|
89
|
+
): (props?: Props) => Selected {
|
|
90
|
+
return (props?: Props) => {
|
|
91
|
+
return useStore(StoreKlass, props, { selector }) as any
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// selector hook
|
|
96
|
+
export function useStoreSelector<A, B extends object, S extends Selector<A, any>>(
|
|
97
|
+
StoreKlass: (new (props: B) => A) | (new () => A),
|
|
98
|
+
selector: S,
|
|
99
|
+
props?: B
|
|
100
|
+
): S extends Selector<any, infer R> ? R : unknown {
|
|
101
|
+
return useStore(StoreKlass, props, { selector }) as any
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type StoreAccessTracker = (store: StoreInfo) => void
|
|
105
|
+
const storeAccessTrackers = new Set<StoreAccessTracker>()
|
|
106
|
+
export function trackStoresAccess(cb: StoreAccessTracker) {
|
|
107
|
+
storeAccessTrackers.add(cb)
|
|
108
|
+
return () => {
|
|
109
|
+
storeAccessTrackers.delete(cb)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getStore<A, B extends object>(
|
|
114
|
+
StoreKlass: (new (props: B) => A) | (new () => A) | null | undefined,
|
|
115
|
+
props?: B
|
|
116
|
+
): A {
|
|
117
|
+
return getStoreInfo(StoreKlass, props)?.store as any
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getOrCreateStore<A, B extends object>(
|
|
121
|
+
StoreKlass: (new (props: B) => A) | (new () => A) | null | undefined,
|
|
122
|
+
props?: B
|
|
123
|
+
): A {
|
|
124
|
+
return getOrCreateStoreInfo(StoreKlass, props, {
|
|
125
|
+
refuseCreation: false,
|
|
126
|
+
})?.store as any
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// just like getOrCreateStoreInfo but refuses to create
|
|
130
|
+
export function getStoreInfo(StoreKlass: any, props: any) {
|
|
131
|
+
return getOrCreateStoreInfo(StoreKlass, props, {
|
|
132
|
+
refuseCreation: true,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type CreateStoreListener = (storeInfo: StoreInfo) => void
|
|
137
|
+
|
|
138
|
+
const onCreateListeners = new Set<CreateStoreListener>()
|
|
139
|
+
|
|
140
|
+
export function onCreateStore(cb: CreateStoreListener) {
|
|
141
|
+
onCreateListeners.add(cb)
|
|
142
|
+
return () => {
|
|
143
|
+
onCreateListeners.delete(cb)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getOrCreateStoreInfo(
|
|
148
|
+
StoreKlass: any,
|
|
149
|
+
props: any,
|
|
150
|
+
options?: UseStoreOptions & { avoidCache?: boolean; refuseCreation?: boolean },
|
|
151
|
+
propsKeyCalculated?: string
|
|
152
|
+
) {
|
|
153
|
+
if (!StoreKlass) {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
const uid = getStoreUid(StoreKlass, propsKeyCalculated ?? props)
|
|
157
|
+
if (!options?.avoidCache && cache.has(uid)) {
|
|
158
|
+
return cache.get(uid)!
|
|
159
|
+
}
|
|
160
|
+
if (options?.refuseCreation) {
|
|
161
|
+
throw new Error(`No store exists (${StoreKlass.name}) with props: ${props}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// init
|
|
165
|
+
const storeInstance = new StoreKlass(props!)
|
|
166
|
+
// add props
|
|
167
|
+
storeInstance.props = props
|
|
168
|
+
|
|
169
|
+
const getters = {}
|
|
170
|
+
const actions = {}
|
|
171
|
+
const stateKeys = new Set<string>()
|
|
172
|
+
const descriptors = getStoreDescriptors(storeInstance)
|
|
173
|
+
for (const key in descriptors) {
|
|
174
|
+
const descriptor = descriptors[key]
|
|
175
|
+
if (typeof descriptor.value === 'function') {
|
|
176
|
+
// actions
|
|
177
|
+
actions[key] = descriptor.value
|
|
178
|
+
} else if (typeof descriptor.get === 'function') {
|
|
179
|
+
getters[key] = descriptor.get
|
|
180
|
+
} else {
|
|
181
|
+
if (key !== 'props' && key[0] !== '_') {
|
|
182
|
+
stateKeys.add(key)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const keyComparators = storeInstance['_comparators']
|
|
188
|
+
const listeners = new Set<Function>()
|
|
189
|
+
|
|
190
|
+
const storeInfo = {
|
|
191
|
+
uid,
|
|
192
|
+
keyComparators,
|
|
193
|
+
storeInstance,
|
|
194
|
+
getters,
|
|
195
|
+
stateKeys,
|
|
196
|
+
props,
|
|
197
|
+
actions,
|
|
198
|
+
debug: options?.debug,
|
|
199
|
+
disableTracking: false,
|
|
200
|
+
gettersState: {
|
|
201
|
+
getCache: new Map<string, any>(),
|
|
202
|
+
depsToGetter: new Map<string, Set<string>>(),
|
|
203
|
+
curGetKeys: new Set<string>(),
|
|
204
|
+
isGetting: false,
|
|
205
|
+
},
|
|
206
|
+
listeners,
|
|
207
|
+
trackers: new Set(),
|
|
208
|
+
version: 0,
|
|
209
|
+
subscribe: (onChanged: Function) => {
|
|
210
|
+
listeners.add(onChanged)
|
|
211
|
+
return () => {
|
|
212
|
+
listeners.delete(onChanged)
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
triggerUpdate: () => {
|
|
216
|
+
storeInfo.version = (storeInfo.version + 1) % Number.MAX_SAFE_INTEGER
|
|
217
|
+
for (const cb of listeners) {
|
|
218
|
+
cb()
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
} satisfies Omit<StoreInfo, 'store'>
|
|
222
|
+
|
|
223
|
+
const store = createProxiedStore(
|
|
224
|
+
// we assign store right after and proxiedStore never accesses it until later on
|
|
225
|
+
storeInfo as any as StoreInfo
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// uses more memory when on
|
|
229
|
+
if (process.env.NODE_ENV === 'development') {
|
|
230
|
+
allStores[StoreKlass.name + uid] = store
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// if has a mount function call it
|
|
234
|
+
store.mount?.()
|
|
235
|
+
|
|
236
|
+
// @ts-ignore
|
|
237
|
+
storeInfo.store = store
|
|
238
|
+
|
|
239
|
+
const result = storeInfo as any as StoreInfo
|
|
240
|
+
|
|
241
|
+
// still set even when avoidCache is true (hmr)
|
|
242
|
+
cache.set(uid, result)
|
|
243
|
+
|
|
244
|
+
onCreateListeners.forEach((cb) => cb(result))
|
|
245
|
+
|
|
246
|
+
return result
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const allStores = {}
|
|
250
|
+
|
|
251
|
+
if (process.env.NODE_ENV === 'development') {
|
|
252
|
+
globalThis['Store'] ||= allStores
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const emptyObj = {}
|
|
256
|
+
const selectKeys = (obj: any, keys: string[]) => {
|
|
257
|
+
if (!keys.length) {
|
|
258
|
+
return emptyObj
|
|
259
|
+
}
|
|
260
|
+
const res = {}
|
|
261
|
+
for (const key of keys) {
|
|
262
|
+
res[key] = obj[key]
|
|
263
|
+
}
|
|
264
|
+
return res
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let isInReaction = false
|
|
268
|
+
export const setIsInReaction = (val: boolean) => {
|
|
269
|
+
isInReaction = val
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function useStoreFromInfo(
|
|
273
|
+
info: StoreInfo | null | undefined,
|
|
274
|
+
userSelector?: Selector<any> | undefined,
|
|
275
|
+
options?: UseStoreOptions
|
|
276
|
+
): any {
|
|
277
|
+
const store = info?.store
|
|
278
|
+
const internal = React.useRef<StoreTracker>(undefined)
|
|
279
|
+
const component = useCurrentComponent()
|
|
280
|
+
if (!internal.current) {
|
|
281
|
+
internal.current = {
|
|
282
|
+
component,
|
|
283
|
+
tracked: new Set<string>(),
|
|
284
|
+
last: null,
|
|
285
|
+
lastKeys: null,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const curInternal = internal.current!
|
|
289
|
+
const shouldPrintDebug = options?.debug
|
|
290
|
+
|
|
291
|
+
const getSnapshot = React.useCallback(() => {
|
|
292
|
+
if (!info || !store) return
|
|
293
|
+
const curInternal = internal.current!
|
|
294
|
+
const isTracking = curInternal.tracked.size
|
|
295
|
+
const keys = [...(!isTracking ? info.stateKeys : curInternal.tracked)]
|
|
296
|
+
const nextKeys = `${info.version}${keys.join('')}${userSelector || ''}`
|
|
297
|
+
const lastKeys = curInternal.lastKeys
|
|
298
|
+
|
|
299
|
+
// avoid updates
|
|
300
|
+
if (nextKeys === curInternal.lastKeys) {
|
|
301
|
+
return curInternal.last
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
curInternal.lastKeys = nextKeys
|
|
305
|
+
|
|
306
|
+
let snap: any
|
|
307
|
+
// dont track during selector
|
|
308
|
+
info.disableTracking = true
|
|
309
|
+
const last = curInternal.last
|
|
310
|
+
if (userSelector) {
|
|
311
|
+
snap = userSelector(store)
|
|
312
|
+
} else {
|
|
313
|
+
snap = selectKeys(store, keys)
|
|
314
|
+
}
|
|
315
|
+
info.disableTracking = false
|
|
316
|
+
|
|
317
|
+
// this wasn't updating in AnimationsStore
|
|
318
|
+
const isUnchanged =
|
|
319
|
+
(!userSelector && !isTracking && last) ||
|
|
320
|
+
(typeof last !== 'undefined' &&
|
|
321
|
+
isEqualSubsetShallow(last, snap, {
|
|
322
|
+
keyComparators: info.keyComparators,
|
|
323
|
+
}))
|
|
324
|
+
|
|
325
|
+
if (shouldPrintDebug) {
|
|
326
|
+
console.info('🌑 getSnapshot', {
|
|
327
|
+
storeState: selectKeys(store, Object.keys(store)),
|
|
328
|
+
userSelector,
|
|
329
|
+
info,
|
|
330
|
+
isUnchanged,
|
|
331
|
+
component,
|
|
332
|
+
keys,
|
|
333
|
+
last,
|
|
334
|
+
snap,
|
|
335
|
+
curInternal,
|
|
336
|
+
nextKeys,
|
|
337
|
+
lastKeys,
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (isUnchanged) {
|
|
342
|
+
return last
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
curInternal.last = snap
|
|
346
|
+
return snap
|
|
347
|
+
}, [store])
|
|
348
|
+
|
|
349
|
+
// sync by default
|
|
350
|
+
const state = React.useSyncExternalStore(
|
|
351
|
+
info?.subscribe || idFn,
|
|
352
|
+
getSnapshot,
|
|
353
|
+
getSnapshot
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if (!info || !store || !state) {
|
|
357
|
+
return state
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (userSelector) {
|
|
361
|
+
return state
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return new Proxy(store, {
|
|
365
|
+
get(target, key) {
|
|
366
|
+
// be sure to touch the value for tracking purposes
|
|
367
|
+
const curVal = Reflect.get(target, key)
|
|
368
|
+
// while in reactions, don't proxy to old state as they aren't inside of react render
|
|
369
|
+
if (isInReaction) {
|
|
370
|
+
return curVal
|
|
371
|
+
}
|
|
372
|
+
const keyString = key as string // fine for our uses
|
|
373
|
+
|
|
374
|
+
if (info.stateKeys.has(keyString) || keyString in info.getters) {
|
|
375
|
+
if (shouldPrintDebug) {
|
|
376
|
+
console.info('👀 tracking', keyString)
|
|
377
|
+
}
|
|
378
|
+
curInternal.tracked.add(keyString)
|
|
379
|
+
}
|
|
380
|
+
if (Reflect.has(state, key)) {
|
|
381
|
+
return Reflect.get(state, key)
|
|
382
|
+
}
|
|
383
|
+
return curVal
|
|
384
|
+
},
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let setters = new Set<any>()
|
|
389
|
+
const logStack = new Set<Set<any[]> | 'end'>()
|
|
390
|
+
|
|
391
|
+
function createProxiedStore(storeInfo: StoreInfo) {
|
|
392
|
+
const { actions, storeInstance, getters, gettersState } = storeInfo
|
|
393
|
+
const { getCache, curGetKeys, depsToGetter } = gettersState
|
|
394
|
+
const constr = storeInstance.constructor
|
|
395
|
+
const shouldDebug = storeInfo.debug ?? DebugStores.has(constr)
|
|
396
|
+
|
|
397
|
+
let didSet = false
|
|
398
|
+
const wrappedActions = {}
|
|
399
|
+
|
|
400
|
+
// pre-setup actions
|
|
401
|
+
for (const key in actions) {
|
|
402
|
+
if (key === 'subscribe') {
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// wrap action and call didSet after
|
|
407
|
+
const actionFn = actions[key]
|
|
408
|
+
|
|
409
|
+
// yes its odd but bug in router needs to be figured out to fix
|
|
410
|
+
// and all it does is deopt slightly and silence get
|
|
411
|
+
const isGetFn = key.startsWith('get')
|
|
412
|
+
|
|
413
|
+
// wrap actions for tracking
|
|
414
|
+
wrappedActions[key] = function useStoreAction(...args: any[]) {
|
|
415
|
+
let res: any
|
|
416
|
+
if (isGetFn || gettersState.isGetting) {
|
|
417
|
+
return Reflect.apply(actionFn, proxiedStore, args)
|
|
418
|
+
}
|
|
419
|
+
if (process.env.NODE_ENV === 'development' && shouldDebug) {
|
|
420
|
+
console.info('(debug) startAction', key)
|
|
421
|
+
}
|
|
422
|
+
res = Reflect.apply(actionFn, proxiedStore, args)
|
|
423
|
+
if (res instanceof Promise) {
|
|
424
|
+
return res.then(finishAction)
|
|
425
|
+
}
|
|
426
|
+
finishAction()
|
|
427
|
+
return res
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// dev mode do nice logging
|
|
431
|
+
if (process.env.NODE_ENV === 'development') {
|
|
432
|
+
if (!key.startsWith('get') && !key.startsWith('_') && key !== 'subscribe') {
|
|
433
|
+
const ogAction = wrappedActions[key]
|
|
434
|
+
wrappedActions[key] = new Proxy(ogAction, {
|
|
435
|
+
apply(target, thisArg, args) {
|
|
436
|
+
const isDebugging = shouldDebug || storeInfo.debug
|
|
437
|
+
const shouldLog =
|
|
438
|
+
process.env.LOG_LEVEL !== '0' &&
|
|
439
|
+
(isDebugging || configureOpts.logLevel !== 'error')
|
|
440
|
+
|
|
441
|
+
if (!shouldLog) {
|
|
442
|
+
return Reflect.apply(target, thisArg, args)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
setters = new Set()
|
|
446
|
+
const curSetters = setters
|
|
447
|
+
const isTopLevelLogger = logStack.size == 0
|
|
448
|
+
const logs = new Set<any[]>()
|
|
449
|
+
logStack.add(logs)
|
|
450
|
+
let res
|
|
451
|
+
const id = counter++
|
|
452
|
+
try {
|
|
453
|
+
// 🏃♀️ run action here now
|
|
454
|
+
res = Reflect.apply(target, thisArg, args)
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.error('Error', err)
|
|
457
|
+
throw err
|
|
458
|
+
} finally {
|
|
459
|
+
logStack.add('end')
|
|
460
|
+
|
|
461
|
+
const name = constr.name
|
|
462
|
+
const color = strColor(name)
|
|
463
|
+
const simpleArgs = args.map(simpleStr)
|
|
464
|
+
logs.add([
|
|
465
|
+
`%c 🌑 ${id} ${name.padStart(
|
|
466
|
+
isTopLevelLogger ? 8 : 4
|
|
467
|
+
)}%c.${key}(${simpleArgs.join(', ')})${
|
|
468
|
+
isTopLevelLogger && logStack.size > 1 ? ` (+${logStack.size - 1})` : ''
|
|
469
|
+
}`,
|
|
470
|
+
|
|
471
|
+
`color: ${color};`,
|
|
472
|
+
'color: black;',
|
|
473
|
+
])
|
|
474
|
+
if (curSetters.size) {
|
|
475
|
+
curSetters.forEach(({ key, value }) => {
|
|
476
|
+
if (
|
|
477
|
+
typeof value === 'string' ||
|
|
478
|
+
typeof value === 'number' ||
|
|
479
|
+
typeof value === 'boolean'
|
|
480
|
+
) {
|
|
481
|
+
logs.add([` SET ${key} ${value}`, value])
|
|
482
|
+
} else {
|
|
483
|
+
logs.add([` SET ${key}`, value])
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (isTopLevelLogger) {
|
|
489
|
+
let error = null
|
|
490
|
+
try {
|
|
491
|
+
for (const item of logStack) {
|
|
492
|
+
if (item === 'end') {
|
|
493
|
+
console.groupEnd()
|
|
494
|
+
continue
|
|
495
|
+
}
|
|
496
|
+
const [head, ...rest] = item
|
|
497
|
+
if (head) {
|
|
498
|
+
console.groupCollapsed(...head)
|
|
499
|
+
console.groupCollapsed('...')
|
|
500
|
+
console.info('args', args)
|
|
501
|
+
console.info('response', res)
|
|
502
|
+
console.groupCollapsed('trace')
|
|
503
|
+
console.trace()
|
|
504
|
+
console.groupEnd()
|
|
505
|
+
console.groupEnd()
|
|
506
|
+
for (const [name, ...log] of rest) {
|
|
507
|
+
console.groupCollapsed(name)
|
|
508
|
+
console.info(...log)
|
|
509
|
+
console.groupEnd()
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
console.info('Weird log', head, ...rest)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch (err: any) {
|
|
516
|
+
error = err
|
|
517
|
+
}
|
|
518
|
+
for (const _ of logStack) {
|
|
519
|
+
console.groupEnd()
|
|
520
|
+
}
|
|
521
|
+
if (error) {
|
|
522
|
+
console.error(`error loggin`, error)
|
|
523
|
+
}
|
|
524
|
+
logStack.clear()
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return res
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
})
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function hashCode(str: string) {
|
|
534
|
+
let hash = 0
|
|
535
|
+
for (let i = 0; i < str.length; i++) {
|
|
536
|
+
hash = str.charCodeAt(i) + ((hash << 5) - hash)
|
|
537
|
+
}
|
|
538
|
+
return hash
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function strColor(str: string) {
|
|
542
|
+
return `hsl(${hashCode(str) % 360}, 90%, 40%)`
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const finishAction = (val?: any) => {
|
|
548
|
+
if (process.env.NODE_ENV === 'development' && shouldDebug) {
|
|
549
|
+
console.info('(debug) finishAction', { didSet })
|
|
550
|
+
}
|
|
551
|
+
if (didSet) {
|
|
552
|
+
storeInfo.triggerUpdate()
|
|
553
|
+
didSet = false
|
|
554
|
+
}
|
|
555
|
+
return val
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let isTriggering = false
|
|
559
|
+
|
|
560
|
+
const proxiedStore = new Proxy(storeInstance, {
|
|
561
|
+
// GET
|
|
562
|
+
get(_, key) {
|
|
563
|
+
// setup action one time
|
|
564
|
+
if (key in wrappedActions) {
|
|
565
|
+
return wrappedActions[key]
|
|
566
|
+
}
|
|
567
|
+
if (key in passThroughKeys) {
|
|
568
|
+
return Reflect.get(storeInstance, key)
|
|
569
|
+
}
|
|
570
|
+
if (key === UNWRAP_PROXY) {
|
|
571
|
+
return storeInstance
|
|
572
|
+
}
|
|
573
|
+
if (key === UNWRAP_STORE_INFO) {
|
|
574
|
+
return storeInfo
|
|
575
|
+
}
|
|
576
|
+
if (storeAccessTrackers.size) {
|
|
577
|
+
storeAccessTrackers.forEach((cb) => cb(storeInfo))
|
|
578
|
+
}
|
|
579
|
+
if (typeof key !== 'string') {
|
|
580
|
+
return Reflect.get(storeInstance, key)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// non-actions...
|
|
584
|
+
|
|
585
|
+
if (!storeInfo.disableTracking) {
|
|
586
|
+
if (gettersState.isGetting) {
|
|
587
|
+
gettersState.curGetKeys.add(key)
|
|
588
|
+
} else {
|
|
589
|
+
// storeInstance[TRACK](key, shouldDebug)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (key in getters) {
|
|
594
|
+
if (getCache.has(key)) {
|
|
595
|
+
return getCache.get(key)
|
|
596
|
+
}
|
|
597
|
+
// track get deps
|
|
598
|
+
curGetKeys.clear()
|
|
599
|
+
const isSubGetter = gettersState.isGetting
|
|
600
|
+
gettersState.isGetting = true
|
|
601
|
+
const res = getters[key].call(proxiedStore)
|
|
602
|
+
if (!isSubGetter) {
|
|
603
|
+
gettersState.isGetting = false
|
|
604
|
+
}
|
|
605
|
+
// store inverse lookup
|
|
606
|
+
for (const gk of curGetKeys) {
|
|
607
|
+
if (!depsToGetter.has(gk)) {
|
|
608
|
+
depsToGetter.set(gk, new Set())
|
|
609
|
+
}
|
|
610
|
+
const cur = depsToGetter.get(gk)!
|
|
611
|
+
cur.add(key)
|
|
612
|
+
}
|
|
613
|
+
// TODO i added this !isSubGetter, seems logical but haven't validated
|
|
614
|
+
// has diff performance tradeoffs, not sure whats desirable
|
|
615
|
+
// if (!isSubGetter) {
|
|
616
|
+
getCache.set(key, res)
|
|
617
|
+
// }
|
|
618
|
+
return res
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return Reflect.get(storeInstance, key)
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
// SET
|
|
625
|
+
set(target, key, value, receiver) {
|
|
626
|
+
const cur = Reflect.get(target, key)
|
|
627
|
+
const res = Reflect.set(target, key, value, receiver)
|
|
628
|
+
|
|
629
|
+
// only update if changed, simple compare
|
|
630
|
+
if (res && cur !== value) {
|
|
631
|
+
// clear getters cache that rely on this
|
|
632
|
+
if (typeof key === 'string') {
|
|
633
|
+
clearGetterCache(key)
|
|
634
|
+
if (shouldDebug) {
|
|
635
|
+
setters.add({ key, value })
|
|
636
|
+
if (getShouldDebug(storeInfo)) {
|
|
637
|
+
console.info('(debug) SET', res, key, value)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (process.env.NODE_ENV === 'development' && shouldDebug) {
|
|
641
|
+
console.info('SET...', { key, value })
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!isTriggering) {
|
|
646
|
+
// trigger only once per event loop
|
|
647
|
+
isTriggering = true
|
|
648
|
+
waitForEventLoop(() => {
|
|
649
|
+
storeInfo.triggerUpdate()
|
|
650
|
+
isTriggering = false
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return res
|
|
655
|
+
},
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
function clearGetterCache(setKey: string) {
|
|
659
|
+
const parentGetters = depsToGetter.get(setKey)
|
|
660
|
+
getCache.delete(setKey)
|
|
661
|
+
|
|
662
|
+
if (!parentGetters) {
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
for (const gk of parentGetters) {
|
|
666
|
+
getCache.delete(gk)
|
|
667
|
+
if (depsToGetter.has(gk)) {
|
|
668
|
+
clearGetterCache(gk)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return proxiedStore
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const waitForEventLoop =
|
|
677
|
+
process.env.NODE_ENV === 'test' || process.env.HANZO_GUI_TARGET === 'native'
|
|
678
|
+
? (cb: Function) => cb()
|
|
679
|
+
: queueMicrotask
|
|
680
|
+
|
|
681
|
+
let counter = 0
|
|
682
|
+
|
|
683
|
+
const passThroughKeys = {
|
|
684
|
+
subscribe: true,
|
|
685
|
+
_version: true,
|
|
686
|
+
_trackers: true,
|
|
687
|
+
$$typeof: true,
|
|
688
|
+
_listeners: true,
|
|
689
|
+
_enableTracking: true,
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export type StoreTracker = {
|
|
693
|
+
tracked: Set<string>
|
|
694
|
+
component?: any
|
|
695
|
+
last?: any
|
|
696
|
+
lastKeys?: any
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function getShouldDebug(storeInfo: StoreInfo) {
|
|
700
|
+
const info = { storeInstance: storeInfo.store }
|
|
701
|
+
const trackers = storeInfo.trackers
|
|
702
|
+
return [...trackers].some(
|
|
703
|
+
(tracker) => tracker.component && shouldDebug(tracker.component, info)
|
|
704
|
+
)
|
|
705
|
+
}
|