kiru 0.44.4
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 +7 -0
- package/README.md +5 -0
- package/package.json +81 -0
- package/src/appContext.ts +186 -0
- package/src/cloneVNode.ts +14 -0
- package/src/constants.ts +146 -0
- package/src/context.ts +56 -0
- package/src/dom.ts +712 -0
- package/src/element.ts +54 -0
- package/src/env.ts +6 -0
- package/src/error.ts +85 -0
- package/src/flags.ts +15 -0
- package/src/form/index.ts +662 -0
- package/src/form/types.ts +261 -0
- package/src/form/utils.ts +19 -0
- package/src/generateId.ts +19 -0
- package/src/globalContext.ts +161 -0
- package/src/globals.ts +21 -0
- package/src/hmr.ts +178 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useAsync.ts +136 -0
- package/src/hooks/useCallback.ts +31 -0
- package/src/hooks/useContext.ts +79 -0
- package/src/hooks/useEffect.ts +44 -0
- package/src/hooks/useEffectEvent.ts +24 -0
- package/src/hooks/useId.ts +42 -0
- package/src/hooks/useLayoutEffect.ts +47 -0
- package/src/hooks/useMemo.ts +33 -0
- package/src/hooks/useReducer.ts +50 -0
- package/src/hooks/useRef.ts +40 -0
- package/src/hooks/useState.ts +62 -0
- package/src/hooks/useSyncExternalStore.ts +59 -0
- package/src/hooks/useViewTransition.ts +26 -0
- package/src/hooks/utils.ts +259 -0
- package/src/hydration.ts +67 -0
- package/src/index.ts +61 -0
- package/src/jsx.ts +11 -0
- package/src/lazy.ts +238 -0
- package/src/memo.ts +48 -0
- package/src/portal.ts +43 -0
- package/src/profiling.ts +105 -0
- package/src/props.ts +36 -0
- package/src/reconciler.ts +531 -0
- package/src/renderToString.ts +91 -0
- package/src/router/index.ts +2 -0
- package/src/router/route.ts +51 -0
- package/src/router/router.ts +275 -0
- package/src/router/routerUtils.ts +49 -0
- package/src/scheduler.ts +522 -0
- package/src/signals/base.ts +237 -0
- package/src/signals/computed.ts +139 -0
- package/src/signals/effect.ts +60 -0
- package/src/signals/globals.ts +11 -0
- package/src/signals/index.ts +12 -0
- package/src/signals/jsx.ts +45 -0
- package/src/signals/types.ts +10 -0
- package/src/signals/utils.ts +12 -0
- package/src/signals/watch.ts +151 -0
- package/src/ssr/client.ts +29 -0
- package/src/ssr/hydrationBoundary.ts +63 -0
- package/src/ssr/index.ts +1 -0
- package/src/ssr/server.ts +124 -0
- package/src/store.ts +241 -0
- package/src/swr.ts +360 -0
- package/src/transition.ts +80 -0
- package/src/types.dom.ts +1250 -0
- package/src/types.ts +209 -0
- package/src/types.utils.ts +39 -0
- package/src/utils.ts +581 -0
- package/src/warning.ts +9 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { $HMR_ACCEPT, $SIGNAL } from "../constants.js"
|
|
2
|
+
import { __DEV__ } from "../env.js"
|
|
3
|
+
import type { HMRAccept } from "../hmr.js"
|
|
4
|
+
import {
|
|
5
|
+
getVNodeAppContext,
|
|
6
|
+
latest,
|
|
7
|
+
safeStringify,
|
|
8
|
+
sideEffectsEnabled,
|
|
9
|
+
} from "../utils.js"
|
|
10
|
+
import { tracking, signalSubsMap } from "./globals.js"
|
|
11
|
+
import { type SignalSubscriber, ReadonlySignal } from "./types.js"
|
|
12
|
+
import { node } from "../globals.js"
|
|
13
|
+
import { useHook } from "../hooks/utils.js"
|
|
14
|
+
import { generateRandomID } from "../generateId.js"
|
|
15
|
+
|
|
16
|
+
export class Signal<T> {
|
|
17
|
+
[$SIGNAL] = true;
|
|
18
|
+
[$HMR_ACCEPT]?: HMRAccept<Signal<any>>
|
|
19
|
+
displayName?: string
|
|
20
|
+
private onBeforeRead?: () => void
|
|
21
|
+
protected $id: string
|
|
22
|
+
protected $value: T
|
|
23
|
+
protected $initialValue?: string
|
|
24
|
+
protected __next?: Signal<T>
|
|
25
|
+
protected $isDisposed?: boolean
|
|
26
|
+
|
|
27
|
+
constructor(initial: T, displayName?: string) {
|
|
28
|
+
this.$id = generateRandomID()
|
|
29
|
+
signalSubsMap.set(this.$id, new Set())
|
|
30
|
+
|
|
31
|
+
this.$value = initial
|
|
32
|
+
if (displayName) this.displayName = displayName
|
|
33
|
+
if (__DEV__) {
|
|
34
|
+
this.$initialValue = safeStringify(initial)
|
|
35
|
+
this[$HMR_ACCEPT] = {
|
|
36
|
+
provide: () => {
|
|
37
|
+
return this as Signal<any>
|
|
38
|
+
},
|
|
39
|
+
inject: (prev) => {
|
|
40
|
+
if (this.$initialValue === prev.$initialValue) {
|
|
41
|
+
this.$value = prev.$value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
signalSubsMap.get(this.$id)?.clear?.()
|
|
45
|
+
signalSubsMap.delete(this.$id)
|
|
46
|
+
this.$id = prev.$id
|
|
47
|
+
// @ts-ignore - this handles scenarios where a reference to the prev has been encapsulated
|
|
48
|
+
// and we need to be able to refer to the latest version of the signal.
|
|
49
|
+
prev.__next = this
|
|
50
|
+
},
|
|
51
|
+
destroy: () => {},
|
|
52
|
+
} satisfies HMRAccept<Signal<any>>
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get value() {
|
|
57
|
+
this.onBeforeRead?.()
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
const tgt = latest(this)
|
|
60
|
+
Signal.entangle(tgt)
|
|
61
|
+
return tgt.$value
|
|
62
|
+
}
|
|
63
|
+
Signal.entangle(this)
|
|
64
|
+
return this.$value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
set value(next: T) {
|
|
68
|
+
if (__DEV__) {
|
|
69
|
+
const tgt = latest(this)
|
|
70
|
+
if (Object.is(tgt.$value, next)) return
|
|
71
|
+
tgt.$value = next
|
|
72
|
+
tgt.notify()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (Object.is(this.$value, next)) return
|
|
76
|
+
this.$value = next
|
|
77
|
+
this.notify()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
peek() {
|
|
81
|
+
this.onBeforeRead?.()
|
|
82
|
+
if (__DEV__) {
|
|
83
|
+
return latest(this).$value
|
|
84
|
+
}
|
|
85
|
+
return this.$value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
sneak(newValue: T) {
|
|
89
|
+
if (__DEV__) {
|
|
90
|
+
const tgt = latest(this)
|
|
91
|
+
tgt.$value = newValue
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
this.$value = newValue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
toString() {
|
|
98
|
+
this.onBeforeRead?.()
|
|
99
|
+
if (__DEV__) {
|
|
100
|
+
const tgt = latest(this)
|
|
101
|
+
Signal.entangle(tgt)
|
|
102
|
+
return `${tgt.$value}`
|
|
103
|
+
}
|
|
104
|
+
Signal.entangle(this)
|
|
105
|
+
return `${this.$value}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
subscribe(cb: (state: T) => void): () => void {
|
|
109
|
+
const subs = signalSubsMap.get(this.$id)!
|
|
110
|
+
subs!.add(cb)
|
|
111
|
+
return () => signalSubsMap.get(this.$id)?.delete(cb)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
notify(options?: { filter?: (sub: Function | Kaioken.VNode) => boolean }) {
|
|
115
|
+
signalSubsMap.get(this.$id)?.forEach((sub) => {
|
|
116
|
+
if (options?.filter && !options.filter(sub)) return
|
|
117
|
+
if (typeof sub === "function") {
|
|
118
|
+
if (__DEV__) {
|
|
119
|
+
const value = latest(this).$value
|
|
120
|
+
return sub(value)
|
|
121
|
+
}
|
|
122
|
+
return sub(this.$value)
|
|
123
|
+
}
|
|
124
|
+
getVNodeAppContext(sub).requestUpdate(sub)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static isSignal(x: any): x is Signal<any> {
|
|
129
|
+
return typeof x === "object" && !!x && $SIGNAL in x
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static unsubscribe(sub: SignalSubscriber, id: string) {
|
|
133
|
+
signalSubsMap.get(id)?.delete(sub)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static subscribers(signal: Signal<any>) {
|
|
137
|
+
return signalSubsMap.get(signal.$id)!
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static makeReadonly<T>(signal: Signal<T>): ReadonlySignal<T> {
|
|
141
|
+
const desc = Object.getOwnPropertyDescriptor(signal, "value")
|
|
142
|
+
if (desc && !desc.writable) return signal
|
|
143
|
+
return Object.defineProperty(signal, "value", {
|
|
144
|
+
get: function (this: Signal<T>) {
|
|
145
|
+
Signal.entangle(this)
|
|
146
|
+
return this.$value
|
|
147
|
+
},
|
|
148
|
+
configurable: true,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static makeWritable<T>(signal: Signal<T>): Signal<T> {
|
|
153
|
+
const desc = Object.getOwnPropertyDescriptor(signal, "value")
|
|
154
|
+
if (desc && desc.writable) return signal
|
|
155
|
+
return Object.defineProperty(signal, "value", {
|
|
156
|
+
get: function (this: Signal<T>) {
|
|
157
|
+
Signal.entangle(this)
|
|
158
|
+
return this.$value
|
|
159
|
+
},
|
|
160
|
+
set: function (this: Signal<T>, value) {
|
|
161
|
+
this.$value = value
|
|
162
|
+
this.notify()
|
|
163
|
+
},
|
|
164
|
+
configurable: true,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static getId<T>(signal: Signal<T>) {
|
|
169
|
+
return signal.$id
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static entangle<T>(signal: Signal<T>) {
|
|
173
|
+
const vNode = node.current
|
|
174
|
+
const trackedSignalObservations = tracking.current()
|
|
175
|
+
if (trackedSignalObservations) {
|
|
176
|
+
if (!vNode || (vNode && sideEffectsEnabled())) {
|
|
177
|
+
trackedSignalObservations.set(signal.$id, signal)
|
|
178
|
+
}
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (!vNode || !sideEffectsEnabled()) return
|
|
182
|
+
;(vNode.subs ??= new Set()).add(signal.$id)
|
|
183
|
+
Signal.subscribers(signal).add(vNode)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static configure(signal: Signal<any>, onBeforeRead?: () => void) {
|
|
187
|
+
signal.onBeforeRead = onBeforeRead
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static dispose(signal: Signal<any>) {
|
|
191
|
+
signal.$isDisposed = true
|
|
192
|
+
signalSubsMap.delete(signal.$id)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const signal = <T>(initial: T, displayName?: string) => {
|
|
197
|
+
return new Signal(initial, displayName)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const useSignal = <T>(initial: T, displayName?: string) => {
|
|
201
|
+
return useHook(
|
|
202
|
+
"useSignal",
|
|
203
|
+
{ signal: null! as Signal<T> },
|
|
204
|
+
({ hook, isInit, isHMR }) => {
|
|
205
|
+
if (__DEV__) {
|
|
206
|
+
if (isInit) {
|
|
207
|
+
hook.dev = {
|
|
208
|
+
devtools: {
|
|
209
|
+
get: () => ({
|
|
210
|
+
displayName: hook.signal.displayName,
|
|
211
|
+
value: hook.signal.peek(),
|
|
212
|
+
}),
|
|
213
|
+
set: ({ value }) => {
|
|
214
|
+
hook.signal.value = value
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
initialArgs: [initial, displayName],
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (isHMR) {
|
|
221
|
+
const [v, name] = hook.dev!.initialArgs
|
|
222
|
+
if (v !== initial || name !== displayName) {
|
|
223
|
+
hook.cleanup?.()
|
|
224
|
+
isInit = true
|
|
225
|
+
hook.dev!.initialArgs = [initial, displayName]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (isInit) {
|
|
231
|
+
hook.cleanup = () => Signal.dispose(hook.signal)
|
|
232
|
+
hook.signal = new Signal(initial, displayName)
|
|
233
|
+
}
|
|
234
|
+
return hook.signal
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { __DEV__ } from "../env.js"
|
|
2
|
+
import { Signal } from "./base.js"
|
|
3
|
+
import { effectQueue, signalSubsMap } from "./globals.js"
|
|
4
|
+
import { $HMR_ACCEPT } from "../constants.js"
|
|
5
|
+
import type { HMRAccept } from "../hmr.js"
|
|
6
|
+
import { useHook } from "../hooks/utils.js"
|
|
7
|
+
import { executeWithTracking } from "./effect.js"
|
|
8
|
+
import { latest } from "../utils.js"
|
|
9
|
+
|
|
10
|
+
export class ComputedSignal<T> extends Signal<T> {
|
|
11
|
+
protected $getter: (prev?: T) => T
|
|
12
|
+
protected $unsubs: Map<string, Function>
|
|
13
|
+
protected $isDirty: boolean
|
|
14
|
+
constructor(getter: (prev?: T) => T, displayName?: string) {
|
|
15
|
+
super(void 0 as T, displayName)
|
|
16
|
+
this.$getter = getter
|
|
17
|
+
this.$unsubs = new Map()
|
|
18
|
+
this.$isDirty = true
|
|
19
|
+
|
|
20
|
+
if (__DEV__) {
|
|
21
|
+
const inject = this[$HMR_ACCEPT]!.inject!
|
|
22
|
+
// @ts-expect-error this is fine 😅
|
|
23
|
+
this[$HMR_ACCEPT] = {
|
|
24
|
+
provide: () => {
|
|
25
|
+
return this
|
|
26
|
+
},
|
|
27
|
+
inject: (prev) => {
|
|
28
|
+
inject(prev)
|
|
29
|
+
ComputedSignal.stop(prev)
|
|
30
|
+
this.$isDirty = prev.$isDirty
|
|
31
|
+
},
|
|
32
|
+
destroy: () => {},
|
|
33
|
+
} satisfies HMRAccept<ComputedSignal<T>>
|
|
34
|
+
}
|
|
35
|
+
Signal.configure(this, () => {
|
|
36
|
+
if (!this.$isDirty) return
|
|
37
|
+
if (__DEV__) {
|
|
38
|
+
/**
|
|
39
|
+
* This is a safeguard for dev-mode only, where a 'read' on an
|
|
40
|
+
* already-disposed signal during HMR update => `dom.setSignalProp`
|
|
41
|
+
* would throw due to invalid subs-map access.
|
|
42
|
+
*
|
|
43
|
+
* Perhaps in future we could handle this better by carrying over
|
|
44
|
+
* the previous signal's ID and not disposing it / deleting the
|
|
45
|
+
* map entry.
|
|
46
|
+
*/
|
|
47
|
+
if (this.$isDisposed) return
|
|
48
|
+
}
|
|
49
|
+
ComputedSignal.run(this)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get value() {
|
|
54
|
+
return super.value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// @ts-expect-error
|
|
58
|
+
set value(next: T) {}
|
|
59
|
+
|
|
60
|
+
subscribe(cb: (state: T) => void): () => void {
|
|
61
|
+
if (this.$isDirty) {
|
|
62
|
+
ComputedSignal.run(this)
|
|
63
|
+
}
|
|
64
|
+
return super.subscribe(cb)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static dispose(signal: ComputedSignal<any>): void {
|
|
68
|
+
ComputedSignal.stop(signal)
|
|
69
|
+
Signal.dispose(signal)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private static stop<T>(computed: ComputedSignal<T>) {
|
|
73
|
+
const { $id, $unsubs } = latest(computed)
|
|
74
|
+
|
|
75
|
+
effectQueue.delete($id)
|
|
76
|
+
$unsubs.forEach((unsub) => unsub())
|
|
77
|
+
$unsubs.clear()
|
|
78
|
+
computed.$isDirty = true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private static run<T>(computed: ComputedSignal<T>) {
|
|
82
|
+
const $computed = latest(computed)
|
|
83
|
+
const { $id: id, $getter, $unsubs: subs } = $computed
|
|
84
|
+
|
|
85
|
+
const value = executeWithTracking({
|
|
86
|
+
id,
|
|
87
|
+
subs,
|
|
88
|
+
fn: () => $getter($computed.$value),
|
|
89
|
+
onDepChanged: () => {
|
|
90
|
+
$computed.$isDirty = true
|
|
91
|
+
if (!signalSubsMap?.get(id)?.size) return
|
|
92
|
+
ComputedSignal.run($computed)
|
|
93
|
+
$computed.notify()
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
$computed.sneak(value)
|
|
97
|
+
$computed.$isDirty = false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const computed = <T>(
|
|
102
|
+
getter: (prev?: T) => T,
|
|
103
|
+
displayName?: string
|
|
104
|
+
): ComputedSignal<T> => {
|
|
105
|
+
return new ComputedSignal(getter, displayName)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const useComputed = <T>(
|
|
109
|
+
getter: (prev?: T) => T,
|
|
110
|
+
displayName?: string
|
|
111
|
+
) => {
|
|
112
|
+
return useHook(
|
|
113
|
+
"useComputedSignal",
|
|
114
|
+
{ signal: null! as ComputedSignal<T> },
|
|
115
|
+
({ hook, isInit, isHMR }) => {
|
|
116
|
+
if (__DEV__) {
|
|
117
|
+
hook.dev = {
|
|
118
|
+
devtools: {
|
|
119
|
+
get: () => ({
|
|
120
|
+
displayName: hook.signal.displayName,
|
|
121
|
+
value: hook.signal.peek(),
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
if (isHMR) {
|
|
126
|
+
// useComputed is always considered side-effecty, so we need to re-run
|
|
127
|
+
hook.cleanup?.()
|
|
128
|
+
isInit = true
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (isInit) {
|
|
132
|
+
hook.cleanup = () => ComputedSignal.dispose(hook.signal)
|
|
133
|
+
hook.signal = computed(getter, displayName)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return hook.signal
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { node } from "../globals.js"
|
|
2
|
+
import { sideEffectsEnabled } from "../utils.js"
|
|
3
|
+
import { tracking, effectQueue } from "./globals.js"
|
|
4
|
+
import type { Signal } from "./base.js"
|
|
5
|
+
import type { SignalValues } from "./types.js"
|
|
6
|
+
|
|
7
|
+
type TrackedExecutionContext<T, Deps extends readonly Signal<unknown>[]> = {
|
|
8
|
+
id: string
|
|
9
|
+
subs: Map<string, Function>
|
|
10
|
+
fn: (...values: SignalValues<Deps>) => T
|
|
11
|
+
deps?: Deps
|
|
12
|
+
onDepChanged: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Executes an effect function with dependency tracking enabled, and manages
|
|
17
|
+
* the effect's subscriptions.
|
|
18
|
+
* @param ctx - The execution context
|
|
19
|
+
* @returns The result of the effect function
|
|
20
|
+
*/
|
|
21
|
+
export function executeWithTracking<T, Deps extends readonly Signal<unknown>[]>(
|
|
22
|
+
ctx: TrackedExecutionContext<T, Deps>
|
|
23
|
+
): T {
|
|
24
|
+
const { id, subs, fn, deps = [], onDepChanged } = ctx
|
|
25
|
+
let observations: Map<string, Signal<unknown>> | undefined
|
|
26
|
+
|
|
27
|
+
effectQueue.delete(id)
|
|
28
|
+
const isServer = !!node.current && !sideEffectsEnabled()
|
|
29
|
+
|
|
30
|
+
if (!isServer) {
|
|
31
|
+
observations = new Map<string, Signal<unknown>>()
|
|
32
|
+
tracking.stack.push(observations)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = fn(...(deps.map((s) => s.value) as SignalValues<Deps>))
|
|
36
|
+
|
|
37
|
+
if (!isServer) {
|
|
38
|
+
for (const [id, unsub] of subs) {
|
|
39
|
+
if (observations!.has(id)) continue
|
|
40
|
+
unsub()
|
|
41
|
+
subs.delete(id)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const effect = () => {
|
|
45
|
+
if (!effectQueue.has(id)) {
|
|
46
|
+
queueMicrotask(() => effectQueue.get(id)?.())
|
|
47
|
+
}
|
|
48
|
+
effectQueue.set(id, onDepChanged)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [id, sig] of observations!) {
|
|
52
|
+
if (subs.has(id)) continue
|
|
53
|
+
const unsub = sig.subscribe(effect)
|
|
54
|
+
subs.set(id, unsub)
|
|
55
|
+
}
|
|
56
|
+
tracking.stack.pop()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Signal } from "./base.js"
|
|
2
|
+
import type { SignalSubscriber } from "./types.js"
|
|
3
|
+
|
|
4
|
+
export const tracking = {
|
|
5
|
+
stack: new Array<Map<string, Signal<unknown>>>(),
|
|
6
|
+
current: function (): Map<string, Signal<unknown>> | undefined {
|
|
7
|
+
return this.stack[this.stack.length - 1]
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
export const effectQueue = new Map<string, Function>()
|
|
11
|
+
export const signalSubsMap: Map<string, Set<SignalSubscriber>> = new Map()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISSUES:
|
|
3
|
+
* 1. signal subscription when mixing local and global signals get reversed in hmr for ONLY the first update after HMR is dom
|
|
4
|
+
* 2. global computed will lose its vNode subscription on HMR
|
|
5
|
+
* */
|
|
6
|
+
|
|
7
|
+
export { Signal, signal, useSignal } from "./base.js"
|
|
8
|
+
export { ComputedSignal, computed, useComputed } from "./computed.js"
|
|
9
|
+
export { WatchEffect, watch, useWatch } from "./watch.js"
|
|
10
|
+
export { unwrap, tick } from "./utils.js"
|
|
11
|
+
export * from "./jsx.js"
|
|
12
|
+
export * from "./types.js"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Signal } from "./base.js"
|
|
2
|
+
import type { SignalValues } from "./types.js"
|
|
3
|
+
|
|
4
|
+
type ForProps<T extends Signal<any[]>> = {
|
|
5
|
+
each: T
|
|
6
|
+
fallback?: JSX.Element
|
|
7
|
+
children: (
|
|
8
|
+
value: T extends Signal<infer U>
|
|
9
|
+
? U extends Array<infer V>
|
|
10
|
+
? V
|
|
11
|
+
: never
|
|
12
|
+
: never,
|
|
13
|
+
index: number
|
|
14
|
+
) => JSX.Element
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function For<T extends Signal<any[]>>({
|
|
18
|
+
each,
|
|
19
|
+
fallback,
|
|
20
|
+
children,
|
|
21
|
+
}: ForProps<T>) {
|
|
22
|
+
const items = each.value
|
|
23
|
+
if (items.length === 0) return fallback
|
|
24
|
+
return items.map((v, i) => children(v, i))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type DeriveChildrenArgs<T extends Signal<any> | Signal<any>[]> =
|
|
28
|
+
T extends Signal<any>[]
|
|
29
|
+
? SignalValues<T>
|
|
30
|
+
: [T extends Signal<infer V> ? V : never]
|
|
31
|
+
|
|
32
|
+
export type DeriveProps<T extends Signal<any> | Signal<any>[]> = {
|
|
33
|
+
from: T
|
|
34
|
+
children: (...values: DeriveChildrenArgs<T>) => JSX.Children
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Derive<const T extends Signal<any> | Signal<any>[]>({
|
|
38
|
+
from,
|
|
39
|
+
children,
|
|
40
|
+
}: DeriveProps<T>) {
|
|
41
|
+
const args = (Array.isArray(from) ? from : [from]).map(
|
|
42
|
+
(s) => s.value
|
|
43
|
+
) as DeriveChildrenArgs<T>
|
|
44
|
+
return children(...args)
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Signal } from "./base"
|
|
2
|
+
|
|
3
|
+
export type ReadonlySignal<T> = Signal<T> & {
|
|
4
|
+
readonly value: T
|
|
5
|
+
}
|
|
6
|
+
export type SignalSubscriber = Kaioken.VNode | Function
|
|
7
|
+
|
|
8
|
+
export type SignalValues<T extends readonly Signal<unknown>[]> = {
|
|
9
|
+
[I in keyof T]: T[I] extends Signal<infer V> ? V : never
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Signal } from "./base.js"
|
|
2
|
+
import { effectQueue } from "./globals.js"
|
|
3
|
+
|
|
4
|
+
export function unwrap<T>(value: T | Signal<T>, reactive = false): T {
|
|
5
|
+
if (!Signal.isSignal(value)) return value
|
|
6
|
+
return reactive ? value.value : value.peek()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const tick = () => {
|
|
10
|
+
effectQueue.forEach((fn) => fn())
|
|
11
|
+
effectQueue.clear()
|
|
12
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { __DEV__ } from "../env.js"
|
|
2
|
+
import type { HMRAccept } from "../hmr.js"
|
|
3
|
+
import { $HMR_ACCEPT } from "../constants.js"
|
|
4
|
+
import { sideEffectsEnabled, useHook } from "../hooks/utils.js"
|
|
5
|
+
import { effectQueue } from "./globals.js"
|
|
6
|
+
import { generateRandomID } from "../generateId.js"
|
|
7
|
+
import { executeWithTracking } from "./effect.js"
|
|
8
|
+
import { latest } from "../utils.js"
|
|
9
|
+
import type { Signal } from "./base.js"
|
|
10
|
+
import type { SignalValues } from "./types.js"
|
|
11
|
+
|
|
12
|
+
type WatchCallbackReturn = (() => void) | void
|
|
13
|
+
|
|
14
|
+
export class WatchEffect<const Deps extends readonly Signal<unknown>[] = []> {
|
|
15
|
+
protected id: string
|
|
16
|
+
protected getter: (...values: SignalValues<Deps>) => WatchCallbackReturn
|
|
17
|
+
protected deps?: Deps
|
|
18
|
+
protected unsubs: Map<string, Function>
|
|
19
|
+
protected cleanup: (() => void) | null
|
|
20
|
+
protected isRunning?: boolean
|
|
21
|
+
protected [$HMR_ACCEPT]?: HMRAccept<WatchEffect<Deps>>
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
getter: (...values: SignalValues<Deps>) => WatchCallbackReturn,
|
|
25
|
+
deps?: Deps
|
|
26
|
+
) {
|
|
27
|
+
this.id = generateRandomID()
|
|
28
|
+
this.getter = getter
|
|
29
|
+
this.deps = deps
|
|
30
|
+
this.unsubs = new Map()
|
|
31
|
+
this.isRunning = false
|
|
32
|
+
this.cleanup = null
|
|
33
|
+
if (__DEV__) {
|
|
34
|
+
this[$HMR_ACCEPT] = {
|
|
35
|
+
provide: () => this,
|
|
36
|
+
inject: (prev) => {
|
|
37
|
+
if (prev.isRunning) return
|
|
38
|
+
this.stop()
|
|
39
|
+
},
|
|
40
|
+
destroy: () => {
|
|
41
|
+
this.stop()
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
if ("window" in globalThis) {
|
|
45
|
+
const signals = window.__kaioken!.HMRContext!.signals
|
|
46
|
+
if (signals.isWaitingForNextWatchCall()) {
|
|
47
|
+
signals.pushWatch(this as WatchEffect)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
this.start()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
start() {
|
|
55
|
+
if (this.isRunning) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.isRunning = true
|
|
60
|
+
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
// postpone execution during HMR
|
|
63
|
+
if (
|
|
64
|
+
"window" in globalThis &&
|
|
65
|
+
window.__kaioken?.HMRContext?.isReplacement()
|
|
66
|
+
) {
|
|
67
|
+
return queueMicrotask(() => {
|
|
68
|
+
if (this.isRunning) {
|
|
69
|
+
WatchEffect.run(this as WatchEffect)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
WatchEffect.run(this as WatchEffect)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
stop() {
|
|
78
|
+
effectQueue.delete(this.id)
|
|
79
|
+
this.unsubs.forEach((fn) => fn())
|
|
80
|
+
this.unsubs.clear()
|
|
81
|
+
this.cleanup?.()
|
|
82
|
+
this.cleanup = null
|
|
83
|
+
this.isRunning = false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static run(watchEffect: WatchEffect) {
|
|
87
|
+
const effect = latest(watchEffect)
|
|
88
|
+
const { id, getter, unsubs: subs, deps } = effect
|
|
89
|
+
|
|
90
|
+
effect.cleanup =
|
|
91
|
+
executeWithTracking({
|
|
92
|
+
id,
|
|
93
|
+
subs,
|
|
94
|
+
fn: getter,
|
|
95
|
+
deps,
|
|
96
|
+
onDepChanged: () => {
|
|
97
|
+
effect.cleanup?.()
|
|
98
|
+
WatchEffect.run(effect)
|
|
99
|
+
},
|
|
100
|
+
}) ?? null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function watch(getter: () => WatchCallbackReturn): WatchEffect
|
|
105
|
+
export function watch<const Deps extends readonly Signal<unknown>[]>(
|
|
106
|
+
dependencies: Deps,
|
|
107
|
+
getter: (...values: SignalValues<Deps>) => WatchCallbackReturn
|
|
108
|
+
): WatchEffect<Deps>
|
|
109
|
+
export function watch<const Deps extends readonly Signal<unknown>[]>(
|
|
110
|
+
depsOrGetter: Deps | (() => WatchCallbackReturn),
|
|
111
|
+
getter?: (...values: SignalValues<Deps>) => WatchCallbackReturn
|
|
112
|
+
): WatchEffect<Deps> | WatchEffect {
|
|
113
|
+
if (typeof depsOrGetter === "function") {
|
|
114
|
+
return new WatchEffect<[]>(depsOrGetter)
|
|
115
|
+
}
|
|
116
|
+
const dependencies = depsOrGetter
|
|
117
|
+
const effectGetter = getter!
|
|
118
|
+
return new WatchEffect(effectGetter, dependencies)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function useWatch(getter: () => WatchCallbackReturn): WatchEffect
|
|
122
|
+
export function useWatch<const Deps extends readonly Signal<unknown>[]>(
|
|
123
|
+
dependencies: Deps,
|
|
124
|
+
getter: (...values: SignalValues<Deps>) => WatchCallbackReturn
|
|
125
|
+
): WatchEffect<Deps> | undefined
|
|
126
|
+
|
|
127
|
+
export function useWatch<const Deps extends readonly Signal<unknown>[]>(
|
|
128
|
+
depsOrGetter: Deps | (() => WatchCallbackReturn),
|
|
129
|
+
getter?: (...values: SignalValues<Deps>) => WatchCallbackReturn
|
|
130
|
+
): WatchEffect<Deps> | WatchEffect | undefined {
|
|
131
|
+
if (!sideEffectsEnabled()) return
|
|
132
|
+
|
|
133
|
+
return useHook(
|
|
134
|
+
"useWatch",
|
|
135
|
+
{ watcher: null as any as WatchEffect<Deps> },
|
|
136
|
+
({ hook, isInit, isHMR }) => {
|
|
137
|
+
if (__DEV__) {
|
|
138
|
+
if (isHMR) {
|
|
139
|
+
hook.cleanup?.()
|
|
140
|
+
isInit = true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (isInit) {
|
|
144
|
+
const watcher = (hook.watcher = watch(depsOrGetter as Deps, getter!))
|
|
145
|
+
hook.cleanup = () => watcher.stop()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return hook.watcher
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
}
|