@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.
Files changed (51) hide show
  1. package/README.md +17 -0
  2. package/dist/index.cjs +4224 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +1572 -0
  5. package/dist/index.d.ts +1572 -0
  6. package/dist/index.dev.js +4240 -0
  7. package/dist/index.dev.js.map +1 -0
  8. package/dist/index.js +4133 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/jsx-dev-runtime.cjs +44 -0
  11. package/dist/jsx-dev-runtime.cjs.map +1 -0
  12. package/dist/jsx-dev-runtime.js +14 -0
  13. package/dist/jsx-dev-runtime.js.map +1 -0
  14. package/dist/jsx-runtime.cjs +44 -0
  15. package/dist/jsx-runtime.cjs.map +1 -0
  16. package/dist/jsx-runtime.js +14 -0
  17. package/dist/jsx-runtime.js.map +1 -0
  18. package/dist/slim.cjs +3384 -0
  19. package/dist/slim.cjs.map +1 -0
  20. package/dist/slim.d.cts +475 -0
  21. package/dist/slim.d.ts +475 -0
  22. package/dist/slim.js +3335 -0
  23. package/dist/slim.js.map +1 -0
  24. package/package.json +68 -0
  25. package/src/binding.ts +2127 -0
  26. package/src/constants.ts +456 -0
  27. package/src/cycle-guard.ts +134 -0
  28. package/src/devtools.ts +17 -0
  29. package/src/dom.ts +683 -0
  30. package/src/effect.ts +83 -0
  31. package/src/error-boundary.ts +118 -0
  32. package/src/hooks.ts +72 -0
  33. package/src/index.ts +184 -0
  34. package/src/jsx-dev-runtime.ts +2 -0
  35. package/src/jsx-runtime.ts +2 -0
  36. package/src/jsx.ts +786 -0
  37. package/src/lifecycle.ts +273 -0
  38. package/src/list-helpers.ts +619 -0
  39. package/src/memo.ts +14 -0
  40. package/src/node-ops.ts +185 -0
  41. package/src/props.ts +212 -0
  42. package/src/reconcile.ts +151 -0
  43. package/src/ref.ts +25 -0
  44. package/src/scheduler.ts +12 -0
  45. package/src/signal.ts +1278 -0
  46. package/src/slim.ts +68 -0
  47. package/src/store.ts +210 -0
  48. package/src/suspense.ts +187 -0
  49. package/src/transition.ts +128 -0
  50. package/src/types.ts +172 -0
  51. 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