@fictjs/runtime 0.0.3 → 0.0.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fictjs/runtime",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Fict reactive runtime",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -50,7 +50,7 @@
50
50
  "src"
51
51
  ],
52
52
  "devDependencies": {
53
- "jsdom": "^27.2.0",
53
+ "jsdom": "^27.4.0",
54
54
  "tsup": "^8.5.1"
55
55
  },
56
56
  "scripts": {
package/src/binding.ts CHANGED
@@ -92,6 +92,44 @@ export function unwrap<T>(value: MaybeReactive<T>): T {
92
92
  return isReactive(value) ? (value as () => T)() : value
93
93
  }
94
94
 
95
+ /**
96
+ * Invoke an event handler or handler accessor in a safe way.
97
+ * Supports handlers that return another handler and handlers that expect an
98
+ * optional data payload followed by the event.
99
+ */
100
+ export function callEventHandler(
101
+ handler: EventListenerOrEventListenerObject | null | undefined,
102
+ event: Event,
103
+ node?: EventTarget | null,
104
+ data?: unknown,
105
+ ): void {
106
+ if (!handler) return
107
+
108
+ const context = (node ?? event.currentTarget ?? undefined) as EventTarget | undefined
109
+ const invoke = (fn: EventListenerOrEventListenerObject | null | undefined): void => {
110
+ if (typeof fn === 'function') {
111
+ const result =
112
+ data === undefined
113
+ ? (fn as EventListener).call(context, event)
114
+ : (fn as (data: unknown, e: Event) => unknown).call(context, data, event)
115
+
116
+ if (typeof result === 'function' && result !== fn) {
117
+ if (data === undefined) {
118
+ ;(result as EventListener).call(context, event)
119
+ } else {
120
+ ;(result as (data: unknown, e: Event) => unknown).call(context, data, event)
121
+ }
122
+ } else if (result && typeof (result as EventListenerObject).handleEvent === 'function') {
123
+ ;(result as EventListenerObject).handleEvent.call(result as EventListenerObject, event)
124
+ }
125
+ } else if (fn && typeof fn.handleEvent === 'function') {
126
+ fn.handleEvent.call(fn, event)
127
+ }
128
+ }
129
+
130
+ invoke(handler)
131
+ }
132
+
95
133
  export const PRIMITIVE_PROXY = Symbol('fict:primitive-proxy')
96
134
  const PRIMITIVE_PROXY_RAW_VALUE = Symbol('fict:primitive-proxy:raw-value')
97
135
 
@@ -908,15 +946,10 @@ function globalEventHandler(e: Event): void {
908
946
  const hasData = rawData !== undefined
909
947
  const resolvedNodeData = hasData ? resolveData(rawData) : undefined
910
948
  if (typeof handler === 'function') {
911
- // Handler with optional data: handler(data, event?) or handler(event)
912
- if (hasData) {
913
- ;(handler as (data: unknown, e: Event) => void).call(node, resolvedNodeData, e)
914
- } else {
915
- ;(handler as EventListener).call(node, e)
916
- }
949
+ callEventHandler(handler, e, node, hasData ? resolvedNodeData : undefined)
917
950
  } else if (Array.isArray(handler)) {
918
951
  const tupleData = resolveData(handler[1])
919
- ;(handler[0] as (data: unknown, e: Event) => void).call(node, tupleData, e)
952
+ callEventHandler(handler[0], e, node, tupleData)
920
953
  }
921
954
  if (e.cancelBubble) return false
922
955
  }
@@ -1050,31 +1083,20 @@ export function bindEvent(
1050
1083
  // Ensure global delegation is active for this event
1051
1084
  delegateEvents([eventName])
1052
1085
 
1053
- const createWrapped = (
1054
- resolve: () => EventListenerOrEventListenerObject | null | undefined,
1055
- ) => {
1056
- const wrapped = function (this: any, ...args: any[]) {
1057
- try {
1058
- const fn = resolve()
1059
- if (typeof fn === 'function') {
1060
- return (fn as EventListener).apply(this, args as [Event])
1061
- } else if (fn && typeof fn.handleEvent === 'function') {
1062
- return fn.handleEvent.apply(fn, args as [Event])
1063
- }
1064
- } catch (err) {
1065
- handleError(err, { source: 'event', eventName }, rootRef)
1066
- }
1067
- }
1068
- return wrapped
1069
- }
1070
-
1071
1086
  const resolveHandler = isReactive(handler)
1072
1087
  ? (handler as () => EventListenerOrEventListenerObject | null | undefined)
1073
1088
  : () => handler
1074
1089
 
1075
1090
  // Cache a single wrapper that resolves the latest handler when invoked
1076
1091
  // @ts-expect-error - using dynamic property for delegation
1077
- el[key] = createWrapped(resolveHandler)
1092
+ el[key] = function (this: any, ...args: any[]) {
1093
+ try {
1094
+ const fn = resolveHandler()
1095
+ callEventHandler(fn as EventListenerOrEventListenerObject, args[0] as Event, el)
1096
+ } catch (err) {
1097
+ handleError(err, { source: 'event', eventName }, rootRef)
1098
+ }
1099
+ }
1078
1100
 
1079
1101
  // Cleanup: remove property (no effect needed for static or reactive)
1080
1102
  return () => {
@@ -1091,11 +1113,7 @@ export function bindEvent(
1091
1113
  const wrapped: EventListener = event => {
1092
1114
  try {
1093
1115
  const resolved = getHandler()
1094
- if (typeof resolved === 'function') {
1095
- ;(resolved as EventListener)(event)
1096
- } else if (resolved && typeof (resolved as EventListenerObject).handleEvent === 'function') {
1097
- ;(resolved as EventListenerObject).handleEvent(event)
1098
- }
1116
+ callEventHandler(resolved as EventListenerOrEventListenerObject, event, el)
1099
1117
  } catch (err) {
1100
1118
  if (handleError(err, { source: 'event', eventName }, rootRef)) {
1101
1119
  return
package/src/index.ts CHANGED
@@ -3,9 +3,11 @@
3
3
  // ============================================================================
4
4
 
5
5
  export { createSignal, createSelector, type Signal, $state } from './signal'
6
+ export { effectScope } from './signal'
6
7
  export { createStore, type Store } from './store'
7
8
  export { createMemo, type Memo, $memo } from './memo'
8
9
  export { createEffect, createRenderEffect, type Effect, $effect } from './effect'
10
+ export { createScope, runInScope, type ReactiveScope } from './scope'
9
11
  export {
10
12
  __fictUseContext,
11
13
  __fictPushContext,
@@ -83,6 +85,7 @@ export {
83
85
  bindStyle,
84
86
  bindClass,
85
87
  bindEvent,
88
+ callEventHandler,
86
89
  bindProperty,
87
90
  bindRef,
88
91
  insert,
package/src/scope.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { isReactive, type MaybeReactive } from './binding'
2
+ import { createEffect } from './effect'
3
+ import { createRoot, onCleanup, registerRootCleanup } from './lifecycle'
4
+
5
+ export { effectScope } from './signal'
6
+
7
+ export interface ReactiveScope {
8
+ run<T>(fn: () => T): T
9
+ stop(): void
10
+ }
11
+
12
+ /**
13
+ * Create an explicit reactive scope that can contain effects/memos and be stopped manually.
14
+ * The scope registers with the current root for cleanup.
15
+ */
16
+ export function createScope(): ReactiveScope {
17
+ let dispose: (() => void) | null = null
18
+
19
+ const stop = () => {
20
+ if (dispose) {
21
+ dispose()
22
+ dispose = null
23
+ }
24
+ }
25
+
26
+ const run = <T>(fn: () => T): T => {
27
+ stop()
28
+ const { dispose: rootDispose, value } = createRoot(fn)
29
+ dispose = rootDispose
30
+ return value
31
+ }
32
+
33
+ registerRootCleanup(stop)
34
+ return { run, stop }
35
+ }
36
+
37
+ /**
38
+ * Run a block of reactive code inside a managed scope that follows a boolean flag.
39
+ * When the flag turns false, the scope is disposed and all contained effects/memos are cleaned up.
40
+ */
41
+ export function runInScope(flag: MaybeReactive<boolean>, fn: () => void): void {
42
+ const scope = createScope()
43
+ const evaluate = () => (isReactive(flag) ? (flag as () => boolean)() : !!flag)
44
+
45
+ createEffect(() => {
46
+ const enabled = evaluate()
47
+ if (enabled) {
48
+ scope.run(fn)
49
+ } else {
50
+ scope.stop()
51
+ }
52
+ })
53
+
54
+ onCleanup(scope.stop)
55
+ }
package/src/slim.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  export { createSignal, createSelector, $state } from './signal'
10
10
  export { createMemo } from './memo'
11
11
  export { createEffect, createRenderEffect } from './effect'
12
+ export { effectScope, createScope, runInScope, type ReactiveScope } from './scope'
12
13
  export { batch, untrack } from './scheduler'
13
14
 
14
15
  // DOM rendering