@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/node-ops.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level DOM node helpers shared across runtime modules.
|
|
3
|
+
* Keep this file dependency-free to avoid module cycles.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert a value to a flat array of DOM nodes.
|
|
8
|
+
* Defensively handles proxies and non-DOM values.
|
|
9
|
+
*/
|
|
10
|
+
export function toNodeArray(node: Node | Node[] | unknown): Node[] {
|
|
11
|
+
try {
|
|
12
|
+
if (Array.isArray(node)) {
|
|
13
|
+
// Preserve original array reference when it's already a flat Node array
|
|
14
|
+
let allNodes = true
|
|
15
|
+
for (const item of node) {
|
|
16
|
+
let isItemNode = false
|
|
17
|
+
try {
|
|
18
|
+
isItemNode = item instanceof Node
|
|
19
|
+
} catch {
|
|
20
|
+
isItemNode = false
|
|
21
|
+
}
|
|
22
|
+
if (!isItemNode) {
|
|
23
|
+
allNodes = false
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (allNodes) {
|
|
28
|
+
return node as Node[]
|
|
29
|
+
}
|
|
30
|
+
const result: Node[] = []
|
|
31
|
+
for (const item of node) {
|
|
32
|
+
result.push(...toNodeArray(item))
|
|
33
|
+
}
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
36
|
+
if (node === null || node === undefined || node === false) {
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let isNode = false
|
|
44
|
+
try {
|
|
45
|
+
isNode = node instanceof Node
|
|
46
|
+
} catch {
|
|
47
|
+
// If safe check fails, treat as primitive string
|
|
48
|
+
isNode = false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isNode) {
|
|
52
|
+
try {
|
|
53
|
+
if (node instanceof DocumentFragment) {
|
|
54
|
+
return Array.from(node.childNodes)
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Ignore fragment check error
|
|
58
|
+
}
|
|
59
|
+
return [node as Node]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Duck-type BindingHandle-like values
|
|
64
|
+
if (typeof node === 'object' && node !== null && 'marker' in node) {
|
|
65
|
+
return toNodeArray((node as { marker: unknown }).marker)
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Ignore property check error
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Primitive fallback
|
|
72
|
+
try {
|
|
73
|
+
return [document.createTextNode(String(node))]
|
|
74
|
+
} catch {
|
|
75
|
+
return [document.createTextNode('')]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Insert nodes before an anchor node, preserving order.
|
|
81
|
+
* Uses DocumentFragment for batch insertion when inserting multiple nodes.
|
|
82
|
+
*/
|
|
83
|
+
export function insertNodesBefore(
|
|
84
|
+
parent: ParentNode & Node,
|
|
85
|
+
nodes: Node[],
|
|
86
|
+
anchor: Node | null,
|
|
87
|
+
): void {
|
|
88
|
+
if (nodes.length === 0) return
|
|
89
|
+
|
|
90
|
+
// Single node optimization - direct insertion
|
|
91
|
+
if (nodes.length === 1) {
|
|
92
|
+
const node = nodes[0]!
|
|
93
|
+
if (node === undefined || node === null) return
|
|
94
|
+
if (node.ownerDocument !== parent.ownerDocument && parent.ownerDocument) {
|
|
95
|
+
parent.ownerDocument.adoptNode(node)
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
parent.insertBefore(node, anchor)
|
|
99
|
+
} catch (e: unknown) {
|
|
100
|
+
if (parent.ownerDocument) {
|
|
101
|
+
try {
|
|
102
|
+
const clone = parent.ownerDocument.importNode(node, true)
|
|
103
|
+
parent.insertBefore(clone, anchor)
|
|
104
|
+
return
|
|
105
|
+
} catch {
|
|
106
|
+
// Clone fallback failed
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw e
|
|
110
|
+
}
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Batch insertion using DocumentFragment for multiple nodes
|
|
115
|
+
const doc = parent.ownerDocument
|
|
116
|
+
if (doc) {
|
|
117
|
+
const frag = doc.createDocumentFragment()
|
|
118
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
119
|
+
const node = nodes[i]!
|
|
120
|
+
if (node === undefined || node === null) continue
|
|
121
|
+
// Handle DocumentFragment - append children
|
|
122
|
+
if (node.nodeType === 11) {
|
|
123
|
+
const childrenArr = Array.from(node.childNodes)
|
|
124
|
+
for (let j = 0; j < childrenArr.length; j++) {
|
|
125
|
+
frag.appendChild(childrenArr[j]!)
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (node.ownerDocument !== doc) {
|
|
129
|
+
doc.adoptNode(node)
|
|
130
|
+
}
|
|
131
|
+
frag.appendChild(node)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
parent.insertBefore(frag, anchor)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Fallback for non-document contexts (rare)
|
|
139
|
+
const insertSingle = (nodeToInsert: Node, anchorNode: Node | null): Node => {
|
|
140
|
+
if (nodeToInsert.ownerDocument !== parent.ownerDocument && parent.ownerDocument) {
|
|
141
|
+
parent.ownerDocument.adoptNode(nodeToInsert)
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
parent.insertBefore(nodeToInsert, anchorNode)
|
|
145
|
+
return nodeToInsert
|
|
146
|
+
} catch (e: unknown) {
|
|
147
|
+
if (parent.ownerDocument) {
|
|
148
|
+
try {
|
|
149
|
+
const clone = parent.ownerDocument.importNode(nodeToInsert, true)
|
|
150
|
+
parent.insertBefore(clone, anchorNode)
|
|
151
|
+
return clone
|
|
152
|
+
} catch {
|
|
153
|
+
// Clone fallback failed
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw e
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
161
|
+
const node = nodes[i]!
|
|
162
|
+
if (node === undefined || node === null) continue
|
|
163
|
+
|
|
164
|
+
// Handle DocumentFragment - insert children in reverse order
|
|
165
|
+
const isFrag = node.nodeType === 11
|
|
166
|
+
if (isFrag) {
|
|
167
|
+
const childrenArr = Array.from(node.childNodes)
|
|
168
|
+
for (let j = childrenArr.length - 1; j >= 0; j--) {
|
|
169
|
+
const child = childrenArr[j]!
|
|
170
|
+
anchor = insertSingle(child, anchor)
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
anchor = insertSingle(node, anchor)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Remove an array of nodes from the DOM.
|
|
180
|
+
*/
|
|
181
|
+
export function removeNodes(nodes: Node[]): void {
|
|
182
|
+
for (const node of nodes) {
|
|
183
|
+
node.parentNode?.removeChild(node)
|
|
184
|
+
}
|
|
185
|
+
}
|
package/src/props.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createMemo } from './memo'
|
|
2
|
+
|
|
3
|
+
const propGetters = new WeakSet<(...args: unknown[]) => unknown>()
|
|
4
|
+
const rawToProxy = new WeakMap<object, object>()
|
|
5
|
+
const proxyToRaw = new WeakMap<object, object>()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @internal
|
|
9
|
+
* Marks a zero-arg getter so props proxy can lazily evaluate it.
|
|
10
|
+
* Users normally never call this directly; the compiler injects it.
|
|
11
|
+
*/
|
|
12
|
+
export function __fictProp<T>(getter: () => T): () => T {
|
|
13
|
+
if (typeof getter === 'function' && getter.length === 0) {
|
|
14
|
+
propGetters.add(getter)
|
|
15
|
+
}
|
|
16
|
+
return getter
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isPropGetter(value: unknown): value is () => unknown {
|
|
20
|
+
return typeof value === 'function' && propGetters.has(value as (...args: unknown[]) => unknown)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createPropsProxy<T extends Record<string, unknown>>(props: T): T {
|
|
24
|
+
if (!props || typeof props !== 'object') {
|
|
25
|
+
return props
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (proxyToRaw.has(props)) {
|
|
29
|
+
return props
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cached = rawToProxy.get(props)
|
|
33
|
+
if (cached) {
|
|
34
|
+
return cached as T
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const proxy = new Proxy(props, {
|
|
38
|
+
get(target, prop, receiver) {
|
|
39
|
+
const value = Reflect.get(target, prop, receiver)
|
|
40
|
+
if (isPropGetter(value)) {
|
|
41
|
+
return value()
|
|
42
|
+
}
|
|
43
|
+
return value
|
|
44
|
+
},
|
|
45
|
+
set(target, prop, value, receiver) {
|
|
46
|
+
return Reflect.set(target, prop, value, receiver)
|
|
47
|
+
},
|
|
48
|
+
has(target, prop) {
|
|
49
|
+
return prop in target
|
|
50
|
+
},
|
|
51
|
+
ownKeys(target) {
|
|
52
|
+
return Reflect.ownKeys(target)
|
|
53
|
+
},
|
|
54
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
55
|
+
return Object.getOwnPropertyDescriptor(target, prop)
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
rawToProxy.set(props, proxy)
|
|
60
|
+
proxyToRaw.set(proxy, props)
|
|
61
|
+
|
|
62
|
+
return proxy as T
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function unwrapProps<T extends Record<string, unknown>>(props: T): T {
|
|
66
|
+
if (!props || typeof props !== 'object') {
|
|
67
|
+
return props
|
|
68
|
+
}
|
|
69
|
+
return (proxyToRaw.get(props) as T | undefined) ?? props
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a rest-like props object while preserving prop getters.
|
|
74
|
+
* Excludes the specified keys from the returned object.
|
|
75
|
+
*/
|
|
76
|
+
export function __fictPropsRest<T extends Record<string, unknown>>(
|
|
77
|
+
props: T,
|
|
78
|
+
exclude: (string | number | symbol)[],
|
|
79
|
+
): Record<string, unknown> {
|
|
80
|
+
const raw = unwrapProps(props)
|
|
81
|
+
const out: Record<string, unknown> = {}
|
|
82
|
+
const excludeSet = new Set(exclude)
|
|
83
|
+
|
|
84
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
85
|
+
if (excludeSet.has(key)) continue
|
|
86
|
+
out[key as string] = (raw as Record<string | symbol, unknown>)[key]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Wrap in props proxy so getters remain lazy when accessed via rest
|
|
90
|
+
return createPropsProxy(out)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Merge multiple props-like objects while preserving lazy getters.
|
|
95
|
+
* Later sources override earlier ones.
|
|
96
|
+
*
|
|
97
|
+
* Uses lazy lookup strategy - properties are only accessed when read,
|
|
98
|
+
* avoiding upfront iteration of all keys.
|
|
99
|
+
*/
|
|
100
|
+
type MergeSource<T extends Record<string, unknown>> = T | (() => T)
|
|
101
|
+
|
|
102
|
+
export function mergeProps<T extends Record<string, unknown>>(
|
|
103
|
+
...sources: (MergeSource<T> | null | undefined)[]
|
|
104
|
+
): Record<string, unknown> {
|
|
105
|
+
// Filter out null/undefined sources upfront and store as concrete type
|
|
106
|
+
const validSources: MergeSource<T>[] = sources.filter(
|
|
107
|
+
(s): s is MergeSource<T> => s != null && (typeof s === 'object' || typeof s === 'function'),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if (validSources.length === 0) {
|
|
111
|
+
return {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (validSources.length === 1 && typeof validSources[0] === 'object') {
|
|
115
|
+
// Return source directly to preserve getter behavior (consistent with multi-source)
|
|
116
|
+
return validSources[0]!
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resolveSource = (src: MergeSource<T>): T | undefined => {
|
|
120
|
+
const value = typeof src === 'function' ? src() : src
|
|
121
|
+
if (!value || typeof value !== 'object') return undefined
|
|
122
|
+
return unwrapProps(value as T)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return new Proxy({} as Record<string, unknown>, {
|
|
126
|
+
get(_, prop) {
|
|
127
|
+
// Symbol properties (like Symbol.iterator) should return undefined
|
|
128
|
+
if (typeof prop === 'symbol') {
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
// Search sources in reverse order (last wins)
|
|
132
|
+
for (let i = validSources.length - 1; i >= 0; i--) {
|
|
133
|
+
const src = validSources[i]!
|
|
134
|
+
const raw = resolveSource(src)
|
|
135
|
+
if (!raw || !(prop in raw)) continue
|
|
136
|
+
|
|
137
|
+
const value = (raw as Record<string | symbol, unknown>)[prop]
|
|
138
|
+
// Preserve prop getters - let child component's createPropsProxy unwrap lazily
|
|
139
|
+
if (typeof src === 'function' && !isPropGetter(value)) {
|
|
140
|
+
return __fictProp(() => {
|
|
141
|
+
const latest = resolveSource(src)
|
|
142
|
+
if (!latest || !(prop in latest)) return undefined
|
|
143
|
+
return (latest as Record<string | symbol, unknown>)[prop]
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
return value
|
|
147
|
+
}
|
|
148
|
+
return undefined
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
has(_, prop) {
|
|
152
|
+
for (const src of validSources) {
|
|
153
|
+
const raw = resolveSource(src)
|
|
154
|
+
if (raw && prop in raw) {
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return false
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
ownKeys() {
|
|
162
|
+
const keys = new Set<string | symbol>()
|
|
163
|
+
for (const src of validSources) {
|
|
164
|
+
const raw = resolveSource(src)
|
|
165
|
+
if (raw) {
|
|
166
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
167
|
+
keys.add(key)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return Array.from(keys)
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
175
|
+
for (let i = validSources.length - 1; i >= 0; i--) {
|
|
176
|
+
const raw = resolveSource(validSources[i]!)
|
|
177
|
+
if (raw && prop in raw) {
|
|
178
|
+
return {
|
|
179
|
+
enumerable: true,
|
|
180
|
+
configurable: true,
|
|
181
|
+
get: () => {
|
|
182
|
+
const value = (raw as Record<string | symbol, unknown>)[prop]
|
|
183
|
+
// Preserve prop getters - let child component's createPropsProxy unwrap lazily
|
|
184
|
+
return value
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return undefined
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export type PropGetter<T> = (() => T) & { __fictProp: true }
|
|
195
|
+
/**
|
|
196
|
+
* Memoize a prop getter to cache expensive computations.
|
|
197
|
+
* Use when prop expressions involve heavy calculations.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```tsx
|
|
201
|
+
* // Without useProp - recomputes on every access
|
|
202
|
+
* <Child data={expensiveComputation(list, filter)} />
|
|
203
|
+
*
|
|
204
|
+
* // With useProp - cached until dependencies change, auto-unwrapped by props proxy
|
|
205
|
+
* const memoizedData = useProp(() => expensiveComputation(list, filter))
|
|
206
|
+
* <Child data={memoizedData} />
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export function useProp<T>(getter: () => T): PropGetter<T> {
|
|
210
|
+
// Wrap in prop so component props proxy auto-unwraps when passed down.
|
|
211
|
+
return __fictProp(createMemo(getter)) as PropGetter<T>
|
|
212
|
+
}
|
package/src/reconcile.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fict DOM Reconciliation
|
|
3
|
+
*
|
|
4
|
+
* Efficient array reconciliation algorithm based on udomdiff.
|
|
5
|
+
* https://github.com/WebReflection/udomdiff
|
|
6
|
+
*
|
|
7
|
+
* This algorithm uses a 5-step strategy:
|
|
8
|
+
* 1. Common prefix - skip matching nodes at start
|
|
9
|
+
* 2. Common suffix - skip matching nodes at end
|
|
10
|
+
* 3. Append - insert remaining new nodes
|
|
11
|
+
* 4. Remove - remove remaining old nodes
|
|
12
|
+
* 5. Swap/Map fallback - handle complex rearrangements
|
|
13
|
+
*
|
|
14
|
+
* Most real-world updates (95%+) use fast paths without building a Map.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reconcile two arrays of DOM nodes, efficiently updating the DOM.
|
|
19
|
+
*
|
|
20
|
+
* @param parentNode - The parent element containing the nodes
|
|
21
|
+
* @param a - The old array of nodes (currently in DOM)
|
|
22
|
+
* @param b - The new array of nodes (target state)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const oldNodes = [node1, node2, node3]
|
|
27
|
+
* const newNodes = [node1, node4, node3] // node2 replaced with node4
|
|
28
|
+
* reconcileArrays(parent, oldNodes, newNodes)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export default function reconcileArrays(parentNode: ParentNode, a: Node[], b: Node[]): void {
|
|
32
|
+
const bLength = b.length
|
|
33
|
+
let aEnd = a.length
|
|
34
|
+
let bEnd = bLength
|
|
35
|
+
let aStart = 0
|
|
36
|
+
let bStart = 0
|
|
37
|
+
const after = aEnd > 0 ? a[aEnd - 1]!.nextSibling : null
|
|
38
|
+
let map: Map<Node, number> | null = null
|
|
39
|
+
|
|
40
|
+
while (aStart < aEnd || bStart < bEnd) {
|
|
41
|
+
// 1. Common prefix - nodes match at start
|
|
42
|
+
if (a[aStart] === b[bStart]) {
|
|
43
|
+
aStart++
|
|
44
|
+
bStart++
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Common suffix - nodes match at end
|
|
49
|
+
while (a[aEnd - 1] === b[bEnd - 1]) {
|
|
50
|
+
aEnd--
|
|
51
|
+
bEnd--
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Append - old array exhausted, insert remaining new nodes
|
|
55
|
+
if (aEnd === aStart) {
|
|
56
|
+
const node: Node | null =
|
|
57
|
+
bEnd < bLength ? (bStart ? b[bStart - 1]!.nextSibling : (b[bEnd - bStart] ?? null)) : after
|
|
58
|
+
|
|
59
|
+
const count = bEnd - bStart
|
|
60
|
+
const doc = (parentNode as Node).ownerDocument
|
|
61
|
+
if (count > 1 && doc) {
|
|
62
|
+
const frag = doc.createDocumentFragment()
|
|
63
|
+
for (let i = bStart; i < bEnd; i++) {
|
|
64
|
+
frag.appendChild(b[i]!)
|
|
65
|
+
}
|
|
66
|
+
parentNode.insertBefore(frag, node)
|
|
67
|
+
bStart = bEnd
|
|
68
|
+
} else {
|
|
69
|
+
while (bStart < bEnd) {
|
|
70
|
+
parentNode.insertBefore(b[bStart++]!, node)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// 4. Remove - new array exhausted, remove remaining old nodes
|
|
75
|
+
else if (bEnd === bStart) {
|
|
76
|
+
while (aStart < aEnd) {
|
|
77
|
+
const nodeToRemove = a[aStart]!
|
|
78
|
+
if (!map || !map.has(nodeToRemove)) {
|
|
79
|
+
nodeToRemove.parentNode?.removeChild(nodeToRemove)
|
|
80
|
+
}
|
|
81
|
+
aStart++
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 5a. Swap backward - detect backward swap pattern
|
|
85
|
+
else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) {
|
|
86
|
+
const node = a[--aEnd]!.nextSibling
|
|
87
|
+
parentNode.insertBefore(b[bStart++]!, a[aStart++]!.nextSibling)
|
|
88
|
+
parentNode.insertBefore(b[--bEnd]!, node)
|
|
89
|
+
// Update reference in old array for potential future matches
|
|
90
|
+
a[aEnd] = b[bEnd]!
|
|
91
|
+
}
|
|
92
|
+
// 5b. Map fallback - use Map for complex rearrangements
|
|
93
|
+
else {
|
|
94
|
+
// Build map on first use (lazy initialization)
|
|
95
|
+
if (!map) {
|
|
96
|
+
map = new Map()
|
|
97
|
+
let i = bStart
|
|
98
|
+
while (i < bEnd) {
|
|
99
|
+
map.set(b[i]!, i++)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const index = map.get(a[aStart]!)
|
|
104
|
+
|
|
105
|
+
if (index != null) {
|
|
106
|
+
if (bStart < index && index < bEnd) {
|
|
107
|
+
// Check for longest increasing subsequence
|
|
108
|
+
let i = aStart
|
|
109
|
+
let sequence = 1
|
|
110
|
+
let t: number | undefined
|
|
111
|
+
|
|
112
|
+
while (++i < aEnd && i < bEnd) {
|
|
113
|
+
t = map.get(a[i]!)
|
|
114
|
+
if (t == null || t !== index + sequence) break
|
|
115
|
+
sequence++
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Use optimal strategy based on sequence length
|
|
119
|
+
if (sequence > index - bStart) {
|
|
120
|
+
// Sequence is long enough - insert nodes before current
|
|
121
|
+
const node = a[aStart]!
|
|
122
|
+
while (bStart < index) {
|
|
123
|
+
parentNode.insertBefore(b[bStart++]!, node)
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Short sequence - replace
|
|
127
|
+
parentNode.replaceChild(b[bStart++]!, a[aStart++]!)
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
aStart++
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
// Node not in new array - remove it
|
|
134
|
+
const nodeToRemove = a[aStart++]!
|
|
135
|
+
nodeToRemove.parentNode?.removeChild(nodeToRemove)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Simple reconciliation for keyed lists.
|
|
143
|
+
* Uses the same algorithm but works with keyed blocks.
|
|
144
|
+
*
|
|
145
|
+
* @param parentNode - The parent element
|
|
146
|
+
* @param oldNodes - Old nodes in DOM order
|
|
147
|
+
* @param newNodes - New nodes in target order
|
|
148
|
+
*/
|
|
149
|
+
export function reconcileNodes(parentNode: ParentNode, oldNodes: Node[], newNodes: Node[]): void {
|
|
150
|
+
reconcileArrays(parentNode, oldNodes, newNodes)
|
|
151
|
+
}
|
package/src/ref.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RefObject } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a ref object for DOM element references.
|
|
5
|
+
*
|
|
6
|
+
* @returns A ref object with a `current` property initialized to `null`
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { createRef } from 'fict'
|
|
11
|
+
*
|
|
12
|
+
* function Component() {
|
|
13
|
+
* const inputRef = createRef<HTMLInputElement>()
|
|
14
|
+
*
|
|
15
|
+
* $effect(() => {
|
|
16
|
+
* inputRef.current?.focus()
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* return <input ref={inputRef} />
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function createRef<T extends HTMLElement = HTMLElement>(): RefObject<T> {
|
|
24
|
+
return { current: null }
|
|
25
|
+
}
|
package/src/scheduler.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { batch as baseBatch, untrack as baseUntrack } from './signal'
|
|
2
|
+
|
|
3
|
+
export function batch<T>(fn: () => T): T {
|
|
4
|
+
return baseBatch(fn)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function untrack<T>(fn: () => T): T {
|
|
8
|
+
return baseUntrack(fn)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Transition APIs for priority scheduling
|
|
12
|
+
export { startTransition, useTransition, useDeferredValue } from './transition'
|