@fictjs/runtime 0.0.12 → 0.0.13
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/index.cjs +2330 -3203
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -141
- package/dist/index.d.ts +7 -141
- package/dist/index.dev.js +2526 -2836
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +2331 -3197
- package/dist/index.js.map +1 -1
- package/package.json +1 -6
- package/src/binding.ts +25 -422
- package/src/constants.ts +368 -344
- package/src/cycle-guard.ts +124 -97
- package/src/dom.ts +19 -25
- package/src/effect.ts +4 -0
- package/src/hooks.ts +9 -1
- package/src/index.ts +1 -19
- package/src/lifecycle.ts +13 -2
- package/src/list-helpers.ts +6 -65
- package/src/signal.ts +59 -39
- package/dist/slim.cjs +0 -3854
- 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 +0 -3802
- package/dist/slim.js.map +0 -1
- package/src/slim.ts +0 -69
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
|
}
|
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 {
|
package/src/hooks.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { createEffect } from './effect'
|
|
|
2
2
|
import { createMemo } from './memo'
|
|
3
3
|
import { createSignal, type SignalAccessor, type ComputedAccessor } from './signal'
|
|
4
4
|
|
|
5
|
+
const isDev =
|
|
6
|
+
typeof __DEV__ !== 'undefined'
|
|
7
|
+
? __DEV__
|
|
8
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
9
|
+
|
|
5
10
|
interface HookContext {
|
|
6
11
|
slots: unknown[]
|
|
7
12
|
cursor: number
|
|
@@ -12,7 +17,10 @@ const ctxStack: HookContext[] = []
|
|
|
12
17
|
|
|
13
18
|
function assertRenderContext(ctx: HookContext, hookName: string): void {
|
|
14
19
|
if (!ctx.rendering) {
|
|
15
|
-
|
|
20
|
+
const message = isDev
|
|
21
|
+
? `${hookName} can only be used during render execution`
|
|
22
|
+
: 'FICT:E_HOOK_RENDER'
|
|
23
|
+
throw new Error(message)
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
|
package/src/index.ts
CHANGED
|
@@ -101,12 +101,10 @@ export {
|
|
|
101
101
|
isReactive,
|
|
102
102
|
// Advanced bindings
|
|
103
103
|
createConditional,
|
|
104
|
-
createList,
|
|
105
104
|
createPortal,
|
|
106
105
|
createShow,
|
|
107
106
|
// Utility functions
|
|
108
107
|
unwrap,
|
|
109
|
-
unwrapPrimitive,
|
|
110
108
|
} from './binding'
|
|
111
109
|
|
|
112
110
|
// Constants for DOM handling
|
|
@@ -129,13 +127,7 @@ export { default as reconcileArrays } from './reconcile'
|
|
|
129
127
|
// Types
|
|
130
128
|
// ============================================================================
|
|
131
129
|
|
|
132
|
-
export type {
|
|
133
|
-
MaybeReactive,
|
|
134
|
-
BindingHandle,
|
|
135
|
-
KeyFn,
|
|
136
|
-
CreateElementFn,
|
|
137
|
-
AttributeSetter,
|
|
138
|
-
} from './binding'
|
|
130
|
+
export type { MaybeReactive, BindingHandle, CreateElementFn, AttributeSetter } from './binding'
|
|
139
131
|
|
|
140
132
|
export type {
|
|
141
133
|
FictNode,
|
|
@@ -167,21 +159,11 @@ export {
|
|
|
167
159
|
moveNodesBefore,
|
|
168
160
|
removeNodes,
|
|
169
161
|
insertNodesBefore,
|
|
170
|
-
moveMarkerBlock,
|
|
171
|
-
destroyMarkerBlock,
|
|
172
|
-
// Keyed list container
|
|
173
|
-
createKeyedListContainer,
|
|
174
|
-
// Block creation
|
|
175
|
-
createKeyedBlock,
|
|
176
162
|
// High-level list binding (for compiler-generated code)
|
|
177
163
|
createKeyedList,
|
|
178
164
|
// Utilities
|
|
179
165
|
toNodeArray,
|
|
180
|
-
getFirstNodeAfter,
|
|
181
166
|
isNodeBetweenMarkers,
|
|
182
167
|
// Types
|
|
183
|
-
type KeyedBlock,
|
|
184
|
-
type KeyedListContainer,
|
|
185
168
|
type KeyedListBinding,
|
|
186
|
-
type MarkerBlock,
|
|
187
169
|
} from './list-helpers'
|
package/src/lifecycle.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { enterRootGuard, exitRootGuard } from './cycle-guard'
|
|
2
2
|
import type { Cleanup, ErrorInfo, SuspenseToken } from './types'
|
|
3
3
|
|
|
4
|
+
const isDev =
|
|
5
|
+
typeof __DEV__ !== 'undefined'
|
|
6
|
+
? __DEV__
|
|
7
|
+
: typeof process === 'undefined' || process.env?.NODE_ENV !== 'production'
|
|
8
|
+
|
|
4
9
|
type LifecycleFn = () => void | Cleanup
|
|
5
10
|
|
|
6
11
|
export interface RootContext {
|
|
@@ -169,7 +174,10 @@ function runLifecycle(fn: LifecycleFn): void {
|
|
|
169
174
|
|
|
170
175
|
export function registerErrorHandler(fn: ErrorHandler): void {
|
|
171
176
|
if (!currentRoot) {
|
|
172
|
-
|
|
177
|
+
const message = isDev
|
|
178
|
+
? 'registerErrorHandler must be called within a root'
|
|
179
|
+
: 'FICT:E_ROOT_HANDLER'
|
|
180
|
+
throw new Error(message)
|
|
173
181
|
}
|
|
174
182
|
if (!currentRoot.errorHandlers) {
|
|
175
183
|
currentRoot.errorHandlers = []
|
|
@@ -185,7 +193,10 @@ export function registerErrorHandler(fn: ErrorHandler): void {
|
|
|
185
193
|
|
|
186
194
|
export function registerSuspenseHandler(fn: SuspenseHandler): void {
|
|
187
195
|
if (!currentRoot) {
|
|
188
|
-
|
|
196
|
+
const message = isDev
|
|
197
|
+
? 'registerSuspenseHandler must be called within a root'
|
|
198
|
+
: 'FICT:E_ROOT_SUSPENSE'
|
|
199
|
+
throw new Error(message)
|
|
189
200
|
}
|
|
190
201
|
if (!currentRoot.suspenseHandlers) {
|
|
191
202
|
currentRoot.suspenseHandlers = []
|
package/src/list-helpers.ts
CHANGED
|
@@ -37,7 +37,7 @@ const isDev =
|
|
|
37
37
|
/**
|
|
38
38
|
* A keyed block represents a single item in a list with its associated DOM nodes and state
|
|
39
39
|
*/
|
|
40
|
-
|
|
40
|
+
interface KeyedBlock<T = unknown> {
|
|
41
41
|
/** Unique key for this block */
|
|
42
42
|
key: string | number
|
|
43
43
|
/** DOM nodes belonging to this block */
|
|
@@ -57,7 +57,7 @@ export interface KeyedBlock<T = unknown> {
|
|
|
57
57
|
/**
|
|
58
58
|
* Container for managing keyed list blocks
|
|
59
59
|
*/
|
|
60
|
-
|
|
60
|
+
interface KeyedListContainer<T = unknown> {
|
|
61
61
|
/** Start marker comment node */
|
|
62
62
|
startMarker: Comment
|
|
63
63
|
/** End marker comment node */
|
|
@@ -102,15 +102,6 @@ type FineGrainedRenderItem<T> = (
|
|
|
102
102
|
key: string | number,
|
|
103
103
|
) => Node[]
|
|
104
104
|
|
|
105
|
-
/**
|
|
106
|
-
* A block identified by start/end comment markers.
|
|
107
|
-
*/
|
|
108
|
-
export interface MarkerBlock {
|
|
109
|
-
start: Comment
|
|
110
|
-
end: Comment
|
|
111
|
-
root?: RootContext
|
|
112
|
-
}
|
|
113
|
-
|
|
114
105
|
// ============================================================================
|
|
115
106
|
// DOM Manipulation Primitives
|
|
116
107
|
// ============================================================================
|
|
@@ -129,7 +120,8 @@ export function moveNodesBefore(parent: Node, nodes: Node[], anchor: Node | null
|
|
|
129
120
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
130
121
|
const node = nodes[i]!
|
|
131
122
|
if (!node || !(node instanceof Node)) {
|
|
132
|
-
|
|
123
|
+
const message = isDev ? 'Invalid node in moveNodesBefore' : 'FICT:E_NODE'
|
|
124
|
+
throw new Error(message)
|
|
133
125
|
}
|
|
134
126
|
// Only move if not already in correct position
|
|
135
127
|
if (node.nextSibling !== anchor) {
|
|
@@ -162,50 +154,6 @@ export function moveNodesBefore(parent: Node, nodes: Node[], anchor: Node | null
|
|
|
162
154
|
*
|
|
163
155
|
* @param nodes - Array of nodes to remove
|
|
164
156
|
*/
|
|
165
|
-
/**
|
|
166
|
-
* Move an entire marker-delimited block (including markers) before the anchor.
|
|
167
|
-
*/
|
|
168
|
-
export function moveMarkerBlock(parent: Node, block: MarkerBlock, anchor: Node | null): void {
|
|
169
|
-
const nodes = collectBlockNodes(block)
|
|
170
|
-
if (nodes.length === 0) return
|
|
171
|
-
moveNodesBefore(parent, nodes, anchor)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Destroy a marker-delimited block, removing nodes and destroying the associated root.
|
|
176
|
-
*/
|
|
177
|
-
export function destroyMarkerBlock(block: MarkerBlock): void {
|
|
178
|
-
if (block.root) {
|
|
179
|
-
destroyRoot(block.root)
|
|
180
|
-
}
|
|
181
|
-
removeBlockRange(block)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function collectBlockNodes(block: MarkerBlock): Node[] {
|
|
185
|
-
const nodes: Node[] = []
|
|
186
|
-
let cursor: Node | null = block.start
|
|
187
|
-
while (cursor) {
|
|
188
|
-
nodes.push(cursor)
|
|
189
|
-
if (cursor === block.end) {
|
|
190
|
-
break
|
|
191
|
-
}
|
|
192
|
-
cursor = cursor.nextSibling
|
|
193
|
-
}
|
|
194
|
-
return nodes
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function removeBlockRange(block: MarkerBlock): void {
|
|
198
|
-
let cursor: Node | null = block.start
|
|
199
|
-
while (cursor) {
|
|
200
|
-
const next: Node | null = cursor.nextSibling
|
|
201
|
-
cursor.parentNode?.removeChild(cursor)
|
|
202
|
-
if (cursor === block.end) {
|
|
203
|
-
break
|
|
204
|
-
}
|
|
205
|
-
cursor = next
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
157
|
// Number.MAX_SAFE_INTEGER is 2^53 - 1, but we reset earlier to avoid any precision issues
|
|
210
158
|
const MAX_SAFE_VERSION = 0x1fffffffffffff // 2^53 - 1
|
|
211
159
|
|
|
@@ -238,7 +186,7 @@ export function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
|
|
|
238
186
|
*
|
|
239
187
|
* @returns Container object with markers, blocks map, and dispose function
|
|
240
188
|
*/
|
|
241
|
-
|
|
189
|
+
function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
|
|
242
190
|
const startMarker = document.createComment('fict:list:start')
|
|
243
191
|
const endMarker = document.createComment('fict:list:end')
|
|
244
192
|
|
|
@@ -306,7 +254,7 @@ export function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
|
|
|
306
254
|
* @param render - Function that creates the DOM nodes and sets up bindings
|
|
307
255
|
* @returns New KeyedBlock
|
|
308
256
|
*/
|
|
309
|
-
|
|
257
|
+
function createKeyedBlock<T>(
|
|
310
258
|
key: string | number,
|
|
311
259
|
item: T,
|
|
312
260
|
index: number,
|
|
@@ -377,13 +325,6 @@ export function createKeyedBlock<T>(
|
|
|
377
325
|
// Utilities
|
|
378
326
|
// ============================================================================
|
|
379
327
|
|
|
380
|
-
/**
|
|
381
|
-
* Find the first node after the start marker (for getting current anchor)
|
|
382
|
-
*/
|
|
383
|
-
export function getFirstNodeAfter(marker: Comment): Node | null {
|
|
384
|
-
return marker.nextSibling
|
|
385
|
-
}
|
|
386
|
-
|
|
387
328
|
/**
|
|
388
329
|
* Check if a node is between two markers
|
|
389
330
|
*/
|