@fictjs/runtime 0.0.12 → 0.0.14
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/dist/advanced.cjs +79 -0
- package/dist/advanced.cjs.map +1 -0
- package/dist/advanced.d.cts +50 -0
- package/dist/advanced.d.ts +50 -0
- package/dist/advanced.js +79 -0
- package/dist/advanced.js.map +1 -0
- package/dist/chunk-624QY53A.cjs +45 -0
- package/dist/chunk-624QY53A.cjs.map +1 -0
- package/dist/chunk-F3AIYQB7.js +45 -0
- package/dist/chunk-F3AIYQB7.js.map +1 -0
- package/dist/chunk-GJTYOFMO.cjs +109 -0
- package/dist/chunk-GJTYOFMO.cjs.map +1 -0
- package/dist/chunk-IUZXKAAY.js +109 -0
- package/dist/chunk-IUZXKAAY.js.map +1 -0
- package/dist/{slim.cjs → chunk-PMF6MWEV.cjs} +2557 -3110
- package/dist/chunk-PMF6MWEV.cjs.map +1 -0
- package/dist/{slim.js → chunk-RY4WDS6R.js} +2596 -3097
- package/dist/chunk-RY4WDS6R.js.map +1 -0
- package/dist/context-B7UYnfzM.d.ts +153 -0
- package/dist/context-UXySaqI_.d.cts +153 -0
- package/dist/effect-Auji1rz9.d.cts +350 -0
- package/dist/effect-Auji1rz9.d.ts +350 -0
- package/dist/index.cjs +108 -4441
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1492
- package/dist/index.d.ts +5 -1492
- package/dist/index.dev.js +1020 -2788
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +63 -4301
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +901 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +158 -0
- package/dist/internal.d.ts +158 -0
- package/dist/internal.js +901 -0
- package/dist/internal.js.map +1 -0
- package/dist/{jsx-dev-runtime.d.ts → props-CrOMYbLv.d.cts} +107 -18
- package/dist/{jsx-dev-runtime.d.cts → props-ES0Ag_Wd.d.ts} +107 -18
- package/dist/scope-DKYzWfTn.d.cts +55 -0
- package/dist/scope-S6eAzBJZ.d.ts +55 -0
- package/package.json +10 -5
- package/src/advanced.ts +101 -0
- package/src/binding.ts +25 -422
- package/src/constants.ts +345 -344
- package/src/context.ts +300 -0
- package/src/cycle-guard.ts +124 -97
- package/src/delegated-events.ts +24 -0
- package/src/dom.ts +19 -25
- package/src/effect.ts +4 -0
- package/src/hooks.ts +9 -1
- package/src/index.ts +41 -130
- package/src/internal.ts +130 -0
- package/src/lifecycle.ts +13 -2
- package/src/list-helpers.ts +6 -65
- package/src/props.ts +48 -46
- package/src/signal.ts +59 -39
- package/src/store.ts +47 -7
- package/src/versioned-signal.ts +3 -3
- package/dist/jsx-runtime.d.cts +0 -671
- package/dist/jsx-runtime.d.ts +0 -671
- package/dist/slim.cjs.map +0 -1
- package/dist/slim.d.cts +0 -504
- package/dist/slim.d.ts +0 -504
- package/dist/slim.js.map +0 -1
- package/src/slim.ts +0 -69
package/src/context.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Context API for Fict
|
|
3
|
+
*
|
|
4
|
+
* Provides a way to pass data through the component tree without having to pass
|
|
5
|
+
* props down manually at every level. Context is designed for:
|
|
6
|
+
*
|
|
7
|
+
* - SSR isolation (different request = different context values)
|
|
8
|
+
* - Multi-instance support (multiple app roots with different values)
|
|
9
|
+
* - Subtree scoping (override values in specific parts of the tree)
|
|
10
|
+
*
|
|
11
|
+
* ## Design Principles
|
|
12
|
+
*
|
|
13
|
+
* 1. **Reuses existing RootContext hierarchy** - Uses parent chain for value lookup,
|
|
14
|
+
* consistent with handleError/handleSuspend mechanisms.
|
|
15
|
+
*
|
|
16
|
+
* 2. **Zero extra root creation overhead** - Provider doesn't create new root,
|
|
17
|
+
* only mounts value on current root.
|
|
18
|
+
*
|
|
19
|
+
* 3. **Auto-aligned with insert/suspense boundaries** - Because they create child
|
|
20
|
+
* roots that inherit parent, context values propagate correctly.
|
|
21
|
+
*
|
|
22
|
+
* ## Usage
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // Create context with default value
|
|
26
|
+
* const ThemeContext = createContext<'light' | 'dark'>('light')
|
|
27
|
+
*
|
|
28
|
+
* // Provide value to subtree
|
|
29
|
+
* function App() {
|
|
30
|
+
* return (
|
|
31
|
+
* <ThemeContext.Provider value="dark">
|
|
32
|
+
* <ThemedComponent />
|
|
33
|
+
* </ThemeContext.Provider>
|
|
34
|
+
* )
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* // Consume value
|
|
38
|
+
* function ThemedComponent() {
|
|
39
|
+
* const theme = useContext(ThemeContext)
|
|
40
|
+
* return <div class={theme}>...</div>
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @module
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { createElement } from './dom'
|
|
48
|
+
import {
|
|
49
|
+
createRootContext,
|
|
50
|
+
destroyRoot,
|
|
51
|
+
flushOnMount,
|
|
52
|
+
getCurrentRoot,
|
|
53
|
+
popRoot,
|
|
54
|
+
pushRoot,
|
|
55
|
+
type RootContext,
|
|
56
|
+
} from './lifecycle'
|
|
57
|
+
import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
|
|
58
|
+
import { createRenderEffect } from './effect'
|
|
59
|
+
import type { BaseProps, FictNode } from './types'
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Types
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Context object created by createContext.
|
|
67
|
+
* Contains the Provider component and serves as a key for context lookup.
|
|
68
|
+
*/
|
|
69
|
+
export interface Context<T> {
|
|
70
|
+
/** Unique identifier for this context */
|
|
71
|
+
readonly id: symbol
|
|
72
|
+
/** Default value when no provider is found */
|
|
73
|
+
readonly defaultValue: T
|
|
74
|
+
/** Provider component for supplying context values */
|
|
75
|
+
Provider: ContextProvider<T>
|
|
76
|
+
/** Display name for debugging */
|
|
77
|
+
displayName?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Props for the Context Provider component
|
|
82
|
+
*/
|
|
83
|
+
export interface ProviderProps<T> extends BaseProps {
|
|
84
|
+
/** The value to provide to the subtree */
|
|
85
|
+
value: T
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Provider component type
|
|
90
|
+
*/
|
|
91
|
+
export type ContextProvider<T> = (props: ProviderProps<T>) => FictNode
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Internal Context Storage
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* WeakMap to store context values per RootContext.
|
|
99
|
+
* Using WeakMap ensures proper garbage collection when roots are destroyed.
|
|
100
|
+
*/
|
|
101
|
+
const contextStorage = new WeakMap<RootContext, Map<symbol, unknown>>()
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the context map for a root, creating it if needed
|
|
105
|
+
*/
|
|
106
|
+
function getContextMap(root: RootContext): Map<symbol, unknown> {
|
|
107
|
+
let map = contextStorage.get(root)
|
|
108
|
+
if (!map) {
|
|
109
|
+
map = new Map()
|
|
110
|
+
contextStorage.set(root, map)
|
|
111
|
+
}
|
|
112
|
+
return map
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Context API
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a new context with the given default value.
|
|
121
|
+
*
|
|
122
|
+
* Context provides a way to pass values through the component tree without
|
|
123
|
+
* explicit props drilling. It's especially useful for:
|
|
124
|
+
*
|
|
125
|
+
* - Theme data
|
|
126
|
+
* - Locale/i18n settings
|
|
127
|
+
* - Authentication state
|
|
128
|
+
* - Feature flags
|
|
129
|
+
* - Any data that many components at different nesting levels need
|
|
130
|
+
*
|
|
131
|
+
* @param defaultValue - The value to use when no Provider is found above in the tree
|
|
132
|
+
* @returns A context object with a Provider component
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```tsx
|
|
136
|
+
* // Create a theme context
|
|
137
|
+
* const ThemeContext = createContext<'light' | 'dark'>('light')
|
|
138
|
+
*
|
|
139
|
+
* // Use the provider
|
|
140
|
+
* function App() {
|
|
141
|
+
* return (
|
|
142
|
+
* <ThemeContext.Provider value="dark">
|
|
143
|
+
* <Content />
|
|
144
|
+
* </ThemeContext.Provider>
|
|
145
|
+
* )
|
|
146
|
+
* }
|
|
147
|
+
*
|
|
148
|
+
* // Consume the context
|
|
149
|
+
* function Content() {
|
|
150
|
+
* const theme = useContext(ThemeContext)
|
|
151
|
+
* return <div class={`theme-${theme}`}>Hello</div>
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export function createContext<T>(defaultValue: T): Context<T> {
|
|
156
|
+
const id = Symbol('fict.context')
|
|
157
|
+
|
|
158
|
+
const context: Context<T> = {
|
|
159
|
+
id,
|
|
160
|
+
defaultValue,
|
|
161
|
+
Provider: null as unknown as ContextProvider<T>,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Create the Provider component
|
|
165
|
+
context.Provider = function Provider(props: ProviderProps<T>): FictNode {
|
|
166
|
+
const hostRoot = getCurrentRoot()
|
|
167
|
+
|
|
168
|
+
// Create a child root for the provider's subtree
|
|
169
|
+
// This establishes the provider boundary - children will look up from here
|
|
170
|
+
const providerRoot = createRootContext(hostRoot)
|
|
171
|
+
|
|
172
|
+
// Store the context value on this root
|
|
173
|
+
const contextMap = getContextMap(providerRoot)
|
|
174
|
+
contextMap.set(id, props.value)
|
|
175
|
+
|
|
176
|
+
// Create DOM structure
|
|
177
|
+
const fragment = document.createDocumentFragment()
|
|
178
|
+
const marker = document.createComment('fict:ctx')
|
|
179
|
+
fragment.appendChild(marker)
|
|
180
|
+
|
|
181
|
+
let cleanup: (() => void) | undefined
|
|
182
|
+
let activeNodes: Node[] = []
|
|
183
|
+
|
|
184
|
+
const renderChildren = (children: FictNode) => {
|
|
185
|
+
// Cleanup previous render
|
|
186
|
+
if (cleanup) {
|
|
187
|
+
cleanup()
|
|
188
|
+
cleanup = undefined
|
|
189
|
+
}
|
|
190
|
+
if (activeNodes.length) {
|
|
191
|
+
removeNodes(activeNodes)
|
|
192
|
+
activeNodes = []
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (children == null || children === false) {
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const prev = pushRoot(providerRoot)
|
|
200
|
+
let nodes: Node[] = []
|
|
201
|
+
try {
|
|
202
|
+
const output = createElement(children)
|
|
203
|
+
nodes = toNodeArray(output)
|
|
204
|
+
const parentNode = marker.parentNode as (ParentNode & Node) | null
|
|
205
|
+
if (parentNode) {
|
|
206
|
+
insertNodesBefore(parentNode, nodes, marker)
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
popRoot(prev)
|
|
210
|
+
flushOnMount(providerRoot)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
cleanup = () => {
|
|
214
|
+
destroyRoot(providerRoot)
|
|
215
|
+
removeNodes(nodes)
|
|
216
|
+
}
|
|
217
|
+
activeNodes = nodes
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Initial render
|
|
221
|
+
createRenderEffect(() => {
|
|
222
|
+
// Update context value on re-render (if value prop changes reactively)
|
|
223
|
+
contextMap.set(id, props.value)
|
|
224
|
+
renderChildren(props.children)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return fragment
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return context
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Reads the current value of a context.
|
|
235
|
+
*
|
|
236
|
+
* useContext looks up through the RootContext parent chain to find the
|
|
237
|
+
* nearest Provider for this context. If no Provider is found, returns
|
|
238
|
+
* the context's default value.
|
|
239
|
+
*
|
|
240
|
+
* @param context - The context object created by createContext
|
|
241
|
+
* @returns The current context value
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```tsx
|
|
245
|
+
* const ThemeContext = createContext('light')
|
|
246
|
+
*
|
|
247
|
+
* function ThemedButton() {
|
|
248
|
+
* const theme = useContext(ThemeContext)
|
|
249
|
+
* return <button class={theme === 'dark' ? 'btn-dark' : 'btn-light'}>Click</button>
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
export function useContext<T>(context: Context<T>): T {
|
|
254
|
+
let root = getCurrentRoot()
|
|
255
|
+
|
|
256
|
+
// Walk up the parent chain looking for the context value
|
|
257
|
+
while (root) {
|
|
258
|
+
const contextMap = contextStorage.get(root)
|
|
259
|
+
if (contextMap && contextMap.has(context.id)) {
|
|
260
|
+
return contextMap.get(context.id) as T
|
|
261
|
+
}
|
|
262
|
+
root = root.parent
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// No provider found, return default value
|
|
266
|
+
return context.defaultValue
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Checks if a context value is currently provided in the tree.
|
|
271
|
+
*
|
|
272
|
+
* Useful for conditional behavior when a provider may or may not exist.
|
|
273
|
+
*
|
|
274
|
+
* @param context - The context object to check
|
|
275
|
+
* @returns true if a Provider exists above in the tree
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```tsx
|
|
279
|
+
* function OptionalTheme() {
|
|
280
|
+
* if (hasContext(ThemeContext)) {
|
|
281
|
+
* const theme = useContext(ThemeContext)
|
|
282
|
+
* return <div class={theme}>Themed content</div>
|
|
283
|
+
* }
|
|
284
|
+
* return <div>Default content</div>
|
|
285
|
+
* }
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
export function hasContext<T>(context: Context<T>): boolean {
|
|
289
|
+
let root = getCurrentRoot()
|
|
290
|
+
|
|
291
|
+
while (root) {
|
|
292
|
+
const contextMap = contextStorage.get(root)
|
|
293
|
+
if (contextMap && contextMap.has(context.id)) {
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
root = root.parent
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return false
|
|
300
|
+
}
|
package/src/cycle-guard.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { getDevtoolsHook } from './devtools'
|
|
2
2
|
|
|
3
|
+
const isDev =
|
|
4
|
+
typeof __DEV__ !== 'undefined'
|
|
5
|
+
? __DEV__
|
|
6
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
7
|
+
|
|
3
8
|
export interface CycleProtectionOptions {
|
|
4
9
|
maxFlushCyclesPerMicrotask?: number
|
|
5
10
|
maxEffectRunsPerFlush?: number
|
|
@@ -15,120 +20,142 @@ interface CycleWindowEntry {
|
|
|
15
20
|
budget: number
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
devMode: false,
|
|
26
|
-
}
|
|
23
|
+
let setCycleProtectionOptions: (opts: CycleProtectionOptions) => void = () => {}
|
|
24
|
+
let resetCycleProtectionStateForTests: () => void = () => {}
|
|
25
|
+
let beginFlushGuard: () => void = () => {}
|
|
26
|
+
let beforeEffectRunGuard: () => boolean = () => true
|
|
27
|
+
let endFlushGuard: () => void = () => {}
|
|
28
|
+
let enterRootGuard: (root: object) => boolean = () => true
|
|
29
|
+
let exitRootGuard: (root: object) => void = () => {}
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
if (isDev) {
|
|
32
|
+
const defaultOptions = {
|
|
33
|
+
maxFlushCyclesPerMicrotask: 10_000,
|
|
34
|
+
maxEffectRunsPerFlush: 20_000,
|
|
35
|
+
windowSize: 5,
|
|
36
|
+
highUsageRatio: 0.8,
|
|
37
|
+
maxRootReentrantDepth: 10,
|
|
38
|
+
enableWindowWarning: true,
|
|
39
|
+
devMode: false,
|
|
40
|
+
}
|
|
31
41
|
|
|
32
|
-
let
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let flushWarned = false
|
|
36
|
-
let rootWarned = false
|
|
37
|
-
let windowWarned = false
|
|
42
|
+
let options: Required<CycleProtectionOptions> = {
|
|
43
|
+
...defaultOptions,
|
|
44
|
+
} as Required<CycleProtectionOptions>
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
let effectRunsThisFlush = 0
|
|
47
|
+
let windowUsage: CycleWindowEntry[] = []
|
|
48
|
+
let rootDepth = new WeakMap<object, number>()
|
|
49
|
+
let flushWarned = false
|
|
50
|
+
let rootWarned = false
|
|
51
|
+
let windowWarned = false
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
windowUsage = []
|
|
47
|
-
rootDepth = new WeakMap<object, number>()
|
|
48
|
-
flushWarned = false
|
|
49
|
-
rootWarned = false
|
|
50
|
-
windowWarned = false
|
|
51
|
-
}
|
|
53
|
+
setCycleProtectionOptions = opts => {
|
|
54
|
+
options = { ...options, ...opts }
|
|
55
|
+
}
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
resetCycleProtectionStateForTests = () => {
|
|
58
|
+
options = { ...defaultOptions } as Required<CycleProtectionOptions>
|
|
59
|
+
effectRunsThisFlush = 0
|
|
60
|
+
windowUsage = []
|
|
61
|
+
rootDepth = new WeakMap<object, number>()
|
|
62
|
+
flushWarned = false
|
|
63
|
+
rootWarned = false
|
|
64
|
+
windowWarned = false
|
|
65
|
+
}
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
beginFlushGuard = () => {
|
|
68
|
+
effectRunsThisFlush = 0
|
|
69
|
+
flushWarned = false
|
|
70
|
+
windowWarned = false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
beforeEffectRunGuard = () => {
|
|
74
|
+
const next = ++effectRunsThisFlush
|
|
75
|
+
if (next > options.maxFlushCyclesPerMicrotask || next > options.maxEffectRunsPerFlush) {
|
|
76
|
+
const message = `[fict] cycle protection triggered: flush-budget-exceeded`
|
|
77
|
+
if (options.devMode) {
|
|
78
|
+
throw new Error(message)
|
|
79
|
+
}
|
|
80
|
+
if (!flushWarned) {
|
|
81
|
+
flushWarned = true
|
|
82
|
+
console.warn(message, { effectRuns: next })
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
69
85
|
}
|
|
70
|
-
return
|
|
86
|
+
return true
|
|
71
87
|
}
|
|
72
|
-
return true
|
|
73
|
-
}
|
|
74
88
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
89
|
+
endFlushGuard = () => {
|
|
90
|
+
recordWindowUsage(effectRunsThisFlush, options.maxFlushCyclesPerMicrotask)
|
|
91
|
+
effectRunsThisFlush = 0
|
|
92
|
+
}
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
enterRootGuard = root => {
|
|
95
|
+
const depth = (rootDepth.get(root) ?? 0) + 1
|
|
96
|
+
if (depth > options.maxRootReentrantDepth) {
|
|
97
|
+
const message = `[fict] cycle protection triggered: root-reentry`
|
|
98
|
+
if (options.devMode) {
|
|
99
|
+
throw new Error(message)
|
|
100
|
+
}
|
|
101
|
+
if (!rootWarned) {
|
|
102
|
+
rootWarned = true
|
|
103
|
+
console.warn(message, { depth })
|
|
104
|
+
}
|
|
105
|
+
return false
|
|
86
106
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.warn(message, { depth })
|
|
90
|
-
}
|
|
91
|
-
return false
|
|
107
|
+
rootDepth.set(root, depth)
|
|
108
|
+
return true
|
|
92
109
|
}
|
|
93
|
-
rootDepth.set(root, depth)
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
110
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
exitRootGuard = root => {
|
|
112
|
+
const depth = rootDepth.get(root)
|
|
113
|
+
if (depth === undefined) return
|
|
114
|
+
if (depth <= 1) {
|
|
115
|
+
rootDepth.delete(root)
|
|
116
|
+
} else {
|
|
117
|
+
rootDepth.set(root, depth - 1)
|
|
118
|
+
}
|
|
104
119
|
}
|
|
105
|
-
}
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
const recordWindowUsage = (used: number, budget: number): void => {
|
|
122
|
+
if (!options.enableWindowWarning) return
|
|
123
|
+
const entry = { used, budget }
|
|
124
|
+
windowUsage.push(entry)
|
|
125
|
+
if (windowUsage.length > options.windowSize) {
|
|
126
|
+
windowUsage.shift()
|
|
127
|
+
}
|
|
128
|
+
if (windowWarned) return
|
|
129
|
+
if (
|
|
130
|
+
windowUsage.length >= options.windowSize &&
|
|
131
|
+
windowUsage.every(
|
|
132
|
+
item => item.budget > 0 && item.used / item.budget >= options.highUsageRatio,
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
windowWarned = true
|
|
136
|
+
reportCycle('high-usage-window', {
|
|
137
|
+
windowSize: options.windowSize,
|
|
138
|
+
ratio: options.highUsageRatio,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
113
141
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
ratio: options.highUsageRatio,
|
|
123
|
-
})
|
|
142
|
+
|
|
143
|
+
const reportCycle = (
|
|
144
|
+
reason: string,
|
|
145
|
+
detail: Record<string, unknown> | undefined = undefined,
|
|
146
|
+
): void => {
|
|
147
|
+
const hook = getDevtoolsHook()
|
|
148
|
+
hook?.cycleDetected?.(detail ? { reason, detail } : { reason })
|
|
149
|
+
console.warn(`[fict] cycle protection triggered: ${reason}`, detail ?? '')
|
|
124
150
|
}
|
|
125
151
|
}
|
|
126
152
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
export {
|
|
154
|
+
setCycleProtectionOptions,
|
|
155
|
+
resetCycleProtectionStateForTests,
|
|
156
|
+
beginFlushGuard,
|
|
157
|
+
beforeEffectRunGuard,
|
|
158
|
+
endFlushGuard,
|
|
159
|
+
enterRootGuard,
|
|
160
|
+
exitRootGuard,
|
|
134
161
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DelegatedEventNames = [
|
|
2
|
+
'beforeinput',
|
|
3
|
+
'click',
|
|
4
|
+
'dblclick',
|
|
5
|
+
'contextmenu',
|
|
6
|
+
'focusin',
|
|
7
|
+
'focusout',
|
|
8
|
+
'input',
|
|
9
|
+
'keydown',
|
|
10
|
+
'keyup',
|
|
11
|
+
'mousedown',
|
|
12
|
+
'mousemove',
|
|
13
|
+
'mouseout',
|
|
14
|
+
'mouseover',
|
|
15
|
+
'mouseup',
|
|
16
|
+
'pointerdown',
|
|
17
|
+
'pointermove',
|
|
18
|
+
'pointerout',
|
|
19
|
+
'pointerover',
|
|
20
|
+
'pointerup',
|
|
21
|
+
'touchend',
|
|
22
|
+
'touchmove',
|
|
23
|
+
'touchstart',
|
|
24
|
+
] as const
|
package/src/dom.ts
CHANGED
|
@@ -20,19 +20,11 @@ import {
|
|
|
20
20
|
createChildBinding,
|
|
21
21
|
bindEvent,
|
|
22
22
|
isReactive,
|
|
23
|
-
PRIMITIVE_PROXY,
|
|
24
23
|
type MaybeReactive,
|
|
25
24
|
type AttributeSetter,
|
|
26
25
|
type BindingHandle,
|
|
27
26
|
} from './binding'
|
|
28
|
-
import {
|
|
29
|
-
Properties,
|
|
30
|
-
ChildProperties,
|
|
31
|
-
Aliases,
|
|
32
|
-
getPropAlias,
|
|
33
|
-
SVGElements,
|
|
34
|
-
SVGNamespace,
|
|
35
|
-
} from './constants'
|
|
27
|
+
import { Properties, ChildProperties, getPropAlias, SVGElements, SVGNamespace } from './constants'
|
|
36
28
|
import { __fictPushContext, __fictPopContext } from './hooks'
|
|
37
29
|
import { Fragment } from './jsx'
|
|
38
30
|
import {
|
|
@@ -128,7 +120,7 @@ function resolveNamespace(tagName: string, namespace: NamespaceContext): Namespa
|
|
|
128
120
|
if (tagName === 'math') return 'mathml'
|
|
129
121
|
if (namespace === 'mathml') return 'mathml'
|
|
130
122
|
if (namespace === 'svg') return 'svg'
|
|
131
|
-
if (SVGElements.has(tagName)) return 'svg'
|
|
123
|
+
if (isDev && SVGElements.has(tagName)) return 'svg'
|
|
132
124
|
return null
|
|
133
125
|
}
|
|
134
126
|
|
|
@@ -143,9 +135,8 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
143
135
|
return document.createTextNode('')
|
|
144
136
|
}
|
|
145
137
|
|
|
146
|
-
// Primitive proxy produced by keyed list binding
|
|
147
138
|
if (typeof node === 'object' && node !== null && !(node instanceof Node)) {
|
|
148
|
-
// Handle BindingHandle (
|
|
139
|
+
// Handle BindingHandle (list/conditional bindings, etc)
|
|
149
140
|
if ('marker' in node) {
|
|
150
141
|
const handle = node as { marker: unknown; dispose?: () => void; flush?: () => void }
|
|
151
142
|
// Register dispose cleanup if available
|
|
@@ -164,14 +155,6 @@ function createElementWithContext(node: FictNode, namespace: NamespaceContext):
|
|
|
164
155
|
}
|
|
165
156
|
return createElement(handle.marker as FictNode)
|
|
166
157
|
}
|
|
167
|
-
|
|
168
|
-
const nodeRecord = node as unknown as Record<PropertyKey, unknown>
|
|
169
|
-
if (nodeRecord[PRIMITIVE_PROXY]) {
|
|
170
|
-
const primitiveGetter = nodeRecord[Symbol.toPrimitive]
|
|
171
|
-
const value =
|
|
172
|
-
typeof primitiveGetter === 'function' ? primitiveGetter.call(node, 'default') : node
|
|
173
|
-
return document.createTextNode(value == null || value === false ? '' : String(value))
|
|
174
|
-
}
|
|
175
158
|
}
|
|
176
159
|
|
|
177
160
|
// Array - create fragment
|
|
@@ -559,7 +542,13 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
|
|
|
559
542
|
}
|
|
560
543
|
|
|
561
544
|
// Child properties (innerHTML, textContent, etc.)
|
|
562
|
-
if (
|
|
545
|
+
if (
|
|
546
|
+
(isDev && ChildProperties.has(key)) ||
|
|
547
|
+
key === 'innerHTML' ||
|
|
548
|
+
key === 'textContent' ||
|
|
549
|
+
key === 'innerText' ||
|
|
550
|
+
key === 'children'
|
|
551
|
+
) {
|
|
563
552
|
createAttributeBinding(el, key, value as MaybeReactive<unknown>, setProperty)
|
|
564
553
|
continue
|
|
565
554
|
}
|
|
@@ -583,13 +572,18 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
|
|
|
583
572
|
}
|
|
584
573
|
|
|
585
574
|
// Check for property alias (element-specific mappings)
|
|
586
|
-
const propAlias = !isSVG ? getPropAlias(key, tagName) : undefined
|
|
575
|
+
const propAlias = !isSVG && isDev ? getPropAlias(key, tagName) : undefined
|
|
576
|
+
const isProperty = !isSVG
|
|
577
|
+
? isDev
|
|
578
|
+
? Properties.has(key)
|
|
579
|
+
: key in (el as unknown as Record<string, unknown>)
|
|
580
|
+
: false
|
|
587
581
|
|
|
588
582
|
// Handle properties and element-specific attributes
|
|
589
|
-
if (propAlias ||
|
|
583
|
+
if (propAlias || isProperty || (isCE && !isSVG)) {
|
|
590
584
|
const propName = propAlias || key
|
|
591
585
|
// Custom elements use toPropertyName conversion
|
|
592
|
-
if (isCE && !
|
|
586
|
+
if (isCE && !isProperty && !propAlias) {
|
|
593
587
|
createAttributeBinding(
|
|
594
588
|
el,
|
|
595
589
|
toPropertyName(propName),
|
|
@@ -616,7 +610,7 @@ function applyProps(el: Element, props: Record<string, unknown>, isSVG = false):
|
|
|
616
610
|
|
|
617
611
|
// Regular attributes (potentially reactive)
|
|
618
612
|
// Apply alias mapping (className -> class, htmlFor -> for)
|
|
619
|
-
const attrName =
|
|
613
|
+
const attrName = key === 'htmlFor' ? 'for' : key
|
|
620
614
|
createAttributeBinding(el, attrName, value as MaybeReactive<unknown>, setAttribute)
|
|
621
615
|
}
|
|
622
616
|
}
|
package/src/effect.ts
CHANGED
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
import { effectWithCleanup } from './signal'
|
|
9
9
|
import type { Cleanup } from './types'
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Effect callback run synchronously; async callbacks are not tracked after the first await.
|
|
13
|
+
* TypeScript will reject `async () => {}` here—split async work or read signals before awaiting.
|
|
14
|
+
*/
|
|
11
15
|
export type Effect = () => void | Cleanup
|
|
12
16
|
|
|
13
17
|
export function createEffect(fn: Effect): () => void {
|