@fictjs/runtime 0.5.2 → 0.7.0
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 +13 -9
- package/dist/advanced.cjs.map +1 -1
- package/dist/advanced.d.cts +4 -4
- package/dist/advanced.d.ts +4 -4
- package/dist/advanced.js +8 -4
- package/dist/advanced.js.map +1 -1
- package/dist/{chunk-D2IWOO4X.js → chunk-4LCHQ7U4.js} +250 -99
- package/dist/chunk-4LCHQ7U4.js.map +1 -0
- package/dist/{chunk-LRFMCJY3.js → chunk-7YQK3XKY.js} +120 -27
- package/dist/chunk-7YQK3XKY.js.map +1 -0
- package/dist/{chunk-QB2UD62G.cjs → chunk-CEV6TO5U.cjs} +8 -8
- package/dist/{chunk-QB2UD62G.cjs.map → chunk-CEV6TO5U.cjs.map} +1 -1
- package/dist/{chunk-ZR435MDC.cjs → chunk-FSCBL7RI.cjs} +120 -27
- package/dist/chunk-FSCBL7RI.cjs.map +1 -0
- package/dist/{chunk-KNGHYGK4.cjs → chunk-HHDHQGJY.cjs} +17 -17
- package/dist/{chunk-KNGHYGK4.cjs.map → chunk-HHDHQGJY.cjs.map} +1 -1
- package/dist/{chunk-Z6M3HKLG.cjs → chunk-PRF4QG73.cjs} +400 -249
- package/dist/chunk-PRF4QG73.cjs.map +1 -0
- package/dist/{chunk-4NUHM77Z.js → chunk-TLDT76RV.js} +3 -3
- package/dist/{chunk-SLFAEVKJ.js → chunk-WRU3IZOA.js} +3 -3
- package/dist/{context-CTBE00S_.d.cts → context-BFbHf9nC.d.cts} +1 -1
- package/dist/{context-lkLhbkFJ.d.ts → context-C4vBQbb4.d.ts} +1 -1
- package/dist/{effect-BpSNEJJz.d.cts → effect-DAzpH7Mm.d.cts} +33 -1
- package/dist/{effect-BpSNEJJz.d.ts → effect-DAzpH7Mm.d.ts} +33 -1
- package/dist/index.cjs +42 -42
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.dev.js +206 -46
- package/dist/index.dev.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/internal.cjs +55 -41
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +17 -3
- package/dist/internal.js.map +1 -1
- package/dist/loader.cjs +9 -9
- package/dist/loader.js +1 -1
- package/dist/{props-XTHYD19o.d.cts → props-84UJeWO8.d.cts} +1 -1
- package/dist/{props-x-HbI-jX.d.ts → props-BRhFK50f.d.ts} +1 -1
- package/dist/{scope-CdbGmsFf.d.ts → scope-D3DpsfoG.d.ts} +1 -1
- package/dist/{scope-DfcP9I-A.d.cts → scope-DlCBL1Ft.d.cts} +1 -1
- package/package.json +1 -1
- package/src/advanced.ts +1 -1
- package/src/binding.ts +229 -101
- package/src/constants.ts +1 -1
- package/src/cycle-guard.ts +4 -3
- package/src/dom.ts +15 -4
- package/src/hooks.ts +1 -1
- package/src/internal.ts +7 -0
- package/src/lifecycle.ts +1 -1
- package/src/props.ts +60 -1
- package/src/signal.ts +60 -10
- package/src/store.ts +131 -18
- package/src/transition.ts +46 -9
- package/dist/chunk-D2IWOO4X.js.map +0 -1
- package/dist/chunk-LRFMCJY3.js.map +0 -1
- package/dist/chunk-Z6M3HKLG.cjs.map +0 -1
- package/dist/chunk-ZR435MDC.cjs.map +0 -1
- package/dist/jsx-dev-runtime.d.cts +0 -671
- package/dist/jsx-dev-runtime.d.ts +0 -671
- /package/dist/{chunk-4NUHM77Z.js.map → chunk-TLDT76RV.js.map} +0 -0
- /package/dist/{chunk-SLFAEVKJ.js.map → chunk-WRU3IZOA.js.map} +0 -0
package/src/signal.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { SuspenseToken } from './types'
|
|
|
13
13
|
const isDev =
|
|
14
14
|
typeof __DEV__ !== 'undefined'
|
|
15
15
|
? __DEV__
|
|
16
|
-
: typeof process
|
|
16
|
+
: typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'
|
|
17
17
|
|
|
18
18
|
// ============================================================================
|
|
19
19
|
// Type Definitions
|
|
@@ -844,6 +844,9 @@ function updateComputed<T>(c: ComputedNode<T>): boolean {
|
|
|
844
844
|
} catch (e) {
|
|
845
845
|
activeSub = prevSub
|
|
846
846
|
c.flags &= ~Running
|
|
847
|
+
// Keep dependency graph consistent even when getter throws.
|
|
848
|
+
// Without this, stale old deps can remain subscribed.
|
|
849
|
+
purgeDeps(c)
|
|
847
850
|
throw e
|
|
848
851
|
}
|
|
849
852
|
}
|
|
@@ -881,6 +884,9 @@ function runEffect(e: EffectNode): void {
|
|
|
881
884
|
} catch (err) {
|
|
882
885
|
activeSub = prevSub
|
|
883
886
|
e.flags = Watching
|
|
887
|
+
// Keep dependency graph consistent even when effect throws.
|
|
888
|
+
// Without this, stale old deps can remain subscribed.
|
|
889
|
+
purgeDeps(e)
|
|
884
890
|
throw err
|
|
885
891
|
}
|
|
886
892
|
} else if (flags & Pending && e.deps) {
|
|
@@ -920,6 +926,9 @@ function runEffect(e: EffectNode): void {
|
|
|
920
926
|
} catch (err) {
|
|
921
927
|
activeSub = prevSub
|
|
922
928
|
e.flags = Watching
|
|
929
|
+
// Keep dependency graph consistent even when effect throws.
|
|
930
|
+
// Without this, stale old deps can remain subscribed.
|
|
931
|
+
purgeDeps(e)
|
|
923
932
|
throw err
|
|
924
933
|
}
|
|
925
934
|
} else {
|
|
@@ -1050,10 +1059,10 @@ export function signal<T>(initialValue: T, options?: SignalOptions<T>): SignalAc
|
|
|
1050
1059
|
subsTail: undefined,
|
|
1051
1060
|
flags: Mutable,
|
|
1052
1061
|
__id: undefined as number | undefined,
|
|
1053
|
-
...(options?.equals !== undefined ? { equals: options.equals } : {}),
|
|
1054
|
-
...(options?.name !== undefined ? { name: options.name } : {}),
|
|
1055
|
-
...(options?.devToolsSource !== undefined ? { devToolsSource: options.devToolsSource } : {}),
|
|
1056
1062
|
}
|
|
1063
|
+
if (options?.equals !== undefined) s.equals = options.equals
|
|
1064
|
+
if (options?.name !== undefined) s.name = options.name
|
|
1065
|
+
if (options?.devToolsSource !== undefined) s.devToolsSource = options.devToolsSource
|
|
1057
1066
|
if (isDev) registerSignalDevtools(s)
|
|
1058
1067
|
const accessor = signalOper.bind(s as any) as SignalAccessor<T> & Record<symbol, boolean>
|
|
1059
1068
|
accessor[SIGNAL_MARKER] = true
|
|
@@ -1125,10 +1134,10 @@ export function computed<T>(
|
|
|
1125
1134
|
flags: 0,
|
|
1126
1135
|
getter,
|
|
1127
1136
|
__id: undefined as number | undefined,
|
|
1128
|
-
...(options?.equals !== undefined ? { equals: options.equals } : {}),
|
|
1129
|
-
...(options?.name !== undefined ? { name: options.name } : {}),
|
|
1130
|
-
...(options?.devToolsSource !== undefined ? { devToolsSource: options.devToolsSource } : {}),
|
|
1131
1137
|
}
|
|
1138
|
+
if (options?.equals !== undefined) c.equals = options.equals
|
|
1139
|
+
if (options?.name !== undefined) c.name = options.name
|
|
1140
|
+
if (options?.devToolsSource !== undefined) c.devToolsSource = options.devToolsSource
|
|
1132
1141
|
if (isDev) registerComputedDevtools(c)
|
|
1133
1142
|
const bound = (computedOper as (this: ComputedNode<T>) => T).bind(
|
|
1134
1143
|
c as any,
|
|
@@ -1163,14 +1172,23 @@ function computedOper<T>(this: ComputedNode<T>): T {
|
|
|
1163
1172
|
this.flags = flags & ~Pending
|
|
1164
1173
|
}
|
|
1165
1174
|
} else if (!flags) {
|
|
1175
|
+
this.depsTail = undefined
|
|
1166
1176
|
this.flags = MutableRunning
|
|
1167
1177
|
const prevSub = setActiveSub(this)
|
|
1168
1178
|
try {
|
|
1169
1179
|
this.value = this.getter(undefined)
|
|
1170
1180
|
if (isDev) updateComputedDevtools(this, this.value)
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
// Initial evaluation failed: remove partially tracked dependencies
|
|
1183
|
+
// and allow a future read to retry from a clean slate.
|
|
1184
|
+
this.flags = 0
|
|
1185
|
+
purgeDeps(this)
|
|
1186
|
+
throw err
|
|
1171
1187
|
} finally {
|
|
1172
1188
|
setActiveSub(prevSub)
|
|
1173
|
-
this.flags
|
|
1189
|
+
if (this.flags & Running) {
|
|
1190
|
+
this.flags &= ~Running
|
|
1191
|
+
}
|
|
1174
1192
|
}
|
|
1175
1193
|
}
|
|
1176
1194
|
|
|
@@ -1206,13 +1224,24 @@ export function effect(fn: () => void): EffectDisposer {
|
|
|
1206
1224
|
if (prevSub !== undefined) link(e, prevSub, 0)
|
|
1207
1225
|
activeSub = e
|
|
1208
1226
|
|
|
1227
|
+
let didThrow = false
|
|
1228
|
+
let thrown: unknown
|
|
1209
1229
|
try {
|
|
1210
1230
|
if (isDev) effectRunDevtools(e)
|
|
1211
1231
|
fn()
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
didThrow = true
|
|
1234
|
+
thrown = err
|
|
1212
1235
|
} finally {
|
|
1213
1236
|
activeSub = prevSub
|
|
1214
|
-
|
|
1237
|
+
if (didThrow) {
|
|
1238
|
+
// Initial execution failed: fully detach partially collected graph links.
|
|
1239
|
+
disposeNode(e)
|
|
1240
|
+
} else {
|
|
1241
|
+
e.flags &= ~Running
|
|
1242
|
+
}
|
|
1215
1243
|
}
|
|
1244
|
+
if (didThrow) throw thrown
|
|
1216
1245
|
|
|
1217
1246
|
const disposer = effectOper.bind(e) as EffectDisposer & Record<symbol, boolean>
|
|
1218
1247
|
disposer[EFFECT_MARKER] = true
|
|
@@ -1254,13 +1283,24 @@ export function effectWithCleanup(
|
|
|
1254
1283
|
if (prevSub !== undefined) link(e, prevSub, 0)
|
|
1255
1284
|
activeSub = e
|
|
1256
1285
|
|
|
1286
|
+
let didThrow = false
|
|
1287
|
+
let thrown: unknown
|
|
1257
1288
|
try {
|
|
1258
1289
|
if (isDev) effectRunDevtools(e)
|
|
1259
1290
|
fn()
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
didThrow = true
|
|
1293
|
+
thrown = err
|
|
1260
1294
|
} finally {
|
|
1261
1295
|
activeSub = prevSub
|
|
1262
|
-
|
|
1296
|
+
if (didThrow) {
|
|
1297
|
+
// Initial execution failed: fully detach partially collected graph links.
|
|
1298
|
+
disposeNode(e)
|
|
1299
|
+
} else {
|
|
1300
|
+
e.flags &= ~Running
|
|
1301
|
+
}
|
|
1263
1302
|
}
|
|
1303
|
+
if (didThrow) throw thrown
|
|
1264
1304
|
|
|
1265
1305
|
const disposer = effectOper.bind(e) as EffectDisposer & Record<symbol, boolean>
|
|
1266
1306
|
disposer[EFFECT_MARKER] = true
|
|
@@ -1285,11 +1325,21 @@ export function effectScope(fn: () => void): EffectScopeDisposer {
|
|
|
1285
1325
|
if (prevSub !== undefined) link(e, prevSub, 0)
|
|
1286
1326
|
activeSub = e
|
|
1287
1327
|
|
|
1328
|
+
let didThrow = false
|
|
1329
|
+
let thrown: unknown
|
|
1288
1330
|
try {
|
|
1289
1331
|
fn()
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
didThrow = true
|
|
1334
|
+
thrown = err
|
|
1290
1335
|
} finally {
|
|
1291
1336
|
activeSub = prevSub
|
|
1337
|
+
if (didThrow) {
|
|
1338
|
+
// Scope construction failed: detach nested effects/memos linked to this scope.
|
|
1339
|
+
disposeNode(e)
|
|
1340
|
+
}
|
|
1292
1341
|
}
|
|
1342
|
+
if (didThrow) throw thrown
|
|
1293
1343
|
|
|
1294
1344
|
const disposer = effectScopeOper.bind(e) as EffectScopeDisposer & Record<symbol, boolean>
|
|
1295
1345
|
disposer[EFFECT_SCOPE_MARKER] = true
|
package/src/store.ts
CHANGED
|
@@ -38,6 +38,24 @@ export function createStore<T extends object>(
|
|
|
38
38
|
const proxyCache = new WeakMap<object, unknown>()
|
|
39
39
|
// Map of target object -> Map<key, Signal>
|
|
40
40
|
const signalCache = new WeakMap<object, Map<string | symbol, SignalAccessor<unknown>>>()
|
|
41
|
+
// Map of target object -> monotonically increasing iterate version
|
|
42
|
+
const iterateVersionCache = new WeakMap<object, number>()
|
|
43
|
+
|
|
44
|
+
function getIterateVersion(target: object): number {
|
|
45
|
+
return iterateVersionCache.get(target) ?? 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bumpIterateVersion(target: object): number {
|
|
49
|
+
const next = getIterateVersion(target) + 1
|
|
50
|
+
iterateVersionCache.set(target, next)
|
|
51
|
+
return next
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isArrayIndexKey(prop: string | symbol): prop is string {
|
|
55
|
+
if (typeof prop !== 'string') return false
|
|
56
|
+
const index = Number(prop)
|
|
57
|
+
return Number.isInteger(index) && index >= 0 && String(index) === prop
|
|
58
|
+
}
|
|
41
59
|
|
|
42
60
|
function wrap<T>(value: T): T {
|
|
43
61
|
if (value === null || typeof value !== 'object') return value
|
|
@@ -75,7 +93,9 @@ function wrap<T>(value: T): T {
|
|
|
75
93
|
if (prop === PROXY || prop === TARGET) return false
|
|
76
94
|
|
|
77
95
|
const isArrayLength = Array.isArray(target) && prop === 'length'
|
|
78
|
-
const
|
|
96
|
+
const isArrayIndex = Array.isArray(target) && isArrayIndexKey(prop)
|
|
97
|
+
const oldLength =
|
|
98
|
+
(isArrayLength || isArrayIndex) && Array.isArray(target) ? target.length : undefined
|
|
79
99
|
const hadKey = Object.prototype.hasOwnProperty.call(target, prop)
|
|
80
100
|
const oldValue = Reflect.get(target, prop, receiver)
|
|
81
101
|
if (oldValue === value) return true
|
|
@@ -86,6 +106,12 @@ function wrap<T>(value: T): T {
|
|
|
86
106
|
if (!hadKey) {
|
|
87
107
|
trigger(target, ITERATE_KEY)
|
|
88
108
|
}
|
|
109
|
+
if (isArrayIndex) {
|
|
110
|
+
const nextLength = target.length
|
|
111
|
+
if (typeof oldLength === 'number' && nextLength !== oldLength) {
|
|
112
|
+
trigger(target, 'length')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
89
115
|
if (isArrayLength) {
|
|
90
116
|
const nextLength = target.length
|
|
91
117
|
if (typeof oldLength === 'number' && nextLength < oldLength) {
|
|
@@ -148,8 +174,7 @@ function track(target: object, prop: string | symbol) {
|
|
|
148
174
|
|
|
149
175
|
let s = signals.get(prop)
|
|
150
176
|
if (!s) {
|
|
151
|
-
const initial =
|
|
152
|
-
prop === ITERATE_KEY ? (Reflect.ownKeys(target).length as number) : getLastValue(target, prop)
|
|
177
|
+
const initial = prop === ITERATE_KEY ? getIterateVersion(target) : getLastValue(target, prop)
|
|
153
178
|
s = signal(initial)
|
|
154
179
|
signals.set(prop, s)
|
|
155
180
|
}
|
|
@@ -162,7 +187,7 @@ function trigger(target: object, prop: string | symbol) {
|
|
|
162
187
|
const s = signals.get(prop)
|
|
163
188
|
if (s) {
|
|
164
189
|
if (prop === ITERATE_KEY) {
|
|
165
|
-
s(
|
|
190
|
+
s(bumpIterateVersion(target))
|
|
166
191
|
} else {
|
|
167
192
|
s(getLastValue(target, prop)) // notify with new value
|
|
168
193
|
}
|
|
@@ -174,10 +199,28 @@ function getLastValue(target: object, prop: string | symbol) {
|
|
|
174
199
|
return Reflect.get(target, prop)
|
|
175
200
|
}
|
|
176
201
|
|
|
202
|
+
function isPlainObject(value: object): boolean {
|
|
203
|
+
const proto = Object.getPrototypeOf(value)
|
|
204
|
+
return proto === Object.prototype || proto === null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isReconcilableObject(value: unknown): value is object {
|
|
208
|
+
if (value === null || typeof value !== 'object') return false
|
|
209
|
+
const raw = unwrap(value as object)
|
|
210
|
+
return Array.isArray(raw) || isPlainObject(raw)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function canReconcileNestedValues(current: unknown, next: unknown): current is object {
|
|
214
|
+
if (!isReconcilableObject(current) || !isReconcilableObject(next)) return false
|
|
215
|
+
const currentRaw = unwrap(current as object)
|
|
216
|
+
const nextRaw = unwrap(next as object)
|
|
217
|
+
return Array.isArray(currentRaw) === Array.isArray(nextRaw)
|
|
218
|
+
}
|
|
219
|
+
|
|
177
220
|
/**
|
|
178
|
-
* Reconcile a store path with a new value (
|
|
221
|
+
* Reconcile a store path with a new value (recursive structural diff)
|
|
179
222
|
*/
|
|
180
|
-
function reconcile(target: object, value: unknown) {
|
|
223
|
+
function reconcile(target: object, value: unknown, seenPairs?: WeakMap<object, WeakSet<object>>) {
|
|
181
224
|
if (target === value) return
|
|
182
225
|
if (value === null || typeof value !== 'object') {
|
|
183
226
|
throw new Error(
|
|
@@ -189,23 +232,43 @@ function reconcile(target: object, value: unknown) {
|
|
|
189
232
|
|
|
190
233
|
const realTarget = unwrap(target)
|
|
191
234
|
const realValue = unwrap(value)
|
|
235
|
+
const seen = seenPairs ?? new WeakMap<object, WeakSet<object>>()
|
|
236
|
+
let visitedValues = seen.get(realTarget)
|
|
237
|
+
if (!visitedValues) {
|
|
238
|
+
visitedValues = new WeakSet<object>()
|
|
239
|
+
seen.set(realTarget, visitedValues)
|
|
240
|
+
}
|
|
241
|
+
if (visitedValues.has(realValue)) return
|
|
242
|
+
visitedValues.add(realValue)
|
|
192
243
|
|
|
193
244
|
const keys = new Set([...Object.keys(realTarget), ...Object.keys(realValue)])
|
|
194
245
|
for (const key of keys) {
|
|
195
246
|
const rTarget = realTarget as Record<string, unknown>
|
|
196
247
|
const rValue = realValue as Record<string, unknown>
|
|
248
|
+
const hasCurrent = Object.prototype.hasOwnProperty.call(rTarget, key)
|
|
249
|
+
const hasNext = Object.prototype.hasOwnProperty.call(rValue, key)
|
|
250
|
+
const current = rTarget[key]
|
|
251
|
+
const next = rValue[key]
|
|
197
252
|
|
|
198
|
-
if (
|
|
253
|
+
if (!hasNext && hasCurrent) {
|
|
199
254
|
// deleted
|
|
200
255
|
delete (target as Record<string, unknown>)[key] // Triggers proxy trap
|
|
201
|
-
} else if (
|
|
202
|
-
|
|
256
|
+
} else if (hasNext && (!hasCurrent || current !== next)) {
|
|
257
|
+
if (canReconcileNestedValues(current, next)) {
|
|
258
|
+
reconcile((target as Record<string, unknown>)[key] as object, next, seen)
|
|
259
|
+
} else {
|
|
260
|
+
;(target as Record<string, unknown>)[key] = next // Triggers proxy trap
|
|
261
|
+
}
|
|
203
262
|
}
|
|
204
263
|
}
|
|
205
264
|
|
|
206
265
|
// Fix array length if needed
|
|
207
|
-
if (
|
|
208
|
-
|
|
266
|
+
if (
|
|
267
|
+
Array.isArray(realTarget) &&
|
|
268
|
+
Array.isArray(realValue) &&
|
|
269
|
+
realTarget.length !== realValue.length
|
|
270
|
+
) {
|
|
271
|
+
;(target as unknown as unknown[]).length = realValue.length
|
|
209
272
|
}
|
|
210
273
|
}
|
|
211
274
|
|
|
@@ -222,6 +285,16 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
|
222
285
|
let currentValue = unwrap(initialValue)
|
|
223
286
|
const signals = new Map<string | symbol, SignalAccessor<unknown>>()
|
|
224
287
|
let iterateSignal: SignalAccessor<number> | undefined
|
|
288
|
+
let iterateVersion = 0
|
|
289
|
+
let ownKeysSnapshot = Reflect.ownKeys(currentValue as object)
|
|
290
|
+
|
|
291
|
+
const hasSameOwnKeys = (aKeys: (string | symbol)[], bKeys: (string | symbol)[]): boolean => {
|
|
292
|
+
if (aKeys.length !== bKeys.length) return false
|
|
293
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
294
|
+
if (aKeys[i] !== bKeys[i]) return false
|
|
295
|
+
}
|
|
296
|
+
return true
|
|
297
|
+
}
|
|
225
298
|
|
|
226
299
|
const getPropSignal = (prop: string | symbol) => {
|
|
227
300
|
let s = signals.get(prop)
|
|
@@ -234,14 +307,23 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
|
234
307
|
|
|
235
308
|
const trackIterate = () => {
|
|
236
309
|
if (!iterateSignal) {
|
|
237
|
-
iterateSignal = signal(
|
|
310
|
+
iterateSignal = signal(iterateVersion)
|
|
238
311
|
}
|
|
239
312
|
iterateSignal()
|
|
240
313
|
}
|
|
241
314
|
|
|
242
|
-
const
|
|
243
|
-
if (iterateSignal)
|
|
244
|
-
|
|
315
|
+
const bumpIterate = () => {
|
|
316
|
+
if (!iterateSignal) return
|
|
317
|
+
iterateVersion += 1
|
|
318
|
+
iterateSignal(iterateVersion)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const updateIterateFromOwnKeys = (next: object): void => {
|
|
322
|
+
const nextKeys = Reflect.ownKeys(next)
|
|
323
|
+
const changed = !hasSameOwnKeys(ownKeysSnapshot, nextKeys)
|
|
324
|
+
ownKeysSnapshot = nextKeys
|
|
325
|
+
if (changed) {
|
|
326
|
+
bumpIterate()
|
|
245
327
|
}
|
|
246
328
|
}
|
|
247
329
|
|
|
@@ -265,7 +347,38 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
|
265
347
|
},
|
|
266
348
|
getOwnPropertyDescriptor(target, prop) {
|
|
267
349
|
getPropSignal(prop)()
|
|
268
|
-
|
|
350
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(currentValue, prop)
|
|
351
|
+
if (!descriptor) return undefined
|
|
352
|
+
|
|
353
|
+
// Proxy target is a synthetic empty object. Returning a non-configurable
|
|
354
|
+
// descriptor from the wrapped value can violate Proxy invariants.
|
|
355
|
+
if (descriptor.configurable !== false) {
|
|
356
|
+
return descriptor
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if ('value' in descriptor) {
|
|
360
|
+
return {
|
|
361
|
+
configurable: true,
|
|
362
|
+
enumerable: descriptor.enumerable ?? true,
|
|
363
|
+
writable: descriptor.writable ?? true,
|
|
364
|
+
value: descriptor.value,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const normalized: PropertyDescriptor = {
|
|
369
|
+
configurable: true,
|
|
370
|
+
enumerable: descriptor.enumerable ?? true,
|
|
371
|
+
}
|
|
372
|
+
if (descriptor.get) normalized.get = descriptor.get
|
|
373
|
+
if (descriptor.set) normalized.set = descriptor.set
|
|
374
|
+
return normalized
|
|
375
|
+
},
|
|
376
|
+
set(_, prop) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`[Fict] Cannot set "${String(
|
|
379
|
+
prop,
|
|
380
|
+
)}" on a diffing signal proxy directly. Update the source value and call its writer instead.`,
|
|
381
|
+
)
|
|
269
382
|
},
|
|
270
383
|
})
|
|
271
384
|
|
|
@@ -283,7 +396,7 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
|
283
396
|
const newVal = Reflect.get(next as object, prop)
|
|
284
397
|
s(newVal)
|
|
285
398
|
}
|
|
286
|
-
|
|
399
|
+
updateIterateFromOwnKeys(next as object)
|
|
287
400
|
return
|
|
288
401
|
}
|
|
289
402
|
|
|
@@ -297,7 +410,7 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
|
|
|
297
410
|
s(newVal)
|
|
298
411
|
}
|
|
299
412
|
}
|
|
300
|
-
|
|
413
|
+
updateIterateFromOwnKeys(next as object)
|
|
301
414
|
|
|
302
415
|
// Note: If new properties appeared that weren't tracked, we don't care
|
|
303
416
|
// because no one is listening.
|
package/src/transition.ts
CHANGED
|
@@ -67,21 +67,58 @@ export function startTransition(fn: () => void): void {
|
|
|
67
67
|
* }
|
|
68
68
|
* ```
|
|
69
69
|
*/
|
|
70
|
-
export function useTransition(): [() => boolean, (fn: () => void) => void] {
|
|
70
|
+
export function useTransition(): [() => boolean, (fn: () => void | PromiseLike<unknown>) => void] {
|
|
71
71
|
const pending = signal(false)
|
|
72
|
+
let pendingCount = 0
|
|
72
73
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// with the actual transition execution state.
|
|
77
|
-
startTransition(() => {
|
|
74
|
+
const beginPending = () => {
|
|
75
|
+
pendingCount += 1
|
|
76
|
+
if (pendingCount === 1) {
|
|
78
77
|
pending(true)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const endPending = () => {
|
|
82
|
+
if (pendingCount === 0) return
|
|
83
|
+
pendingCount -= 1
|
|
84
|
+
if (pendingCount === 0) {
|
|
85
|
+
pending(false)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const start = (fn: () => void | PromiseLike<unknown>) => {
|
|
90
|
+
beginPending()
|
|
91
|
+
let result: void | PromiseLike<unknown> | undefined
|
|
92
|
+
let thrown: unknown
|
|
93
|
+
let didThrow = false
|
|
94
|
+
|
|
95
|
+
startTransition(() => {
|
|
79
96
|
try {
|
|
80
|
-
fn()
|
|
81
|
-
}
|
|
82
|
-
|
|
97
|
+
result = fn()
|
|
98
|
+
} catch (err) {
|
|
99
|
+
thrown = err
|
|
100
|
+
didThrow = true
|
|
83
101
|
}
|
|
84
102
|
})
|
|
103
|
+
|
|
104
|
+
if (didThrow) {
|
|
105
|
+
endPending()
|
|
106
|
+
throw thrown
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (result && typeof (result as PromiseLike<unknown>).then === 'function') {
|
|
110
|
+
Promise.resolve(result).finally(() => {
|
|
111
|
+
endPending()
|
|
112
|
+
})
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Keep pending true for at least one microtask so UI can observe it.
|
|
117
|
+
if (typeof queueMicrotask === 'function') {
|
|
118
|
+
queueMicrotask(endPending)
|
|
119
|
+
} else {
|
|
120
|
+
Promise.resolve().then(endPending)
|
|
121
|
+
}
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
return [() => pending(), start]
|