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,522 @@
1
+ import type { AppContext } from "./appContext"
2
+ import type {
3
+ ContextProviderNode,
4
+ DomVNode,
5
+ FunctionVNode,
6
+ } from "./types.utils"
7
+ import { flags } from "./flags.js"
8
+ import {
9
+ $CONTEXT_PROVIDER,
10
+ CONSECUTIVE_DIRTY_LIMIT,
11
+ FLAG,
12
+ } from "./constants.js"
13
+ import { commitWork, createDom, hydrateDom } from "./dom.js"
14
+ import { __DEV__ } from "./env.js"
15
+ import { KaiokenError } from "./error.js"
16
+ import { ctx, hookIndex, node, nodeToCtxMap, renderMode } from "./globals.js"
17
+ import { hydrationStack } from "./hydration.js"
18
+ import { assertValidElementProps } from "./props.js"
19
+ import { reconcileChildren } from "./reconciler.js"
20
+ import {
21
+ willMemoBlockUpdate,
22
+ latest,
23
+ traverseApply,
24
+ vNodeContains,
25
+ isExoticType,
26
+ } from "./utils.js"
27
+ import { Signal } from "./signals/base.js"
28
+
29
+ type VNode = Kaioken.VNode
30
+
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
39
+ }
40
+
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
64
+
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
+ }
75
+ }
76
+
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()
87
+ }
88
+
89
+ function wake() {
90
+ if (isRunning) return
91
+ isRunning = true
92
+ requestIdleCallback(workLoop)
93
+ }
94
+
95
+ function sleep() {
96
+ isRunning = false
97
+ if (frameHandle !== null) {
98
+ globalThis.cancelAnimationFrame(frameHandle)
99
+ frameHandle = null
100
+ }
101
+ }
102
+
103
+ function nextIdle(fn: (scheduler: Scheduler) => void, wakeUpIfIdle = true) {
104
+ nextIdleEffects.push(fn)
105
+ if (wakeUpIfIdle) wake()
106
+ }
107
+
108
+ function flushSync() {
109
+ if (frameHandle !== null) {
110
+ globalThis.cancelAnimationFrame(frameHandle)
111
+ frameHandle = null
112
+ }
113
+ workLoop()
114
+ }
115
+
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.__kaioken?.profilingContext?.emit("updateDirtied", appCtx)
126
+ }
127
+ isRenderDirtied = true
128
+ return
129
+ }
130
+
131
+ // If it's already the next unit of work, no need to queue again
132
+ if (nextUnitOfWork === vNode) {
133
+ return
134
+ }
135
+
136
+ if (nextUnitOfWork === null) {
137
+ treesInProgress.push(vNode)
138
+ 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
156
+ }
157
+
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
+ }
176
+
177
+ return
178
+ }
179
+
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
209
+ } else {
210
+ // i > currentTreeIndex
211
+ treesInProgress.splice(i, 1)
212
+ if (!didReplaceTree) {
213
+ shouldQueueAtEnd = true
214
+ didReplaceTree = true
215
+ }
216
+ // no increment
217
+ }
218
+ }
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
+ }
225
+
226
+ function queueDelete(vNode: VNode) {
227
+ traverseApply(vNode, (n) => (n.flags = flags.set(n.flags, FLAG.DELETION)))
228
+ deletions.push(vNode)
229
+ }
230
+
231
+ function isFlushReady() {
232
+ return !nextUnitOfWork && (deletions.length || treesInProgress.length)
233
+ }
234
+
235
+ function workLoop(deadline?: IdleDeadline): void {
236
+ if (__DEV__) {
237
+ window.__kaioken?.profilingContext?.beginTick(appCtx)
238
+ }
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
247
+ }
248
+
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.__kaioken?.profilingContext?.endTick(appCtx)
271
+ window.__kaioken?.profilingContext?.emit("updateDirtied", appCtx)
272
+ }
273
+ return workLoop()
274
+ }
275
+ consecutiveDirtyCount = 0
276
+
277
+ flushEffects(effectCallbacks.post)
278
+ window.__kaioken!.emit("update", appCtx)
279
+ if (__DEV__) {
280
+ window.__kaioken?.profilingContext?.emit("update", appCtx)
281
+ }
282
+ }
283
+
284
+ if (!nextUnitOfWork) {
285
+ sleep()
286
+ while (nextIdleEffects.length) {
287
+ nextIdleEffects.shift()!(scheduler)
288
+ }
289
+ if (__DEV__) {
290
+ window.__kaioken?.profilingContext?.endTick(appCtx)
291
+ }
292
+ return
293
+ }
294
+
295
+ requestIdleCallback(workLoop)
296
+ }
297
+
298
+ function requestIdleCallback(callback: IdleRequestCallback) {
299
+ frameHandle = globalThis.requestAnimationFrame((time) => {
300
+ frameDeadline = time + maxFrameMs
301
+ pendingCallback = callback
302
+ channel.port1.postMessage(null)
303
+ })
304
+ }
305
+
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
330
+ }
331
+ }
332
+ jobRoots.push(dep)
333
+ })
334
+ })
335
+
336
+ pendingContextChanges.clear()
337
+ treesInProgress.push(...jobRoots)
338
+ return jobRoots[0] ?? null
339
+ }
340
+
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
+ }
358
+ }
359
+ vNode.child = reconcileChildren(vNode, props.children)
360
+ vNode.deletions?.forEach((d) => queueDelete(d))
361
+ } else {
362
+ renderChild = updateFunctionComponent(vNode as FunctionVNode)
363
+ }
364
+ } catch (error) {
365
+ window.__kaioken?.emit(
366
+ "error",
367
+ appCtx,
368
+ error instanceof Error ? error : new Error(String(error))
369
+ )
370
+ if (KaiokenError.isKaiokenError(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
381
+ }
382
+ setTimeout(() => {
383
+ throw error
384
+ })
385
+ }
386
+
387
+ if (renderChild && vNode.child) {
388
+ return vNode.child
389
+ }
390
+
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
+ }
406
+
407
+ nextNode = nextNode.parent
408
+ if (renderMode.current === "hydrate" && nextNode?.dom) {
409
+ hydrationStack.pop()
410
+ }
411
+ }
412
+ }
413
+
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
+ }
425
+ }
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
+ }
450
+
451
+ if (__DEV__) {
452
+ newChild = latest(type)(props)
453
+ delete vNode.hmrUpdated
454
+ if (++renderTryCount > CONSECUTIVE_DIRTY_LIMIT) {
455
+ throw new KaiokenError({
456
+ message:
457
+ "Too many re-renders. Kaioken limits the number of renders to prevent an infinite loop.",
458
+ fatal: true,
459
+ vNode,
460
+ })
461
+ }
462
+ continue
463
+ }
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
+ }
472
+ }
473
+
474
+ function updateHostComponent(vNode: DomVNode) {
475
+ const { props } = vNode
476
+ if (__DEV__) {
477
+ assertValidElementProps(vNode)
478
+ }
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
487
+ vNode.dom.__kaiokenNode = vNode
488
+ }
489
+ }
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
+
496
+ if (vNode.child && renderMode.current === "hydrate") {
497
+ hydrationStack.push(vNode.dom!)
498
+ }
499
+ }
500
+
501
+ function checkForTooManyConsecutiveDirtyRenders() {
502
+ if (consecutiveDirtyCount > CONSECUTIVE_DIRTY_LIMIT) {
503
+ throw new KaiokenError(
504
+ "Maximum update depth exceeded. This can happen when a component repeatedly calls setState during render or in useLayoutEffect. Kaioken limits the number of nested updates to prevent infinite loops."
505
+ )
506
+ }
507
+ }
508
+
509
+ function flushEffects(effectArr: Function[]) {
510
+ while (effectArr.length) effectArr.shift()!()
511
+ }
512
+
513
+ return (scheduler = {
514
+ clear,
515
+ wake,
516
+ sleep,
517
+ nextIdle,
518
+ flushSync,
519
+ queueUpdate,
520
+ queueDelete,
521
+ })
522
+ }