@fictjs/runtime 0.0.2
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/README.md +17 -0
- package/dist/index.cjs +4224 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1572 -0
- package/dist/index.d.ts +1572 -0
- package/dist/index.dev.js +4240 -0
- package/dist/index.dev.js.map +1 -0
- package/dist/index.js +4133 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs +44 -0
- package/dist/jsx-dev-runtime.cjs.map +1 -0
- package/dist/jsx-dev-runtime.js +14 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-runtime.cjs +44 -0
- package/dist/jsx-runtime.cjs.map +1 -0
- package/dist/jsx-runtime.js +14 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/slim.cjs +3384 -0
- package/dist/slim.cjs.map +1 -0
- package/dist/slim.d.cts +475 -0
- package/dist/slim.d.ts +475 -0
- package/dist/slim.js +3335 -0
- package/dist/slim.js.map +1 -0
- package/package.json +68 -0
- package/src/binding.ts +2127 -0
- package/src/constants.ts +456 -0
- package/src/cycle-guard.ts +134 -0
- package/src/devtools.ts +17 -0
- package/src/dom.ts +683 -0
- package/src/effect.ts +83 -0
- package/src/error-boundary.ts +118 -0
- package/src/hooks.ts +72 -0
- package/src/index.ts +184 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +2 -0
- package/src/jsx.ts +786 -0
- package/src/lifecycle.ts +273 -0
- package/src/list-helpers.ts +619 -0
- package/src/memo.ts +14 -0
- package/src/node-ops.ts +185 -0
- package/src/props.ts +212 -0
- package/src/reconcile.ts +151 -0
- package/src/ref.ts +25 -0
- package/src/scheduler.ts +12 -0
- package/src/signal.ts +1278 -0
- package/src/slim.ts +68 -0
- package/src/store.ts +210 -0
- package/src/suspense.ts +187 -0
- package/src/transition.ts +128 -0
- package/src/types.ts +172 -0
- package/src/versioned-signal.ts +58 -0
package/src/constants.ts
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fict DOM Constants
|
|
3
|
+
*
|
|
4
|
+
* Property constants and configurations for DOM attribute handling.
|
|
5
|
+
* Borrowed from dom-expressions for comprehensive DOM support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Boolean Attributes
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Complete list of boolean attributes (lowercase)
|
|
14
|
+
* These attributes are set as empty strings when true, removed when false
|
|
15
|
+
*/
|
|
16
|
+
const booleans = [
|
|
17
|
+
'allowfullscreen',
|
|
18
|
+
'async',
|
|
19
|
+
'alpha', // HTMLInputElement
|
|
20
|
+
'autofocus', // HTMLElement prop
|
|
21
|
+
'autoplay',
|
|
22
|
+
'checked',
|
|
23
|
+
'controls',
|
|
24
|
+
'default',
|
|
25
|
+
'disabled',
|
|
26
|
+
'formnovalidate',
|
|
27
|
+
'hidden', // HTMLElement prop
|
|
28
|
+
'indeterminate',
|
|
29
|
+
'inert', // HTMLElement prop
|
|
30
|
+
'ismap',
|
|
31
|
+
'loop',
|
|
32
|
+
'multiple',
|
|
33
|
+
'muted',
|
|
34
|
+
'nomodule',
|
|
35
|
+
'novalidate',
|
|
36
|
+
'open',
|
|
37
|
+
'playsinline',
|
|
38
|
+
'readonly',
|
|
39
|
+
'required',
|
|
40
|
+
'reversed',
|
|
41
|
+
'seamless', // HTMLIframeElement - non-standard
|
|
42
|
+
'selected',
|
|
43
|
+
// Experimental attributes
|
|
44
|
+
'adauctionheaders',
|
|
45
|
+
'browsingtopics',
|
|
46
|
+
'credentialless',
|
|
47
|
+
'defaultchecked',
|
|
48
|
+
'defaultmuted',
|
|
49
|
+
'defaultselected',
|
|
50
|
+
'defer',
|
|
51
|
+
'disablepictureinpicture',
|
|
52
|
+
'disableremoteplayback',
|
|
53
|
+
'preservespitch',
|
|
54
|
+
'shadowrootclonable',
|
|
55
|
+
'shadowrootcustomelementregistry',
|
|
56
|
+
'shadowrootdelegatesfocus',
|
|
57
|
+
'shadowrootserializable',
|
|
58
|
+
'sharedstoragewritable',
|
|
59
|
+
] as const
|
|
60
|
+
|
|
61
|
+
export const BooleanAttributes = new Set<string>(booleans)
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Properties Set
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Properties that should be set via DOM property (not attribute)
|
|
69
|
+
* Includes camelCase versions of boolean attributes
|
|
70
|
+
*/
|
|
71
|
+
export const Properties = new Set<string>([
|
|
72
|
+
// Core properties
|
|
73
|
+
'className',
|
|
74
|
+
'value',
|
|
75
|
+
|
|
76
|
+
// CamelCase booleans
|
|
77
|
+
'readOnly',
|
|
78
|
+
'noValidate',
|
|
79
|
+
'formNoValidate',
|
|
80
|
+
'isMap',
|
|
81
|
+
'noModule',
|
|
82
|
+
'playsInline',
|
|
83
|
+
|
|
84
|
+
// Experimental (camelCase)
|
|
85
|
+
'adAuctionHeaders',
|
|
86
|
+
'allowFullscreen',
|
|
87
|
+
'browsingTopics',
|
|
88
|
+
'defaultChecked',
|
|
89
|
+
'defaultMuted',
|
|
90
|
+
'defaultSelected',
|
|
91
|
+
'disablePictureInPicture',
|
|
92
|
+
'disableRemotePlayback',
|
|
93
|
+
'preservesPitch',
|
|
94
|
+
'shadowRootClonable',
|
|
95
|
+
'shadowRootCustomElementRegistry',
|
|
96
|
+
'shadowRootDelegatesFocus',
|
|
97
|
+
'shadowRootSerializable',
|
|
98
|
+
'sharedStorageWritable',
|
|
99
|
+
|
|
100
|
+
// All lowercase booleans
|
|
101
|
+
...booleans,
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Child Properties
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Properties that represent children/content
|
|
110
|
+
*/
|
|
111
|
+
export const ChildProperties = new Set<string>([
|
|
112
|
+
'innerHTML',
|
|
113
|
+
'textContent',
|
|
114
|
+
'innerText',
|
|
115
|
+
'children',
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Property Aliases
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* React compatibility aliases (className -> class)
|
|
124
|
+
*/
|
|
125
|
+
export const Aliases: Record<string, string> = {
|
|
126
|
+
className: 'class',
|
|
127
|
+
htmlFor: 'for',
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Element-specific property aliases
|
|
132
|
+
* Maps lowercase attribute names to their camelCase property equivalents
|
|
133
|
+
* Only for specific elements that have these properties
|
|
134
|
+
*/
|
|
135
|
+
export const PropAliases: Record<
|
|
136
|
+
string,
|
|
137
|
+
string | { $: string; [tagName: string]: string | number }
|
|
138
|
+
> = {
|
|
139
|
+
// Direct mapping
|
|
140
|
+
class: 'className',
|
|
141
|
+
|
|
142
|
+
// Element-specific mappings
|
|
143
|
+
novalidate: {
|
|
144
|
+
$: 'noValidate',
|
|
145
|
+
FORM: 1,
|
|
146
|
+
},
|
|
147
|
+
formnovalidate: {
|
|
148
|
+
$: 'formNoValidate',
|
|
149
|
+
BUTTON: 1,
|
|
150
|
+
INPUT: 1,
|
|
151
|
+
},
|
|
152
|
+
ismap: {
|
|
153
|
+
$: 'isMap',
|
|
154
|
+
IMG: 1,
|
|
155
|
+
},
|
|
156
|
+
nomodule: {
|
|
157
|
+
$: 'noModule',
|
|
158
|
+
SCRIPT: 1,
|
|
159
|
+
},
|
|
160
|
+
playsinline: {
|
|
161
|
+
$: 'playsInline',
|
|
162
|
+
VIDEO: 1,
|
|
163
|
+
},
|
|
164
|
+
readonly: {
|
|
165
|
+
$: 'readOnly',
|
|
166
|
+
INPUT: 1,
|
|
167
|
+
TEXTAREA: 1,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// Experimental element-specific
|
|
171
|
+
adauctionheaders: {
|
|
172
|
+
$: 'adAuctionHeaders',
|
|
173
|
+
IFRAME: 1,
|
|
174
|
+
},
|
|
175
|
+
allowfullscreen: {
|
|
176
|
+
$: 'allowFullscreen',
|
|
177
|
+
IFRAME: 1,
|
|
178
|
+
},
|
|
179
|
+
browsingtopics: {
|
|
180
|
+
$: 'browsingTopics',
|
|
181
|
+
IMG: 1,
|
|
182
|
+
},
|
|
183
|
+
defaultchecked: {
|
|
184
|
+
$: 'defaultChecked',
|
|
185
|
+
INPUT: 1,
|
|
186
|
+
},
|
|
187
|
+
defaultmuted: {
|
|
188
|
+
$: 'defaultMuted',
|
|
189
|
+
AUDIO: 1,
|
|
190
|
+
VIDEO: 1,
|
|
191
|
+
},
|
|
192
|
+
defaultselected: {
|
|
193
|
+
$: 'defaultSelected',
|
|
194
|
+
OPTION: 1,
|
|
195
|
+
},
|
|
196
|
+
disablepictureinpicture: {
|
|
197
|
+
$: 'disablePictureInPicture',
|
|
198
|
+
VIDEO: 1,
|
|
199
|
+
},
|
|
200
|
+
disableremoteplayback: {
|
|
201
|
+
$: 'disableRemotePlayback',
|
|
202
|
+
AUDIO: 1,
|
|
203
|
+
VIDEO: 1,
|
|
204
|
+
},
|
|
205
|
+
preservespitch: {
|
|
206
|
+
$: 'preservesPitch',
|
|
207
|
+
AUDIO: 1,
|
|
208
|
+
VIDEO: 1,
|
|
209
|
+
},
|
|
210
|
+
shadowrootclonable: {
|
|
211
|
+
$: 'shadowRootClonable',
|
|
212
|
+
TEMPLATE: 1,
|
|
213
|
+
},
|
|
214
|
+
shadowrootdelegatesfocus: {
|
|
215
|
+
$: 'shadowRootDelegatesFocus',
|
|
216
|
+
TEMPLATE: 1,
|
|
217
|
+
},
|
|
218
|
+
shadowrootserializable: {
|
|
219
|
+
$: 'shadowRootSerializable',
|
|
220
|
+
TEMPLATE: 1,
|
|
221
|
+
},
|
|
222
|
+
sharedstoragewritable: {
|
|
223
|
+
$: 'sharedStorageWritable',
|
|
224
|
+
IFRAME: 1,
|
|
225
|
+
IMG: 1,
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get the property alias for a given attribute and tag name
|
|
231
|
+
*/
|
|
232
|
+
export function getPropAlias(prop: string, tagName: string): string | undefined {
|
|
233
|
+
const a = PropAliases[prop]
|
|
234
|
+
if (typeof a === 'object') {
|
|
235
|
+
return a[tagName] ? a['$'] : undefined
|
|
236
|
+
}
|
|
237
|
+
return a
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Event Delegation
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Symbol for storing delegated events on the document
|
|
246
|
+
*/
|
|
247
|
+
export const $$EVENTS = '_$FICT_DELEGATE'
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Events that should use event delegation for performance
|
|
251
|
+
* These events bubble and are commonly used across many elements
|
|
252
|
+
*/
|
|
253
|
+
export const DelegatedEvents = new Set<string>([
|
|
254
|
+
'beforeinput',
|
|
255
|
+
'click',
|
|
256
|
+
'dblclick',
|
|
257
|
+
'contextmenu',
|
|
258
|
+
'focusin',
|
|
259
|
+
'focusout',
|
|
260
|
+
'input',
|
|
261
|
+
'keydown',
|
|
262
|
+
'keyup',
|
|
263
|
+
'mousedown',
|
|
264
|
+
'mousemove',
|
|
265
|
+
'mouseout',
|
|
266
|
+
'mouseover',
|
|
267
|
+
'mouseup',
|
|
268
|
+
'pointerdown',
|
|
269
|
+
'pointermove',
|
|
270
|
+
'pointerout',
|
|
271
|
+
'pointerover',
|
|
272
|
+
'pointerup',
|
|
273
|
+
'touchend',
|
|
274
|
+
'touchmove',
|
|
275
|
+
'touchstart',
|
|
276
|
+
])
|
|
277
|
+
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// SVG Support
|
|
280
|
+
// ============================================================================
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* SVG element names (excluding common ones that overlap with HTML)
|
|
284
|
+
*/
|
|
285
|
+
export const SVGElements = new Set<string>([
|
|
286
|
+
'altGlyph',
|
|
287
|
+
'altGlyphDef',
|
|
288
|
+
'altGlyphItem',
|
|
289
|
+
'animate',
|
|
290
|
+
'animateColor',
|
|
291
|
+
'animateMotion',
|
|
292
|
+
'animateTransform',
|
|
293
|
+
'circle',
|
|
294
|
+
'clipPath',
|
|
295
|
+
'color-profile',
|
|
296
|
+
'cursor',
|
|
297
|
+
'defs',
|
|
298
|
+
'desc',
|
|
299
|
+
'ellipse',
|
|
300
|
+
'feBlend',
|
|
301
|
+
'feColorMatrix',
|
|
302
|
+
'feComponentTransfer',
|
|
303
|
+
'feComposite',
|
|
304
|
+
'feConvolveMatrix',
|
|
305
|
+
'feDiffuseLighting',
|
|
306
|
+
'feDisplacementMap',
|
|
307
|
+
'feDistantLight',
|
|
308
|
+
'feDropShadow',
|
|
309
|
+
'feFlood',
|
|
310
|
+
'feFuncA',
|
|
311
|
+
'feFuncB',
|
|
312
|
+
'feFuncG',
|
|
313
|
+
'feFuncR',
|
|
314
|
+
'feGaussianBlur',
|
|
315
|
+
'feImage',
|
|
316
|
+
'feMerge',
|
|
317
|
+
'feMergeNode',
|
|
318
|
+
'feMorphology',
|
|
319
|
+
'feOffset',
|
|
320
|
+
'fePointLight',
|
|
321
|
+
'feSpecularLighting',
|
|
322
|
+
'feSpotLight',
|
|
323
|
+
'feTile',
|
|
324
|
+
'feTurbulence',
|
|
325
|
+
'filter',
|
|
326
|
+
'font',
|
|
327
|
+
'font-face',
|
|
328
|
+
'font-face-format',
|
|
329
|
+
'font-face-name',
|
|
330
|
+
'font-face-src',
|
|
331
|
+
'font-face-uri',
|
|
332
|
+
'foreignObject',
|
|
333
|
+
'g',
|
|
334
|
+
'glyph',
|
|
335
|
+
'glyphRef',
|
|
336
|
+
'hkern',
|
|
337
|
+
'image',
|
|
338
|
+
'line',
|
|
339
|
+
'linearGradient',
|
|
340
|
+
'marker',
|
|
341
|
+
'mask',
|
|
342
|
+
'metadata',
|
|
343
|
+
'missing-glyph',
|
|
344
|
+
'mpath',
|
|
345
|
+
'path',
|
|
346
|
+
'pattern',
|
|
347
|
+
'polygon',
|
|
348
|
+
'polyline',
|
|
349
|
+
'radialGradient',
|
|
350
|
+
'rect',
|
|
351
|
+
'set',
|
|
352
|
+
'stop',
|
|
353
|
+
'svg',
|
|
354
|
+
'switch',
|
|
355
|
+
'symbol',
|
|
356
|
+
'text',
|
|
357
|
+
'textPath',
|
|
358
|
+
'tref',
|
|
359
|
+
'tspan',
|
|
360
|
+
'use',
|
|
361
|
+
'view',
|
|
362
|
+
'vkern',
|
|
363
|
+
])
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* SVG attribute namespaces
|
|
367
|
+
*/
|
|
368
|
+
export const SVGNamespace: Record<string, string> = {
|
|
369
|
+
xlink: 'http://www.w3.org/1999/xlink',
|
|
370
|
+
xml: 'http://www.w3.org/XML/1998/namespace',
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// Unitless CSS Properties
|
|
375
|
+
// ============================================================================
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* CSS properties that don't need a unit (like 'px')
|
|
379
|
+
*/
|
|
380
|
+
export const UnitlessStyles = new Set<string>([
|
|
381
|
+
'animationIterationCount',
|
|
382
|
+
'animation-iteration-count',
|
|
383
|
+
'borderImageOutset',
|
|
384
|
+
'border-image-outset',
|
|
385
|
+
'borderImageSlice',
|
|
386
|
+
'border-image-slice',
|
|
387
|
+
'borderImageWidth',
|
|
388
|
+
'border-image-width',
|
|
389
|
+
'boxFlex',
|
|
390
|
+
'box-flex',
|
|
391
|
+
'boxFlexGroup',
|
|
392
|
+
'box-flex-group',
|
|
393
|
+
'boxOrdinalGroup',
|
|
394
|
+
'box-ordinal-group',
|
|
395
|
+
'columnCount',
|
|
396
|
+
'column-count',
|
|
397
|
+
'columns',
|
|
398
|
+
'flex',
|
|
399
|
+
'flexGrow',
|
|
400
|
+
'flex-grow',
|
|
401
|
+
'flexPositive',
|
|
402
|
+
'flex-positive',
|
|
403
|
+
'flexShrink',
|
|
404
|
+
'flex-shrink',
|
|
405
|
+
'flexNegative',
|
|
406
|
+
'flex-negative',
|
|
407
|
+
'flexOrder',
|
|
408
|
+
'flex-order',
|
|
409
|
+
'gridRow',
|
|
410
|
+
'grid-row',
|
|
411
|
+
'gridRowEnd',
|
|
412
|
+
'grid-row-end',
|
|
413
|
+
'gridRowSpan',
|
|
414
|
+
'grid-row-span',
|
|
415
|
+
'gridRowStart',
|
|
416
|
+
'grid-row-start',
|
|
417
|
+
'gridColumn',
|
|
418
|
+
'grid-column',
|
|
419
|
+
'gridColumnEnd',
|
|
420
|
+
'grid-column-end',
|
|
421
|
+
'gridColumnSpan',
|
|
422
|
+
'grid-column-span',
|
|
423
|
+
'gridColumnStart',
|
|
424
|
+
'grid-column-start',
|
|
425
|
+
'fontWeight',
|
|
426
|
+
'font-weight',
|
|
427
|
+
'lineClamp',
|
|
428
|
+
'line-clamp',
|
|
429
|
+
'lineHeight',
|
|
430
|
+
'line-height',
|
|
431
|
+
'opacity',
|
|
432
|
+
'order',
|
|
433
|
+
'orphans',
|
|
434
|
+
'tabSize',
|
|
435
|
+
'tab-size',
|
|
436
|
+
'widows',
|
|
437
|
+
'zIndex',
|
|
438
|
+
'z-index',
|
|
439
|
+
'zoom',
|
|
440
|
+
'fillOpacity',
|
|
441
|
+
'fill-opacity',
|
|
442
|
+
'floodOpacity',
|
|
443
|
+
'flood-opacity',
|
|
444
|
+
'stopOpacity',
|
|
445
|
+
'stop-opacity',
|
|
446
|
+
'strokeDasharray',
|
|
447
|
+
'stroke-dasharray',
|
|
448
|
+
'strokeDashoffset',
|
|
449
|
+
'stroke-dashoffset',
|
|
450
|
+
'strokeMiterlimit',
|
|
451
|
+
'stroke-miterlimit',
|
|
452
|
+
'strokeOpacity',
|
|
453
|
+
'stroke-opacity',
|
|
454
|
+
'strokeWidth',
|
|
455
|
+
'stroke-width',
|
|
456
|
+
])
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { getDevtoolsHook } from './devtools'
|
|
2
|
+
|
|
3
|
+
export interface CycleProtectionOptions {
|
|
4
|
+
maxFlushCyclesPerMicrotask?: number
|
|
5
|
+
maxEffectRunsPerFlush?: number
|
|
6
|
+
windowSize?: number
|
|
7
|
+
highUsageRatio?: number
|
|
8
|
+
maxRootReentrantDepth?: number
|
|
9
|
+
enableWindowWarning?: boolean
|
|
10
|
+
devMode?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CycleWindowEntry {
|
|
14
|
+
used: number
|
|
15
|
+
budget: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const defaultOptions = {
|
|
19
|
+
maxFlushCyclesPerMicrotask: 10_000,
|
|
20
|
+
maxEffectRunsPerFlush: 20_000,
|
|
21
|
+
windowSize: 5,
|
|
22
|
+
highUsageRatio: 0.8,
|
|
23
|
+
maxRootReentrantDepth: 10,
|
|
24
|
+
enableWindowWarning: true,
|
|
25
|
+
devMode: false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let options: Required<CycleProtectionOptions> = {
|
|
29
|
+
...defaultOptions,
|
|
30
|
+
} as Required<CycleProtectionOptions>
|
|
31
|
+
|
|
32
|
+
let effectRunsThisFlush = 0
|
|
33
|
+
let windowUsage: CycleWindowEntry[] = []
|
|
34
|
+
let rootDepth = new WeakMap<object, number>()
|
|
35
|
+
let flushWarned = false
|
|
36
|
+
let rootWarned = false
|
|
37
|
+
let windowWarned = false
|
|
38
|
+
|
|
39
|
+
export function setCycleProtectionOptions(opts: CycleProtectionOptions): void {
|
|
40
|
+
options = { ...options, ...opts }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resetCycleProtectionStateForTests(): void {
|
|
44
|
+
options = { ...defaultOptions } as Required<CycleProtectionOptions>
|
|
45
|
+
effectRunsThisFlush = 0
|
|
46
|
+
windowUsage = []
|
|
47
|
+
rootDepth = new WeakMap<object, number>()
|
|
48
|
+
flushWarned = false
|
|
49
|
+
rootWarned = false
|
|
50
|
+
windowWarned = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function beginFlushGuard(): void {
|
|
54
|
+
effectRunsThisFlush = 0
|
|
55
|
+
flushWarned = false
|
|
56
|
+
windowWarned = false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function beforeEffectRunGuard(): boolean {
|
|
60
|
+
const next = ++effectRunsThisFlush
|
|
61
|
+
if (next > options.maxFlushCyclesPerMicrotask || next > options.maxEffectRunsPerFlush) {
|
|
62
|
+
const message = `[fict] cycle protection triggered: flush-budget-exceeded`
|
|
63
|
+
if (options.devMode) {
|
|
64
|
+
throw new Error(message)
|
|
65
|
+
}
|
|
66
|
+
if (!flushWarned) {
|
|
67
|
+
flushWarned = true
|
|
68
|
+
console.warn(message, { effectRuns: next })
|
|
69
|
+
}
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function endFlushGuard(): void {
|
|
76
|
+
recordWindowUsage(effectRunsThisFlush, options.maxFlushCyclesPerMicrotask)
|
|
77
|
+
effectRunsThisFlush = 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function enterRootGuard(root: object): boolean {
|
|
81
|
+
const depth = (rootDepth.get(root) ?? 0) + 1
|
|
82
|
+
if (depth > options.maxRootReentrantDepth) {
|
|
83
|
+
const message = `[fict] cycle protection triggered: root-reentry`
|
|
84
|
+
if (options.devMode) {
|
|
85
|
+
throw new Error(message)
|
|
86
|
+
}
|
|
87
|
+
if (!rootWarned) {
|
|
88
|
+
rootWarned = true
|
|
89
|
+
console.warn(message, { depth })
|
|
90
|
+
}
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
rootDepth.set(root, depth)
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function exitRootGuard(root: object): void {
|
|
98
|
+
const depth = rootDepth.get(root)
|
|
99
|
+
if (depth === undefined) return
|
|
100
|
+
if (depth <= 1) {
|
|
101
|
+
rootDepth.delete(root)
|
|
102
|
+
} else {
|
|
103
|
+
rootDepth.set(root, depth - 1)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function recordWindowUsage(used: number, budget: number): void {
|
|
108
|
+
if (!options.enableWindowWarning) return
|
|
109
|
+
const entry = { used, budget }
|
|
110
|
+
windowUsage.push(entry)
|
|
111
|
+
if (windowUsage.length > options.windowSize) {
|
|
112
|
+
windowUsage.shift()
|
|
113
|
+
}
|
|
114
|
+
if (windowWarned) return
|
|
115
|
+
if (
|
|
116
|
+
windowUsage.length >= options.windowSize &&
|
|
117
|
+
windowUsage.every(item => item.budget > 0 && item.used / item.budget >= options.highUsageRatio)
|
|
118
|
+
) {
|
|
119
|
+
windowWarned = true
|
|
120
|
+
reportCycle('high-usage-window', {
|
|
121
|
+
windowSize: options.windowSize,
|
|
122
|
+
ratio: options.highUsageRatio,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function reportCycle(
|
|
128
|
+
reason: string,
|
|
129
|
+
detail: Record<string, unknown> | undefined = undefined,
|
|
130
|
+
): void {
|
|
131
|
+
const hook = getDevtoolsHook()
|
|
132
|
+
hook?.cycleDetected?.(detail ? { reason, detail } : { reason })
|
|
133
|
+
console.warn(`[fict] cycle protection triggered: ${reason}`, detail ?? '')
|
|
134
|
+
}
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface FictDevtoolsHook {
|
|
2
|
+
registerSignal: (id: number, value: unknown) => void
|
|
3
|
+
updateSignal: (id: number, value: unknown) => void
|
|
4
|
+
registerEffect: (id: number) => void
|
|
5
|
+
effectRun: (id: number) => void
|
|
6
|
+
cycleDetected?: (payload: { reason: string; detail?: Record<string, unknown> }) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getGlobalHook(): FictDevtoolsHook | undefined {
|
|
10
|
+
if (typeof globalThis === 'undefined') return undefined
|
|
11
|
+
return (globalThis as typeof globalThis & { __FICT_DEVTOOLS_HOOK__?: FictDevtoolsHook })
|
|
12
|
+
.__FICT_DEVTOOLS_HOOK__
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getDevtoolsHook(): FictDevtoolsHook | undefined {
|
|
16
|
+
return getGlobalHook()
|
|
17
|
+
}
|