@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/slim.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slim fine-grained runtime entry for bundle-sensitive builds.
|
|
3
|
+
*
|
|
4
|
+
* Exposes only the DOM + signals surface required by compiler output and
|
|
5
|
+
* leaves out stores, resources, SSR, devtools, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Reactive primitives
|
|
9
|
+
export { createSignal, createSelector, $state } from './signal'
|
|
10
|
+
export { createMemo } from './memo'
|
|
11
|
+
export { createEffect, createRenderEffect } from './effect'
|
|
12
|
+
export { batch, untrack } from './scheduler'
|
|
13
|
+
|
|
14
|
+
// DOM rendering
|
|
15
|
+
export { render, template, createElement } from './dom'
|
|
16
|
+
export { Fragment } from './jsx'
|
|
17
|
+
|
|
18
|
+
// Core bindings used by compiler output
|
|
19
|
+
export {
|
|
20
|
+
insert,
|
|
21
|
+
bindText,
|
|
22
|
+
bindAttribute,
|
|
23
|
+
bindProperty,
|
|
24
|
+
bindClass,
|
|
25
|
+
bindStyle,
|
|
26
|
+
bindEvent,
|
|
27
|
+
bindRef,
|
|
28
|
+
createConditional,
|
|
29
|
+
createList,
|
|
30
|
+
delegateEvents,
|
|
31
|
+
clearDelegatedEvents,
|
|
32
|
+
} from './binding'
|
|
33
|
+
|
|
34
|
+
// Keyed list helpers (fine-grained DOM)
|
|
35
|
+
export {
|
|
36
|
+
createKeyedListContainer,
|
|
37
|
+
createKeyedList,
|
|
38
|
+
createKeyedBlock,
|
|
39
|
+
moveMarkerBlock,
|
|
40
|
+
destroyMarkerBlock,
|
|
41
|
+
insertNodesBefore,
|
|
42
|
+
removeNodes,
|
|
43
|
+
toNodeArray,
|
|
44
|
+
getFirstNodeAfter,
|
|
45
|
+
} from './list-helpers'
|
|
46
|
+
|
|
47
|
+
// Minimal hooks surface for builds that rely on hook helpers
|
|
48
|
+
export {
|
|
49
|
+
__fictUseContext,
|
|
50
|
+
__fictUseSignal,
|
|
51
|
+
__fictUseMemo,
|
|
52
|
+
__fictUseEffect,
|
|
53
|
+
__fictRender,
|
|
54
|
+
__fictPushContext,
|
|
55
|
+
__fictPopContext,
|
|
56
|
+
__fictResetContext,
|
|
57
|
+
} from './hooks'
|
|
58
|
+
|
|
59
|
+
// Props helpers (kept minimal for compatibility with compiler output)
|
|
60
|
+
export {
|
|
61
|
+
__fictProp,
|
|
62
|
+
__fictProp as prop,
|
|
63
|
+
__fictPropsRest,
|
|
64
|
+
mergeProps,
|
|
65
|
+
useProp,
|
|
66
|
+
createPropsProxy,
|
|
67
|
+
} from './props'
|
|
68
|
+
export { onDestroy } from './lifecycle'
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { signal, batch, type SignalAccessor } from './signal'
|
|
2
|
+
|
|
3
|
+
const PROXY = Symbol('fict:store-proxy')
|
|
4
|
+
const TARGET = Symbol('fict:store-target')
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Store (Deep Proxy)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export type Store<T> = T
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a Store: a reactive proxy that allows fine-grained access and mutation.
|
|
14
|
+
*
|
|
15
|
+
* @param initialValue - The initial state object
|
|
16
|
+
* @returns [store, setStore]
|
|
17
|
+
*/
|
|
18
|
+
export function createStore<T extends object>(
|
|
19
|
+
initialValue: T,
|
|
20
|
+
): [Store<T>, (fn: (state: T) => void | T) => void] {
|
|
21
|
+
const unwrapped = unwrap(initialValue)
|
|
22
|
+
const wrapped = wrap(unwrapped)
|
|
23
|
+
|
|
24
|
+
function setStore(fn: (state: T) => void | T) {
|
|
25
|
+
batch(() => {
|
|
26
|
+
const result = fn(wrapped)
|
|
27
|
+
if (result !== undefined) {
|
|
28
|
+
reconcile(wrapped, result)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [wrapped, setStore]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Map of target object -> Proxy
|
|
37
|
+
const proxyCache = new WeakMap<object, any>()
|
|
38
|
+
// Map of target object -> Map<key, Signal>
|
|
39
|
+
const signalCache = new WeakMap<object, Map<string | symbol, SignalAccessor<any>>>()
|
|
40
|
+
|
|
41
|
+
function wrap<T>(value: T): T {
|
|
42
|
+
if (value === null || typeof value !== 'object') return value
|
|
43
|
+
if ((value as any)[PROXY]) return value
|
|
44
|
+
|
|
45
|
+
if (proxyCache.has(value)) return proxyCache.get(value)
|
|
46
|
+
|
|
47
|
+
const handler: ProxyHandler<any> = {
|
|
48
|
+
get(target, prop, receiver) {
|
|
49
|
+
if (prop === PROXY) return true
|
|
50
|
+
if (prop === TARGET) return target
|
|
51
|
+
|
|
52
|
+
const value = Reflect.get(target, prop, receiver)
|
|
53
|
+
|
|
54
|
+
// Track property access
|
|
55
|
+
track(target, prop)
|
|
56
|
+
|
|
57
|
+
// Recursively wrap objects
|
|
58
|
+
return wrap(value)
|
|
59
|
+
},
|
|
60
|
+
set(target, prop, value, receiver) {
|
|
61
|
+
if (prop === PROXY || prop === TARGET) return false
|
|
62
|
+
|
|
63
|
+
const oldValue = Reflect.get(target, prop, receiver)
|
|
64
|
+
if (oldValue === value) return true
|
|
65
|
+
|
|
66
|
+
const result = Reflect.set(target, prop, value, receiver)
|
|
67
|
+
if (result) {
|
|
68
|
+
trigger(target, prop)
|
|
69
|
+
}
|
|
70
|
+
return result
|
|
71
|
+
},
|
|
72
|
+
deleteProperty(target, prop) {
|
|
73
|
+
const result = Reflect.deleteProperty(target, prop)
|
|
74
|
+
if (result) {
|
|
75
|
+
trigger(target, prop)
|
|
76
|
+
}
|
|
77
|
+
return result
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const proxy = new Proxy(value, handler)
|
|
82
|
+
proxyCache.set(value, proxy)
|
|
83
|
+
return proxy as T
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function unwrap<T>(value: T): T {
|
|
87
|
+
if (value && typeof value === 'object' && (value as any)[PROXY]) {
|
|
88
|
+
return (value as any)[TARGET]
|
|
89
|
+
}
|
|
90
|
+
return value
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function track(target: object, prop: string | symbol) {
|
|
94
|
+
let signals = signalCache.get(target)
|
|
95
|
+
if (!signals) {
|
|
96
|
+
signals = new Map()
|
|
97
|
+
signalCache.set(target, signals)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let s = signals.get(prop)
|
|
101
|
+
if (!s) {
|
|
102
|
+
s = signal(getLastValue(target, prop))
|
|
103
|
+
signals.set(prop, s)
|
|
104
|
+
}
|
|
105
|
+
s() // subscribe
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function trigger(target: object, prop: string | symbol) {
|
|
109
|
+
const signals = signalCache.get(target)
|
|
110
|
+
if (signals) {
|
|
111
|
+
const s = signals.get(prop)
|
|
112
|
+
if (s) {
|
|
113
|
+
s(getLastValue(target, prop)) // notify with new value
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getLastValue(target: any, prop: string | symbol) {
|
|
119
|
+
return target[prop]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reconcile a store path with a new value (shallow merge/diff)
|
|
124
|
+
*/
|
|
125
|
+
function reconcile(target: any, value: any) {
|
|
126
|
+
if (target === value) return
|
|
127
|
+
if (value === null || typeof value !== 'object') return // Should replace?
|
|
128
|
+
|
|
129
|
+
const realTarget = unwrap(target)
|
|
130
|
+
const realValue = unwrap(value)
|
|
131
|
+
|
|
132
|
+
const keys = new Set([...Object.keys(realTarget), ...Object.keys(realValue)])
|
|
133
|
+
for (const key of keys) {
|
|
134
|
+
if (realValue[key] === undefined && realTarget[key] !== undefined) {
|
|
135
|
+
// deleted
|
|
136
|
+
delete target[key] // Triggers proxy trap
|
|
137
|
+
} else if (realTarget[key] !== realValue[key]) {
|
|
138
|
+
target[key] = realValue[key] // Triggers proxy trap
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Diffing Signal (for List Items)
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates a signal that returns a Stable Proxy.
|
|
149
|
+
* Updates to the signal (via set) will diff the new value against the old value
|
|
150
|
+
* and trigger property-specific updates.
|
|
151
|
+
*/
|
|
152
|
+
export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
153
|
+
let currentValue = unwrap(initialValue)
|
|
154
|
+
const signals = new Map<string | symbol, SignalAccessor<any>>()
|
|
155
|
+
|
|
156
|
+
// The stable proxy we return
|
|
157
|
+
const proxy = new Proxy({} as T, {
|
|
158
|
+
get(_, prop) {
|
|
159
|
+
if (prop === PROXY) return true
|
|
160
|
+
if (prop === TARGET) return currentValue
|
|
161
|
+
|
|
162
|
+
// Subscribe to property
|
|
163
|
+
let s = signals.get(prop)
|
|
164
|
+
if (!s) {
|
|
165
|
+
// Initialize signal with current property value
|
|
166
|
+
s = signal((currentValue as any)[prop])
|
|
167
|
+
signals.set(prop, s)
|
|
168
|
+
}
|
|
169
|
+
return s()
|
|
170
|
+
},
|
|
171
|
+
ownKeys() {
|
|
172
|
+
return Reflect.ownKeys(currentValue)
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const read = () => proxy
|
|
177
|
+
|
|
178
|
+
const write = (newValue: T) => {
|
|
179
|
+
const next = unwrap(newValue)
|
|
180
|
+
const prev = currentValue
|
|
181
|
+
currentValue = next
|
|
182
|
+
|
|
183
|
+
if (prev === next) {
|
|
184
|
+
// Same ref update: re-evaluate all tracked signals
|
|
185
|
+
// This is necessary for in-place mutations
|
|
186
|
+
for (const [prop, s] of signals) {
|
|
187
|
+
const newVal = (next as any)[prop]
|
|
188
|
+
s(newVal)
|
|
189
|
+
}
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Diff logic
|
|
194
|
+
// We only trigger signals for properties that exist in our cache (tracked)
|
|
195
|
+
// and have changed.
|
|
196
|
+
for (const [prop, s] of signals) {
|
|
197
|
+
const oldVal = (prev as any)[prop]
|
|
198
|
+
const newVal = (next as any)[prop]
|
|
199
|
+
if (oldVal !== newVal) {
|
|
200
|
+
s(newVal)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Note: If new properties appeared that weren't tracked, we don't care
|
|
205
|
+
// because no one is listening.
|
|
206
|
+
// If we assume shape stability (Keyed List), this is efficient.
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return [read, write] as const
|
|
210
|
+
}
|
package/src/suspense.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createElement } from './dom'
|
|
2
|
+
import { createEffect } from './effect'
|
|
3
|
+
import {
|
|
4
|
+
createRootContext,
|
|
5
|
+
destroyRoot,
|
|
6
|
+
flushOnMount,
|
|
7
|
+
getCurrentRoot,
|
|
8
|
+
handleError,
|
|
9
|
+
pushRoot,
|
|
10
|
+
popRoot,
|
|
11
|
+
registerSuspenseHandler,
|
|
12
|
+
} from './lifecycle'
|
|
13
|
+
import { insertNodesBefore, removeNodes, toNodeArray } from './node-ops'
|
|
14
|
+
import { createSignal } from './signal'
|
|
15
|
+
import type { BaseProps, FictNode, SuspenseToken } from './types'
|
|
16
|
+
|
|
17
|
+
export interface SuspenseProps extends BaseProps {
|
|
18
|
+
fallback: FictNode | ((err?: unknown) => FictNode)
|
|
19
|
+
onResolve?: () => void
|
|
20
|
+
onReject?: (err: unknown) => void
|
|
21
|
+
resetKeys?: unknown | (() => unknown)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SuspenseHandle {
|
|
25
|
+
token: SuspenseToken
|
|
26
|
+
resolve: () => void
|
|
27
|
+
reject: (err: unknown) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createSuspenseToken(): SuspenseHandle {
|
|
31
|
+
let resolve!: () => void
|
|
32
|
+
let reject!: (err: unknown) => void
|
|
33
|
+
const promise = new Promise<void>((res, rej) => {
|
|
34
|
+
resolve = res
|
|
35
|
+
reject = rej
|
|
36
|
+
})
|
|
37
|
+
return {
|
|
38
|
+
token: {
|
|
39
|
+
then: promise.then.bind(promise),
|
|
40
|
+
},
|
|
41
|
+
resolve,
|
|
42
|
+
reject,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isThenable = (value: unknown): value is PromiseLike<unknown> =>
|
|
47
|
+
typeof value === 'object' &&
|
|
48
|
+
value !== null &&
|
|
49
|
+
typeof (value as PromiseLike<unknown>).then === 'function'
|
|
50
|
+
|
|
51
|
+
export function Suspense(props: SuspenseProps): FictNode {
|
|
52
|
+
const currentView = createSignal<FictNode | null>(props.children ?? null)
|
|
53
|
+
const pending = createSignal(0)
|
|
54
|
+
let resolvedOnce = false
|
|
55
|
+
let epoch = 0
|
|
56
|
+
const hostRoot = getCurrentRoot()
|
|
57
|
+
|
|
58
|
+
const toFallback = (err?: unknown) =>
|
|
59
|
+
typeof props.fallback === 'function'
|
|
60
|
+
? (props.fallback as (e?: unknown) => FictNode)(err)
|
|
61
|
+
: props.fallback
|
|
62
|
+
|
|
63
|
+
const switchView = (view: FictNode | null) => {
|
|
64
|
+
currentView(view)
|
|
65
|
+
renderView(view)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const renderView = (view: FictNode | null) => {
|
|
69
|
+
if (cleanup) {
|
|
70
|
+
cleanup()
|
|
71
|
+
cleanup = undefined
|
|
72
|
+
}
|
|
73
|
+
if (activeNodes.length) {
|
|
74
|
+
removeNodes(activeNodes)
|
|
75
|
+
activeNodes = []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (view == null || view === false) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const root = createRootContext(hostRoot)
|
|
83
|
+
const prev = pushRoot(root)
|
|
84
|
+
let nodes: Node[] = []
|
|
85
|
+
try {
|
|
86
|
+
const output = createElement(view)
|
|
87
|
+
nodes = toNodeArray(output)
|
|
88
|
+
// Suspended view: child threw a suspense token and was handled upstream.
|
|
89
|
+
// Avoid replacing existing fallback content; tear down this attempt.
|
|
90
|
+
const suspendedAttempt =
|
|
91
|
+
nodes.length > 0 &&
|
|
92
|
+
nodes.every(node => node instanceof Comment && (node as Comment).data === 'fict:suspend')
|
|
93
|
+
if (suspendedAttempt) {
|
|
94
|
+
popRoot(prev)
|
|
95
|
+
destroyRoot(root)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
const parentNode = marker.parentNode as (ParentNode & Node) | null
|
|
99
|
+
if (parentNode) {
|
|
100
|
+
insertNodesBefore(parentNode, nodes, marker)
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
popRoot(prev)
|
|
104
|
+
flushOnMount(root)
|
|
105
|
+
destroyRoot(root)
|
|
106
|
+
handleError(err, { source: 'render' })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
popRoot(prev)
|
|
110
|
+
flushOnMount(root)
|
|
111
|
+
|
|
112
|
+
cleanup = () => {
|
|
113
|
+
destroyRoot(root)
|
|
114
|
+
removeNodes(nodes)
|
|
115
|
+
}
|
|
116
|
+
activeNodes = nodes
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const fragment = document.createDocumentFragment()
|
|
120
|
+
const marker = document.createComment('fict:suspense')
|
|
121
|
+
fragment.appendChild(marker)
|
|
122
|
+
let cleanup: (() => void) | undefined
|
|
123
|
+
let activeNodes: Node[] = []
|
|
124
|
+
|
|
125
|
+
const onResolveMaybe = () => {
|
|
126
|
+
if (!resolvedOnce) {
|
|
127
|
+
resolvedOnce = true
|
|
128
|
+
props.onResolve?.()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
registerSuspenseHandler(token => {
|
|
133
|
+
const tokenEpoch = epoch
|
|
134
|
+
pending(pending() + 1)
|
|
135
|
+
switchView(toFallback())
|
|
136
|
+
|
|
137
|
+
const thenable = (token as SuspenseToken).then
|
|
138
|
+
? (token as SuspenseToken)
|
|
139
|
+
: isThenable(token)
|
|
140
|
+
? token
|
|
141
|
+
: null
|
|
142
|
+
|
|
143
|
+
if (thenable) {
|
|
144
|
+
thenable.then(
|
|
145
|
+
() => {
|
|
146
|
+
if (epoch !== tokenEpoch) return
|
|
147
|
+
pending(Math.max(0, pending() - 1))
|
|
148
|
+
if (pending() === 0) {
|
|
149
|
+
switchView(props.children ?? null)
|
|
150
|
+
onResolveMaybe()
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
err => {
|
|
154
|
+
if (epoch !== tokenEpoch) return
|
|
155
|
+
pending(Math.max(0, pending() - 1))
|
|
156
|
+
props.onReject?.(err)
|
|
157
|
+
handleError(err, { source: 'render' }, hostRoot)
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return false
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
createEffect(() => {
|
|
167
|
+
renderView(currentView())
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (props.resetKeys !== undefined) {
|
|
171
|
+
const isGetter =
|
|
172
|
+
typeof props.resetKeys === 'function' && (props.resetKeys as () => unknown).length === 0
|
|
173
|
+
const getter = isGetter ? (props.resetKeys as () => unknown) : undefined
|
|
174
|
+
let prev = isGetter ? getter!() : props.resetKeys
|
|
175
|
+
createEffect(() => {
|
|
176
|
+
const next = getter ? getter() : props.resetKeys
|
|
177
|
+
if (prev !== next) {
|
|
178
|
+
prev = next
|
|
179
|
+
epoch++
|
|
180
|
+
pending(0)
|
|
181
|
+
switchView(props.children ?? null)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return fragment
|
|
187
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createEffect } from './effect'
|
|
2
|
+
import { setTransitionContext, signal, scheduleFlush, untrack } from './signal'
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// startTransition - Mark updates as low priority
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Execute a function with low-priority scheduling.
|
|
10
|
+
* Updates triggered inside the callback will be processed after any high-priority updates.
|
|
11
|
+
* This keeps the UI responsive during expensive operations.
|
|
12
|
+
*
|
|
13
|
+
* @param fn - The function to execute in transition context
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const handleInput = (e) => {
|
|
18
|
+
* query = e.target.value // High priority: immediate
|
|
19
|
+
* startTransition(() => {
|
|
20
|
+
* // Low priority: processed after high priority updates
|
|
21
|
+
* filteredItems = allItems.filter(x => x.includes(query))
|
|
22
|
+
* })
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function startTransition(fn: () => void): void {
|
|
27
|
+
const prev = setTransitionContext(true)
|
|
28
|
+
try {
|
|
29
|
+
fn()
|
|
30
|
+
} finally {
|
|
31
|
+
setTransitionContext(prev)
|
|
32
|
+
scheduleFlush()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// useTransition - Hook for managing transition state
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* React-style useTransition hook.
|
|
42
|
+
* Returns a pending signal and a startTransition function.
|
|
43
|
+
*
|
|
44
|
+
* @returns A tuple of [isPending accessor, startTransition function]
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* function SearchComponent() {
|
|
49
|
+
* let query = $state('')
|
|
50
|
+
* const [isPending, start] = useTransition()
|
|
51
|
+
*
|
|
52
|
+
* const handleChange = (e) => {
|
|
53
|
+
* query = e.target.value
|
|
54
|
+
* start(() => {
|
|
55
|
+
* // Expensive filtering happens in low priority
|
|
56
|
+
* filteredResults = expensiveFilter(allData, query)
|
|
57
|
+
* })
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* return (
|
|
61
|
+
* <>
|
|
62
|
+
* <input value={query} onInput={handleChange} />
|
|
63
|
+
* {isPending() && <Spinner />}
|
|
64
|
+
* <Results items={filteredResults} />
|
|
65
|
+
* </>
|
|
66
|
+
* )
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function useTransition(): [() => boolean, (fn: () => void) => void] {
|
|
71
|
+
const pending = signal(false)
|
|
72
|
+
|
|
73
|
+
const start = (fn: () => void) => {
|
|
74
|
+
pending(true)
|
|
75
|
+
startTransition(() => {
|
|
76
|
+
try {
|
|
77
|
+
fn()
|
|
78
|
+
} finally {
|
|
79
|
+
pending(false)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [() => pending(), start]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// useDeferredValue - Defer value updates to low priority
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a deferred version of a value that updates with low priority.
|
|
93
|
+
* The returned accessor will lag behind the source value during rapid updates,
|
|
94
|
+
* allowing high-priority work to complete first.
|
|
95
|
+
*
|
|
96
|
+
* @param getValue - Accessor function that returns the source value
|
|
97
|
+
* @returns Accessor function that returns the deferred value
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* function SearchResults({ query }) {
|
|
102
|
+
* const deferredQuery = useDeferredValue(() => query)
|
|
103
|
+
*
|
|
104
|
+
* // deferredQuery lags behind query during rapid typing
|
|
105
|
+
* const results = expensiveSearch(deferredQuery())
|
|
106
|
+
*
|
|
107
|
+
* return <ResultList items={results} />
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function useDeferredValue<T>(getValue: () => T): () => T {
|
|
112
|
+
const deferredValue = signal(getValue())
|
|
113
|
+
|
|
114
|
+
// Track source value changes and update deferred value in transition
|
|
115
|
+
createEffect(() => {
|
|
116
|
+
const newValue = getValue()
|
|
117
|
+
// Use untrack to read current deferred value without creating a dependency
|
|
118
|
+
// This prevents the effect from re-running when deferredValue changes
|
|
119
|
+
const currentDeferred = untrack(() => deferredValue())
|
|
120
|
+
if (currentDeferred !== newValue) {
|
|
121
|
+
startTransition(() => {
|
|
122
|
+
deferredValue(newValue)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return () => deferredValue()
|
|
128
|
+
}
|