@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
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Helpers for Compiler-Generated Fine-Grained Updates
|
|
3
|
+
*
|
|
4
|
+
* These helpers are used by the compiler to generate efficient keyed list rendering.
|
|
5
|
+
* They provide low-level primitives for DOM node manipulation without rebuilding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createElement } from './dom'
|
|
9
|
+
import { createRenderEffect } from './effect'
|
|
10
|
+
import {
|
|
11
|
+
createRootContext,
|
|
12
|
+
destroyRoot,
|
|
13
|
+
flushOnMount,
|
|
14
|
+
popRoot,
|
|
15
|
+
pushRoot,
|
|
16
|
+
type RootContext,
|
|
17
|
+
} from './lifecycle'
|
|
18
|
+
import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
|
|
19
|
+
import reconcileArrays from './reconcile'
|
|
20
|
+
import { batch } from './scheduler'
|
|
21
|
+
import { createSignal, setActiveSub, type Signal } from './signal'
|
|
22
|
+
import type { FictNode } from './types'
|
|
23
|
+
|
|
24
|
+
// Re-export shared DOM helpers for compiler-generated code
|
|
25
|
+
export { insertNodesBefore, removeNodes, toNodeArray }
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Types
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A keyed block represents a single item in a list with its associated DOM nodes and state
|
|
33
|
+
*/
|
|
34
|
+
export interface KeyedBlock<T = unknown> {
|
|
35
|
+
/** Unique key for this block */
|
|
36
|
+
key: string | number
|
|
37
|
+
/** DOM nodes belonging to this block */
|
|
38
|
+
nodes: Node[]
|
|
39
|
+
/** Root context for lifecycle management */
|
|
40
|
+
root: RootContext
|
|
41
|
+
/** Signal containing the current item value */
|
|
42
|
+
item: Signal<T>
|
|
43
|
+
/** Signal containing the current index */
|
|
44
|
+
index: Signal<number>
|
|
45
|
+
/** Last raw item value assigned to this block */
|
|
46
|
+
rawItem: T
|
|
47
|
+
/** Last raw index value assigned to this block */
|
|
48
|
+
rawIndex: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Container for managing keyed list blocks
|
|
53
|
+
*/
|
|
54
|
+
export interface KeyedListContainer<T = unknown> {
|
|
55
|
+
/** Start marker comment node */
|
|
56
|
+
startMarker: Comment
|
|
57
|
+
/** End marker comment node */
|
|
58
|
+
endMarker: Comment
|
|
59
|
+
/** Map of key to block */
|
|
60
|
+
blocks: Map<string | number, KeyedBlock<T>>
|
|
61
|
+
/** Scratch map reused for the next render */
|
|
62
|
+
nextBlocks: Map<string | number, KeyedBlock<T>>
|
|
63
|
+
/** Current nodes in DOM order (including markers) */
|
|
64
|
+
currentNodes: Node[]
|
|
65
|
+
/** Next-frame node buffer to avoid reallocations */
|
|
66
|
+
nextNodes: Node[]
|
|
67
|
+
/** Ordered blocks in current DOM order */
|
|
68
|
+
orderedBlocks: KeyedBlock<T>[]
|
|
69
|
+
/** Next-frame ordered block buffer to avoid reallocations */
|
|
70
|
+
nextOrderedBlocks: KeyedBlock<T>[]
|
|
71
|
+
/** Track position of keys in the ordered buffer to handle duplicates */
|
|
72
|
+
orderedIndexByKey: Map<string | number, number>
|
|
73
|
+
/** Cleanup function */
|
|
74
|
+
dispose: () => void
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Binding handle returned by createKeyedList for compiler-generated code
|
|
79
|
+
*/
|
|
80
|
+
export interface KeyedListBinding {
|
|
81
|
+
/** Document fragment placeholder inserted by the compiler/runtime */
|
|
82
|
+
marker: DocumentFragment
|
|
83
|
+
/** Start marker comment node */
|
|
84
|
+
startMarker: Comment
|
|
85
|
+
/** End marker comment node */
|
|
86
|
+
endMarker: Comment
|
|
87
|
+
/** Flush pending items - call after markers are inserted into DOM */
|
|
88
|
+
flush?: () => void
|
|
89
|
+
/** Cleanup function */
|
|
90
|
+
dispose: () => void
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type FineGrainedRenderItem<T> = (
|
|
94
|
+
itemSig: Signal<T>,
|
|
95
|
+
indexSig: Signal<number>,
|
|
96
|
+
key: string | number,
|
|
97
|
+
) => Node[]
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A block identified by start/end comment markers.
|
|
101
|
+
*/
|
|
102
|
+
export interface MarkerBlock {
|
|
103
|
+
start: Comment
|
|
104
|
+
end: Comment
|
|
105
|
+
root?: RootContext
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// DOM Manipulation Primitives
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Move nodes to a position before the anchor node.
|
|
114
|
+
* This is optimized to avoid unnecessary DOM operations.
|
|
115
|
+
*
|
|
116
|
+
* @param parent - Parent node to move nodes within
|
|
117
|
+
* @param nodes - Array of nodes to move
|
|
118
|
+
* @param anchor - Node to insert before (or null for end)
|
|
119
|
+
*/
|
|
120
|
+
export function moveNodesBefore(parent: Node, nodes: Node[], anchor: Node | null): void {
|
|
121
|
+
// Insert in reverse order to maintain correct sequence
|
|
122
|
+
// This way each node becomes the new anchor for the next
|
|
123
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
124
|
+
const node = nodes[i]!
|
|
125
|
+
if (!node || !(node instanceof Node)) {
|
|
126
|
+
throw new Error('Invalid node in moveNodesBefore')
|
|
127
|
+
}
|
|
128
|
+
// Only move if not already in correct position
|
|
129
|
+
if (node.nextSibling !== anchor) {
|
|
130
|
+
if (node.ownerDocument !== parent.ownerDocument && parent.ownerDocument) {
|
|
131
|
+
parent.ownerDocument.adoptNode(node)
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
parent.insertBefore(node, anchor)
|
|
135
|
+
} catch (e: any) {
|
|
136
|
+
if (parent.ownerDocument) {
|
|
137
|
+
try {
|
|
138
|
+
const clone = parent.ownerDocument.importNode(node, true)
|
|
139
|
+
parent.insertBefore(clone, anchor)
|
|
140
|
+
// Note: Cloning during move breaks references in KeyedBlock.nodes
|
|
141
|
+
// This is a worst-case fallback for tests.
|
|
142
|
+
continue
|
|
143
|
+
} catch {
|
|
144
|
+
// Clone fallback failed
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw e
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
anchor = node
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Remove an array of nodes from the DOM
|
|
156
|
+
*
|
|
157
|
+
* @param nodes - Array of nodes to remove
|
|
158
|
+
*/
|
|
159
|
+
/**
|
|
160
|
+
* Move an entire marker-delimited block (including markers) before the anchor.
|
|
161
|
+
*/
|
|
162
|
+
export function moveMarkerBlock(parent: Node, block: MarkerBlock, anchor: Node | null): void {
|
|
163
|
+
const nodes = collectBlockNodes(block)
|
|
164
|
+
if (nodes.length === 0) return
|
|
165
|
+
moveNodesBefore(parent, nodes, anchor)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Destroy a marker-delimited block, removing nodes and destroying the associated root.
|
|
170
|
+
*/
|
|
171
|
+
export function destroyMarkerBlock(block: MarkerBlock): void {
|
|
172
|
+
if (block.root) {
|
|
173
|
+
destroyRoot(block.root)
|
|
174
|
+
}
|
|
175
|
+
removeBlockRange(block)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function collectBlockNodes(block: MarkerBlock): Node[] {
|
|
179
|
+
const nodes: Node[] = []
|
|
180
|
+
let cursor: Node | null = block.start
|
|
181
|
+
while (cursor) {
|
|
182
|
+
nodes.push(cursor)
|
|
183
|
+
if (cursor === block.end) {
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
cursor = cursor.nextSibling
|
|
187
|
+
}
|
|
188
|
+
return nodes
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function removeBlockRange(block: MarkerBlock): void {
|
|
192
|
+
let cursor: Node | null = block.start
|
|
193
|
+
while (cursor) {
|
|
194
|
+
const next: Node | null = cursor.nextSibling
|
|
195
|
+
cursor.parentNode?.removeChild(cursor)
|
|
196
|
+
if (cursor === block.end) {
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
cursor = next
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function createVersionedSignalAccessor<T>(initialValue: T): Signal<T> {
|
|
204
|
+
let current = initialValue
|
|
205
|
+
let version = 0
|
|
206
|
+
const track = createSignal(version)
|
|
207
|
+
|
|
208
|
+
function accessor(value?: T): T | void {
|
|
209
|
+
if (arguments.length === 0) {
|
|
210
|
+
track()
|
|
211
|
+
return current
|
|
212
|
+
}
|
|
213
|
+
current = value as T
|
|
214
|
+
version++
|
|
215
|
+
track(version)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return accessor as Signal<T>
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Keyed List Container
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Create a container for managing a keyed list.
|
|
227
|
+
* This sets up the marker nodes and provides cleanup.
|
|
228
|
+
*
|
|
229
|
+
* @returns Container object with markers, blocks map, and dispose function
|
|
230
|
+
*/
|
|
231
|
+
export function createKeyedListContainer<T = unknown>(): KeyedListContainer<T> {
|
|
232
|
+
const startMarker = document.createComment('fict:list:start')
|
|
233
|
+
const endMarker = document.createComment('fict:list:end')
|
|
234
|
+
|
|
235
|
+
const dispose = () => {
|
|
236
|
+
// Clean up all blocks
|
|
237
|
+
for (const block of container.blocks.values()) {
|
|
238
|
+
destroyRoot(block.root)
|
|
239
|
+
// Nodes are removed by parent disposal or specific cleanup if needed
|
|
240
|
+
// But for list disposal, we just clear the container
|
|
241
|
+
}
|
|
242
|
+
container.blocks.clear()
|
|
243
|
+
container.nextBlocks.clear()
|
|
244
|
+
|
|
245
|
+
// Remove nodes (including markers)
|
|
246
|
+
const range = document.createRange()
|
|
247
|
+
range.setStartBefore(startMarker)
|
|
248
|
+
range.setEndAfter(endMarker)
|
|
249
|
+
range.deleteContents()
|
|
250
|
+
|
|
251
|
+
// Clear cache
|
|
252
|
+
container.currentNodes = []
|
|
253
|
+
container.nextNodes = []
|
|
254
|
+
container.nextBlocks.clear()
|
|
255
|
+
container.orderedBlocks.length = 0
|
|
256
|
+
container.nextOrderedBlocks.length = 0
|
|
257
|
+
container.orderedIndexByKey.clear()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const container: KeyedListContainer<T> = {
|
|
261
|
+
startMarker,
|
|
262
|
+
endMarker,
|
|
263
|
+
blocks: new Map<string | number, KeyedBlock<T>>(),
|
|
264
|
+
nextBlocks: new Map<string | number, KeyedBlock<T>>(),
|
|
265
|
+
currentNodes: [startMarker, endMarker],
|
|
266
|
+
nextNodes: [],
|
|
267
|
+
orderedBlocks: [],
|
|
268
|
+
nextOrderedBlocks: [],
|
|
269
|
+
orderedIndexByKey: new Map<string | number, number>(),
|
|
270
|
+
dispose,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return container
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Block Creation Helpers
|
|
278
|
+
// ============================================================================
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Create a new keyed block with the given render function
|
|
282
|
+
*
|
|
283
|
+
* @param key - Unique key for this block
|
|
284
|
+
* @param item - Initial item value
|
|
285
|
+
* @param index - Initial index
|
|
286
|
+
* @param render - Function that creates the DOM nodes and sets up bindings
|
|
287
|
+
* @returns New KeyedBlock
|
|
288
|
+
*/
|
|
289
|
+
export function createKeyedBlock<T>(
|
|
290
|
+
key: string | number,
|
|
291
|
+
item: T,
|
|
292
|
+
index: number,
|
|
293
|
+
render: (item: Signal<T>, index: Signal<number>, key: string | number) => Node[],
|
|
294
|
+
needsIndex = true,
|
|
295
|
+
): KeyedBlock<T> {
|
|
296
|
+
// Use versioned signal for all item types; avoid diffing proxy overhead for objects
|
|
297
|
+
const itemSig = createVersionedSignalAccessor(item)
|
|
298
|
+
|
|
299
|
+
const indexSig = needsIndex
|
|
300
|
+
? createSignal<number>(index)
|
|
301
|
+
: (((next?: number) => {
|
|
302
|
+
if (arguments.length === 0) return index
|
|
303
|
+
index = next as number
|
|
304
|
+
return index
|
|
305
|
+
}) as Signal<number>)
|
|
306
|
+
const root = createRootContext()
|
|
307
|
+
const prevRoot = pushRoot(root)
|
|
308
|
+
|
|
309
|
+
// Isolate child effects from the outer effect (e.g., performDiff) by clearing activeSub.
|
|
310
|
+
// This prevents child effects from being purged when the outer effect re-runs.
|
|
311
|
+
const prevSub = setActiveSub(undefined)
|
|
312
|
+
|
|
313
|
+
let nodes: Node[] = []
|
|
314
|
+
try {
|
|
315
|
+
const rendered = render(itemSig, indexSig, key)
|
|
316
|
+
// If render returns real DOM nodes/arrays, preserve them to avoid
|
|
317
|
+
// reparenting side-effects (tests may pre-insert them).
|
|
318
|
+
if (
|
|
319
|
+
rendered instanceof Node ||
|
|
320
|
+
(Array.isArray(rendered) && rendered.every(n => n instanceof Node))
|
|
321
|
+
) {
|
|
322
|
+
nodes = toNodeArray(rendered)
|
|
323
|
+
} else {
|
|
324
|
+
const element = createElement(rendered as unknown as FictNode)
|
|
325
|
+
nodes = toNodeArray(element)
|
|
326
|
+
}
|
|
327
|
+
} finally {
|
|
328
|
+
setActiveSub(prevSub)
|
|
329
|
+
popRoot(prevRoot)
|
|
330
|
+
flushOnMount(root)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
key,
|
|
335
|
+
nodes,
|
|
336
|
+
root,
|
|
337
|
+
item: itemSig,
|
|
338
|
+
index: indexSig,
|
|
339
|
+
rawItem: item,
|
|
340
|
+
rawIndex: index,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// Utilities
|
|
346
|
+
// ============================================================================
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Find the first node after the start marker (for getting current anchor)
|
|
350
|
+
*/
|
|
351
|
+
export function getFirstNodeAfter(marker: Comment): Node | null {
|
|
352
|
+
return marker.nextSibling
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check if a node is between two markers
|
|
357
|
+
*/
|
|
358
|
+
export function isNodeBetweenMarkers(
|
|
359
|
+
node: Node,
|
|
360
|
+
startMarker: Comment,
|
|
361
|
+
endMarker: Comment,
|
|
362
|
+
): boolean {
|
|
363
|
+
let current: Node | null = startMarker.nextSibling
|
|
364
|
+
while (current && current !== endMarker) {
|
|
365
|
+
if (current === node) return true
|
|
366
|
+
current = current.nextSibling
|
|
367
|
+
}
|
|
368
|
+
return false
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ============================================================================
|
|
372
|
+
// High-Level List Binding (for compiler-generated code)
|
|
373
|
+
// ============================================================================
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Create a keyed list binding with automatic diffing and DOM updates.
|
|
377
|
+
* This is used by compiler-generated code for efficient list rendering.
|
|
378
|
+
*
|
|
379
|
+
* @param getItems - Function that returns the current array of items
|
|
380
|
+
* @param keyFn - Function to extract unique key from each item
|
|
381
|
+
* @param renderItem - Function that creates DOM nodes for each item
|
|
382
|
+
* @returns Binding handle with markers and dispose function
|
|
383
|
+
*/
|
|
384
|
+
export function createKeyedList<T>(
|
|
385
|
+
getItems: () => T[],
|
|
386
|
+
keyFn: (item: T, index: number) => string | number,
|
|
387
|
+
renderItem: FineGrainedRenderItem<T>,
|
|
388
|
+
needsIndex?: boolean,
|
|
389
|
+
): KeyedListBinding {
|
|
390
|
+
const resolvedNeedsIndex =
|
|
391
|
+
arguments.length >= 4 ? !!needsIndex : renderItem.length > 1 /* has index param */
|
|
392
|
+
return createFineGrainedKeyedList(getItems, keyFn, renderItem, resolvedNeedsIndex)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function createFineGrainedKeyedList<T>(
|
|
396
|
+
getItems: () => T[],
|
|
397
|
+
keyFn: (item: T, index: number) => string | number,
|
|
398
|
+
renderItem: FineGrainedRenderItem<T>,
|
|
399
|
+
needsIndex: boolean,
|
|
400
|
+
): KeyedListBinding {
|
|
401
|
+
const container = createKeyedListContainer<T>()
|
|
402
|
+
const fragment = document.createDocumentFragment()
|
|
403
|
+
fragment.append(container.startMarker, container.endMarker)
|
|
404
|
+
let pendingItems: T[] | null = null
|
|
405
|
+
let disposed = false
|
|
406
|
+
|
|
407
|
+
const performDiff = () => {
|
|
408
|
+
if (disposed) return
|
|
409
|
+
|
|
410
|
+
batch(() => {
|
|
411
|
+
const newItems = pendingItems || getItems()
|
|
412
|
+
pendingItems = null
|
|
413
|
+
|
|
414
|
+
const oldBlocks = container.blocks
|
|
415
|
+
const newBlocks = container.nextBlocks
|
|
416
|
+
const prevOrderedBlocks = container.orderedBlocks
|
|
417
|
+
const nextOrderedBlocks = container.nextOrderedBlocks
|
|
418
|
+
const orderedIndexByKey = container.orderedIndexByKey
|
|
419
|
+
newBlocks.clear()
|
|
420
|
+
nextOrderedBlocks.length = 0
|
|
421
|
+
orderedIndexByKey.clear()
|
|
422
|
+
|
|
423
|
+
const endParent = container.endMarker.parentNode
|
|
424
|
+
const startParent = container.startMarker.parentNode
|
|
425
|
+
const parent =
|
|
426
|
+
endParent && startParent && endParent === startParent && (endParent as Node).isConnected
|
|
427
|
+
? (endParent as ParentNode & Node)
|
|
428
|
+
: null
|
|
429
|
+
|
|
430
|
+
// If markers aren't mounted yet, store items and retry in microtask
|
|
431
|
+
if (!parent) {
|
|
432
|
+
pendingItems = newItems
|
|
433
|
+
queueMicrotask(performDiff)
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (newItems.length === 0) {
|
|
438
|
+
if (oldBlocks.size > 0) {
|
|
439
|
+
for (const block of oldBlocks.values()) {
|
|
440
|
+
destroyRoot(block.root)
|
|
441
|
+
removeNodes(block.nodes)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
oldBlocks.clear()
|
|
445
|
+
newBlocks.clear()
|
|
446
|
+
prevOrderedBlocks.length = 0
|
|
447
|
+
nextOrderedBlocks.length = 0
|
|
448
|
+
orderedIndexByKey.clear()
|
|
449
|
+
container.currentNodes.length = 0
|
|
450
|
+
container.currentNodes.push(container.startMarker, container.endMarker)
|
|
451
|
+
container.nextNodes.length = 0
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const prevCount = prevOrderedBlocks.length
|
|
456
|
+
let appendCandidate = prevCount > 0 && newItems.length >= prevCount
|
|
457
|
+
const appendedBlocks: KeyedBlock<T>[] = []
|
|
458
|
+
|
|
459
|
+
// Phase 1: Build new blocks map (reuse or create)
|
|
460
|
+
newItems.forEach((item, index) => {
|
|
461
|
+
const key = keyFn(item, index)
|
|
462
|
+
const existed = oldBlocks.has(key)
|
|
463
|
+
let block = oldBlocks.get(key)
|
|
464
|
+
|
|
465
|
+
if (block) {
|
|
466
|
+
if (block.rawItem !== item) {
|
|
467
|
+
block.rawItem = item
|
|
468
|
+
block.item(item)
|
|
469
|
+
}
|
|
470
|
+
if (needsIndex && block.rawIndex !== index) {
|
|
471
|
+
block.rawIndex = index
|
|
472
|
+
block.index(index)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// If newBlocks already has this key (duplicate key case), clean up the previous block
|
|
477
|
+
const existingBlock = newBlocks.get(key)
|
|
478
|
+
if (existingBlock && existingBlock !== block) {
|
|
479
|
+
destroyRoot(existingBlock.root)
|
|
480
|
+
removeNodes(existingBlock.nodes)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (block) {
|
|
484
|
+
newBlocks.set(key, block)
|
|
485
|
+
oldBlocks.delete(key)
|
|
486
|
+
} else {
|
|
487
|
+
const existingBlock = newBlocks.get(key)
|
|
488
|
+
if (existingBlock) {
|
|
489
|
+
destroyRoot(existingBlock.root)
|
|
490
|
+
removeNodes(existingBlock.nodes)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Create new block
|
|
494
|
+
block = createKeyedBlock(key, item, index, renderItem, needsIndex)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const resolvedBlock = block!
|
|
498
|
+
|
|
499
|
+
newBlocks.set(key, resolvedBlock)
|
|
500
|
+
|
|
501
|
+
const position = orderedIndexByKey.get(key)
|
|
502
|
+
if (position !== undefined) {
|
|
503
|
+
appendCandidate = false
|
|
504
|
+
}
|
|
505
|
+
if (appendCandidate) {
|
|
506
|
+
if (index < prevCount) {
|
|
507
|
+
if (!prevOrderedBlocks[index] || prevOrderedBlocks[index]!.key !== key) {
|
|
508
|
+
appendCandidate = false
|
|
509
|
+
}
|
|
510
|
+
} else if (existed) {
|
|
511
|
+
appendCandidate = false
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (position !== undefined) {
|
|
515
|
+
const prior = nextOrderedBlocks[position]
|
|
516
|
+
if (prior && prior !== resolvedBlock) {
|
|
517
|
+
destroyRoot(prior.root)
|
|
518
|
+
removeNodes(prior.nodes)
|
|
519
|
+
}
|
|
520
|
+
nextOrderedBlocks[position] = resolvedBlock
|
|
521
|
+
} else {
|
|
522
|
+
orderedIndexByKey.set(key, nextOrderedBlocks.length)
|
|
523
|
+
nextOrderedBlocks.push(resolvedBlock)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (appendCandidate && index >= prevCount) {
|
|
527
|
+
appendedBlocks.push(resolvedBlock)
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
const canAppend =
|
|
532
|
+
appendCandidate &&
|
|
533
|
+
prevCount > 0 &&
|
|
534
|
+
newItems.length > prevCount &&
|
|
535
|
+
oldBlocks.size === 0 &&
|
|
536
|
+
appendedBlocks.length > 0
|
|
537
|
+
if (canAppend) {
|
|
538
|
+
const appendedNodes: Node[] = []
|
|
539
|
+
for (const block of appendedBlocks) {
|
|
540
|
+
for (let i = 0; i < block.nodes.length; i++) {
|
|
541
|
+
appendedNodes.push(block.nodes[i]!)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (appendedNodes.length > 0) {
|
|
545
|
+
insertNodesBefore(parent, appendedNodes, container.endMarker)
|
|
546
|
+
const currentNodes = container.currentNodes
|
|
547
|
+
currentNodes.pop()
|
|
548
|
+
for (let i = 0; i < appendedNodes.length; i++) {
|
|
549
|
+
currentNodes.push(appendedNodes[i]!)
|
|
550
|
+
}
|
|
551
|
+
currentNodes.push(container.endMarker)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
container.blocks = newBlocks
|
|
555
|
+
container.nextBlocks = oldBlocks
|
|
556
|
+
container.orderedBlocks = nextOrderedBlocks
|
|
557
|
+
container.nextOrderedBlocks = prevOrderedBlocks
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Phase 2: Remove old blocks that are no longer in the list
|
|
562
|
+
if (oldBlocks.size > 0) {
|
|
563
|
+
for (const block of oldBlocks.values()) {
|
|
564
|
+
destroyRoot(block.root)
|
|
565
|
+
removeNodes(block.nodes)
|
|
566
|
+
}
|
|
567
|
+
oldBlocks.clear()
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Phase 3: Reconcile DOM with buffered node arrays
|
|
571
|
+
if (newBlocks.size > 0 || container.currentNodes.length > 0) {
|
|
572
|
+
const prevNodes = container.currentNodes
|
|
573
|
+
const nextNodes = container.nextNodes
|
|
574
|
+
nextNodes.length = 0
|
|
575
|
+
nextNodes.push(container.startMarker)
|
|
576
|
+
|
|
577
|
+
for (let i = 0; i < nextOrderedBlocks.length; i++) {
|
|
578
|
+
const nodes = nextOrderedBlocks[i]!.nodes
|
|
579
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
580
|
+
nextNodes.push(nodes[j]!)
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
nextNodes.push(container.endMarker)
|
|
585
|
+
|
|
586
|
+
reconcileArrays(parent, prevNodes, nextNodes)
|
|
587
|
+
|
|
588
|
+
// Swap buffers to reuse arrays on next diff
|
|
589
|
+
container.currentNodes = nextNodes
|
|
590
|
+
container.nextNodes = prevNodes
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Swap block maps for reuse
|
|
594
|
+
container.blocks = newBlocks
|
|
595
|
+
container.nextBlocks = oldBlocks
|
|
596
|
+
container.orderedBlocks = nextOrderedBlocks
|
|
597
|
+
container.nextOrderedBlocks = prevOrderedBlocks
|
|
598
|
+
})
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const effectDispose = createRenderEffect(performDiff)
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
marker: fragment,
|
|
605
|
+
startMarker: container.startMarker,
|
|
606
|
+
endMarker: container.endMarker,
|
|
607
|
+
// Flush pending items - call after markers are inserted into DOM
|
|
608
|
+
flush: () => {
|
|
609
|
+
if (pendingItems !== null) {
|
|
610
|
+
performDiff()
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
dispose: () => {
|
|
614
|
+
disposed = true
|
|
615
|
+
effectDispose?.()
|
|
616
|
+
container.dispose()
|
|
617
|
+
},
|
|
618
|
+
}
|
|
619
|
+
}
|
package/src/memo.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { computed } from './signal'
|
|
2
|
+
import type { Signal } from './signal'
|
|
3
|
+
|
|
4
|
+
export type Memo<T> = () => T
|
|
5
|
+
|
|
6
|
+
export function createMemo<T>(fn: () => T): Memo<T> {
|
|
7
|
+
return computed(fn)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function fromSignal<T>(signal: Signal<T>): Memo<T> {
|
|
11
|
+
return () => signal()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const $memo = createMemo
|