@fictjs/runtime 0.9.0 → 0.11.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 (87) hide show
  1. package/dist/advanced.cjs +9 -9
  2. package/dist/advanced.d.cts +4 -4
  3. package/dist/advanced.d.ts +4 -4
  4. package/dist/advanced.js +4 -4
  5. package/dist/{binding-BWchH3Kp.d.cts → binding-DcnhUSQK.d.ts} +5 -3
  6. package/dist/{binding-BWchH3Kp.d.ts → binding-FRyTeLDn.d.cts} +5 -3
  7. package/dist/{chunk-FVX77557.js → chunk-2UR2UWE2.js} +3 -3
  8. package/dist/{chunk-LBE6DC3V.cjs → chunk-44EQF3AR.cjs} +63 -52
  9. package/dist/chunk-44EQF3AR.cjs.map +1 -0
  10. package/dist/{chunk-OAM7HABA.cjs → chunk-4QGEN5SJ.cjs} +340 -263
  11. package/dist/chunk-4QGEN5SJ.cjs.map +1 -0
  12. package/dist/{chunk-PD6IQY2Y.cjs → chunk-C5IE4WUG.cjs} +8 -8
  13. package/dist/{chunk-PD6IQY2Y.cjs.map → chunk-C5IE4WUG.cjs.map} +1 -1
  14. package/dist/{chunk-DXG3TARY.js → chunk-DIK33H5U.js} +202 -30
  15. package/dist/chunk-DIK33H5U.js.map +1 -0
  16. package/dist/{chunk-JVYH76ZX.js → chunk-FESAXMHT.js} +7 -6
  17. package/dist/{chunk-JVYH76ZX.js.map → chunk-FESAXMHT.js.map} +1 -1
  18. package/dist/chunk-FHQZCAAK.cjs +112 -0
  19. package/dist/chunk-FHQZCAAK.cjs.map +1 -0
  20. package/dist/{chunk-UBFDB6OL.cjs → chunk-QNMYVXRL.cjs} +222 -50
  21. package/dist/chunk-QNMYVXRL.cjs.map +1 -0
  22. package/dist/{chunk-N6ODUM2Y.js → chunk-S63VBIWN.js} +27 -16
  23. package/dist/chunk-S63VBIWN.js.map +1 -0
  24. package/dist/{chunk-T2LNV5Q5.js → chunk-WIHNVN6L.js} +153 -76
  25. package/dist/chunk-WIHNVN6L.js.map +1 -0
  26. package/dist/{devtools-BDp76luf.d.ts → devtools-BtIkN77t.d.cts} +14 -2
  27. package/dist/{devtools-5AipK9CX.d.cts → devtools-D2z4llpA.d.ts} +14 -2
  28. package/dist/index.cjs +60 -58
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +5 -5
  31. package/dist/index.d.ts +5 -5
  32. package/dist/index.dev.js +300 -74
  33. package/dist/index.dev.js.map +1 -1
  34. package/dist/index.js +13 -11
  35. package/dist/index.js.map +1 -1
  36. package/dist/internal-list.cjs +4 -4
  37. package/dist/internal-list.d.cts +2 -2
  38. package/dist/internal-list.d.ts +2 -2
  39. package/dist/internal-list.js +3 -3
  40. package/dist/internal.cjs +5 -5
  41. package/dist/internal.d.cts +6 -6
  42. package/dist/internal.d.ts +6 -6
  43. package/dist/internal.js +4 -4
  44. package/dist/jsx-dev-runtime.d.cts +671 -0
  45. package/dist/jsx-dev-runtime.d.ts +671 -0
  46. package/dist/jsx-runtime.d.cts +671 -0
  47. package/dist/jsx-runtime.d.ts +671 -0
  48. package/dist/{list-DL5DOFcO.d.ts → list-BKM6YOPq.d.ts} +2 -2
  49. package/dist/{list-hP7hQ9Vk.d.cts → list-Bi8dDF8Q.d.cts} +2 -2
  50. package/dist/loader.cjs +34 -28
  51. package/dist/loader.cjs.map +1 -1
  52. package/dist/loader.d.cts +2 -2
  53. package/dist/loader.d.ts +2 -2
  54. package/dist/loader.js +17 -11
  55. package/dist/loader.js.map +1 -1
  56. package/dist/{props-BpZz0AOq.d.cts → props-9chMyBGb.d.cts} +2 -2
  57. package/dist/{props-CjLH0JE-.d.ts → props-D1nj2p_3.d.ts} +2 -2
  58. package/dist/{resume-BJ4oHLi_.d.cts → resume-C5IKAIdh.d.ts} +2 -2
  59. package/dist/{resume-CuyJWXP_.d.ts → resume-DPZxmA95.d.cts} +2 -2
  60. package/dist/{scope-jPt5DHRT.d.ts → scope-BSkhJr0a.d.ts} +1 -1
  61. package/dist/{scope-BJCtq8hJ.d.cts → scope-Bn3sxem5.d.cts} +1 -1
  62. package/dist/{signal-C4ISF17w.d.cts → signal-Z4KkDk9h.d.cts} +12 -1
  63. package/dist/{signal-C4ISF17w.d.ts → signal-Z4KkDk9h.d.ts} +12 -1
  64. package/package.json +2 -2
  65. package/src/binding.ts +59 -29
  66. package/src/context.ts +4 -3
  67. package/src/devtools.ts +19 -2
  68. package/src/dom.ts +122 -42
  69. package/src/effect.ts +5 -5
  70. package/src/error-boundary.ts +5 -5
  71. package/src/hooks.ts +13 -5
  72. package/src/lifecycle.ts +48 -3
  73. package/src/list-helpers.ts +30 -13
  74. package/src/loader.ts +20 -12
  75. package/src/node-ops.ts +8 -5
  76. package/src/signal.ts +191 -18
  77. package/src/suspense.ts +5 -4
  78. package/src/transition.ts +9 -3
  79. package/dist/chunk-DXG3TARY.js.map +0 -1
  80. package/dist/chunk-LBE6DC3V.cjs.map +0 -1
  81. package/dist/chunk-N6ODUM2Y.js.map +0 -1
  82. package/dist/chunk-OAM7HABA.cjs.map +0 -1
  83. package/dist/chunk-PG4QX2I2.cjs +0 -111
  84. package/dist/chunk-PG4QX2I2.cjs.map +0 -1
  85. package/dist/chunk-T2LNV5Q5.js.map +0 -1
  86. package/dist/chunk-UBFDB6OL.cjs.map +0 -1
  87. /package/dist/{chunk-FVX77557.js.map → chunk-2UR2UWE2.js.map} +0 -0
@@ -203,9 +203,12 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
203
203
  function createKeyedListContainer<T = unknown>(
204
204
  startOverride?: Comment,
205
205
  endOverride?: Comment,
206
+ defaultOwnerDocument?: Document,
206
207
  ): KeyedListContainer<T> {
207
- const startMarker = startOverride ?? document.createComment('fict:list:start')
208
- const endMarker = endOverride ?? document.createComment('fict:list:end')
208
+ const markerOwnerDocument =
209
+ startOverride?.ownerDocument ?? endOverride?.ownerDocument ?? defaultOwnerDocument ?? document
210
+ const startMarker = startOverride ?? markerOwnerDocument.createComment('fict:list:start')
211
+ const endMarker = endOverride ?? markerOwnerDocument.createComment('fict:list:end')
209
212
 
210
213
  const dispose = () => {
211
214
  // Clean up all blocks
@@ -228,7 +231,9 @@ function createKeyedListContainer<T = unknown>(
228
231
  container.orderedIndexByKey.clear()
229
232
  return
230
233
  }
231
- const range = document.createRange()
234
+ const rangeOwnerDocument =
235
+ startMarker.ownerDocument ?? endMarker.ownerDocument ?? markerOwnerDocument
236
+ const range = rangeOwnerDocument.createRange()
232
237
  range.setStartBefore(startMarker)
233
238
  range.setEndAfter(endMarker)
234
239
  range.deleteContents()
@@ -284,12 +289,13 @@ function createKeyedBlock<T>(
284
289
 
285
290
  const indexSig = needsIndex
286
291
  ? createSignal<number>(index)
287
- : (((next?: number) => {
292
+ : (function indexSignal(next?: number) {
288
293
  if (arguments.length === 0) return index
289
294
  index = next as number
290
295
  return index
291
- }) as Signal<number>)
296
+ } as Signal<number>)
292
297
  const root = createRootContext(hostRoot)
298
+ const nodeOwnerDocument = root.ownerDocument ?? hostRoot?.ownerDocument ?? document
293
299
  const prevRoot = pushRoot(root)
294
300
  // maintaining proper cleanup chain. The scope will be disposed when
295
301
  // the root is destroyed, ensuring nested effects are properly cleaned up.
@@ -310,10 +316,10 @@ function createKeyedBlock<T>(
310
316
  rendered instanceof Node ||
311
317
  (Array.isArray(rendered) && rendered.every(n => n instanceof Node))
312
318
  ) {
313
- nodes = toNodeArray(rendered)
319
+ nodes = toNodeArray(rendered, nodeOwnerDocument)
314
320
  } else {
315
321
  const element = createElement(rendered as unknown as FictNode)
316
- nodes = toNodeArray(element)
322
+ nodes = toNodeArray(element, nodeOwnerDocument)
317
323
  }
318
324
  })
319
325
 
@@ -500,10 +506,18 @@ function createFineGrainedKeyedList<T>(
500
506
  startOverride?: Comment,
501
507
  endOverride?: Comment,
502
508
  ): KeyedListBinding {
503
- const container = createKeyedListContainer<T>(startOverride, endOverride)
504
509
  const hostRoot = getCurrentRoot()
510
+ const container = createKeyedListContainer<T>(
511
+ startOverride,
512
+ endOverride,
513
+ hostRoot?.ownerDocument ?? document,
514
+ )
515
+ const markerOwnerDocument =
516
+ container.startMarker.ownerDocument ?? hostRoot?.ownerDocument ?? document
505
517
  const useProvided = !!(startOverride && endOverride)
506
- const fragment = useProvided ? container.startMarker : document.createDocumentFragment()
518
+ const fragment = useProvided
519
+ ? container.startMarker
520
+ : markerOwnerDocument.createDocumentFragment()
507
521
  if (!useProvided) {
508
522
  ;(fragment as DocumentFragment).append(container.startMarker, container.endMarker)
509
523
  }
@@ -580,7 +594,7 @@ function createFineGrainedKeyedList<T>(
580
594
  withHydrationRange(
581
595
  container.startMarker.nextSibling,
582
596
  container.endMarker,
583
- parent.ownerDocument ?? document,
597
+ parent.ownerDocument ?? markerOwnerDocument,
584
598
  () => {
585
599
  for (let index = 0; index < newItems.length; index++) {
586
600
  const item = newItems[index]!
@@ -632,7 +646,7 @@ function createFineGrainedKeyedList<T>(
632
646
  destroyRoot(block.root)
633
647
  }
634
648
  // Use Range.deleteContents for efficient bulk DOM removal
635
- const range = document.createRange()
649
+ const range = (parent.ownerDocument ?? markerOwnerDocument).createRange()
636
650
  range.setStartAfter(container.startMarker)
637
651
  range.setEndBefore(container.endMarker)
638
652
  range.deleteContents()
@@ -934,7 +948,7 @@ function createFineGrainedKeyedList<T>(
934
948
 
935
949
  const waitForConnection = () => {
936
950
  if (connectObserver || typeof MutationObserver === 'undefined') return
937
- const root = container.startMarker.getRootNode?.() ?? document
951
+ const root = container.startMarker.getRootNode?.() ?? markerOwnerDocument
938
952
  const shadowRoot =
939
953
  root && root.nodeType === 11 && isShadowRoot(root as Node) ? (root as ShadowRoot) : null
940
954
  connectObserver = new MutationObserver(() => {
@@ -946,7 +960,10 @@ function createFineGrainedKeyedList<T>(
946
960
  }
947
961
  }
948
962
  })
949
- connectObserver.observe(document, { childList: true, subtree: true })
963
+ connectObserver.observe(markerOwnerDocument, { childList: true, subtree: true })
964
+ if (root && root.nodeType === 11) {
965
+ connectObserver.observe(root as Node, { childList: true, subtree: true })
966
+ }
950
967
  if (shadowRoot) {
951
968
  connectObserver.observe(shadowRoot, { childList: true, subtree: true })
952
969
  }
package/src/loader.ts CHANGED
@@ -447,19 +447,20 @@ function setupHoverPrefetch(doc: Document, delay: number): () => void {
447
447
  }
448
448
 
449
449
  function prefetchElementQrls(el: Element): void {
450
+ const ownerDocument = el.ownerDocument ?? (typeof document !== 'undefined' ? document : undefined)
450
451
  // Prefetch event handler QRLs
451
452
  const eventAttrs = ['on:click', 'on:input', 'on:change', 'on:submit', 'on:keydown', 'on:keyup']
452
453
  for (const attr of eventAttrs) {
453
454
  const qrl = el.getAttribute(attr)
454
455
  if (qrl) {
455
- prefetchQrl(qrl)
456
+ prefetchQrl(qrl, ownerDocument)
456
457
  }
457
458
  }
458
459
 
459
460
  // Prefetch resume handler QRL
460
461
  const resumeQrl = el.getAttribute('data-fict-h')
461
462
  if (resumeQrl) {
462
- prefetchQrl(resumeQrl)
463
+ prefetchQrl(resumeQrl, ownerDocument)
463
464
  }
464
465
 
465
466
  // Also check children for nested QRLs
@@ -470,17 +471,17 @@ function prefetchElementQrls(el: Element): void {
470
471
  for (const attr of eventAttrs) {
471
472
  const qrl = child.getAttribute(attr)
472
473
  if (qrl) {
473
- prefetchQrl(qrl)
474
+ prefetchQrl(qrl, ownerDocument)
474
475
  }
475
476
  }
476
477
  const childResumeQrl = child.getAttribute('data-fict-h')
477
478
  if (childResumeQrl) {
478
- prefetchQrl(childResumeQrl)
479
+ prefetchQrl(childResumeQrl, ownerDocument)
479
480
  }
480
481
  })
481
482
  }
482
483
 
483
- function prefetchQrl(qrl: string): void {
484
+ function prefetchQrl(qrl: string, ownerDocument?: Document): void {
484
485
  const { url } = parseQrl(qrl)
485
486
  if (!url || prefetchedUrls.has(url)) return
486
487
 
@@ -490,12 +491,13 @@ function prefetchQrl(qrl: string): void {
490
491
  const resolvedUrl = resolveModuleUrl(url)
491
492
 
492
493
  // Use modulepreload link for best browser support
493
- if (typeof document !== 'undefined') {
494
- const link = document.createElement('link')
494
+ const doc = ownerDocument ?? (typeof document !== 'undefined' ? document : undefined)
495
+ if (doc) {
496
+ const link = doc.createElement('link')
495
497
  link.rel = 'modulepreload'
496
498
  link.href = resolvedUrl
497
499
  link.crossOrigin = 'anonymous'
498
- document.head.appendChild(link)
500
+ doc.head?.appendChild(link)
499
501
  }
500
502
  }
501
503
 
@@ -507,9 +509,15 @@ function prefetchQrl(qrl: string): void {
507
509
  function handleResumableEvent(event: Event): void {
508
510
  const promise = handleResumableEventAsync(event)
509
511
  pendingHandlers.add(promise)
510
- promise.finally(() => {
511
- pendingHandlers.delete(promise)
512
- })
512
+ void promise
513
+ .catch(error => {
514
+ if (typeof console !== 'undefined' && typeof console.error === 'function') {
515
+ console.error('[fict/loader] Failed to handle resumable event.', error)
516
+ }
517
+ })
518
+ .finally(() => {
519
+ pendingHandlers.delete(promise)
520
+ })
513
521
  }
514
522
 
515
523
  async function handleResumableEventAsync(event: Event): Promise<void> {
@@ -535,7 +543,7 @@ async function handleResumableEventAsync(event: Event): Promise<void> {
535
543
  expectedVersion: FICT_SSR_SNAPSHOT_SCHEMA_VERSION,
536
544
  scopeId,
537
545
  })
538
- return
546
+ continue
539
547
  }
540
548
  __fictEnsureScope(scopeId, host, snapshot)
541
549
 
package/src/node-ops.ts CHANGED
@@ -7,7 +7,10 @@
7
7
  * Convert a value to a flat array of DOM nodes.
8
8
  * Defensively handles proxies and non-DOM values.
9
9
  */
10
- export function toNodeArray(node: Node | Node[] | unknown): Node[] {
10
+ export function toNodeArray(
11
+ node: Node | Node[] | unknown,
12
+ ownerDocument: Document = document,
13
+ ): Node[] {
11
14
  try {
12
15
  if (Array.isArray(node)) {
13
16
  // Preserve original array reference when it's already a flat Node array
@@ -29,7 +32,7 @@ export function toNodeArray(node: Node | Node[] | unknown): Node[] {
29
32
  }
30
33
  const result: Node[] = []
31
34
  for (const item of node) {
32
- result.push(...toNodeArray(item))
35
+ result.push(...toNodeArray(item, ownerDocument))
33
36
  }
34
37
  return result
35
38
  }
@@ -62,7 +65,7 @@ export function toNodeArray(node: Node | Node[] | unknown): Node[] {
62
65
  try {
63
66
  // Duck-type BindingHandle-like values
64
67
  if (typeof node === 'object' && node !== null && 'marker' in node) {
65
- return toNodeArray((node as { marker: unknown }).marker)
68
+ return toNodeArray((node as { marker: unknown }).marker, ownerDocument)
66
69
  }
67
70
  } catch {
68
71
  // Ignore property check error
@@ -70,9 +73,9 @@ export function toNodeArray(node: Node | Node[] | unknown): Node[] {
70
73
 
71
74
  // Primitive fallback
72
75
  try {
73
- return [document.createTextNode(String(node))]
76
+ return [ownerDocument.createTextNode(String(node))]
74
77
  } catch {
75
- return [document.createTextNode('')]
78
+ return [ownerDocument.createTextNode('')]
76
79
  }
77
80
  }
78
81
 
package/src/signal.ts CHANGED
@@ -93,6 +93,18 @@ export interface MemoOptions<T> {
93
93
  name?: string
94
94
  /** Source location */
95
95
  devToolsSource?: string
96
+ /** Internal memo created by compiler runtime plumbing (hidden from DevTools) */
97
+ internal?: boolean
98
+ }
99
+
100
+ /**
101
+ * Options for creating an effect
102
+ */
103
+ export interface EffectOptions {
104
+ /** Debug name */
105
+ name?: string
106
+ /** Source location */
107
+ devToolsSource?: string
96
108
  }
97
109
 
98
110
  /**
@@ -145,6 +157,8 @@ export interface ComputedNode<T = unknown> extends BaseNode {
145
157
  name?: string
146
158
  /** Source location */
147
159
  devToolsSource?: string
160
+ /** Hide this computed from DevTools (used by compiler-internal memos) */
161
+ devToolsInternal?: boolean
148
162
  }
149
163
 
150
164
  /**
@@ -161,6 +175,10 @@ export interface EffectNode extends BaseNode {
161
175
  runCleanup?: () => void
162
176
  /** Root context for error/suspense handling */
163
177
  root?: RootContext
178
+ /** Debug name */
179
+ name?: string
180
+ /** Source location */
181
+ devToolsSource?: string
164
182
  /** Devtools ID */
165
183
  __id?: number | undefined
166
184
  }
@@ -788,6 +806,16 @@ function purgeDeps(sub: ReactiveNode): void {
788
806
  * @param node - The node to dispose
789
807
  */
790
808
  function disposeNode(node: ReactiveNode): void {
809
+ if (isDev) {
810
+ if ('fn' in node && typeof node.fn === 'function') {
811
+ disposeEffectDevtools(node as EffectNode)
812
+ } else if ('getter' in node && typeof node.getter === 'function') {
813
+ disposeComputedDevtools(node as ComputedNode)
814
+ } else if ('currentValue' in node) {
815
+ disposeSignalDevtools(node as SignalNode)
816
+ }
817
+ }
818
+
791
819
  node.depsTail = undefined
792
820
  node.flags = 0
793
821
  purgeDeps(node)
@@ -858,6 +886,7 @@ function runEffect(e: EffectNode): void {
858
886
  const flags = e.flags
859
887
  const runCleanup = () => {
860
888
  if (!e.runCleanup) return
889
+ if (isDev) effectCleanupDevtools(e)
861
890
  inCleanup = true
862
891
  activeCleanupFlushId = currentFlushId
863
892
  try {
@@ -871,7 +900,6 @@ function runEffect(e: EffectNode): void {
871
900
  // Run cleanup before re-run; values are still the previous commit.
872
901
  runCleanup()
873
902
  ++cycle
874
- if (isDev) effectRunDevtools(e)
875
903
  e.depsTail = undefined
876
904
  e.flags = WatchingRunning
877
905
  const prevSub = activeSub
@@ -913,7 +941,6 @@ function runEffect(e: EffectNode): void {
913
941
  // Cleanup reads should observe previous values for this flush.
914
942
  runCleanup()
915
943
  ++cycle
916
- if (isDev) effectRunDevtools(e)
917
944
  e.depsTail = undefined
918
945
  e.flags = WatchingRunning
919
946
  const prevSub = activeSub
@@ -956,20 +983,31 @@ export function scheduleFlush(): void {
956
983
  */
957
984
  function flush(): void {
958
985
  beginFlushGuard()
986
+ let flushReported = false
987
+ const finishFlush = () => {
988
+ if (flushReported && isDev) {
989
+ flushEndDevtools()
990
+ }
991
+ endFlushGuard()
992
+ }
959
993
  if (batchDepth > 0) {
960
994
  // If batching is active, defer until the batch completes
961
995
  scheduleFlush()
962
- endFlushGuard()
996
+ finishFlush()
963
997
  return
964
998
  }
965
999
  const hasWork = highPriorityQueue.length > 0 || lowPriorityQueue.length > 0
966
1000
  if (!hasWork) {
967
1001
  flushScheduled = false
968
- endFlushGuard()
1002
+ finishFlush()
969
1003
  return
970
1004
  }
971
1005
  currentFlushId++
972
1006
  flushScheduled = false
1007
+ if (isDev) {
1008
+ flushStartDevtools()
1009
+ flushReported = true
1010
+ }
973
1011
 
974
1012
  // 1. Process all high-priority effects first
975
1013
  let highIndex = 0
@@ -993,7 +1031,7 @@ function flush(): void {
993
1031
  highPriorityQueue.length = 0
994
1032
  lowPriorityQueue.length = 0
995
1033
  flushScheduled = false
996
- endFlushGuard()
1034
+ finishFlush()
997
1035
  return
998
1036
  }
999
1037
  highIndex++
@@ -1011,7 +1049,7 @@ function flush(): void {
1011
1049
  lowPriorityQueue.length -= lowIndex
1012
1050
  }
1013
1051
  scheduleFlush()
1014
- endFlushGuard()
1052
+ finishFlush()
1015
1053
  return
1016
1054
  }
1017
1055
  const e = lowPriorityQueue[lowIndex]!
@@ -1033,7 +1071,7 @@ function flush(): void {
1033
1071
  highPriorityQueue.length = 0
1034
1072
  lowPriorityQueue.length = 0
1035
1073
  flushScheduled = false
1036
- endFlushGuard()
1074
+ finishFlush()
1037
1075
  return
1038
1076
  }
1039
1077
  lowIndex++
@@ -1041,7 +1079,7 @@ function flush(): void {
1041
1079
  }
1042
1080
  lowPriorityQueue.length = 0
1043
1081
 
1044
- endFlushGuard()
1082
+ finishFlush()
1045
1083
  }
1046
1084
  // ============================================================================
1047
1085
  // Signal - Inline optimized version
@@ -1138,6 +1176,7 @@ export function computed<T>(
1138
1176
  if (options?.equals !== undefined) c.equals = options.equals
1139
1177
  if (options?.name !== undefined) c.name = options.name
1140
1178
  if (options?.devToolsSource !== undefined) c.devToolsSource = options.devToolsSource
1179
+ if (options?.internal === true) c.devToolsInternal = true
1141
1180
  if (isDev) registerComputedDevtools(c)
1142
1181
  const bound = (computedOper as (this: ComputedNode<T>) => T).bind(
1143
1182
  c as any,
@@ -1201,9 +1240,10 @@ function computedOper<T>(this: ComputedNode<T>): T {
1201
1240
  /**
1202
1241
  * Create a reactive effect
1203
1242
  * @param fn - The effect function
1243
+ * @param options - Effect options
1204
1244
  * @returns An effect disposer function
1205
1245
  */
1206
- export function effect(fn: () => void): EffectDisposer {
1246
+ export function effect(fn: () => void, options?: EffectOptions): EffectDisposer {
1207
1247
  const e: EffectNode = {
1208
1248
  fn,
1209
1249
  subs: undefined,
@@ -1211,6 +1251,8 @@ export function effect(fn: () => void): EffectDisposer {
1211
1251
  deps: undefined,
1212
1252
  depsTail: undefined,
1213
1253
  flags: WatchingRunning,
1254
+ ...(options?.name !== undefined ? { name: options.name } : {}),
1255
+ ...(options?.devToolsSource !== undefined ? { devToolsSource: options.devToolsSource } : {}),
1214
1256
  __id: undefined as number | undefined,
1215
1257
  }
1216
1258
  const root = getCurrentRoot()
@@ -1219,6 +1261,7 @@ export function effect(fn: () => void): EffectDisposer {
1219
1261
  }
1220
1262
 
1221
1263
  if (isDev) registerEffectDevtools(e)
1264
+ e.fn = wrapEffectFnWithDevtoolsTiming(e, fn)
1222
1265
 
1223
1266
  const prevSub = activeSub
1224
1267
  if (prevSub !== undefined) link(e, prevSub, 0)
@@ -1227,8 +1270,7 @@ export function effect(fn: () => void): EffectDisposer {
1227
1270
  let didThrow = false
1228
1271
  let thrown: unknown
1229
1272
  try {
1230
- if (isDev) effectRunDevtools(e)
1231
- fn()
1273
+ e.fn()
1232
1274
  } catch (err) {
1233
1275
  didThrow = true
1234
1276
  thrown = err
@@ -1261,6 +1303,7 @@ export function effectWithCleanup(
1261
1303
  fn: () => void,
1262
1304
  cleanupRunner: () => void,
1263
1305
  root?: RootContext,
1306
+ options?: EffectOptions,
1264
1307
  ): EffectDisposer {
1265
1308
  const e: EffectNode = {
1266
1309
  fn,
@@ -1270,6 +1313,8 @@ export function effectWithCleanup(
1270
1313
  depsTail: undefined,
1271
1314
  flags: WatchingRunning,
1272
1315
  runCleanup: cleanupRunner,
1316
+ ...(options?.name !== undefined ? { name: options.name } : {}),
1317
+ ...(options?.devToolsSource !== undefined ? { devToolsSource: options.devToolsSource } : {}),
1273
1318
  __id: undefined as number | undefined,
1274
1319
  }
1275
1320
  const resolvedRoot = root ?? getCurrentRoot()
@@ -1278,6 +1323,7 @@ export function effectWithCleanup(
1278
1323
  }
1279
1324
 
1280
1325
  if (isDev) registerEffectDevtools(e)
1326
+ e.fn = wrapEffectFnWithDevtoolsTiming(e, fn)
1281
1327
 
1282
1328
  const prevSub = activeSub
1283
1329
  if (prevSub !== undefined) link(e, prevSub, 0)
@@ -1286,8 +1332,7 @@ export function effectWithCleanup(
1286
1332
  let didThrow = false
1287
1333
  let thrown: unknown
1288
1334
  try {
1289
- if (isDev) effectRunDevtools(e)
1290
- fn()
1335
+ e.fn()
1291
1336
  } catch (err) {
1292
1337
  didThrow = true
1293
1338
  thrown = err
@@ -1385,13 +1430,24 @@ export function trigger(fn: () => void): void {
1385
1430
  * Start a batch of updates
1386
1431
  */
1387
1432
  export function startBatch(): void {
1433
+ const enteringOuterBatch = batchDepth === 0
1388
1434
  ++batchDepth
1435
+ if (enteringOuterBatch && isDev) {
1436
+ batchStartDevtools()
1437
+ }
1389
1438
  }
1390
1439
  /**
1391
1440
  * End a batch of updates and flush effects
1392
1441
  */
1393
1442
  export function endBatch(): void {
1394
- if (--batchDepth === 0) flush()
1443
+ if (batchDepth === 0) return
1444
+ --batchDepth
1445
+ if (batchDepth === 0) {
1446
+ if (isDev) {
1447
+ batchEndDevtools()
1448
+ }
1449
+ flush()
1450
+ }
1395
1451
  }
1396
1452
  /**
1397
1453
  * Execute a function in a batch
@@ -1399,7 +1455,11 @@ export function endBatch(): void {
1399
1455
  * @returns The return value of the function
1400
1456
  */
1401
1457
  export function batch<T>(fn: () => T): T {
1458
+ const enteringOuterBatch = batchDepth === 0
1402
1459
  ++batchDepth
1460
+ if (enteringOuterBatch && isDev) {
1461
+ batchStartDevtools()
1462
+ }
1403
1463
  let result!: T
1404
1464
  let error: unknown
1405
1465
  try {
@@ -1409,6 +1469,9 @@ export function batch<T>(fn: () => T): T {
1409
1469
  } finally {
1410
1470
  --batchDepth
1411
1471
  if (batchDepth === 0) {
1472
+ if (isDev) {
1473
+ batchEndDevtools()
1474
+ }
1412
1475
  try {
1413
1476
  flush()
1414
1477
  } catch (flushErr) {
@@ -1463,6 +1526,7 @@ export function __resetReactiveState(): void {
1463
1526
  cycle = 0
1464
1527
  currentFlushId = 0
1465
1528
  activeCleanupFlushId = 0
1529
+ clearDevtoolsSignalSetters()
1466
1530
  }
1467
1531
  /**
1468
1532
  * Execute a function without tracking dependencies
@@ -1588,12 +1652,25 @@ interface DevtoolsIdentifiable {
1588
1652
 
1589
1653
  let registerSignalDevtools: <T>(node: SignalNode<T>) => number | undefined = () => undefined
1590
1654
  let updateSignalDevtools: <T>(node: SignalNode<T>, value: unknown) => void = () => {}
1655
+ let disposeSignalDevtools: <T>(node: SignalNode<T>) => void = () => {}
1591
1656
  let registerComputedDevtools: <T>(node: ComputedNode<T>) => number | undefined = () => undefined
1592
1657
  let updateComputedDevtools: <T>(node: ComputedNode<T>, value: unknown) => void = () => {}
1658
+ let disposeComputedDevtools: <T>(node: ComputedNode<T>) => void = () => {}
1593
1659
  let registerEffectDevtools: (node: EffectNode) => number | undefined = () => undefined
1594
- let effectRunDevtools: (node: EffectNode) => void = () => {}
1660
+ let effectRunDevtools: (node: EffectNode, duration?: number) => void = () => {}
1661
+ let wrapEffectFnWithDevtoolsTiming: (node: EffectNode, fn: () => void) => () => void = (
1662
+ _node,
1663
+ fn,
1664
+ ) => fn
1665
+ let effectCleanupDevtools: (node: EffectNode) => void = () => {}
1666
+ let disposeEffectDevtools: (node: EffectNode) => void = () => {}
1595
1667
  let trackDependencyDevtools: (dep: ReactiveNode, sub: ReactiveNode) => void = () => {}
1596
1668
  let untrackDependencyDevtools: (dep: ReactiveNode, sub: ReactiveNode) => void = () => {}
1669
+ let batchStartDevtools: () => void = () => {}
1670
+ let batchEndDevtools: () => void = () => {}
1671
+ let flushStartDevtools: () => void = () => {}
1672
+ let flushEndDevtools: () => void = () => {}
1673
+ let clearDevtoolsSignalSetters: () => void = () => {}
1597
1674
 
1598
1675
  // Keep this as a direct conditional expression (instead of `if (isDev)`) so
1599
1676
  // bundlers can eliminate the entire devtools setup block when `__DEV__` is
@@ -1606,6 +1683,25 @@ if (
1606
1683
  // Unified ID counter for all reactive nodes (signal/computed/effect)
1607
1684
  // to prevent ID collisions when storing in single devtools maps
1608
1685
  let nextDevtoolsId = 0
1686
+ const getSignalSetterMap = () => {
1687
+ if (typeof globalThis === 'undefined') return undefined
1688
+ const global = globalThis as typeof globalThis & {
1689
+ __FICT_DEVTOOLS_SIGNALS__?: Map<number, (value: unknown) => void>
1690
+ }
1691
+ if (!global.__FICT_DEVTOOLS_SIGNALS__) {
1692
+ global.__FICT_DEVTOOLS_SIGNALS__ = new Map<number, (value: unknown) => void>()
1693
+ }
1694
+ return global.__FICT_DEVTOOLS_SIGNALS__
1695
+ }
1696
+
1697
+ const getExistingSignalSetterMap = () => {
1698
+ if (typeof globalThis === 'undefined') return undefined
1699
+ return (
1700
+ globalThis as typeof globalThis & {
1701
+ __FICT_DEVTOOLS_SIGNALS__?: Map<number, (value: unknown) => void>
1702
+ }
1703
+ ).__FICT_DEVTOOLS_SIGNALS__
1704
+ }
1609
1705
 
1610
1706
  registerSignalDevtools = node => {
1611
1707
  const hook = getDevtoolsHook()
@@ -1618,6 +1714,9 @@ if (
1618
1714
  if (ownerId !== undefined) (options as any).ownerId = ownerId
1619
1715
  hook.registerSignal(id, node.currentValue, options)
1620
1716
  ;(node as SignalNode & DevtoolsIdentifiable).__id = id
1717
+ getSignalSetterMap()?.set(id, value => {
1718
+ signalOper.call(node as SignalNode<unknown>, value)
1719
+ })
1621
1720
  return id
1622
1721
  }
1623
1722
 
@@ -1628,9 +1727,20 @@ if (
1628
1727
  if (id) hook.updateSignal(id, value)
1629
1728
  }
1630
1729
 
1730
+ disposeSignalDevtools = node => {
1731
+ const identifiable = node as SignalNode & DevtoolsIdentifiable
1732
+ const id = identifiable.__id
1733
+ if (!id) return
1734
+ const hook = getDevtoolsHook()
1735
+ hook?.disposeSignal?.(id)
1736
+ getExistingSignalSetterMap()?.delete(id)
1737
+ delete identifiable.__id
1738
+ }
1739
+
1631
1740
  registerComputedDevtools = node => {
1632
1741
  const hook = getDevtoolsHook()
1633
1742
  if (!hook) return undefined
1743
+ if (node.devToolsInternal) return undefined
1634
1744
  const id = ++nextDevtoolsId
1635
1745
  const options: { name?: string; source?: string } = {}
1636
1746
  if (node.name !== undefined) options.name = node.name
@@ -1650,21 +1760,60 @@ if (
1650
1760
  if (id) hook.updateComputed(id, value)
1651
1761
  }
1652
1762
 
1763
+ disposeComputedDevtools = node => {
1764
+ const identifiable = node as ComputedNode & DevtoolsIdentifiable
1765
+ const id = identifiable.__id
1766
+ if (!id) return
1767
+ const hook = getDevtoolsHook()
1768
+ hook?.disposeComputed?.(id)
1769
+ delete identifiable.__id
1770
+ }
1771
+
1653
1772
  registerEffectDevtools = node => {
1654
1773
  const hook = getDevtoolsHook()
1655
1774
  if (!hook) return undefined
1656
1775
  const id = ++nextDevtoolsId
1776
+ const options: { ownerId?: number; source?: string } = {}
1657
1777
  const ownerId = __fictGetCurrentComponentId()
1658
- hook.registerEffect(id, ownerId !== undefined ? { ownerId } : undefined)
1778
+ if (ownerId !== undefined) options.ownerId = ownerId
1779
+ if (node.devToolsSource !== undefined) options.source = node.devToolsSource
1780
+ hook.registerEffect(id, Object.keys(options).length > 0 ? options : undefined)
1659
1781
  ;(node as EffectNode & DevtoolsIdentifiable).__id = id
1660
1782
  return id
1661
1783
  }
1662
1784
 
1663
- effectRunDevtools = node => {
1785
+ effectRunDevtools = (node, duration) => {
1786
+ const hook = getDevtoolsHook()
1787
+ if (!hook) return
1788
+ const id = (node as EffectNode & DevtoolsIdentifiable).__id
1789
+ if (id) hook.effectRun(id, duration)
1790
+ }
1791
+
1792
+ wrapEffectFnWithDevtoolsTiming = (node, fn) => {
1793
+ return () => {
1794
+ const startedAt = performance.now()
1795
+ try {
1796
+ fn()
1797
+ } finally {
1798
+ effectRunDevtools(node, performance.now() - startedAt)
1799
+ }
1800
+ }
1801
+ }
1802
+
1803
+ effectCleanupDevtools = node => {
1664
1804
  const hook = getDevtoolsHook()
1665
1805
  if (!hook) return
1666
1806
  const id = (node as EffectNode & DevtoolsIdentifiable).__id
1667
- if (id) hook.effectRun(id)
1807
+ if (id) hook.effectCleanup?.(id)
1808
+ }
1809
+
1810
+ disposeEffectDevtools = node => {
1811
+ const identifiable = node as EffectNode & DevtoolsIdentifiable
1812
+ const id = identifiable.__id
1813
+ if (!id) return
1814
+ const hook = getDevtoolsHook()
1815
+ hook?.disposeEffect?.(id)
1816
+ delete identifiable.__id
1668
1817
  }
1669
1818
 
1670
1819
  trackDependencyDevtools = (dep, sub) => {
@@ -1682,6 +1831,30 @@ if (
1682
1831
  const subId = (sub as ReactiveNode & DevtoolsIdentifiable).__id
1683
1832
  if (depId && subId) hook.untrackDependency(subId, depId)
1684
1833
  }
1834
+
1835
+ batchStartDevtools = () => {
1836
+ const hook = getDevtoolsHook()
1837
+ hook?.batchStart?.()
1838
+ }
1839
+
1840
+ batchEndDevtools = () => {
1841
+ const hook = getDevtoolsHook()
1842
+ hook?.batchEnd?.()
1843
+ }
1844
+
1845
+ flushStartDevtools = () => {
1846
+ const hook = getDevtoolsHook()
1847
+ hook?.flushStart?.()
1848
+ }
1849
+
1850
+ flushEndDevtools = () => {
1851
+ const hook = getDevtoolsHook()
1852
+ hook?.flushEnd?.()
1853
+ }
1854
+
1855
+ clearDevtoolsSignalSetters = () => {
1856
+ getExistingSignalSetterMap()?.clear()
1857
+ }
1685
1858
  }
1686
1859
 
1687
1860
  // ============================================================================