@fictjs/runtime 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.cjs +8 -8
- package/dist/advanced.d.cts +3 -3
- package/dist/advanced.d.ts +3 -3
- package/dist/advanced.js +3 -3
- package/dist/{chunk-YQ4IB7NC.cjs → chunk-7EAEROZ5.cjs} +7 -7
- package/dist/{chunk-YQ4IB7NC.cjs.map → chunk-7EAEROZ5.cjs.map} +1 -1
- package/dist/{chunk-CF3OHML2.js → chunk-7TPCESQS.js} +2 -2
- package/dist/{chunk-BWZFJXUI.js → chunk-FOLRR3NZ.js} +84 -10
- package/dist/chunk-FOLRR3NZ.js.map +1 -0
- package/dist/{chunk-7WAGAQLT.cjs → chunk-MWI3USXB.cjs} +84 -10
- package/dist/chunk-MWI3USXB.cjs.map +1 -0
- package/dist/{chunk-V62XZLDU.js → chunk-VVNMIER7.js} +2 -2
- package/dist/{chunk-V62XZLDU.js.map → chunk-VVNMIER7.js.map} +1 -1
- package/dist/{chunk-Q4EN6BXV.cjs → chunk-Z5WRKD7Y.cjs} +16 -16
- package/dist/{chunk-Q4EN6BXV.cjs.map → chunk-Z5WRKD7Y.cjs.map} +1 -1
- package/dist/{context-B7UYnfzM.d.ts → context-4woHo7-L.d.ts} +1 -1
- package/dist/{context-UXySaqI_.d.cts → context-9gFXOdJl.d.cts} +1 -1
- package/dist/{effect-Auji1rz9.d.cts → effect-ClARNUCc.d.cts} +23 -2
- package/dist/{effect-Auji1rz9.d.ts → effect-ClARNUCc.d.ts} +23 -2
- package/dist/index.cjs +51 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.dev.js +71 -28
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +15 -22
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +64 -42
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +4 -4
- package/dist/internal.d.ts +4 -4
- package/dist/internal.js +32 -10
- package/dist/internal.js.map +1 -1
- package/dist/{props-BfmSLuyp.d.cts → props-CBwuh35e.d.cts} +4 -4
- package/dist/{props-BBi8Tkks.d.ts → props-DAyeRPwH.d.ts} +4 -4
- package/dist/{scope-S6eAzBJZ.d.ts → scope-DvgMquEy.d.ts} +1 -1
- package/dist/{scope-DKYzWfTn.d.cts → scope-xmdo6lVU.d.cts} +1 -1
- package/package.json +1 -1
- package/src/binding.ts +58 -5
- package/src/context.ts +1 -1
- package/src/effect.ts +9 -2
- package/src/error-boundary.ts +9 -9
- package/src/lifecycle.ts +13 -3
- package/src/signal.ts +44 -4
- package/src/store.ts +42 -20
- package/src/suspense.ts +14 -15
- package/dist/chunk-7WAGAQLT.cjs.map +0 -1
- package/dist/chunk-BWZFJXUI.js.map +0 -1
- /package/dist/{chunk-CF3OHML2.js.map → chunk-7TPCESQS.js.map} +0 -0
package/src/signal.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { beginFlushGuard, beforeEffectRunGuard, endFlushGuard } from './cycle-guard'
|
|
2
2
|
import { getDevtoolsHook } from './devtools'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getCurrentRoot,
|
|
5
|
+
handleError,
|
|
6
|
+
handleSuspend,
|
|
7
|
+
registerRootCleanup,
|
|
8
|
+
type RootContext,
|
|
9
|
+
} from './lifecycle'
|
|
10
|
+
import type { SuspenseToken } from './types'
|
|
4
11
|
|
|
5
12
|
const isDev =
|
|
6
13
|
typeof __DEV__ !== 'undefined'
|
|
@@ -103,6 +110,8 @@ export interface EffectNode extends BaseNode {
|
|
|
103
110
|
depsTail: Link | undefined
|
|
104
111
|
/** Optional cleanup runner to be called before checkDirty */
|
|
105
112
|
runCleanup?: () => void
|
|
113
|
+
/** Root context for error/suspense handling */
|
|
114
|
+
root?: RootContext
|
|
106
115
|
/** Devtools ID */
|
|
107
116
|
__id?: number | undefined
|
|
108
117
|
}
|
|
@@ -808,7 +817,25 @@ function runEffect(e: EffectNode): void {
|
|
|
808
817
|
inCleanup = false
|
|
809
818
|
}
|
|
810
819
|
}
|
|
811
|
-
|
|
820
|
+
let isDirty = false
|
|
821
|
+
try {
|
|
822
|
+
isDirty = checkDirty(e.deps, e)
|
|
823
|
+
} catch (err) {
|
|
824
|
+
if (handleSuspend(err as SuspenseToken, e.root)) {
|
|
825
|
+
if (e.flags !== 0) {
|
|
826
|
+
e.flags = Watching
|
|
827
|
+
}
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
if (handleError(err, { source: 'effect' }, e.root)) {
|
|
831
|
+
if (e.flags !== 0) {
|
|
832
|
+
e.flags = Watching
|
|
833
|
+
}
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
throw err
|
|
837
|
+
}
|
|
838
|
+
if (isDirty) {
|
|
812
839
|
++cycle
|
|
813
840
|
effectRunDevtools(e)
|
|
814
841
|
e.depsTail = undefined
|
|
@@ -1031,7 +1058,7 @@ function computedOper<T>(this: ComputedNode<T>): T {
|
|
|
1031
1058
|
* @returns An effect disposer function
|
|
1032
1059
|
*/
|
|
1033
1060
|
export function effect(fn: () => void): EffectDisposer {
|
|
1034
|
-
const e = {
|
|
1061
|
+
const e: EffectNode = {
|
|
1035
1062
|
fn,
|
|
1036
1063
|
subs: undefined,
|
|
1037
1064
|
subsTail: undefined,
|
|
@@ -1040,6 +1067,10 @@ export function effect(fn: () => void): EffectDisposer {
|
|
|
1040
1067
|
flags: WatchingRunning,
|
|
1041
1068
|
__id: undefined as number | undefined,
|
|
1042
1069
|
}
|
|
1070
|
+
const root = getCurrentRoot()
|
|
1071
|
+
if (root) {
|
|
1072
|
+
e.root = root
|
|
1073
|
+
}
|
|
1043
1074
|
|
|
1044
1075
|
registerEffectDevtools(e)
|
|
1045
1076
|
|
|
@@ -1066,9 +1097,14 @@ export function effect(fn: () => void): EffectDisposer {
|
|
|
1066
1097
|
* cleanup functions to access the previous values of signals.
|
|
1067
1098
|
* @param fn - The effect function
|
|
1068
1099
|
* @param cleanupRunner - Function to run cleanups before signal value commit
|
|
1100
|
+
* @param root - Root context for error/suspense handling (defaults to current root)
|
|
1069
1101
|
* @returns An effect disposer function
|
|
1070
1102
|
*/
|
|
1071
|
-
export function effectWithCleanup(
|
|
1103
|
+
export function effectWithCleanup(
|
|
1104
|
+
fn: () => void,
|
|
1105
|
+
cleanupRunner: () => void,
|
|
1106
|
+
root?: RootContext,
|
|
1107
|
+
): EffectDisposer {
|
|
1072
1108
|
const e: EffectNode = {
|
|
1073
1109
|
fn,
|
|
1074
1110
|
subs: undefined,
|
|
@@ -1079,6 +1115,10 @@ export function effectWithCleanup(fn: () => void, cleanupRunner: () => void): Ef
|
|
|
1079
1115
|
runCleanup: cleanupRunner,
|
|
1080
1116
|
__id: undefined as number | undefined,
|
|
1081
1117
|
}
|
|
1118
|
+
const resolvedRoot = root ?? getCurrentRoot()
|
|
1119
|
+
if (resolvedRoot) {
|
|
1120
|
+
e.root = resolvedRoot
|
|
1121
|
+
}
|
|
1082
1122
|
|
|
1083
1123
|
registerEffectDevtools(e)
|
|
1084
1124
|
|
package/src/store.ts
CHANGED
|
@@ -35,17 +35,17 @@ export function createStore<T extends object>(
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// Map of target object -> Proxy
|
|
38
|
-
const proxyCache = new WeakMap<object,
|
|
38
|
+
const proxyCache = new WeakMap<object, unknown>()
|
|
39
39
|
// Map of target object -> Map<key, Signal>
|
|
40
|
-
const signalCache = new WeakMap<object, Map<string | symbol, SignalAccessor<
|
|
40
|
+
const signalCache = new WeakMap<object, Map<string | symbol, SignalAccessor<unknown>>>()
|
|
41
41
|
|
|
42
42
|
function wrap<T>(value: T): T {
|
|
43
43
|
if (value === null || typeof value !== 'object') return value
|
|
44
|
-
if ((value
|
|
44
|
+
if (Reflect.get(value, PROXY)) return value
|
|
45
45
|
|
|
46
|
-
if (proxyCache.has(value)) return proxyCache.get(value)
|
|
46
|
+
if (proxyCache.has(value)) return proxyCache.get(value) as T
|
|
47
47
|
|
|
48
|
-
const handler: ProxyHandler<
|
|
48
|
+
const handler: ProxyHandler<object> = {
|
|
49
49
|
get(target, prop, receiver) {
|
|
50
50
|
if (prop === PROXY) return true
|
|
51
51
|
if (prop === TARGET) return target
|
|
@@ -74,6 +74,8 @@ function wrap<T>(value: T): T {
|
|
|
74
74
|
set(target, prop, value, receiver) {
|
|
75
75
|
if (prop === PROXY || prop === TARGET) return false
|
|
76
76
|
|
|
77
|
+
const isArrayLength = Array.isArray(target) && prop === 'length'
|
|
78
|
+
const oldLength = isArrayLength ? target.length : undefined
|
|
77
79
|
const hadKey = Object.prototype.hasOwnProperty.call(target, prop)
|
|
78
80
|
const oldValue = Reflect.get(target, prop, receiver)
|
|
79
81
|
if (oldValue === value) return true
|
|
@@ -84,6 +86,23 @@ function wrap<T>(value: T): T {
|
|
|
84
86
|
if (!hadKey) {
|
|
85
87
|
trigger(target, ITERATE_KEY)
|
|
86
88
|
}
|
|
89
|
+
if (isArrayLength) {
|
|
90
|
+
const nextLength = target.length
|
|
91
|
+
if (typeof oldLength === 'number' && nextLength < oldLength) {
|
|
92
|
+
const signals = signalCache.get(target)
|
|
93
|
+
if (signals) {
|
|
94
|
+
for (const key of signals.keys()) {
|
|
95
|
+
if (typeof key !== 'string') continue
|
|
96
|
+
const index = Number(key)
|
|
97
|
+
if (!Number.isInteger(index) || String(index) !== key) continue
|
|
98
|
+
if (index >= nextLength && index < oldLength) {
|
|
99
|
+
trigger(target, key)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
trigger(target, ITERATE_KEY)
|
|
105
|
+
}
|
|
87
106
|
}
|
|
88
107
|
return result
|
|
89
108
|
},
|
|
@@ -106,8 +125,8 @@ function wrap<T>(value: T): T {
|
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
function unwrap<T>(value: T): T {
|
|
109
|
-
if (value && typeof value === 'object' && (value
|
|
110
|
-
return (value as
|
|
128
|
+
if (value && typeof value === 'object' && Reflect.get(value, PROXY)) {
|
|
129
|
+
return Reflect.get(value, TARGET) as T
|
|
111
130
|
}
|
|
112
131
|
return value
|
|
113
132
|
}
|
|
@@ -143,14 +162,14 @@ function trigger(target: object, prop: string | symbol) {
|
|
|
143
162
|
}
|
|
144
163
|
}
|
|
145
164
|
|
|
146
|
-
function getLastValue(target:
|
|
147
|
-
return target
|
|
165
|
+
function getLastValue(target: object, prop: string | symbol) {
|
|
166
|
+
return Reflect.get(target, prop)
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
/**
|
|
151
170
|
* Reconcile a store path with a new value (shallow merge/diff)
|
|
152
171
|
*/
|
|
153
|
-
function reconcile(target:
|
|
172
|
+
function reconcile(target: object, value: unknown) {
|
|
154
173
|
if (target === value) return
|
|
155
174
|
if (value === null || typeof value !== 'object') {
|
|
156
175
|
throw new Error(
|
|
@@ -165,16 +184,19 @@ function reconcile(target: any, value: any) {
|
|
|
165
184
|
|
|
166
185
|
const keys = new Set([...Object.keys(realTarget), ...Object.keys(realValue)])
|
|
167
186
|
for (const key of keys) {
|
|
168
|
-
|
|
187
|
+
const rTarget = realTarget as Record<string, unknown>
|
|
188
|
+
const rValue = realValue as Record<string, unknown>
|
|
189
|
+
|
|
190
|
+
if (rValue[key] === undefined && rTarget[key] !== undefined) {
|
|
169
191
|
// deleted
|
|
170
|
-
delete target[key] // Triggers proxy trap
|
|
171
|
-
} else if (
|
|
172
|
-
target[key] =
|
|
192
|
+
delete (target as Record<string, unknown>)[key] // Triggers proxy trap
|
|
193
|
+
} else if (rTarget[key] !== rValue[key]) {
|
|
194
|
+
;(target as Record<string, unknown>)[key] = rValue[key] // Triggers proxy trap
|
|
173
195
|
}
|
|
174
196
|
}
|
|
175
197
|
|
|
176
198
|
// Fix array length if needed
|
|
177
|
-
if (Array.isArray(target) && target.length !== realValue.length) {
|
|
199
|
+
if (Array.isArray(target) && Array.isArray(realValue) && target.length !== realValue.length) {
|
|
178
200
|
target.length = realValue.length
|
|
179
201
|
}
|
|
180
202
|
}
|
|
@@ -190,13 +212,13 @@ function reconcile(target: any, value: any) {
|
|
|
190
212
|
*/
|
|
191
213
|
export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
192
214
|
let currentValue = unwrap(initialValue)
|
|
193
|
-
const signals = new Map<string | symbol, SignalAccessor<
|
|
215
|
+
const signals = new Map<string | symbol, SignalAccessor<unknown>>()
|
|
194
216
|
let iterateSignal: SignalAccessor<number> | undefined
|
|
195
217
|
|
|
196
218
|
const getPropSignal = (prop: string | symbol) => {
|
|
197
219
|
let s = signals.get(prop)
|
|
198
220
|
if (!s) {
|
|
199
|
-
s = signal((currentValue as
|
|
221
|
+
s = signal(Reflect.get(currentValue as object, prop))
|
|
200
222
|
signals.set(prop, s)
|
|
201
223
|
}
|
|
202
224
|
return s
|
|
@@ -250,7 +272,7 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
|
250
272
|
// Same ref update: re-evaluate all tracked signals
|
|
251
273
|
// This is necessary for in-place mutations
|
|
252
274
|
for (const [prop, s] of signals) {
|
|
253
|
-
const newVal = (next as
|
|
275
|
+
const newVal = Reflect.get(next as object, prop)
|
|
254
276
|
s(newVal)
|
|
255
277
|
}
|
|
256
278
|
updateIterate(next)
|
|
@@ -261,8 +283,8 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
|
261
283
|
// We only trigger signals for properties that exist in our cache (tracked)
|
|
262
284
|
// and have changed.
|
|
263
285
|
for (const [prop, s] of signals) {
|
|
264
|
-
const oldVal = (prev as
|
|
265
|
-
const newVal = (next as
|
|
286
|
+
const oldVal = Reflect.get(prev as object, prop)
|
|
287
|
+
const newVal = Reflect.get(next as object, prop)
|
|
266
288
|
if (oldVal !== newVal) {
|
|
267
289
|
s(newVal)
|
|
268
290
|
}
|
package/src/suspense.ts
CHANGED
|
@@ -49,7 +49,6 @@ const isThenable = (value: unknown): value is PromiseLike<unknown> =>
|
|
|
49
49
|
typeof (value as PromiseLike<unknown>).then === 'function'
|
|
50
50
|
|
|
51
51
|
export function Suspense(props: SuspenseProps): FictNode {
|
|
52
|
-
const currentView = createSignal<FictNode | null>(props.children ?? null)
|
|
53
52
|
const pending = createSignal(0)
|
|
54
53
|
let resolvedOnce = false
|
|
55
54
|
let epoch = 0
|
|
@@ -60,11 +59,6 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
60
59
|
? (props.fallback as (e?: unknown) => FictNode)(err)
|
|
61
60
|
: props.fallback
|
|
62
61
|
|
|
63
|
-
const switchView = (view: FictNode | null) => {
|
|
64
|
-
currentView(view)
|
|
65
|
-
renderView(view)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
62
|
const renderView = (view: FictNode | null) => {
|
|
69
63
|
if (cleanup) {
|
|
70
64
|
cleanup()
|
|
@@ -88,8 +82,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
88
82
|
// Suspended view: child threw a suspense token and was handled upstream.
|
|
89
83
|
// Avoid replacing existing fallback content; tear down this attempt.
|
|
90
84
|
const suspendedAttempt =
|
|
91
|
-
|
|
92
|
-
nodes.
|
|
85
|
+
root.suspended ||
|
|
86
|
+
(nodes.length > 0 &&
|
|
87
|
+
nodes.every(node => node instanceof Comment && (node as Comment).data === 'fict:suspend'))
|
|
93
88
|
if (suspendedAttempt) {
|
|
94
89
|
popRoot(prev)
|
|
95
90
|
destroyRoot(root)
|
|
@@ -101,7 +96,6 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
101
96
|
}
|
|
102
97
|
} catch (err) {
|
|
103
98
|
popRoot(prev)
|
|
104
|
-
flushOnMount(root)
|
|
105
99
|
destroyRoot(root)
|
|
106
100
|
if (!handleError(err, { source: 'render' }, hostRoot)) {
|
|
107
101
|
throw err
|
|
@@ -134,7 +128,9 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
134
128
|
registerSuspenseHandler(token => {
|
|
135
129
|
const tokenEpoch = epoch
|
|
136
130
|
pending(pending() + 1)
|
|
137
|
-
switchView
|
|
131
|
+
// Directly render fallback instead of using switchView to avoid
|
|
132
|
+
// triggering the effect which would cause duplicate renders
|
|
133
|
+
renderView(toFallback())
|
|
138
134
|
|
|
139
135
|
const thenable = (token as SuspenseToken).then
|
|
140
136
|
? (token as SuspenseToken)
|
|
@@ -157,7 +153,8 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
157
153
|
const newPending = Math.max(0, pending() - 1)
|
|
158
154
|
pending(newPending)
|
|
159
155
|
if (newPending === 0) {
|
|
160
|
-
|
|
156
|
+
// Directly render children instead of using switchView
|
|
157
|
+
renderView(props.children ?? null)
|
|
161
158
|
onResolveMaybe()
|
|
162
159
|
}
|
|
163
160
|
},
|
|
@@ -180,9 +177,10 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
180
177
|
return false
|
|
181
178
|
})
|
|
182
179
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
// Initial render - render children directly
|
|
181
|
+
// Note: This will be called synchronously during component creation.
|
|
182
|
+
// If children suspend, the handler above will be called and switch to fallback.
|
|
183
|
+
renderView(props.children ?? null)
|
|
186
184
|
|
|
187
185
|
if (props.resetKeys !== undefined) {
|
|
188
186
|
const isGetter =
|
|
@@ -195,7 +193,8 @@ export function Suspense(props: SuspenseProps): FictNode {
|
|
|
195
193
|
prev = next
|
|
196
194
|
epoch++
|
|
197
195
|
pending(0)
|
|
198
|
-
|
|
196
|
+
// Directly render children instead of using switchView
|
|
197
|
+
renderView(props.children ?? null)
|
|
199
198
|
}
|
|
200
199
|
})
|
|
201
200
|
}
|