@fictjs/runtime 0.16.0 → 0.17.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 (81) hide show
  1. package/dist/advanced.cjs +9 -9
  2. package/dist/advanced.d.cts +3 -3
  3. package/dist/advanced.d.ts +3 -3
  4. package/dist/advanced.js +4 -4
  5. package/dist/{binding-CQUGLLBI.d.ts → binding-BfzY9rae.d.ts} +2 -2
  6. package/dist/{binding-BlABuUiG.d.cts → binding-CDR2ERoq.d.cts} +2 -2
  7. package/dist/{chunk-CBRGOLTR.cjs → chunk-2J4INHDT.cjs} +40 -40
  8. package/dist/{chunk-CBRGOLTR.cjs.map → chunk-2J4INHDT.cjs.map} +1 -1
  9. package/dist/{chunk-BADX4WTQ.cjs → chunk-CKKZDUHM.cjs} +21 -18
  10. package/dist/chunk-CKKZDUHM.cjs.map +1 -0
  11. package/dist/{chunk-ZWQLXWSV.js → chunk-DHRRJJ6W.js} +8 -5
  12. package/dist/chunk-DHRRJJ6W.js.map +1 -0
  13. package/dist/{chunk-4P4DYWLQ.js → chunk-LFLFSJFU.js} +3 -3
  14. package/dist/{chunk-WJMZ7X46.cjs → chunk-NBDEMBBX.cjs} +47 -85
  15. package/dist/chunk-NBDEMBBX.cjs.map +1 -0
  16. package/dist/{chunk-MAHWGB55.js → chunk-OKPQWORE.js} +47 -85
  17. package/dist/chunk-OKPQWORE.js.map +1 -0
  18. package/dist/{chunk-RK2WSQYL.js → chunk-OLHZBAIF.js} +3 -3
  19. package/dist/{chunk-ZJZ6LMDN.js → chunk-R2HYEOP7.js} +470 -172
  20. package/dist/chunk-R2HYEOP7.js.map +1 -0
  21. package/dist/{chunk-AR2T7JEX.cjs → chunk-UG2IFQOY.cjs} +650 -352
  22. package/dist/chunk-UG2IFQOY.cjs.map +1 -0
  23. package/dist/{chunk-ECNK25S4.cjs → chunk-VP2WC7X3.cjs} +8 -8
  24. package/dist/{chunk-ECNK25S4.cjs.map → chunk-VP2WC7X3.cjs.map} +1 -1
  25. package/dist/{devtools-DWIZRe7L.d.cts → devtools-BwkkQ6DN.d.cts} +1 -1
  26. package/dist/{devtools-DNnnDGu1.d.ts → devtools-CK3SVU_w.d.ts} +1 -1
  27. package/dist/index.cjs +55 -42
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +4 -4
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.dev.js +260 -156
  32. package/dist/index.dev.js.map +1 -1
  33. package/dist/index.js +16 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/internal-list.cjs +4 -4
  36. package/dist/internal-list.js +3 -3
  37. package/dist/internal.cjs +5 -5
  38. package/dist/internal.d.cts +4 -4
  39. package/dist/internal.d.ts +4 -4
  40. package/dist/internal.js +4 -4
  41. package/dist/jsx-dev-runtime.cjs.map +1 -1
  42. package/dist/jsx-dev-runtime.d.cts +46 -0
  43. package/dist/jsx-dev-runtime.d.ts +46 -0
  44. package/dist/jsx-dev-runtime.js.map +1 -1
  45. package/dist/jsx-runtime.cjs.map +1 -1
  46. package/dist/jsx-runtime.d.cts +46 -0
  47. package/dist/jsx-runtime.d.ts +46 -0
  48. package/dist/jsx-runtime.js.map +1 -1
  49. package/dist/loader.cjs +143 -26
  50. package/dist/loader.cjs.map +1 -1
  51. package/dist/loader.d.cts +1 -1
  52. package/dist/loader.d.ts +1 -1
  53. package/dist/loader.js +122 -5
  54. package/dist/loader.js.map +1 -1
  55. package/dist/{props-DabFQwLR.d.ts → props-CFoQ471Y.d.ts} +47 -1
  56. package/dist/{props-tImUZAty.d.cts → props-D4tK8Gn0.d.cts} +47 -1
  57. package/dist/{resume-C5IKAIdh.d.ts → resume-C166aAVg.d.ts} +2 -2
  58. package/dist/{resume-DPZxmA95.d.cts → resume-C20cRVj9.d.cts} +2 -2
  59. package/dist/{scope-gpOMWTlf.d.ts → scope-BFzD_7hx.d.ts} +1 -1
  60. package/dist/{scope-GwC4DJ50.d.cts → scope-Ck3mTQVS.d.cts} +1 -1
  61. package/package.json +1 -1
  62. package/src/binding.ts +561 -166
  63. package/src/context.ts +8 -1
  64. package/src/dom.ts +26 -44
  65. package/src/effect.ts +9 -12
  66. package/src/error-boundary.ts +8 -0
  67. package/src/hydration.ts +25 -6
  68. package/src/jsx.ts +46 -0
  69. package/src/lifecycle.ts +31 -79
  70. package/src/loader.ts +153 -4
  71. package/src/resume.ts +5 -5
  72. package/src/signal.ts +4 -1
  73. package/src/suspense.ts +8 -0
  74. package/dist/chunk-AR2T7JEX.cjs.map +0 -1
  75. package/dist/chunk-BADX4WTQ.cjs.map +0 -1
  76. package/dist/chunk-MAHWGB55.js.map +0 -1
  77. package/dist/chunk-WJMZ7X46.cjs.map +0 -1
  78. package/dist/chunk-ZJZ6LMDN.js.map +0 -1
  79. package/dist/chunk-ZWQLXWSV.js.map +0 -1
  80. /package/dist/{chunk-4P4DYWLQ.js.map → chunk-LFLFSJFU.js.map} +0 -0
  81. /package/dist/{chunk-RK2WSQYL.js.map → chunk-OLHZBAIF.js.map} +0 -0
package/src/context.ts CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  type RootContext,
57
57
  } from './lifecycle'
58
58
  import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
59
+ import { untrack } from './signal'
59
60
  import type { BaseProps, FictNode } from './types'
60
61
 
61
62
  // ============================================================================
@@ -222,7 +223,13 @@ export function createContext<T>(defaultValue: T): Context<T> {
222
223
  createRenderEffect(() => {
223
224
  // Update context value on re-render (if value prop changes reactively)
224
225
  contextMap.set(id, props.value)
225
- renderChildren(props.children)
226
+
227
+ // Provider value updates should not subscribe this effect to arbitrary
228
+ // signal reads that happen while rendering descendants. Child trees own
229
+ // their own reactivity; the provider only needs to react to its props.
230
+ untrack(() => {
231
+ renderChildren(props.children)
232
+ })
226
233
  })
227
234
 
228
235
  return fragment
package/src/dom.ts CHANGED
@@ -19,7 +19,9 @@ import {
19
19
  createClassBinding,
20
20
  createChildBinding,
21
21
  bindEvent,
22
+ bindRef,
22
23
  isReactive,
24
+ registerCreateElement,
23
25
  type MaybeReactive,
24
26
  type AttributeSetter,
25
27
  type BindingHandle,
@@ -27,7 +29,7 @@ import {
27
29
  import { Properties, ChildProperties, getPropAlias, SVGElements, SVGNamespace } from './constants'
28
30
  import { getDevtoolsHook } from './devtools'
29
31
  import { __fictPushContext, __fictPopContext, __fictGetCurrentComponentId } from './hooks'
30
- import { claimNodes, isHydratingActive, withHydration } from './hydration'
32
+ import { claimNodes, claimText, isHydratingActive, withHydration } from './hydration'
31
33
  import { Fragment } from './jsx'
32
34
  import {
33
35
  createRootContext,
@@ -51,7 +53,7 @@ import {
51
53
  __fictExitHydration,
52
54
  } from './resume'
53
55
  import { untrack } from './scheduler'
54
- import type { DOMElement, FictNode, FictVNode, RefObject } from './types'
56
+ import type { DOMElement, FictNode, FictVNode } from './types'
55
57
 
56
58
  type NamespaceContext = 'svg' | 'mathml' | null
57
59
 
@@ -214,6 +216,8 @@ export function createElement(node: FictNode): DOMElement {
214
216
  return createElementWithContext(node, null, resolveOwnerDocument())
215
217
  }
216
218
 
219
+ registerCreateElement(createElement)
220
+
217
221
  function resolveNamespace(tagName: string, namespace: NamespaceContext): NamespaceContext {
218
222
  if (tagName === 'svg') return 'svg'
219
223
  if (tagName === 'math') return 'mathml'
@@ -227,6 +231,13 @@ function resolveOwnerDocument(ownerDocument?: Document): Document {
227
231
  return ownerDocument ?? getCurrentRoot()?.ownerDocument ?? document
228
232
  }
229
233
 
234
+ function createTextNodeWithHydration(value: string, ownerDocument: Document): Text {
235
+ if (!isHydratingActive()) {
236
+ return ownerDocument.createTextNode(value)
237
+ }
238
+ return claimText(value, () => ownerDocument.createTextNode(value))
239
+ }
240
+
230
241
  function createElementWithContext(
231
242
  node: FictNode,
232
243
  namespace: NamespaceContext,
@@ -239,14 +250,14 @@ function createElementWithContext(
239
250
 
240
251
  // Null/undefined/false - empty placeholder
241
252
  if (node === null || node === undefined || node === false) {
242
- return ownerDocument.createTextNode('')
253
+ return createTextNodeWithHydration('', ownerDocument)
243
254
  }
244
255
 
245
256
  // Reactive getter function - resolve to actual node
246
257
  if (isReactive(node)) {
247
258
  const resolved = (node as () => FictNode)()
248
259
  if (resolved === node) {
249
- return ownerDocument.createTextNode('')
260
+ return createTextNodeWithHydration('', ownerDocument)
250
261
  }
251
262
  return createElementWithContext(resolved, namespace, ownerDocument)
252
263
  }
@@ -254,7 +265,7 @@ function createElementWithContext(
254
265
  // Non-reactive function values are not valid DOM nodes.
255
266
  // Keep callback values inert instead of stringifying function source.
256
267
  if (typeof node === 'function') {
257
- return ownerDocument.createTextNode('')
268
+ return createTextNodeWithHydration('', ownerDocument)
258
269
  }
259
270
 
260
271
  if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
@@ -290,11 +301,11 @@ function createElementWithContext(
290
301
 
291
302
  // Primitive values - text node
292
303
  if (typeof node === 'string' || typeof node === 'number') {
293
- return ownerDocument.createTextNode(String(node))
304
+ return createTextNodeWithHydration(String(node), ownerDocument)
294
305
  }
295
306
 
296
307
  if (typeof node === 'boolean') {
297
- return ownerDocument.createTextNode('')
308
+ return createTextNodeWithHydration('', ownerDocument)
298
309
  }
299
310
 
300
311
  // VNode
@@ -615,7 +626,7 @@ function appendChildNode(
615
626
  // Cast to Node for remaining logic
616
627
  let domNode: Node
617
628
  if (typeof child !== 'object' || child === null) {
618
- domNode = parentOwnerDocument.createTextNode(String(child ?? ''))
629
+ domNode = createTextNodeWithHydration(String(child ?? ''), parentOwnerDocument)
619
630
  } else {
620
631
  domNode = createElementWithContext(child as any, namespace, parentOwnerDocument) as Node
621
632
  }
@@ -675,43 +686,14 @@ function appendChildren(
675
686
  * Both types are automatically cleaned up on unmount.
676
687
  */
677
688
  function applyRef(el: Element, value: unknown): void {
678
- if (typeof value === 'function') {
679
- // Callback ref
680
- const refFn = value as (el: Element | null) => void
681
- refFn(el)
682
-
683
- // Match React behavior: call ref(null) on unmount
684
- const root = getCurrentRoot()
685
- if (root) {
686
- registerRootCleanup(() => {
687
- refFn(null)
688
- })
689
- } else if (isDev) {
690
- console.warn(
691
- '[fict] Ref applied outside of a root context. ' +
692
- 'The ref cleanup (setting to null) will not run automatically. ' +
693
- 'Consider using createRoot() or ensure the element is created within a component.',
694
- )
695
- }
696
- } else if (value && typeof value === 'object' && 'current' in value) {
697
- // Object ref
698
- const refObj = value as RefObject<Element>
699
- refObj.current = el
700
-
701
- // Auto-cleanup on unmount
702
- const root = getCurrentRoot()
703
- if (root) {
704
- registerRootCleanup(() => {
705
- refObj.current = null
706
- })
707
- } else if (isDev) {
708
- console.warn(
709
- '[fict] Ref applied outside of a root context. ' +
710
- 'The ref cleanup (setting to null) will not run automatically. ' +
711
- 'Consider using createRoot() or ensure the element is created within a component.',
712
- )
713
- }
689
+ if (!getCurrentRoot() && isDev) {
690
+ console.warn(
691
+ '[fict] Ref applied outside of a root context. ' +
692
+ 'The ref cleanup (setting to null) will not run automatically. ' +
693
+ 'Consider using createRoot() or ensure the element is created within a component.',
694
+ )
714
695
  }
696
+ bindRef(el, value)
715
697
  }
716
698
 
717
699
  // ============================================================================
package/src/effect.ts CHANGED
@@ -21,7 +21,7 @@ export function createEffect(fn: Effect, options?: EffectOptions): () => void {
21
21
 
22
22
  // Cleanup runner - called by runEffect BEFORE signal values are committed
23
23
  const doCleanup = () => {
24
- runCleanupList(cleanups)
24
+ runCleanupList(cleanups, rootForError)
25
25
  cleanups = []
26
26
  }
27
27
 
@@ -49,7 +49,7 @@ export function createEffect(fn: Effect, options?: EffectOptions): () => void {
49
49
 
50
50
  const disposeEffect = effectWithCleanup(run, doCleanup, rootForError, options)
51
51
  const teardown = () => {
52
- runCleanupList(cleanups)
52
+ runCleanupList(cleanups, rootForError)
53
53
  disposeEffect()
54
54
  }
55
55
 
@@ -61,15 +61,13 @@ export function createEffect(fn: Effect, options?: EffectOptions): () => void {
61
61
  export const $effect = createEffect
62
62
 
63
63
  export function createRenderEffect(fn: Effect, options?: EffectOptions): () => void {
64
- let cleanup: Cleanup | undefined
64
+ let cleanups: Cleanup[] = []
65
65
  const rootForError = getCurrentRoot()
66
66
 
67
67
  // Cleanup runner - called by runEffect BEFORE signal values are committed
68
68
  const doCleanup = () => {
69
- if (cleanup) {
70
- cleanup()
71
- cleanup = undefined
72
- }
69
+ runCleanupList(cleanups, rootForError)
70
+ cleanups = []
73
71
  }
74
72
 
75
73
  const run = () => {
@@ -77,7 +75,9 @@ export function createRenderEffect(fn: Effect, options?: EffectOptions): () => v
77
75
  try {
78
76
  const maybeCleanup = fn()
79
77
  if (typeof maybeCleanup === 'function') {
80
- cleanup = maybeCleanup
78
+ cleanups = [maybeCleanup]
79
+ } else {
80
+ cleanups = []
81
81
  }
82
82
  } catch (err) {
83
83
  if (handleSuspend(err as any, rootForError)) {
@@ -93,10 +93,7 @@ export function createRenderEffect(fn: Effect, options?: EffectOptions): () => v
93
93
 
94
94
  const disposeEffect = effectWithCleanup(run, doCleanup, rootForError, options)
95
95
  const teardown = () => {
96
- if (cleanup) {
97
- cleanup()
98
- cleanup = undefined
99
- }
96
+ runCleanupList(cleanups, rootForError)
100
97
  disposeEffect()
101
98
  }
102
99
 
@@ -8,6 +8,7 @@ import {
8
8
  pushRoot,
9
9
  popRoot,
10
10
  registerErrorHandler,
11
+ registerRootCleanup,
11
12
  } from './lifecycle'
12
13
  import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
13
14
  import type { BaseProps, FictNode } from './types'
@@ -105,6 +106,13 @@ export function ErrorBoundary(props: ErrorBoundaryProps): FictNode {
105
106
 
106
107
  renderValue(props.children ?? null)
107
108
 
109
+ registerRootCleanup(() => {
110
+ if (cleanup) {
111
+ cleanup()
112
+ cleanup = undefined
113
+ }
114
+ })
115
+
108
116
  registerErrorHandler(err => {
109
117
  renderValue(toView(err))
110
118
  props.onError?.(err)
package/src/hydration.ts CHANGED
@@ -6,7 +6,7 @@ interface HydrationContext {
6
6
 
7
7
  const hydrationStack: HydrationContext[] = []
8
8
 
9
- export function withHydration(root: ParentNode & Node, fn: () => void): void {
9
+ export function withHydration<T>(root: ParentNode & Node, fn: () => T): T {
10
10
  const owner = root.ownerDocument ?? document
11
11
  hydrationStack.push({
12
12
  cursor: root.firstChild,
@@ -14,25 +14,25 @@ export function withHydration(root: ParentNode & Node, fn: () => void): void {
14
14
  owner,
15
15
  })
16
16
  try {
17
- fn()
17
+ return fn()
18
18
  } finally {
19
19
  hydrationStack.pop()
20
20
  }
21
21
  }
22
22
 
23
- export function withHydrationRange(
23
+ export function withHydrationRange<T>(
24
24
  start: Node | null,
25
25
  end: Node | null,
26
26
  owner: Document,
27
- fn: () => void,
28
- ): void {
27
+ fn: () => T,
28
+ ): T {
29
29
  hydrationStack.push({
30
30
  cursor: start,
31
31
  boundary: end,
32
32
  owner,
33
33
  })
34
34
  try {
35
- fn()
35
+ return fn()
36
36
  } finally {
37
37
  hydrationStack.pop()
38
38
  }
@@ -70,6 +70,25 @@ export function claimNodes(templateRoot: Node, fallback: () => Node): Node {
70
70
  return frag
71
71
  }
72
72
 
73
+ export function claimText(value: string, fallback: () => Text): Text {
74
+ const ctx = hydrationStack[hydrationStack.length - 1]
75
+ if (
76
+ !ctx ||
77
+ !ctx.cursor ||
78
+ ctx.cursor === ctx.boundary ||
79
+ ctx.cursor.nodeType !== Node.TEXT_NODE
80
+ ) {
81
+ return fallback()
82
+ }
83
+
84
+ const text = ctx.cursor as Text
85
+ ctx.cursor = text.nextSibling
86
+ if (text.data !== value) {
87
+ text.data = value
88
+ }
89
+ return text
90
+ }
91
+
73
92
  export function isHydratingActive(): boolean {
74
93
  return hydrationStack.length > 0
75
94
  }
package/src/jsx.ts CHANGED
@@ -207,59 +207,105 @@ interface HTMLAttributes<T> {
207
207
 
208
208
  // Event handlers
209
209
  onClick?: (e: MouseEvent) => void
210
+ onClick$?: (e: MouseEvent) => void
210
211
  onDblClick?: (e: MouseEvent) => void
212
+ onDblClick$?: (e: MouseEvent) => void
211
213
  onMouseDown?: (e: MouseEvent) => void
214
+ onMouseDown$?: (e: MouseEvent) => void
212
215
  onMouseUp?: (e: MouseEvent) => void
216
+ onMouseUp$?: (e: MouseEvent) => void
213
217
  onMouseMove?: (e: MouseEvent) => void
218
+ onMouseMove$?: (e: MouseEvent) => void
214
219
  onMouseEnter?: (e: MouseEvent) => void
220
+ onMouseEnter$?: (e: MouseEvent) => void
215
221
  onMouseLeave?: (e: MouseEvent) => void
222
+ onMouseLeave$?: (e: MouseEvent) => void
216
223
  onMouseOver?: (e: MouseEvent) => void
224
+ onMouseOver$?: (e: MouseEvent) => void
217
225
  onMouseOut?: (e: MouseEvent) => void
226
+ onMouseOut$?: (e: MouseEvent) => void
218
227
  onContextMenu?: (e: MouseEvent) => void
228
+ onContextMenu$?: (e: MouseEvent) => void
219
229
  onInput?: (e: InputEvent) => void
230
+ onInput$?: (e: InputEvent) => void
220
231
  onChange?: (e: Event) => void
232
+ onChange$?: (e: Event) => void
221
233
  onSubmit?: (e: SubmitEvent) => void
234
+ onSubmit$?: (e: SubmitEvent) => void
222
235
  onReset?: (e: Event) => void
236
+ onReset$?: (e: Event) => void
223
237
  onKeyDown?: (e: KeyboardEvent) => void
238
+ onKeyDown$?: (e: KeyboardEvent) => void
224
239
  onKeyUp?: (e: KeyboardEvent) => void
240
+ onKeyUp$?: (e: KeyboardEvent) => void
225
241
  onKeyPress?: (e: KeyboardEvent) => void
242
+ onKeyPress$?: (e: KeyboardEvent) => void
226
243
  onFocus?: (e: FocusEvent) => void
244
+ onFocus$?: (e: FocusEvent) => void
227
245
  onBlur?: (e: FocusEvent) => void
246
+ onBlur$?: (e: FocusEvent) => void
228
247
  onScroll?: (e: Event) => void
248
+ onScroll$?: (e: Event) => void
229
249
  onWheel?: (e: WheelEvent) => void
250
+ onWheel$?: (e: WheelEvent) => void
230
251
  onLoad?: (e: Event) => void
252
+ onLoad$?: (e: Event) => void
231
253
  onError?: (e: Event) => void
254
+ onError$?: (e: Event) => void
232
255
 
233
256
  // Drag events
234
257
  onDrag?: (e: DragEvent) => void
258
+ onDrag$?: (e: DragEvent) => void
235
259
  onDragStart?: (e: DragEvent) => void
260
+ onDragStart$?: (e: DragEvent) => void
236
261
  onDragEnd?: (e: DragEvent) => void
262
+ onDragEnd$?: (e: DragEvent) => void
237
263
  onDragEnter?: (e: DragEvent) => void
264
+ onDragEnter$?: (e: DragEvent) => void
238
265
  onDragLeave?: (e: DragEvent) => void
266
+ onDragLeave$?: (e: DragEvent) => void
239
267
  onDragOver?: (e: DragEvent) => void
268
+ onDragOver$?: (e: DragEvent) => void
240
269
  onDrop?: (e: DragEvent) => void
270
+ onDrop$?: (e: DragEvent) => void
241
271
 
242
272
  // Touch events
243
273
  onTouchStart?: (e: TouchEvent) => void
274
+ onTouchStart$?: (e: TouchEvent) => void
244
275
  onTouchMove?: (e: TouchEvent) => void
276
+ onTouchMove$?: (e: TouchEvent) => void
245
277
  onTouchEnd?: (e: TouchEvent) => void
278
+ onTouchEnd$?: (e: TouchEvent) => void
246
279
  onTouchCancel?: (e: TouchEvent) => void
280
+ onTouchCancel$?: (e: TouchEvent) => void
247
281
 
248
282
  // Animation events
249
283
  onAnimationStart?: (e: AnimationEvent) => void
284
+ onAnimationStart$?: (e: AnimationEvent) => void
250
285
  onAnimationEnd?: (e: AnimationEvent) => void
286
+ onAnimationEnd$?: (e: AnimationEvent) => void
251
287
  onAnimationIteration?: (e: AnimationEvent) => void
288
+ onAnimationIteration$?: (e: AnimationEvent) => void
252
289
  onTransitionEnd?: (e: TransitionEvent) => void
290
+ onTransitionEnd$?: (e: TransitionEvent) => void
253
291
 
254
292
  // Pointer events
255
293
  onPointerDown?: (e: PointerEvent) => void
294
+ onPointerDown$?: (e: PointerEvent) => void
256
295
  onPointerUp?: (e: PointerEvent) => void
296
+ onPointerUp$?: (e: PointerEvent) => void
257
297
  onPointerMove?: (e: PointerEvent) => void
298
+ onPointerMove$?: (e: PointerEvent) => void
258
299
  onPointerEnter?: (e: PointerEvent) => void
300
+ onPointerEnter$?: (e: PointerEvent) => void
259
301
  onPointerLeave?: (e: PointerEvent) => void
302
+ onPointerLeave$?: (e: PointerEvent) => void
260
303
  onPointerOver?: (e: PointerEvent) => void
304
+ onPointerOver$?: (e: PointerEvent) => void
261
305
  onPointerOut?: (e: PointerEvent) => void
306
+ onPointerOut$?: (e: PointerEvent) => void
262
307
  onPointerCancel?: (e: PointerEvent) => void
308
+ onPointerCancel$?: (e: PointerEvent) => void
263
309
 
264
310
  // Ref
265
311
  ref?: ((el: T | null) => void) | { current: T | null }
package/src/lifecycle.ts CHANGED
@@ -29,8 +29,6 @@ type SuspenseHandler = (token: SuspenseToken | PromiseLike<unknown>) => boolean
29
29
 
30
30
  let currentRoot: RootContext | undefined
31
31
  let currentEffectCleanups: Cleanup[] | undefined
32
- const globalErrorHandlers = new WeakMap<RootContext, ErrorHandler[]>()
33
- const globalSuspenseHandlers = new WeakMap<RootContext, SuspenseHandler[]>()
34
32
  const rootDevtoolsIds = new WeakMap<RootContext, number>()
35
33
  let nextRootDevtoolsId = 0
36
34
 
@@ -115,20 +113,28 @@ export function onCleanup(fn: Cleanup): void {
115
113
  export function flushOnMount(root: RootContext): void {
116
114
  const cbs = root.onMountCallbacks
117
115
  if (!cbs || cbs.length === 0) return
118
- // Temporarily restore root context so onCleanup calls inside
119
- // mount callbacks register correctly
116
+ try {
117
+ withRootContext(root, () => {
118
+ for (let i = 0; i < cbs.length; i++) {
119
+ const cleanup = cbs[i]!()
120
+ if (typeof cleanup === 'function') {
121
+ root.cleanups.push(cleanup)
122
+ }
123
+ }
124
+ })
125
+ } finally {
126
+ cbs.length = 0
127
+ }
128
+ }
129
+
130
+ export function withRootContext<T>(root: RootContext | undefined, fn: () => T): T {
131
+ if (!root) return fn()
120
132
  const prevRoot = currentRoot
121
133
  currentRoot = root
122
134
  try {
123
- for (let i = 0; i < cbs.length; i++) {
124
- const cleanup = cbs[i]!()
125
- if (typeof cleanup === 'function') {
126
- root.cleanups.push(cleanup)
127
- }
128
- }
135
+ return fn()
129
136
  } finally {
130
137
  currentRoot = prevRoot
131
- cbs.length = 0
132
138
  }
133
139
  }
134
140
 
@@ -139,7 +145,7 @@ export function registerRootCleanup(fn: Cleanup): void {
139
145
  }
140
146
 
141
147
  export function clearRoot(root: RootContext): void {
142
- runCleanupList(root.cleanups)
148
+ runCleanupList(root.cleanups, root)
143
149
  if (root.onMountCallbacks) {
144
150
  root.onMountCallbacks.length = 0
145
151
  }
@@ -147,19 +153,13 @@ export function clearRoot(root: RootContext): void {
147
153
 
148
154
  export function destroyRoot(root: RootContext): void {
149
155
  clearRoot(root)
150
- runCleanupList(root.destroyCallbacks)
156
+ runCleanupList(root.destroyCallbacks, root)
151
157
  if (root.errorHandlers) {
152
158
  root.errorHandlers.length = 0
153
159
  }
154
- if (globalErrorHandlers.has(root)) {
155
- globalErrorHandlers.delete(root)
156
- }
157
160
  if (root.suspenseHandlers) {
158
161
  root.suspenseHandlers.length = 0
159
162
  }
160
- if (globalSuspenseHandlers.has(root)) {
161
- globalSuspenseHandlers.delete(root)
162
- }
163
163
  disposeRootDevtools(root)
164
164
  }
165
165
 
@@ -201,21 +201,23 @@ export function registerEffectCleanup(fn: Cleanup): void {
201
201
  }
202
202
  }
203
203
 
204
- export function runCleanupList(list: Cleanup[]): void {
204
+ export function runCleanupList(list: Cleanup[], root?: RootContext): void {
205
205
  let error: unknown
206
- for (let i = list.length - 1; i >= 0; i--) {
207
- try {
208
- const cleanup = list[i]
209
- if (cleanup) cleanup()
210
- } catch (err) {
211
- if (error === undefined) {
212
- error = err
206
+ withRootContext(root, () => {
207
+ for (let i = list.length - 1; i >= 0; i--) {
208
+ try {
209
+ const cleanup = list[i]
210
+ if (cleanup) cleanup()
211
+ } catch (err) {
212
+ if (error === undefined) {
213
+ error = err
214
+ }
213
215
  }
214
216
  }
215
- }
217
+ })
216
218
  list.length = 0
217
219
  if (error !== undefined) {
218
- if (!handleError(error, { source: 'cleanup' })) {
220
+ if (!handleError(error, { source: 'cleanup' }, root)) {
219
221
  throw error
220
222
  }
221
223
  }
@@ -239,12 +241,6 @@ export function registerErrorHandler(fn: ErrorHandler): void {
239
241
  currentRoot.errorHandlers = []
240
242
  }
241
243
  currentRoot.errorHandlers.push(fn)
242
- const existing = globalErrorHandlers.get(currentRoot)
243
- if (existing) {
244
- existing.push(fn)
245
- } else {
246
- globalErrorHandlers.set(currentRoot, [fn])
247
- }
248
244
  }
249
245
 
250
246
  export function registerSuspenseHandler(fn: SuspenseHandler): void {
@@ -258,12 +254,6 @@ export function registerSuspenseHandler(fn: SuspenseHandler): void {
258
254
  currentRoot.suspenseHandlers = []
259
255
  }
260
256
  currentRoot.suspenseHandlers.push(fn)
261
- const existing = globalSuspenseHandlers.get(currentRoot)
262
- if (existing) {
263
- existing.push(fn)
264
- } else {
265
- globalSuspenseHandlers.set(currentRoot, [fn])
266
- }
267
257
  }
268
258
 
269
259
  export function handleError(err: unknown, info?: ErrorInfo, startRoot?: RootContext): boolean {
@@ -286,24 +276,6 @@ export function handleError(err: unknown, info?: ErrorInfo, startRoot?: RootCont
286
276
  }
287
277
  root = root.parent
288
278
  }
289
- const globalForRoot = startRoot
290
- ? globalErrorHandlers.get(startRoot)
291
- : currentRoot
292
- ? globalErrorHandlers.get(currentRoot)
293
- : undefined
294
- if (globalForRoot && globalForRoot.length) {
295
- for (let i = globalForRoot.length - 1; i >= 0; i--) {
296
- const handler = globalForRoot[i]!
297
- try {
298
- const handled = handler(error, info)
299
- if (handled !== false) {
300
- return true
301
- }
302
- } catch (nextErr) {
303
- error = nextErr
304
- }
305
- }
306
- }
307
279
  // The caller (e.g., runCleanupList) can decide whether to rethrow.
308
280
  // This makes the API consistent: handleError always returns a boolean
309
281
  // indicating whether the error was handled.
@@ -334,25 +306,5 @@ export function handleSuspend(
334
306
  }
335
307
  root = root.parent
336
308
  }
337
- const globalForRoot =
338
- startRoot && globalSuspenseHandlers.get(startRoot)
339
- ? globalSuspenseHandlers.get(startRoot)
340
- : currentRoot
341
- ? globalSuspenseHandlers.get(currentRoot)
342
- : undefined
343
- if (globalForRoot && globalForRoot.length) {
344
- for (let i = globalForRoot.length - 1; i >= 0; i--) {
345
- const handler = globalForRoot[i]!
346
- const handled = handler(token)
347
- if (handled !== false) {
348
- // Only set suspended = true when a handler actually handles the token
349
- if (originRoot) {
350
- originRoot.suspended = true
351
- setRootSuspendDevtools(originRoot, true)
352
- }
353
- return true
354
- }
355
- }
356
- }
357
309
  return false
358
310
  }