kiru 0.45.3 → 0.46.1

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