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.
Files changed (70) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +5 -0
  3. package/package.json +81 -0
  4. package/src/appContext.ts +186 -0
  5. package/src/cloneVNode.ts +14 -0
  6. package/src/constants.ts +146 -0
  7. package/src/context.ts +56 -0
  8. package/src/dom.ts +712 -0
  9. package/src/element.ts +54 -0
  10. package/src/env.ts +6 -0
  11. package/src/error.ts +85 -0
  12. package/src/flags.ts +15 -0
  13. package/src/form/index.ts +662 -0
  14. package/src/form/types.ts +261 -0
  15. package/src/form/utils.ts +19 -0
  16. package/src/generateId.ts +19 -0
  17. package/src/globalContext.ts +161 -0
  18. package/src/globals.ts +21 -0
  19. package/src/hmr.ts +178 -0
  20. package/src/hooks/index.ts +14 -0
  21. package/src/hooks/useAsync.ts +136 -0
  22. package/src/hooks/useCallback.ts +31 -0
  23. package/src/hooks/useContext.ts +79 -0
  24. package/src/hooks/useEffect.ts +44 -0
  25. package/src/hooks/useEffectEvent.ts +24 -0
  26. package/src/hooks/useId.ts +42 -0
  27. package/src/hooks/useLayoutEffect.ts +47 -0
  28. package/src/hooks/useMemo.ts +33 -0
  29. package/src/hooks/useReducer.ts +50 -0
  30. package/src/hooks/useRef.ts +40 -0
  31. package/src/hooks/useState.ts +62 -0
  32. package/src/hooks/useSyncExternalStore.ts +59 -0
  33. package/src/hooks/useViewTransition.ts +26 -0
  34. package/src/hooks/utils.ts +259 -0
  35. package/src/hydration.ts +67 -0
  36. package/src/index.ts +61 -0
  37. package/src/jsx.ts +11 -0
  38. package/src/lazy.ts +238 -0
  39. package/src/memo.ts +48 -0
  40. package/src/portal.ts +43 -0
  41. package/src/profiling.ts +105 -0
  42. package/src/props.ts +36 -0
  43. package/src/reconciler.ts +531 -0
  44. package/src/renderToString.ts +91 -0
  45. package/src/router/index.ts +2 -0
  46. package/src/router/route.ts +51 -0
  47. package/src/router/router.ts +275 -0
  48. package/src/router/routerUtils.ts +49 -0
  49. package/src/scheduler.ts +522 -0
  50. package/src/signals/base.ts +237 -0
  51. package/src/signals/computed.ts +139 -0
  52. package/src/signals/effect.ts +60 -0
  53. package/src/signals/globals.ts +11 -0
  54. package/src/signals/index.ts +12 -0
  55. package/src/signals/jsx.ts +45 -0
  56. package/src/signals/types.ts +10 -0
  57. package/src/signals/utils.ts +12 -0
  58. package/src/signals/watch.ts +151 -0
  59. package/src/ssr/client.ts +29 -0
  60. package/src/ssr/hydrationBoundary.ts +63 -0
  61. package/src/ssr/index.ts +1 -0
  62. package/src/ssr/server.ts +124 -0
  63. package/src/store.ts +241 -0
  64. package/src/swr.ts +360 -0
  65. package/src/transition.ts +80 -0
  66. package/src/types.dom.ts +1250 -0
  67. package/src/types.ts +209 -0
  68. package/src/types.utils.ts +39 -0
  69. package/src/utils.ts +581 -0
  70. 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
+ }