kiru 0.45.3 → 0.46.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.
Files changed (119) hide show
  1. package/dist/appContext.d.ts +0 -13
  2. package/dist/appContext.d.ts.map +1 -1
  3. package/dist/appContext.js +15 -55
  4. package/dist/appContext.js.map +1 -1
  5. package/dist/constants.d.ts +5 -7
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +5 -7
  8. package/dist/constants.js.map +1 -1
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/context.js +2 -4
  11. package/dist/context.js.map +1 -1
  12. package/dist/dom.d.ts +2 -2
  13. package/dist/dom.d.ts.map +1 -1
  14. package/dist/dom.js +36 -32
  15. package/dist/dom.js.map +1 -1
  16. package/dist/globals.d.ts +1 -6
  17. package/dist/globals.d.ts.map +1 -1
  18. package/dist/globals.js +1 -5
  19. package/dist/globals.js.map +1 -1
  20. package/dist/hmr.d.ts +1 -1
  21. package/dist/hmr.d.ts.map +1 -1
  22. package/dist/hmr.js +2 -4
  23. package/dist/hmr.js.map +1 -1
  24. package/dist/hooks/useViewTransition.d.ts.map +1 -1
  25. package/dist/hooks/useViewTransition.js +3 -3
  26. package/dist/hooks/useViewTransition.js.map +1 -1
  27. package/dist/hooks/utils.d.ts +1 -5
  28. package/dist/hooks/utils.d.ts.map +1 -1
  29. package/dist/hooks/utils.js +8 -20
  30. package/dist/hooks/utils.js.map +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/lazy.d.ts.map +1 -1
  36. package/dist/lazy.js +4 -4
  37. package/dist/lazy.js.map +1 -1
  38. package/dist/portal.d.ts.map +1 -1
  39. package/dist/portal.js +2 -3
  40. package/dist/portal.js.map +1 -1
  41. package/dist/props.js +1 -1
  42. package/dist/props.js.map +1 -1
  43. package/dist/reconciler.d.ts.map +1 -1
  44. package/dist/reconciler.js +105 -79
  45. package/dist/reconciler.js.map +1 -1
  46. package/dist/renderToString.d.ts.map +1 -1
  47. package/dist/renderToString.js +5 -8
  48. package/dist/renderToString.js.map +1 -1
  49. package/dist/router/router.d.ts.map +1 -1
  50. package/dist/router/router.js +5 -6
  51. package/dist/router/router.js.map +1 -1
  52. package/dist/scheduler.d.ts +4 -11
  53. package/dist/scheduler.d.ts.map +1 -1
  54. package/dist/scheduler.js +365 -390
  55. package/dist/scheduler.js.map +1 -1
  56. package/dist/signals/base.d.ts +4 -4
  57. package/dist/signals/base.d.ts.map +1 -1
  58. package/dist/signals/base.js +38 -24
  59. package/dist/signals/base.js.map +1 -1
  60. package/dist/signals/computed.d.ts +2 -2
  61. package/dist/signals/computed.d.ts.map +1 -1
  62. package/dist/signals/computed.js +11 -3
  63. package/dist/signals/computed.js.map +1 -1
  64. package/dist/signals/effect.d.ts +1 -1
  65. package/dist/signals/effect.d.ts.map +1 -1
  66. package/dist/signals/globals.d.ts +2 -2
  67. package/dist/signals/globals.d.ts.map +1 -1
  68. package/dist/signals/globals.js.map +1 -1
  69. package/dist/signals/jsx.d.ts +4 -3
  70. package/dist/signals/jsx.d.ts.map +1 -1
  71. package/dist/signals/jsx.js +1 -1
  72. package/dist/signals/jsx.js.map +1 -1
  73. package/dist/signals/types.d.ts +2 -2
  74. package/dist/signals/types.d.ts.map +1 -1
  75. package/dist/signals/utils.d.ts +1 -1
  76. package/dist/signals/utils.d.ts.map +1 -1
  77. package/dist/signals/utils.js +1 -1
  78. package/dist/signals/utils.js.map +1 -1
  79. package/dist/ssr/server.d.ts.map +1 -1
  80. package/dist/ssr/server.js +1 -5
  81. package/dist/ssr/server.js.map +1 -1
  82. package/dist/types.d.ts +9 -2
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/utils.d.ts +1 -1
  85. package/dist/utils.d.ts.map +1 -1
  86. package/dist/utils.js +17 -17
  87. package/dist/utils.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/appContext.ts +21 -67
  90. package/src/constants.ts +8 -7
  91. package/src/context.ts +2 -4
  92. package/src/dom.ts +50 -29
  93. package/src/globals.ts +1 -9
  94. package/src/hmr.ts +3 -5
  95. package/src/hooks/useViewTransition.ts +3 -3
  96. package/src/hooks/utils.ts +7 -22
  97. package/src/index.ts +3 -3
  98. package/src/lazy.ts +4 -4
  99. package/src/portal.ts +2 -3
  100. package/src/props.ts +1 -1
  101. package/src/reconciler.ts +116 -90
  102. package/src/renderToString.ts +5 -8
  103. package/src/router/router.ts +4 -6
  104. package/src/scheduler.ts +369 -407
  105. package/src/signals/base.ts +40 -33
  106. package/src/signals/computed.ts +8 -3
  107. package/src/signals/effect.ts +1 -1
  108. package/src/signals/globals.ts +2 -2
  109. package/src/signals/jsx.ts +12 -11
  110. package/src/signals/types.ts +2 -2
  111. package/src/signals/utils.ts +1 -1
  112. package/src/ssr/server.ts +1 -5
  113. package/src/types.ts +9 -2
  114. package/src/utils.ts +26 -21
  115. package/dist/flags.d.ts +0 -6
  116. package/dist/flags.d.ts.map +0 -1
  117. package/dist/flags.js +0 -16
  118. package/dist/flags.js.map +0 -1
  119. package/src/flags.ts +0 -15
package/src/scheduler.ts CHANGED
@@ -1,19 +1,17 @@
1
- import type { AppContext } from "./appContext"
2
1
  import type {
3
2
  ContextProviderNode,
4
3
  DomVNode,
5
4
  FunctionVNode,
6
5
  } from "./types.utils"
7
- import { flags } from "./flags.js"
8
6
  import {
9
7
  $CONTEXT_PROVIDER,
10
8
  CONSECUTIVE_DIRTY_LIMIT,
11
- FLAG,
9
+ FLAG_DELETION,
12
10
  } from "./constants.js"
13
- import { commitWork, createDom, hydrateDom } from "./dom.js"
11
+ import { commitDeletion, commitWork, createDom, hydrateDom } from "./dom.js"
14
12
  import { __DEV__ } from "./env.js"
15
13
  import { KiruError } from "./error.js"
16
- import { ctx, hookIndex, node, nodeToCtxMap, renderMode } from "./globals.js"
14
+ import { hookIndex, node, renderMode } from "./globals.js"
17
15
  import { hydrationStack } from "./hydration.js"
18
16
  import { assertValidElementProps } from "./props.js"
19
17
  import { reconcileChildren } from "./reconciler.js"
@@ -23,500 +21,464 @@ import {
23
21
  traverseApply,
24
22
  vNodeContains,
25
23
  isExoticType,
24
+ getVNodeAppContext,
26
25
  } from "./utils.js"
27
- import { Signal } from "./signals/base.js"
26
+ import type { AppContext } from "./appContext"
28
27
 
29
28
  type VNode = Kiru.VNode
30
29
 
31
- export interface Scheduler {
32
- clear(): void
33
- wake(): void
34
- sleep(): void
35
- nextIdle(fn: (scheduler: Scheduler) => void, wakeUpIfIdle?: boolean): void
36
- flushSync(): void
37
- queueUpdate(vNode: VNode): void
38
- queueDelete(vNode: VNode): void
30
+ let appCtx: AppContext | null
31
+ let nextUnitOfWork: VNode | null = null
32
+ let treesInProgress: VNode[] = []
33
+ let currentTreeIndex = 0
34
+ let isRunning = false
35
+ let nextIdleEffects: (() => void)[] = []
36
+ let deletions: VNode[] = []
37
+ let isImmediateEffectsMode = false
38
+ let immediateEffectDirtiedRender = false
39
+ let isRenderDirtied = false
40
+ let consecutiveDirtyCount = 0
41
+ let pendingContextChanges = new Set<ContextProviderNode<any>>()
42
+ let preEffects: Array<Function> = []
43
+ let postEffects: Array<Function> = []
44
+
45
+ export function nextIdle(fn: () => void, wakeUpIfIdle = true) {
46
+ nextIdleEffects.push(fn)
47
+ if (wakeUpIfIdle) wake()
39
48
  }
40
49
 
41
- export function createScheduler(
42
- appCtx: AppContext<any>,
43
- maxFrameMs = 50
44
- ): Scheduler {
45
- let nextUnitOfWork: VNode | null = null
46
- let treesInProgress: VNode[] = []
47
- let currentTreeIndex = 0
48
- let isRunning = false
49
- let nextIdleEffects: ((scheduler: Scheduler) => void)[] = []
50
- let deletions: VNode[] = []
51
- let frameDeadline = 0
52
- let pendingCallback: IdleRequestCallback | null = null
53
- let frameHandle: number | null = null
54
- let isImmediateEffectsMode = false
55
- let immediateEffectDirtiedRender = false
56
- let isRenderDirtied = false
57
- let consecutiveDirtyCount = 0
58
- let pendingContextChanges = new Set<ContextProviderNode<any>>()
59
- let effectCallbacks = {
60
- pre: [] as Function[],
61
- post: [] as Function[],
62
- }
63
- let scheduler: Scheduler
50
+ export function flushSync() {
51
+ workLoop()
52
+ }
64
53
 
65
- const timeRemaining = () => frameDeadline - window.performance.now()
66
- const deadline = {
67
- didTimeout: false,
68
- timeRemaining,
54
+ export function requestUpdate(vNode: VNode): void {
55
+ if (vNode.flags & FLAG_DELETION) return
56
+ if (__DEV__) {
57
+ // if (options?.debug?.onRequestUpdate) {
58
+ // options.debug.onRequestUpdate(vNode)
59
+ // }
69
60
  }
70
- const channel = new MessageChannel()
71
- channel.port2.onmessage = () => {
72
- if (typeof pendingCallback === "function") {
73
- pendingCallback(deadline)
74
- }
61
+ if (renderMode.current === "hydrate") {
62
+ return nextIdle(() => {
63
+ vNode.flags & FLAG_DELETION || queueUpdate(vNode)
64
+ })
75
65
  }
66
+ queueUpdate(vNode)
67
+ }
76
68
 
77
- function clear() {
78
- nextUnitOfWork = null
79
- treesInProgress = []
80
- currentTreeIndex = 0
81
- nextIdleEffects = []
82
- deletions = []
83
- effectCallbacks = { pre: [], post: [] }
84
- frameDeadline = 0
85
- pendingCallback = null
86
- sleep()
69
+ export function requestDelete(vNode: VNode): void {
70
+ if (vNode.flags & FLAG_DELETION) return
71
+ if (renderMode.current === "hydrate") {
72
+ return nextIdle(() => {
73
+ vNode.flags & FLAG_DELETION || queueDelete(vNode)
74
+ })
87
75
  }
76
+ queueDelete(vNode)
77
+ }
88
78
 
89
- function wake() {
90
- if (isRunning) return
91
- isRunning = true
92
- requestIdleCallback(workLoop)
79
+ function queueWorkLoop() {
80
+ queueMicrotask(workLoop)
81
+ }
82
+
83
+ function wake() {
84
+ if (isRunning) return
85
+ isRunning = true
86
+ queueWorkLoop()
87
+ }
88
+
89
+ function sleep() {
90
+ isRunning = false
91
+ }
92
+
93
+ function queueUpdate(vNode: VNode) {
94
+ // In immediate effect mode (useLayoutEffect), immediately mark the render as dirty
95
+ if (isImmediateEffectsMode) {
96
+ immediateEffectDirtiedRender = true
93
97
  }
94
98
 
95
- function sleep() {
96
- isRunning = false
97
- if (frameHandle !== null) {
98
- globalThis.cancelAnimationFrame(frameHandle)
99
- frameHandle = null
99
+ // If this node is currently being rendered, just mark it dirty
100
+ if (node.current === vNode) {
101
+ if (__DEV__) {
102
+ window.__kiru?.profilingContext?.emit("updateDirtied", appCtx!)
100
103
  }
104
+ isRenderDirtied = true
105
+ return
101
106
  }
102
107
 
103
- function nextIdle(fn: (scheduler: Scheduler) => void, wakeUpIfIdle = true) {
104
- nextIdleEffects.push(fn)
105
- if (wakeUpIfIdle) wake()
108
+ // If it's already the next unit of work, no need to queue again
109
+ if (nextUnitOfWork === vNode) {
110
+ return
106
111
  }
107
112
 
108
- function flushSync() {
109
- if (frameHandle !== null) {
110
- globalThis.cancelAnimationFrame(frameHandle)
111
- frameHandle = null
112
- }
113
- workLoop()
113
+ if (nextUnitOfWork === null) {
114
+ treesInProgress.push(vNode)
115
+ nextUnitOfWork = vNode
116
+ return wake()
114
117
  }
115
118
 
116
- function queueUpdate(vNode: VNode) {
117
- // In immediate effect mode (useLayoutEffect), immediately mark the render as dirty
118
- if (isImmediateEffectsMode) {
119
- immediateEffectDirtiedRender = true
120
- }
121
-
122
- // If this node is currently being rendered, just mark it dirty
123
- if (node.current === vNode) {
124
- if (__DEV__) {
125
- window.__kiru?.profilingContext?.emit("updateDirtied", appCtx)
126
- }
127
- isRenderDirtied = true
128
- return
119
+ for (let i = 0; i < treesInProgress.length; i++) {
120
+ const tree = treesInProgress[i]
121
+ if (tree !== vNode) continue
122
+ if (i < currentTreeIndex) {
123
+ // It was already processed; requeue it to the end
124
+ currentTreeIndex--
125
+ treesInProgress.splice(i, 1)
126
+ treesInProgress.push(tree)
129
127
  }
128
+ return
129
+ }
130
130
 
131
- // If it's already the next unit of work, no need to queue again
132
- if (nextUnitOfWork === vNode) {
133
- return
134
- }
131
+ // Check if this node is a descendant of any trees already queued
132
+ for (let i = 0; i < treesInProgress.length; i++) {
133
+ const tree = treesInProgress[i]
134
+ if (!vNodeContains(tree, vNode)) continue
135
135
 
136
- if (nextUnitOfWork === null) {
137
- treesInProgress.push(vNode)
136
+ if (i === currentTreeIndex) {
137
+ // It's a child of the currently worked-on tree
138
+ // If it's deeper within the same tree, we can skip
139
+ if (vNodeContains(nextUnitOfWork, vNode)) return
140
+ // If it's not in the current work subtree, move back up to it
138
141
  nextUnitOfWork = vNode
139
- return wake()
140
- }
141
-
142
- // Check if the node is already in the treesInProgress queue
143
- const treeIdx = treesInProgress.indexOf(vNode)
144
- if (treeIdx !== -1) {
145
- if (treeIdx === currentTreeIndex) {
146
- // Replace current node if it's being worked on now
147
- treesInProgress[treeIdx] = vNode
148
- nextUnitOfWork = vNode
149
- } else if (treeIdx < currentTreeIndex) {
150
- // It was already processed; requeue it to the end
151
- currentTreeIndex--
152
- treesInProgress.splice(treeIdx, 1)
153
- treesInProgress.push(vNode)
154
- }
155
- return
142
+ } else if (i < currentTreeIndex) {
143
+ // It's a descendant of an already processed tree; treat as a new update
144
+ treesInProgress.push(vNode)
156
145
  }
157
146
 
158
- const nodeDepth = vNode.depth
159
-
160
- // Check if this node is a descendant of any trees already queued
161
- for (let i = 0; i < treesInProgress.length; i++) {
162
- const tree = treesInProgress[i]
163
- if (tree.depth > nodeDepth) continue // Can't be an ancestor
164
- if (!vNodeContains(tree, vNode)) continue
165
-
166
- if (i === currentTreeIndex) {
167
- // It's a child of the currently worked-on tree
168
- // If it's deeper within the same tree, we can skip
169
- if (vNodeContains(nextUnitOfWork, vNode)) return
170
- // If it's not in the current work subtree, move back up to it
171
- nextUnitOfWork = vNode
172
- } else if (i < currentTreeIndex) {
173
- // It's a descendant of an already processed tree; treat as a new update
174
- treesInProgress.push(vNode)
175
- }
147
+ return
148
+ }
176
149
 
177
- return
150
+ // Check if this node contains any of the currently queued trees
151
+ let didReplaceTree = false
152
+ let shouldQueueAtEnd = false
153
+ for (let i = 0; i < treesInProgress.length; ) {
154
+ const tree = treesInProgress[i]
155
+ if (!vNodeContains(vNode, tree)) {
156
+ i++
157
+ continue
178
158
  }
159
+ // This node contains another update root, replace it
179
160
 
180
- // Check if this node contains any of the currently queued trees
181
- let didReplaceTree = false
182
- let shouldQueueAtEnd = false
183
- for (let i = 0; i < treesInProgress.length; ) {
184
- const tree = treesInProgress[i]
185
- if (tree.depth < nodeDepth || !vNodeContains(vNode, tree)) {
186
- i++
187
- continue
188
- }
189
- // This node contains another update root, replace it
190
-
191
- if (i === currentTreeIndex) {
192
- if (!didReplaceTree) {
193
- treesInProgress.splice(i, 1, vNode)
194
- nextUnitOfWork = vNode
195
- didReplaceTree = true
196
- i++ // advance past replaced node
197
- } else {
198
- treesInProgress.splice(i, 1)
199
- // no increment
200
- }
201
- } else if (i < currentTreeIndex) {
202
- currentTreeIndex--
203
- treesInProgress.splice(i, 1)
204
- if (!didReplaceTree) {
205
- shouldQueueAtEnd = true
206
- didReplaceTree = true
207
- }
208
- // no increment
161
+ if (i === currentTreeIndex) {
162
+ if (!didReplaceTree) {
163
+ treesInProgress.splice(i, 1, vNode)
164
+ nextUnitOfWork = vNode
165
+ didReplaceTree = true
166
+ i++ // advance past replaced node
209
167
  } else {
210
- // i > currentTreeIndex
211
168
  treesInProgress.splice(i, 1)
212
- if (!didReplaceTree) {
213
- shouldQueueAtEnd = true
214
- didReplaceTree = true
215
- }
216
169
  // no increment
217
170
  }
171
+ } else if (i < currentTreeIndex) {
172
+ currentTreeIndex--
173
+ treesInProgress.splice(i, 1)
174
+ if (!didReplaceTree) {
175
+ shouldQueueAtEnd = true
176
+ didReplaceTree = true
177
+ }
178
+ // no increment
179
+ } else {
180
+ // i > currentTreeIndex
181
+ treesInProgress.splice(i, 1)
182
+ if (!didReplaceTree) {
183
+ shouldQueueAtEnd = true
184
+ didReplaceTree = true
185
+ }
186
+ // no increment
218
187
  }
219
- if (!shouldQueueAtEnd && didReplaceTree) {
220
- return
221
- }
222
- // If it doesn't overlap with any queued tree, queue as new independent update root
223
- treesInProgress.push(vNode)
224
188
  }
189
+ if (!shouldQueueAtEnd && didReplaceTree) {
190
+ return
191
+ }
192
+ // If it doesn't overlap with any queued tree, queue as new independent update root
193
+ treesInProgress.push(vNode)
194
+ }
225
195
 
226
- function queueDelete(vNode: VNode) {
227
- traverseApply(vNode, (n) => (n.flags = flags.set(n.flags, FLAG.DELETION)))
228
- deletions.push(vNode)
196
+ function queueDelete(vNode: VNode) {
197
+ traverseApply(vNode, (n) => (n.flags |= FLAG_DELETION))
198
+ deletions.push(vNode)
199
+ }
200
+
201
+ function workLoop(): void {
202
+ if (__DEV__) {
203
+ const n = nextUnitOfWork ?? deletions[0] ?? treesInProgress[0]
204
+ if (n) {
205
+ appCtx = getVNodeAppContext(n)!
206
+ window.__kiru?.profilingContext?.beginTick(appCtx)
207
+ } else {
208
+ appCtx = null
209
+ }
229
210
  }
230
211
 
231
- function isFlushReady() {
232
- return !nextUnitOfWork && (deletions.length || treesInProgress.length)
212
+ while (nextUnitOfWork) {
213
+ nextUnitOfWork =
214
+ performUnitOfWork(nextUnitOfWork) ??
215
+ treesInProgress[++currentTreeIndex] ??
216
+ queueBlockedContextDependencyRoots()
233
217
  }
234
218
 
235
- function workLoop(deadline?: IdleDeadline): void {
236
- if (__DEV__) {
237
- window.__kiru?.profilingContext?.beginTick(appCtx)
219
+ if (!nextUnitOfWork && (deletions.length || treesInProgress.length)) {
220
+ while (deletions.length) {
221
+ commitDeletion(deletions.shift()!)
238
222
  }
239
- ctx.current = appCtx
240
- while (nextUnitOfWork) {
241
- nextUnitOfWork =
242
- performUnitOfWork(nextUnitOfWork) ??
243
- treesInProgress[++currentTreeIndex] ??
244
- queueBlockedContextDependencyRoots()
245
-
246
- if ((deadline?.timeRemaining() ?? 1) < 1) break
223
+ const workRoots = [...treesInProgress]
224
+ treesInProgress.length = 0
225
+ currentTreeIndex = 0
226
+ for (const root of workRoots) {
227
+ commitWork(root)
247
228
  }
248
229
 
249
- if (isFlushReady()) {
250
- while (deletions.length) {
251
- commitWork(deletions.shift()!)
252
- }
253
- const treesInProgressCopy = [...treesInProgress]
254
- treesInProgress = []
255
- currentTreeIndex = 0
256
- for (const tree of treesInProgressCopy) {
257
- commitWork(tree)
258
- }
259
-
260
- isImmediateEffectsMode = true
261
- flushEffects(effectCallbacks.pre)
262
- isImmediateEffectsMode = false
263
-
264
- if (immediateEffectDirtiedRender) {
265
- checkForTooManyConsecutiveDirtyRenders()
266
- flushEffects(effectCallbacks.post)
267
- immediateEffectDirtiedRender = false
268
- consecutiveDirtyCount++
269
- if (__DEV__) {
270
- window.__kiru?.profilingContext?.endTick(appCtx)
271
- window.__kiru?.profilingContext?.emit("updateDirtied", appCtx)
272
- }
273
- return workLoop()
274
- }
275
- consecutiveDirtyCount = 0
230
+ isImmediateEffectsMode = true
231
+ flushEffects(preEffects)
232
+ isImmediateEffectsMode = false
276
233
 
277
- flushEffects(effectCallbacks.post)
278
- window.__kiru!.emit("update", appCtx)
234
+ if (immediateEffectDirtiedRender) {
235
+ checkForTooManyConsecutiveDirtyRenders()
236
+ flushEffects(postEffects)
237
+ immediateEffectDirtiedRender = false
238
+ consecutiveDirtyCount++
279
239
  if (__DEV__) {
280
- window.__kiru?.profilingContext?.emit("update", appCtx)
240
+ window.__kiru?.profilingContext?.endTick(appCtx!)
241
+ window.__kiru?.profilingContext?.emit("updateDirtied", appCtx!)
281
242
  }
243
+ return flushSync()
282
244
  }
245
+ consecutiveDirtyCount = 0
283
246
 
284
- if (!nextUnitOfWork) {
285
- sleep()
286
- while (nextIdleEffects.length) {
287
- nextIdleEffects.shift()!(scheduler)
288
- }
289
- if (__DEV__) {
247
+ flushEffects(postEffects)
248
+ if (__DEV__) {
249
+ window.__kiru!.emit("update", appCtx!)
250
+ window.__kiru?.profilingContext?.emit("update", appCtx!)
251
+ }
252
+ }
253
+
254
+ if (!nextUnitOfWork) {
255
+ sleep()
256
+ while (nextIdleEffects.length) {
257
+ nextIdleEffects.shift()!()
258
+ }
259
+ if (__DEV__) {
260
+ if (appCtx) {
290
261
  window.__kiru?.profilingContext?.endTick(appCtx)
291
262
  }
292
- return
293
263
  }
294
-
295
- requestIdleCallback(workLoop)
264
+ return
296
265
  }
297
266
 
298
- function requestIdleCallback(callback: IdleRequestCallback) {
299
- frameHandle = globalThis.requestAnimationFrame((time) => {
300
- frameDeadline = time + maxFrameMs
301
- pendingCallback = callback
302
- channel.port1.postMessage(null)
303
- })
304
- }
267
+ queueWorkLoop()
268
+ }
305
269
 
306
- function queueBlockedContextDependencyRoots(): VNode | null {
307
- if (pendingContextChanges.size === 0) return null
308
-
309
- // TODO: it's possible that a 'job' created by this process is
310
- // blocked by a parent memo after a queueUpdate -> replaceTree action.
311
- // To prevent this, we might need to add these to a distinct queue.
312
- const jobRoots: VNode[] = []
313
- pendingContextChanges.forEach((provider) => {
314
- provider.props.dependents.forEach((dep) => {
315
- if (!willMemoBlockUpdate(provider, dep)) return
316
- const depDepth = dep.depth
317
- for (let i = 0; i < jobRoots.length; i++) {
318
- const root = jobRoots[i]
319
- const rootDepth = root.depth
320
- if (depDepth > rootDepth && vNodeContains(root, dep)) {
321
- if (willMemoBlockUpdate(root, dep)) {
322
- // root is a parent of dep and there's a memo between them, prevent consolidation and queue as new root
323
- break
324
- }
325
- return
326
- }
327
- if (depDepth < rootDepth && vNodeContains(dep, root)) {
328
- jobRoots[i] = dep
329
- return
270
+ function queueBlockedContextDependencyRoots(): VNode | null {
271
+ if (pendingContextChanges.size === 0) return null
272
+
273
+ // TODO: it's possible that a 'job' created by this process is
274
+ // blocked by a parent memo after a queueUpdate -> replaceTree action.
275
+ // To prevent this, we might need to add these to a distinct queue.
276
+ const jobRoots: VNode[] = []
277
+ pendingContextChanges.forEach((provider) => {
278
+ provider.props.dependents.forEach((dep) => {
279
+ if (!willMemoBlockUpdate(provider, dep)) return
280
+ for (let i = 0; i < jobRoots.length; i++) {
281
+ const root = jobRoots[i]
282
+ if (vNodeContains(root, dep)) {
283
+ if (willMemoBlockUpdate(root, dep)) {
284
+ // root is a parent of dep and there's a memo between them, prevent consolidation and queue as new root
285
+ break
330
286
  }
287
+ return
331
288
  }
332
- jobRoots.push(dep)
333
- })
289
+ if (vNodeContains(dep, root)) {
290
+ jobRoots[i] = dep
291
+ return
292
+ }
293
+ }
294
+ jobRoots.push(dep)
334
295
  })
296
+ })
335
297
 
336
- pendingContextChanges.clear()
337
- treesInProgress.push(...jobRoots)
338
- return jobRoots[0] ?? null
339
- }
298
+ pendingContextChanges.clear()
299
+ treesInProgress.push(...jobRoots)
300
+ return jobRoots[0] ?? null
301
+ }
340
302
 
341
- function performUnitOfWork(vNode: VNode): VNode | void {
342
- let renderChild = true
343
- try {
344
- const { props } = vNode
345
- if (typeof vNode.type === "string") {
346
- updateHostComponent(vNode as DomVNode)
347
- } else if (isExoticType(vNode.type)) {
348
- if (vNode.type === $CONTEXT_PROVIDER) {
349
- const asProvider = vNode as ContextProviderNode<any>
350
- const { dependents, value } = asProvider.props
351
- if (
352
- dependents.size &&
353
- asProvider.prev &&
354
- asProvider.prev.props.value !== value
355
- ) {
356
- pendingContextChanges.add(asProvider)
357
- }
303
+ function performUnitOfWork(vNode: VNode): VNode | void {
304
+ let renderChild = true
305
+ try {
306
+ const { props } = vNode
307
+ if (typeof vNode.type === "string") {
308
+ updateHostComponent(vNode as DomVNode)
309
+ } else if (isExoticType(vNode.type)) {
310
+ if (vNode.type === $CONTEXT_PROVIDER) {
311
+ const asProvider = vNode as ContextProviderNode<any>
312
+ const { dependents, value } = asProvider.props
313
+ if (
314
+ dependents.size &&
315
+ asProvider.prev &&
316
+ asProvider.prev.props.value !== value
317
+ ) {
318
+ pendingContextChanges.add(asProvider)
358
319
  }
359
- vNode.child = reconcileChildren(vNode, props.children)
360
- vNode.deletions?.forEach((d) => queueDelete(d))
361
- } else {
362
- renderChild = updateFunctionComponent(vNode as FunctionVNode)
363
320
  }
364
- } catch (error) {
321
+ vNode.child = reconcileChildren(vNode, props.children)
322
+ queueNodeChildDeletions(vNode)
323
+ } else {
324
+ renderChild = updateFunctionComponent(vNode as FunctionVNode)
325
+ }
326
+ } catch (error) {
327
+ if (__DEV__) {
365
328
  window.__kiru?.emit(
366
329
  "error",
367
- appCtx,
330
+ appCtx!,
368
331
  error instanceof Error ? error : new Error(String(error))
369
332
  )
370
- if (KiruError.isKiruError(error)) {
371
- if (error.customNodeStack) {
372
- setTimeout(() => {
373
- throw new Error(error.customNodeStack)
374
- })
375
- }
376
- if (error.fatal) {
377
- throw error
378
- }
379
- console.error(error)
380
- return
333
+ }
334
+
335
+ if (KiruError.isKiruError(error)) {
336
+ if (error.customNodeStack) {
337
+ setTimeout(() => {
338
+ throw new Error(error.customNodeStack)
339
+ })
381
340
  }
382
- setTimeout(() => {
341
+ if (error.fatal) {
383
342
  throw error
384
- })
343
+ }
344
+ console.error(error)
345
+ return
385
346
  }
347
+ setTimeout(() => {
348
+ throw error
349
+ })
350
+ }
386
351
 
387
- if (renderChild && vNode.child) {
388
- return vNode.child
352
+ if (renderChild && vNode.child) {
353
+ return vNode.child
354
+ }
355
+
356
+ let nextNode: VNode | null = vNode
357
+ while (nextNode) {
358
+ // queue effects upon ascent
359
+ if (nextNode.immediateEffects) {
360
+ preEffects.push(...nextNode.immediateEffects)
361
+ nextNode.immediateEffects = undefined
362
+ }
363
+ if (nextNode.effects) {
364
+ postEffects.push(...nextNode.effects)
365
+ nextNode.effects = undefined
389
366
  }
390
367
 
391
- let nextNode: VNode | null = vNode
392
- while (nextNode) {
393
- // queue effects upon ascent
394
- if (nextNode.immediateEffects) {
395
- effectCallbacks.pre.push(...nextNode.immediateEffects)
396
- nextNode.immediateEffects = undefined
397
- }
398
- if (nextNode.effects) {
399
- effectCallbacks.post.push(...nextNode.effects)
400
- nextNode.effects = undefined
401
- }
402
- if (nextNode === treesInProgress[currentTreeIndex]) return
403
- if (nextNode.sibling) {
404
- return nextNode.sibling
405
- }
368
+ if (nextNode === treesInProgress[currentTreeIndex]) return
369
+ if (nextNode.sibling) {
370
+ return nextNode.sibling
371
+ }
406
372
 
407
- nextNode = nextNode.parent
408
- if (renderMode.current === "hydrate" && nextNode?.dom) {
409
- hydrationStack.pop()
410
- }
373
+ nextNode = nextNode.parent
374
+ if (renderMode.current === "hydrate" && nextNode?.dom) {
375
+ hydrationStack.pop()
411
376
  }
412
377
  }
378
+ }
413
379
 
414
- function updateFunctionComponent(vNode: FunctionVNode) {
415
- const { type, props, subs, prev, isMemoized } = vNode
416
- if (isMemoized) {
417
- vNode.memoizedProps = props
418
- if (
419
- prev?.memoizedProps &&
420
- vNode.arePropsEqual!(prev.memoizedProps, props) &&
421
- !vNode.hmrUpdated
422
- ) {
423
- return false
424
- }
380
+ function updateFunctionComponent(vNode: FunctionVNode) {
381
+ const { type, props, subs, prev, isMemoized } = vNode
382
+ if (isMemoized) {
383
+ vNode.memoizedProps = props
384
+ if (
385
+ prev?.memoizedProps &&
386
+ vNode.arePropsEqual!(prev.memoizedProps, props) &&
387
+ !vNode.hmrUpdated
388
+ ) {
389
+ return false
425
390
  }
426
- try {
427
- node.current = vNode
428
- nodeToCtxMap.set(vNode, appCtx)
429
- let newChild
430
- let renderTryCount = 0
431
- do {
432
- isRenderDirtied = false
433
- hookIndex.current = 0
434
-
435
- /**
436
- * remove previous signal subscriptions (if any) every render.
437
- * this prevents no-longer-observed signals from triggering updates
438
- * in components that are not currently using them.
439
- *
440
- * TODO: in future, we might be able to optimize this by
441
- * only clearing the subscriptions that are no longer needed
442
- * and not clearing the entire set.
443
- */
444
- if (subs) {
445
- for (const sub of subs) {
446
- Signal.unsubscribe(vNode, sub)
447
- }
448
- subs.clear()
449
- }
391
+ }
392
+ try {
393
+ node.current = vNode
394
+ let newChild
395
+ let renderTryCount = 0
396
+ do {
397
+ isRenderDirtied = false
398
+ hookIndex.current = 0
399
+
400
+ /**
401
+ * remove previous signal subscriptions (if any) every render.
402
+ * this prevents no-longer-observed signals from triggering updates
403
+ * in components that are not currently using them.
404
+ *
405
+ * TODO: in future, we might be able to optimize this by
406
+ * only clearing the subscriptions that are no longer needed
407
+ * and not clearing the entire set.
408
+ */
409
+ if (subs) {
410
+ subs.forEach((unsub) => unsub())
411
+ subs.clear()
412
+ }
450
413
 
451
- if (__DEV__) {
452
- newChild = latest(type)(props)
453
- delete vNode.hmrUpdated
454
- if (++renderTryCount > CONSECUTIVE_DIRTY_LIMIT) {
455
- throw new KiruError({
456
- message:
457
- "Too many re-renders. Kiru limits the number of renders to prevent an infinite loop.",
458
- fatal: true,
459
- vNode,
460
- })
461
- }
462
- continue
414
+ if (__DEV__) {
415
+ newChild = latest(type)(props)
416
+ delete vNode.hmrUpdated
417
+ if (++renderTryCount > CONSECUTIVE_DIRTY_LIMIT) {
418
+ throw new KiruError({
419
+ message:
420
+ "Too many re-renders. Kiru limits the number of renders to prevent an infinite loop.",
421
+ fatal: true,
422
+ vNode,
423
+ })
463
424
  }
464
- newChild = type(props)
465
- } while (isRenderDirtied)
466
- vNode.child = reconcileChildren(vNode, newChild)
467
- vNode.deletions?.forEach((d) => queueDelete(d))
468
- return true
469
- } finally {
470
- node.current = null
471
- }
425
+ continue
426
+ }
427
+ newChild = type(props)
428
+ } while (isRenderDirtied)
429
+ vNode.child = reconcileChildren(vNode, newChild)
430
+ queueNodeChildDeletions(vNode)
431
+ return true
432
+ } finally {
433
+ node.current = null
472
434
  }
435
+ }
473
436
 
474
- function updateHostComponent(vNode: DomVNode) {
475
- const { props } = vNode
476
- if (__DEV__) {
477
- assertValidElementProps(vNode)
437
+ function updateHostComponent(vNode: DomVNode) {
438
+ const { props, type } = vNode
439
+ if (__DEV__) {
440
+ assertValidElementProps(vNode)
441
+ }
442
+ if (!vNode.dom) {
443
+ if (renderMode.current === "hydrate") {
444
+ hydrateDom(vNode)
445
+ } else {
446
+ vNode.dom = createDom(vNode)
478
447
  }
479
- if (!vNode.dom) {
480
- if (renderMode.current === "hydrate") {
481
- hydrateDom(vNode)
482
- } else {
483
- vNode.dom = createDom(vNode)
484
- }
485
- if (__DEV__) {
486
- // @ts-expect-error we apply vNode to the dom node
448
+ if (__DEV__) {
449
+ if (vNode.dom instanceof Element) {
487
450
  vNode.dom.__kiruNode = vNode
488
451
  }
489
452
  }
490
- // text should _never_ have children
491
- if (vNode.type !== "#text") {
492
- vNode.child = reconcileChildren(vNode, props.children)
493
- vNode.deletions?.forEach((d) => queueDelete(d))
494
- }
495
-
453
+ }
454
+ // text should _never_ have children
455
+ if (type !== "#text") {
456
+ vNode.child = reconcileChildren(vNode, props.children)
457
+ queueNodeChildDeletions(vNode)
496
458
  if (vNode.child && renderMode.current === "hydrate") {
497
459
  hydrationStack.push(vNode.dom!)
498
460
  }
499
461
  }
462
+ }
500
463
 
501
- function checkForTooManyConsecutiveDirtyRenders() {
502
- if (consecutiveDirtyCount > CONSECUTIVE_DIRTY_LIMIT) {
503
- throw new KiruError(
504
- "Maximum update depth exceeded. This can happen when a component repeatedly calls setState during render or in useLayoutEffect. Kiru limits the number of nested updates to prevent infinite loops."
505
- )
506
- }
464
+ function queueNodeChildDeletions(vNode: VNode) {
465
+ if (vNode.deletions) {
466
+ vNode.deletions.forEach(queueDelete)
467
+ vNode.deletions = null
507
468
  }
469
+ }
508
470
 
509
- function flushEffects(effectArr: Function[]) {
510
- while (effectArr.length) effectArr.shift()!()
471
+ function checkForTooManyConsecutiveDirtyRenders() {
472
+ if (consecutiveDirtyCount > CONSECUTIVE_DIRTY_LIMIT) {
473
+ throw new KiruError(
474
+ "Maximum update depth exceeded. This can happen when a component repeatedly calls setState during render or in useLayoutEffect. Kiru limits the number of nested updates to prevent infinite loops."
475
+ )
511
476
  }
477
+ }
512
478
 
513
- return (scheduler = {
514
- clear,
515
- wake,
516
- sleep,
517
- nextIdle,
518
- flushSync,
519
- queueUpdate,
520
- queueDelete,
521
- })
479
+ function flushEffects(effectArr: Function[]) {
480
+ for (let i = 0; i < effectArr.length; i++) {
481
+ effectArr[i]()
482
+ }
483
+ effectArr.length = 0
522
484
  }