@pyreon/reactivity 0.24.4 → 0.24.6
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/package.json +1 -4
- package/src/batch.ts +0 -196
- package/src/cell.ts +0 -72
- package/src/computed.ts +0 -313
- package/src/createSelector.ts +0 -109
- package/src/debug.ts +0 -134
- package/src/effect.ts +0 -467
- package/src/env.d.ts +0 -6
- package/src/index.ts +0 -60
- package/src/lpih.ts +0 -227
- package/src/manifest.ts +0 -660
- package/src/reactive-devtools.ts +0 -494
- package/src/reactive-trace.ts +0 -142
- package/src/reconcile.ts +0 -118
- package/src/resource.ts +0 -84
- package/src/scope.ts +0 -123
- package/src/signal.ts +0 -261
- package/src/store.ts +0 -250
- package/src/tests/batch.test.ts +0 -751
- package/src/tests/bind.test.ts +0 -84
- package/src/tests/branches.test.ts +0 -343
- package/src/tests/cell.test.ts +0 -159
- package/src/tests/computed.test.ts +0 -436
- package/src/tests/coverage-hardening.test.ts +0 -471
- package/src/tests/createSelector.test.ts +0 -291
- package/src/tests/debug.test.ts +0 -196
- package/src/tests/effect.test.ts +0 -464
- package/src/tests/fanout-repro.test.ts +0 -179
- package/src/tests/lpih-source-location.test.ts +0 -277
- package/src/tests/lpih.test.ts +0 -351
- package/src/tests/manifest-snapshot.test.ts +0 -96
- package/src/tests/reactive-devtools-treeshake.test.ts +0 -48
- package/src/tests/reactive-devtools.test.ts +0 -296
- package/src/tests/reactive-trace.test.ts +0 -102
- package/src/tests/reconcile-security.test.ts +0 -45
- package/src/tests/resource.test.ts +0 -326
- package/src/tests/scope.test.ts +0 -231
- package/src/tests/signal.test.ts +0 -368
- package/src/tests/store.test.ts +0 -286
- package/src/tests/tracking.test.ts +0 -158
- package/src/tests/vue-parity.test.ts +0 -191
- package/src/tests/watch.test.ts +0 -246
- package/src/tracking.ts +0 -139
- package/src/watch.ts +0 -68
package/src/effect.ts
DELETED
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
import { _captureCallerLocation, _rdRecordFire, _rdRegister } from './reactive-devtools'
|
|
2
|
-
import { getCurrentScope } from './scope'
|
|
3
|
-
import { _restoreActiveEffect, _setActiveEffect, setDepsCollector, withTracking } from './tracking'
|
|
4
|
-
|
|
5
|
-
// Dev-time counter sink — see packages/internals/perf-harness for contract.
|
|
6
|
-
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
7
|
-
|
|
8
|
-
export interface Effect {
|
|
9
|
-
dispose(): void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface EffectOptions {
|
|
13
|
-
/**
|
|
14
|
-
* @internal — source location injected by `@pyreon/vite-plugin` at build
|
|
15
|
-
* time. When present, the runtime skips the `new Error().stack` capture
|
|
16
|
-
* in `_rdRegister` — saves ~2.2µs per effect creation when devtools is
|
|
17
|
-
* active. Plain user code should NOT set this; the field is opaque
|
|
18
|
-
* (no public type) so it's not part of the public API surface.
|
|
19
|
-
*
|
|
20
|
-
* Shape: `{ file: string; line: number; col: number }` matching
|
|
21
|
-
* `@pyreon/reactivity`'s `SourceLocation`.
|
|
22
|
-
*/
|
|
23
|
-
__sourceLocation?: { file: string; line: number; col: number }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ─── Effect-scoped snapshot capture (DI from `@pyreon/core`) ─────────────────
|
|
27
|
-
//
|
|
28
|
-
// Effects re-run reactively in response to signal changes. When that re-run
|
|
29
|
-
// happens AFTER the synchronous mount that set the effect up, the surrounding
|
|
30
|
-
// context stack (from `@pyreon/core`'s provide() calls) may have been
|
|
31
|
-
// destructively truncated by `mountReactive`'s `restoreContextStack` cleanup.
|
|
32
|
-
// Without restoring the captured context, signal-driven re-runs of `_bind` /
|
|
33
|
-
// `renderEffect` / `effect` see a half-empty stack and `useContext()` falls
|
|
34
|
-
// back to the default value — silently breaking provider-backed APIs like
|
|
35
|
-
// `useMode()`, `useTheme()`, `useRouter()`, etc. on every reactive update.
|
|
36
|
-
//
|
|
37
|
-
// `@pyreon/reactivity` is below `@pyreon/core` in the dep order, so it can't
|
|
38
|
-
// import `captureContextStack` / `restoreContextStack` directly. Core
|
|
39
|
-
// registers its capture+restore pair via `setSnapshotCapture` at module load.
|
|
40
|
-
// When unset (raw reactivity-only consumers), effects skip context handling
|
|
41
|
-
// — same behavior as before this hook existed.
|
|
42
|
-
export interface ReactiveSnapshotCapture {
|
|
43
|
-
capture: () => unknown
|
|
44
|
-
/** Run `fn` with the previously-captured snapshot active. */
|
|
45
|
-
restore: <T>(snap: unknown, fn: () => T) => T
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let _snapshotCapture: ReactiveSnapshotCapture | null = null
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Register a capture/restore pair so reactivity-layer effects (`_bind`,
|
|
52
|
-
* `renderEffect`, `effect`) can preserve external context (e.g. the core
|
|
53
|
-
* provide/useContext stack) across signal-driven re-runs. Called by
|
|
54
|
-
* `@pyreon/core`'s context module at import time. Idempotent — calling again
|
|
55
|
-
* replaces the previously registered hook.
|
|
56
|
-
*/
|
|
57
|
-
export function setSnapshotCapture(hook: ReactiveSnapshotCapture | null): void {
|
|
58
|
-
_snapshotCapture = hook
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ─── onCleanup ───────────────────────────────────────────────────────────────
|
|
62
|
-
// Thread-local collector for cleanup functions registered via onCleanup()
|
|
63
|
-
// during effect execution. Pushed/popped around the user callback in effect().
|
|
64
|
-
let _cleanupCollector: (() => void)[] | null = null
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Register a cleanup function inside an effect. The cleanup runs:
|
|
68
|
-
* - Before the effect re-runs (when dependencies change)
|
|
69
|
-
* - When the effect is disposed
|
|
70
|
-
*
|
|
71
|
-
* Can be called multiple times — all cleanups run in registration order.
|
|
72
|
-
* Must be called synchronously during effect setup (like onMount/onUnmount).
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* effect(() => {
|
|
76
|
-
* const controller = new AbortController()
|
|
77
|
-
* onCleanup(() => controller.abort())
|
|
78
|
-
* fetch(`/api/user/${userId()}`, { signal: controller.signal })
|
|
79
|
-
* .then(r => r.json())
|
|
80
|
-
* .then(data => user.set(data))
|
|
81
|
-
* })
|
|
82
|
-
*/
|
|
83
|
-
export function onCleanup(fn: () => void): void {
|
|
84
|
-
if (_cleanupCollector) {
|
|
85
|
-
_cleanupCollector.push(fn)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Thread-local collector for nested effects — captures effect() calls made
|
|
90
|
-
// inside another effect's fn() body so the parent can dispose them on
|
|
91
|
-
// re-run / disposal. Without this, inner effects leak across outer
|
|
92
|
-
// lifecycle boundaries (caught by cleanup-nested.test.ts).
|
|
93
|
-
let _innerEffectCollector: Effect[] | null = null
|
|
94
|
-
|
|
95
|
-
// Global error handler — called for unhandled errors thrown inside effects.
|
|
96
|
-
// Defaults to console.error so silent failures are never swallowed.
|
|
97
|
-
//
|
|
98
|
-
// Two-layer model:
|
|
99
|
-
// 1. The user-overridable single handler set via `setErrorHandler` (legacy
|
|
100
|
-
// direct API).
|
|
101
|
-
// 2. A globalThis bridge `__pyreon_report_error__` that `@pyreon/core`
|
|
102
|
-
// installs in `registerErrorHandler` to forward effect errors into the
|
|
103
|
-
// same telemetry pipeline as component / mount / render errors.
|
|
104
|
-
// Pre-fix the two surfaces were disconnected — Sentry/Datadog wiring via
|
|
105
|
-
// core's `registerErrorHandler` silently missed effect-thrown errors.
|
|
106
|
-
// Globalthis-based to avoid an upward import (core depends on
|
|
107
|
-
// reactivity, not the reverse). Same shape as the perf-harness counter
|
|
108
|
-
// sink — zero cost when no consumer is installed.
|
|
109
|
-
//
|
|
110
|
-
// Both surfaces fire on every effect error. The legacy handler stays for
|
|
111
|
-
// backward compat; new consumers should prefer `@pyreon/core`'s
|
|
112
|
-
// `registerErrorHandler`.
|
|
113
|
-
|
|
114
|
-
interface PyreonErrorBridge {
|
|
115
|
-
__pyreon_report_error__?: (err: unknown, phase: 'effect') => void
|
|
116
|
-
}
|
|
117
|
-
const _errorBridge = globalThis as PyreonErrorBridge
|
|
118
|
-
|
|
119
|
-
function _defaultErrorHandler(err: unknown): void {
|
|
120
|
-
// Last-resort unhandled-effect-error reporter — MUST fire in
|
|
121
|
-
// production (silently swallowing uncaught effect errors is a
|
|
122
|
-
// serious bug; React/Vue/Solid all log uncaught errors in prod).
|
|
123
|
-
// Deliberately not __DEV__-gated.
|
|
124
|
-
// pyreon-lint-disable-next-line pyreon/dev-guard-warnings
|
|
125
|
-
console.error('[pyreon] Unhandled effect error:', err)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
let _userErrorHandler: ((err: unknown) => void) | undefined
|
|
129
|
-
|
|
130
|
-
export const _errorHandler: (err: unknown) => void = (err) => {
|
|
131
|
-
// 1. User-set or default direct handler.
|
|
132
|
-
;(_userErrorHandler ?? _defaultErrorHandler)(err)
|
|
133
|
-
// 2. Global telemetry bridge (installed by @pyreon/core's
|
|
134
|
-
// registerErrorHandler). Forwards effect errors into reportError so
|
|
135
|
-
// Sentry/Datadog wiring captures them alongside component errors.
|
|
136
|
-
_errorBridge.__pyreon_report_error__?.(err, 'effect')
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function setErrorHandler(fn: (err: unknown) => void): void {
|
|
140
|
-
_userErrorHandler = fn
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Remove an effect from all dependency subscriber sets (local deps array). */
|
|
144
|
-
function cleanupLocalDeps(deps: Set<() => void>[], fn: () => void): void {
|
|
145
|
-
if (deps.length === 1) {
|
|
146
|
-
;(deps[0] as Set<() => void>).delete(fn)
|
|
147
|
-
deps.length = 0
|
|
148
|
-
} else if (deps.length > 1) {
|
|
149
|
-
for (let i = 0; i < deps.length; i++) (deps[i] as Set<() => void>).delete(fn)
|
|
150
|
-
deps.length = 0
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function effect(
|
|
155
|
-
fn: () => (() => void) | void,
|
|
156
|
-
options?: EffectOptions,
|
|
157
|
-
): Effect {
|
|
158
|
-
// Dev-mode warning for async effect callbacks (audit bug #1). The
|
|
159
|
-
// tracking context is the synchronous frame around `fn()`'s top half;
|
|
160
|
-
// anything after the first `await` runs detached, so signal reads on
|
|
161
|
-
// the back side aren't tracked and the effect won't re-run when those
|
|
162
|
-
// signals change. The fix at the call site is either to read all
|
|
163
|
-
// tracked signals BEFORE the first await, or split the work into two
|
|
164
|
-
// effects (or use `watch` for async-in-callback). Surfacing the warn
|
|
165
|
-
// at registration is the cheapest catch we can offer: an
|
|
166
|
-
// `AsyncFunction.prototype.constructor.name === 'AsyncFunction'`
|
|
167
|
-
// check is true at function-definition time without invoking anything.
|
|
168
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
169
|
-
if (fn.constructor && fn.constructor.name === 'AsyncFunction') {
|
|
170
|
-
// oxlint-disable-next-line no-console
|
|
171
|
-
console.warn(
|
|
172
|
-
'[pyreon] effect() received an async function. Signal reads after the first `await` are NOT tracked — only the synchronous prefix is. ' +
|
|
173
|
-
'Read every tracked signal BEFORE any await, or split into separate effects, or use `watch(source, asyncCb)` for async-in-callback patterns.',
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Capture the scope at creation time — remains correct during future re-runs
|
|
179
|
-
// even after setCurrentScope(null) has been called post-setup.
|
|
180
|
-
const scope = getCurrentScope()
|
|
181
|
-
// Capture the external (core-context) snapshot at SETUP time. Reactive
|
|
182
|
-
// re-runs restore it before invoking fn, so provider lookups stay correct
|
|
183
|
-
// even when the global context stack has been destructively truncated by
|
|
184
|
-
// mountReactive's restoreContextStack cleanup. See `_bind` for the full
|
|
185
|
-
// rationale.
|
|
186
|
-
const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null
|
|
187
|
-
let disposed = false
|
|
188
|
-
let isFirstRun = true
|
|
189
|
-
let cleanup: (() => void) | undefined
|
|
190
|
-
// Local deps array — avoids WeakMap overhead (like renderEffect)
|
|
191
|
-
const deps: Set<() => void>[] = []
|
|
192
|
-
|
|
193
|
-
let cleanups: (() => void)[] | undefined
|
|
194
|
-
// Inner effects created during this effect's fn() body. Disposed on
|
|
195
|
-
// outer re-run (before the next fn()) and on outer dispose(). Without
|
|
196
|
-
// this, nested effects leak across outer lifecycle boundaries.
|
|
197
|
-
let innerEffects: Effect[] | null = null
|
|
198
|
-
|
|
199
|
-
const runCleanup = () => {
|
|
200
|
-
if (innerEffects) {
|
|
201
|
-
for (const ie of innerEffects) {
|
|
202
|
-
try {
|
|
203
|
-
ie.dispose()
|
|
204
|
-
} catch (err) {
|
|
205
|
-
_errorHandler(err)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
innerEffects = null
|
|
209
|
-
}
|
|
210
|
-
if (cleanups) {
|
|
211
|
-
for (const c of cleanups) {
|
|
212
|
-
try {
|
|
213
|
-
c()
|
|
214
|
-
} catch (err) {
|
|
215
|
-
_errorHandler(err)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
cleanups = undefined
|
|
219
|
-
}
|
|
220
|
-
if (typeof cleanup === 'function') {
|
|
221
|
-
try {
|
|
222
|
-
cleanup()
|
|
223
|
-
} catch (err) {
|
|
224
|
-
_errorHandler(err)
|
|
225
|
-
}
|
|
226
|
-
cleanup = undefined
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const run = () => {
|
|
231
|
-
if (disposed) return
|
|
232
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
233
|
-
_countSink.__pyreon_count__?.('reactivity.effectRun')
|
|
234
|
-
_rdRecordFire(run)
|
|
235
|
-
}
|
|
236
|
-
// Run previous cleanup before re-running
|
|
237
|
-
runCleanup()
|
|
238
|
-
// Start a new inner-effect collection window. Effects created during
|
|
239
|
-
// fn() will push themselves into this array and be disposed on the
|
|
240
|
-
// next re-run or on dispose.
|
|
241
|
-
const outerCollector = _innerEffectCollector
|
|
242
|
-
const myInners: Effect[] = []
|
|
243
|
-
_innerEffectCollector = myInners
|
|
244
|
-
try {
|
|
245
|
-
cleanupLocalDeps(deps, run)
|
|
246
|
-
setDepsCollector(deps)
|
|
247
|
-
// Collect onCleanup() registrations during execution
|
|
248
|
-
const collected: (() => void)[] = []
|
|
249
|
-
_cleanupCollector = collected
|
|
250
|
-
// First run executes inside the synchronous mount where the context
|
|
251
|
-
// stack is still intact — call fn directly to avoid pushing the
|
|
252
|
-
// captured snapshot a redundant second time. Subsequent re-runs
|
|
253
|
-
// happen AFTER mountReactive's cleanup has truncated the stack, so
|
|
254
|
-
// they need the snapshot restored to find provider frames.
|
|
255
|
-
const fnToRun =
|
|
256
|
-
isFirstRun || snapshot === null || _snapshotCapture === null
|
|
257
|
-
? fn
|
|
258
|
-
: () => (_snapshotCapture as ReactiveSnapshotCapture).restore(snapshot, fn)
|
|
259
|
-
cleanup = withTracking(run, fnToRun) || undefined
|
|
260
|
-
_cleanupCollector = null
|
|
261
|
-
if (collected.length > 0) cleanups = collected
|
|
262
|
-
setDepsCollector(null)
|
|
263
|
-
} catch (err) {
|
|
264
|
-
_cleanupCollector = null
|
|
265
|
-
setDepsCollector(null)
|
|
266
|
-
_errorHandler(err)
|
|
267
|
-
} finally {
|
|
268
|
-
_innerEffectCollector = outerCollector
|
|
269
|
-
}
|
|
270
|
-
if (myInners.length > 0) innerEffects = myInners
|
|
271
|
-
// Notify scope after each reactive re-run (not the initial synchronous run)
|
|
272
|
-
// so onUpdate hooks fire after the DOM has settled.
|
|
273
|
-
if (!isFirstRun) scope?.notifyEffectRan()
|
|
274
|
-
isFirstRun = false
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (process.env.NODE_ENV !== 'production')
|
|
278
|
-
// skipFrames=1: skip the `effect()` / `renderEffect()` frame, capture the user's call site.
|
|
279
|
-
// Prefer build-time-injected location over the ~2.2µs stack-capture
|
|
280
|
-
// fallback. @pyreon/vite-plugin's `injectSignalNames` rewrites
|
|
281
|
-
// `effect(() => …)` to `effect(() => …, { __sourceLocation: {…} })`.
|
|
282
|
-
_rdRegister(
|
|
283
|
-
run,
|
|
284
|
-
'effect',
|
|
285
|
-
null,
|
|
286
|
-
run,
|
|
287
|
-
undefined,
|
|
288
|
-
options?.__sourceLocation ?? _captureCallerLocation(1),
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
run()
|
|
292
|
-
|
|
293
|
-
const e: Effect = {
|
|
294
|
-
dispose() {
|
|
295
|
-
runCleanup()
|
|
296
|
-
disposed = true
|
|
297
|
-
cleanupLocalDeps(deps, run)
|
|
298
|
-
},
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// If we're inside another effect's run, register with it so the outer
|
|
302
|
-
// disposes this inner automatically.
|
|
303
|
-
if (_innerEffectCollector !== null) {
|
|
304
|
-
_innerEffectCollector.push(e)
|
|
305
|
-
} else {
|
|
306
|
-
// Otherwise auto-register with the active EffectScope (if any)
|
|
307
|
-
getCurrentScope()?.add(e)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return e
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Lightweight effect for DOM render bindings.
|
|
315
|
-
*
|
|
316
|
-
* Differences from `effect()`:
|
|
317
|
-
* - No EffectScope registration (caller owns the dispose lifecycle)
|
|
318
|
-
* - No error handler (errors propagate naturally)
|
|
319
|
-
* - No onUpdate notification
|
|
320
|
-
* - Deps stored in a local array instead of the global WeakMap — faster
|
|
321
|
-
* creation and disposal (~200ns saved per effect vs WeakMap path)
|
|
322
|
-
*
|
|
323
|
-
* Returns a dispose function (not an Effect object — saves 1 allocation).
|
|
324
|
-
*/
|
|
325
|
-
/**
|
|
326
|
-
* Static-dep binding — compiler helper for template expressions.
|
|
327
|
-
*
|
|
328
|
-
* Like renderEffect but assumes dependencies never change (true for all
|
|
329
|
-
* compiler-emitted template bindings like `_tpl()` text/attribute updates).
|
|
330
|
-
*
|
|
331
|
-
* Tracks dependencies only on the first run. Re-runs skip cleanup, re-tracking,
|
|
332
|
-
* and tracking context save/restore entirely — just calls `fn()` directly.
|
|
333
|
-
*
|
|
334
|
-
* Per re-run savings vs renderEffect:
|
|
335
|
-
* - No deps iteration + Set.delete (cleanup)
|
|
336
|
-
* - No setDepsCollector + withTracking (re-registration)
|
|
337
|
-
* - Signal reads hit `if (activeEffect)` null check → instant return
|
|
338
|
-
*/
|
|
339
|
-
export function _bind(fn: () => void): () => void {
|
|
340
|
-
const deps: Set<() => void>[] = []
|
|
341
|
-
let disposed = false
|
|
342
|
-
|
|
343
|
-
// Capture external (core-context) snapshot at SETUP time. Re-runs restore
|
|
344
|
-
// it before invoking fn, so signal-driven re-runs see the same provider
|
|
345
|
-
// chain that was active when the binding was first set up — even if the
|
|
346
|
-
// global context stack has been destructively truncated by mountReactive's
|
|
347
|
-
// restoreContextStack cleanup in the meantime.
|
|
348
|
-
const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null
|
|
349
|
-
|
|
350
|
-
const run = () => {
|
|
351
|
-
if (disposed) return
|
|
352
|
-
if (snapshot !== null && _snapshotCapture) {
|
|
353
|
-
_snapshotCapture.restore(snapshot, fn)
|
|
354
|
-
} else {
|
|
355
|
-
fn()
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// First run: track deps so we know what to unsubscribe on dispose. We
|
|
360
|
-
// intentionally call `fn` directly (not `run`) here — the synchronous
|
|
361
|
-
// mount stack is already intact at this point, so restoring the captured
|
|
362
|
-
// snapshot would just push the same frames again redundantly.
|
|
363
|
-
setDepsCollector(deps)
|
|
364
|
-
withTracking(run, fn)
|
|
365
|
-
setDepsCollector(null)
|
|
366
|
-
|
|
367
|
-
const dispose = () => {
|
|
368
|
-
if (disposed) return
|
|
369
|
-
disposed = true
|
|
370
|
-
for (const s of deps) s.delete(run)
|
|
371
|
-
deps.length = 0
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Auto-register with scope so template bindings are disposed during teardown
|
|
375
|
-
getCurrentScope()?.add({ dispose })
|
|
376
|
-
|
|
377
|
-
return dispose
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Full re-track path for renderEffect: cleanup old deps, evaluate with tracking. */
|
|
381
|
-
function renderEffectFullTrack(deps: Set<() => void>[], run: () => void, fn: () => void): void {
|
|
382
|
-
if (deps.length === 1) {
|
|
383
|
-
;(deps[0] as Set<() => void>).delete(run)
|
|
384
|
-
deps.length = 0
|
|
385
|
-
} else if (deps.length > 1) {
|
|
386
|
-
for (const s of deps) s.delete(run)
|
|
387
|
-
deps.length = 0
|
|
388
|
-
}
|
|
389
|
-
setDepsCollector(deps)
|
|
390
|
-
_setActiveEffect(run)
|
|
391
|
-
try {
|
|
392
|
-
fn()
|
|
393
|
-
} finally {
|
|
394
|
-
_restoreActiveEffect()
|
|
395
|
-
setDepsCollector(null)
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
export function renderEffect(fn: () => void): () => void {
|
|
400
|
-
// Same dev warning as `effect()` — signal reads after the first
|
|
401
|
-
// await aren't tracked. See effect()'s docstring for full reasoning.
|
|
402
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
403
|
-
if (fn.constructor && fn.constructor.name === 'AsyncFunction') {
|
|
404
|
-
// oxlint-disable-next-line no-console
|
|
405
|
-
console.warn(
|
|
406
|
-
'[pyreon] renderEffect() received an async function. Signal reads after the first `await` are NOT tracked — only the synchronous prefix is. ' +
|
|
407
|
-
'Read every tracked signal BEFORE any await, or split into separate effects, or use `watch(source, asyncCb)` for async-in-callback patterns.',
|
|
408
|
-
)
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const deps: Set<() => void>[] = []
|
|
413
|
-
let disposed = false
|
|
414
|
-
let isFirstRun = true
|
|
415
|
-
|
|
416
|
-
// Same rationale as `_bind`: capture the external context snapshot at
|
|
417
|
-
// SETUP and restore it on signal-driven re-runs so provider lookups stay
|
|
418
|
-
// correct even after `mountReactive`'s cleanup truncates the global stack.
|
|
419
|
-
const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null
|
|
420
|
-
|
|
421
|
-
const trackedFn =
|
|
422
|
-
snapshot !== null && _snapshotCapture
|
|
423
|
-
? () => (_snapshotCapture as ReactiveSnapshotCapture).restore(snapshot, fn)
|
|
424
|
-
: fn
|
|
425
|
-
|
|
426
|
-
const run = () => {
|
|
427
|
-
if (disposed) return
|
|
428
|
-
if (isFirstRun) {
|
|
429
|
-
isFirstRun = false
|
|
430
|
-
setDepsCollector(deps)
|
|
431
|
-
_setActiveEffect(run)
|
|
432
|
-
try {
|
|
433
|
-
// First run: stack is still intact (we're inside the synchronous
|
|
434
|
-
// mount), so call fn directly to avoid pushing the snapshot frames
|
|
435
|
-
// a second time.
|
|
436
|
-
fn()
|
|
437
|
-
} finally {
|
|
438
|
-
_restoreActiveEffect()
|
|
439
|
-
setDepsCollector(null)
|
|
440
|
-
}
|
|
441
|
-
} else {
|
|
442
|
-
renderEffectFullTrack(deps, run, trackedFn)
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (process.env.NODE_ENV !== 'production')
|
|
447
|
-
// skipFrames=1: skip the `effect()` / `renderEffect()` frame, capture the user's call site.
|
|
448
|
-
_rdRegister(run, 'effect', null, run, undefined, _captureCallerLocation(1))
|
|
449
|
-
|
|
450
|
-
run()
|
|
451
|
-
|
|
452
|
-
const dispose = () => {
|
|
453
|
-
if (disposed) return
|
|
454
|
-
disposed = true
|
|
455
|
-
if (deps.length === 1) {
|
|
456
|
-
;(deps[0] as Set<() => void>).delete(run)
|
|
457
|
-
} else {
|
|
458
|
-
for (const s of deps) s.delete(run)
|
|
459
|
-
}
|
|
460
|
-
deps.length = 0
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Auto-register with scope so render effects are disposed during teardown
|
|
464
|
-
getCurrentScope()?.add({ dispose })
|
|
465
|
-
|
|
466
|
-
return dispose
|
|
467
|
-
}
|
package/src/env.d.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// @pyreon/reactivity — signals-based reactive primitives
|
|
2
|
-
|
|
3
|
-
export { batch, nextTick } from './batch'
|
|
4
|
-
export { Cell, cell } from './cell'
|
|
5
|
-
export { type Computed, type ComputedOptions, computed } from './computed'
|
|
6
|
-
export { createSelector } from './createSelector'
|
|
7
|
-
export { inspectSignal, onSignalUpdate, why } from './debug'
|
|
8
|
-
export type {
|
|
9
|
-
FireSummary,
|
|
10
|
-
ReactiveEdge,
|
|
11
|
-
ReactiveFire,
|
|
12
|
-
ReactiveGraph,
|
|
13
|
-
ReactiveNode,
|
|
14
|
-
ReactiveNodeKind,
|
|
15
|
-
SourceLocation,
|
|
16
|
-
} from './reactive-devtools'
|
|
17
|
-
export {
|
|
18
|
-
activateReactiveDevtools,
|
|
19
|
-
deactivateReactiveDevtools,
|
|
20
|
-
getFireSummaries,
|
|
21
|
-
getReactiveFires,
|
|
22
|
-
getReactiveGraph,
|
|
23
|
-
isReactiveDevtoolsActive,
|
|
24
|
-
} from './reactive-devtools'
|
|
25
|
-
// `writeLpihCache` + `startLpihPolling` ship at the `@pyreon/reactivity/lpih`
|
|
26
|
-
// subpath. They depend on `node:fs/promises` (Node-only) and are dev-mode
|
|
27
|
-
// integration utilities — separating them keeps the core main-entry bundle
|
|
28
|
-
// smaller AND clarifies that LPIH writes are an opt-in side-channel, not a
|
|
29
|
-
// core reactivity primitive. See `./lpih.ts` and `docs/docs/lpih.md`.
|
|
30
|
-
export type { ReactiveTraceEntry } from './reactive-trace'
|
|
31
|
-
export { clearReactiveTrace, getReactiveTrace } from './reactive-trace'
|
|
32
|
-
export {
|
|
33
|
-
_bind,
|
|
34
|
-
type Effect,
|
|
35
|
-
effect,
|
|
36
|
-
onCleanup,
|
|
37
|
-
type ReactiveSnapshotCapture,
|
|
38
|
-
renderEffect,
|
|
39
|
-
setErrorHandler,
|
|
40
|
-
setSnapshotCapture,
|
|
41
|
-
} from './effect'
|
|
42
|
-
export { reconcile } from './reconcile'
|
|
43
|
-
export { createResource, type Resource } from './resource'
|
|
44
|
-
export {
|
|
45
|
-
EffectScope,
|
|
46
|
-
effectScope,
|
|
47
|
-
getCurrentScope,
|
|
48
|
-
onScopeDispose,
|
|
49
|
-
setCurrentScope,
|
|
50
|
-
} from './scope'
|
|
51
|
-
export {
|
|
52
|
-
type ReadonlySignal,
|
|
53
|
-
type Signal,
|
|
54
|
-
type SignalDebugInfo,
|
|
55
|
-
type SignalOptions,
|
|
56
|
-
signal,
|
|
57
|
-
} from './signal'
|
|
58
|
-
export { createStore, isStore, markRaw, shallowReactive } from './store'
|
|
59
|
-
export { runUntracked, runUntracked as untrack } from './tracking'
|
|
60
|
-
export { type WatchOptions, watch } from './watch'
|