@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.
Files changed (44) hide show
  1. package/package.json +1 -4
  2. package/src/batch.ts +0 -196
  3. package/src/cell.ts +0 -72
  4. package/src/computed.ts +0 -313
  5. package/src/createSelector.ts +0 -109
  6. package/src/debug.ts +0 -134
  7. package/src/effect.ts +0 -467
  8. package/src/env.d.ts +0 -6
  9. package/src/index.ts +0 -60
  10. package/src/lpih.ts +0 -227
  11. package/src/manifest.ts +0 -660
  12. package/src/reactive-devtools.ts +0 -494
  13. package/src/reactive-trace.ts +0 -142
  14. package/src/reconcile.ts +0 -118
  15. package/src/resource.ts +0 -84
  16. package/src/scope.ts +0 -123
  17. package/src/signal.ts +0 -261
  18. package/src/store.ts +0 -250
  19. package/src/tests/batch.test.ts +0 -751
  20. package/src/tests/bind.test.ts +0 -84
  21. package/src/tests/branches.test.ts +0 -343
  22. package/src/tests/cell.test.ts +0 -159
  23. package/src/tests/computed.test.ts +0 -436
  24. package/src/tests/coverage-hardening.test.ts +0 -471
  25. package/src/tests/createSelector.test.ts +0 -291
  26. package/src/tests/debug.test.ts +0 -196
  27. package/src/tests/effect.test.ts +0 -464
  28. package/src/tests/fanout-repro.test.ts +0 -179
  29. package/src/tests/lpih-source-location.test.ts +0 -277
  30. package/src/tests/lpih.test.ts +0 -351
  31. package/src/tests/manifest-snapshot.test.ts +0 -96
  32. package/src/tests/reactive-devtools-treeshake.test.ts +0 -48
  33. package/src/tests/reactive-devtools.test.ts +0 -296
  34. package/src/tests/reactive-trace.test.ts +0 -102
  35. package/src/tests/reconcile-security.test.ts +0 -45
  36. package/src/tests/resource.test.ts +0 -326
  37. package/src/tests/scope.test.ts +0 -231
  38. package/src/tests/signal.test.ts +0 -368
  39. package/src/tests/store.test.ts +0 -286
  40. package/src/tests/tracking.test.ts +0 -158
  41. package/src/tests/vue-parity.test.ts +0 -191
  42. package/src/tests/watch.test.ts +0 -246
  43. package/src/tracking.ts +0 -139
  44. package/src/watch.ts +0 -68
package/src/reconcile.ts DELETED
@@ -1,118 +0,0 @@
1
- /**
2
- * reconcile — surgically diff new state into an existing createStore proxy.
3
- *
4
- * Instead of replacing the store root (which would trigger all downstream effects),
5
- * reconcile walks both the new value and the store in parallel and only calls
6
- * `.set()` on signals whose value actually changed.
7
- *
8
- * Ideal for applying API responses to a long-lived store:
9
- *
10
- * @example
11
- * const state = createStore({ user: { name: "Alice", age: 30 }, items: [] })
12
- *
13
- * // API response arrives:
14
- * reconcile({ user: { name: "Alice", age: 31 }, items: [{ id: 1 }] }, state)
15
- * // → only state.user.age signal fires (name unchanged)
16
- * // → state.items[0] is newly created
17
- *
18
- * Arrays are reconciled by index — elements at the same index are recursively
19
- * diffed rather than replaced wholesale. Excess old elements are removed.
20
- */
21
-
22
- import { isStore } from './store'
23
-
24
- type AnyObject = Record<PropertyKey, unknown>
25
-
26
- // Keys that, written through the bracket-assignment paths below, would
27
- // mutate Object.prototype (or a constructor's prototype) instead of the
28
- // store. `reconcile` is explicitly documented for applying API responses
29
- // directly (`reconcile(JSON.parse(body), store)`), and
30
- // `JSON.parse('{"__proto__":{…}}')` yields an OWN enumerable `__proto__`
31
- // key that `Object.keys` returns — the canonical prototype-pollution
32
- // merge vector. Skip these unconditionally on both the write and the
33
- // stale-key-removal pass.
34
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
35
-
36
- export function reconcile<T extends object>(source: T, target: T): void {
37
- _reconcileInner(source, target, new WeakSet())
38
- }
39
-
40
- function _reconcileInner(source: object, target: object, seen: WeakSet<object>): void {
41
- // The `seen` set is keyed on `source`, not `target` — protects against
42
- // CIRCULAR references in the source tree (avoids infinite recursion). A
43
- // consequence: DIAMOND-shaped sources (the SAME nested object referenced
44
- // from two different parent paths in `source`) only get reconciled into
45
- // their FIRST encountered position in `target`. The second occurrence is
46
- // skipped. This is intentional — reconcile assumes source is a tree, not
47
- // a DAG. Pass distinct object references (or deep-clone before reconcile)
48
- // if your source is a DAG.
49
- if (seen.has(source)) return
50
- seen.add(source)
51
- if (Array.isArray(source) && Array.isArray(target)) {
52
- _reconcileArray(source as unknown[], target as unknown[], seen)
53
- } else {
54
- _reconcileObject(source as AnyObject, target as AnyObject, seen)
55
- }
56
- }
57
-
58
- function _reconcileArray(source: unknown[], target: unknown[], seen: WeakSet<object>): void {
59
- const targetLen = target.length
60
- const sourceLen = source.length
61
-
62
- // Update / add entries
63
- for (let i = 0; i < sourceLen; i++) {
64
- const sv = source[i]
65
- const tv = (target as unknown[])[i]
66
-
67
- if (
68
- i < targetLen &&
69
- sv !== null &&
70
- typeof sv === 'object' &&
71
- tv !== null &&
72
- typeof tv === 'object'
73
- ) {
74
- // Both sides are objects — recurse
75
- _reconcileInner(sv as object, tv as object, seen)
76
- } else {
77
- // Scalar or new entry — write directly (signal will skip if equal via Object.is)
78
- ;(target as unknown[])[i] = sv
79
- }
80
- }
81
-
82
- // Trim excess entries
83
- if (targetLen > sourceLen) {
84
- target.length = sourceLen
85
- }
86
- }
87
-
88
- function _reconcileObject(source: AnyObject, target: AnyObject, seen: WeakSet<object>): void {
89
- const sourceKeys = Object.keys(source)
90
- const targetKeys = new Set(Object.keys(target))
91
-
92
- for (const key of sourceKeys) {
93
- if (DANGEROUS_KEYS.has(key)) continue
94
- const sv = source[key]
95
- const tv = target[key]
96
-
97
- if (sv !== null && typeof sv === 'object' && tv !== null && typeof tv === 'object') {
98
- if (isStore(tv)) {
99
- // Both objects — recurse into the store node
100
- _reconcileInner(sv as object, tv as object, seen)
101
- } else {
102
- // Target is a raw object (not yet proxied) — just assign
103
- target[key] = sv
104
- }
105
- } else {
106
- // Scalar: assign (store proxy's set trap skips if Object.is equal)
107
- target[key] = sv
108
- }
109
-
110
- targetKeys.delete(key)
111
- }
112
-
113
- // Remove keys that no longer exist in source
114
- for (const key of targetKeys) {
115
- if (DANGEROUS_KEYS.has(key)) continue
116
- delete target[key]
117
- }
118
- }
package/src/resource.ts DELETED
@@ -1,84 +0,0 @@
1
- import { effect } from './effect'
2
- import type { Signal } from './signal'
3
- import { signal } from './signal'
4
- import { runUntracked } from './tracking'
5
-
6
- export interface Resource<T> {
7
- /** The latest resolved value (undefined while loading or on error). */
8
- data: Signal<T | undefined>
9
- /** True while a fetch is in flight. */
10
- loading: Signal<boolean>
11
- /** The last error thrown by the fetcher, or undefined. */
12
- error: Signal<unknown>
13
- /** Re-run the fetcher with the current source value. */
14
- refetch(): void
15
- /**
16
- * Stop the source-tracking effect. After dispose(), source changes no
17
- * longer trigger fetches and any in-flight response is ignored. Idempotent.
18
- * Required for resources created outside an `EffectScope` to avoid leaking
19
- * the source-tracking effect for the lifetime of the program.
20
- */
21
- dispose(): void
22
- }
23
-
24
- /**
25
- * Async data primitive. Fetches data reactively whenever `source()` changes.
26
- *
27
- * @example
28
- * const userId = signal(1)
29
- * const user = createResource(userId, (id) => fetchUser(id))
30
- * // user.data() — the fetched user (undefined while loading)
31
- * // user.loading() — true while in flight
32
- * // user.error() — last error
33
- */
34
- export function createResource<T, P>(
35
- source: () => P,
36
- fetcher: (param: P) => Promise<T>,
37
- ): Resource<T> {
38
- const data = signal<T | undefined>(undefined)
39
- const loading = signal(false)
40
- const error = signal<unknown>(undefined)
41
- let requestId = 0
42
-
43
- const doFetch = (param: P) => {
44
- const id = ++requestId
45
- loading.set(true)
46
- error.set(undefined)
47
- fetcher(param)
48
- .then((result) => {
49
- if (id !== requestId) return
50
- data.set(result)
51
- loading.set(false)
52
- })
53
- .catch((err: unknown) => {
54
- if (id !== requestId) return
55
- error.set(err)
56
- loading.set(false)
57
- })
58
- }
59
-
60
- let disposed = false
61
- const sourceEffect = effect(() => {
62
- const param = source()
63
- runUntracked(() => doFetch(param))
64
- })
65
-
66
- return {
67
- data,
68
- loading,
69
- error,
70
- refetch() {
71
- if (disposed) return
72
- runUntracked(() => doFetch(source()))
73
- },
74
- dispose() {
75
- if (disposed) return
76
- disposed = true
77
- // Bump requestId so any pending in-flight response is treated as stale
78
- // and discarded by the .then/.catch handlers — prevents post-dispose
79
- // writes to data/loading/error.
80
- requestId++
81
- sourceEffect.dispose()
82
- },
83
- }
84
- }
package/src/scope.ts DELETED
@@ -1,123 +0,0 @@
1
- // EffectScope — auto-tracks effects created during a component's setup
2
- // and disposes them all at once when the component unmounts.
3
-
4
- export class EffectScope {
5
- private _effects: { dispose(): void }[] | null = null
6
- private _active = true
7
- private _updateHooks: (() => void)[] | null = null
8
- private _updatePending = false
9
-
10
- /** Register an effect/computed to be disposed when this scope stops. */
11
- add(e: { dispose(): void }): void {
12
- if (!this._active) return
13
- if (this._effects === null) this._effects = []
14
- this._effects.push(e)
15
- }
16
-
17
- /**
18
- * Temporarily re-activate this scope so effects created inside `fn` are
19
- * auto-tracked and will be disposed when the scope stops.
20
- * Used to ensure effects created in `onMount` callbacks belong to their
21
- * component's scope rather than leaking as global effects.
22
- */
23
- runInScope<T>(fn: () => T): T {
24
- const prev = _currentScope
25
- _currentScope = this
26
- try {
27
- return fn()
28
- } finally {
29
- _currentScope = prev
30
- }
31
- }
32
-
33
- /** Register a callback to run after any reactive update in this scope. */
34
- addUpdateHook(fn: () => void): void {
35
- // Mirror `add()`'s behavior: silently no-op when scope is stopped.
36
- // Without this, hooks pushed after `stop()` would leak into a freshly-
37
- // allocated `_updateHooks` array and never fire (because `notifyEffectRan`
38
- // checks `_active` first), giving the caller no feedback that the
39
- // registration was futile.
40
- if (!this._active) return
41
- if (this._updateHooks === null) this._updateHooks = []
42
- this._updateHooks.push(fn)
43
- }
44
-
45
- /**
46
- * Called by effects after each non-initial re-run.
47
- * Schedules onUpdate hooks via microtask so all synchronous effects settle first.
48
- */
49
- notifyEffectRan(): void {
50
- if (!this._active || !this._updateHooks || this._updateHooks.length === 0 || this._updatePending) return
51
- this._updatePending = true
52
- queueMicrotask(() => {
53
- this._updatePending = false
54
- if (!this._active || !this._updateHooks) return
55
- for (const fn of this._updateHooks) {
56
- try {
57
- fn()
58
- } catch (err) {
59
- console.error('[pyreon] onUpdate hook error:', err)
60
- }
61
- }
62
- })
63
- }
64
-
65
- /** Dispose all tracked effects. */
66
- stop(): void {
67
- if (!this._active) return
68
- if (this._effects) {
69
- for (const e of this._effects) e.dispose()
70
- }
71
- this._effects = null
72
- this._updateHooks = null
73
- this._updatePending = false
74
- this._active = false
75
- }
76
- }
77
-
78
- let _currentScope: EffectScope | null = null
79
-
80
- export function getCurrentScope(): EffectScope | null {
81
- return _currentScope
82
- }
83
-
84
- export function setCurrentScope(scope: EffectScope | null): void {
85
- _currentScope = scope
86
- }
87
-
88
- /** Create a new EffectScope. */
89
- export function effectScope(): EffectScope {
90
- return new EffectScope()
91
- }
92
-
93
- /**
94
- * Register a callback to run when the current `EffectScope` stops. Vue 3
95
- * parity. Must be called inside `scope.runInScope(fn)` — the registration
96
- * captures the ambient scope, so calling outside any scope is a no-op (with
97
- * a dev warning to surface the missing scope).
98
- *
99
- * Use to clean up resources tied to a scope's lifetime: timers, listeners,
100
- * external subscriptions. Equivalent to calling `getCurrentScope()?.add({
101
- * dispose: fn })` but with the scope capture handled.
102
- *
103
- * @example
104
- * scope.runInScope(() => {
105
- * const ws = new WebSocket(url)
106
- * onScopeDispose(() => ws.close())
107
- * // ws.close() runs when scope.stop() is called
108
- * })
109
- */
110
- export function onScopeDispose(fn: () => void): void {
111
- const scope = _currentScope
112
- if (!scope) {
113
- if (process.env.NODE_ENV !== 'production') {
114
- // oxlint-disable-next-line no-console
115
- console.warn(
116
- '[pyreon] onScopeDispose() called without an active EffectScope — callback will never run. ' +
117
- 'Wrap the call in `scope.runInScope(() => { ... })` or check `getCurrentScope()` before calling.',
118
- )
119
- }
120
- return
121
- }
122
- scope.add({ dispose: fn })
123
- }
package/src/signal.ts DELETED
@@ -1,261 +0,0 @@
1
- import { batch, enqueuePendingNotification, isBatching } from './batch'
2
- import { _notifyTraceListeners, isTracing } from './debug'
3
- import { _captureCallerLocation, _rdRecordFire, _rdRegister } from './reactive-devtools'
4
- import { _recordSignalWrite } from './reactive-trace'
5
- import { notifySubscribers, trackSubscriber } from './tracking'
6
-
7
- // Dev-time counter sink — see packages/internals/perf-harness for contract.
8
- const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
9
-
10
- export interface SignalDebugInfo<T> {
11
- /** Signal name (set via options or inferred) */
12
- name: string | undefined
13
- /** Current value (same as peek()) */
14
- value: T
15
- /** Number of active subscribers */
16
- subscriberCount: number
17
- }
18
-
19
- /**
20
- * Read-only reactive value — the common interface that both Signal and Computed satisfy.
21
- * Use this as the parameter type when a function only needs to read a reactive value.
22
- */
23
- export type ReadonlySignal<T> = () => T
24
-
25
- export interface Signal<T> {
26
- (): T
27
- /** Read the current value WITHOUT registering a reactive dependency. */
28
- peek(): T
29
- set(value: T): void
30
- update(fn: (current: T) => T): void
31
- /**
32
- * Subscribe a static listener directly — no effect overhead (no withTracking,
33
- * no cleanupEffect, no effectDeps WeakMap). Use when the dependency is fixed
34
- * and dynamic re-tracking is not needed.
35
- * Returns a disposer that removes the subscription.
36
- */
37
- subscribe(listener: () => void): () => void
38
- /**
39
- * Register a direct updater — even lighter than subscribe().
40
- * Intended for compiler-emitted DOM bindings (_bindText, _bindDirect).
41
- * Returns a disposer that removes the updater (O(1)); the live set
42
- * stays bounded under register/dispose churn.
43
- */
44
- direct(updater: () => void): () => void
45
- /**
46
- * Debug name — useful for devtools and logging. Set via the `name` option at
47
- * creation; can be reassigned at any time (`s.label = 'renamed'`) since it's
48
- * stored as a regular own property on the signal function.
49
- */
50
- label: string | undefined
51
- /** Returns a snapshot of the signal's debug info (value, name, subscriber count). */
52
- debug(): SignalDebugInfo<T>
53
- }
54
-
55
- export interface SignalOptions {
56
- /** Debug name for this signal — shows up in devtools and debug() output. */
57
- name?: string
58
- /**
59
- * @internal — source location injected by `@pyreon/vite-plugin` at build
60
- * time. When present, the runtime skips the `new Error().stack` capture
61
- * in `_rdRegister` — saves ~2.2µs per signal creation when devtools is
62
- * active. Plain user code should NOT set this; the field is opaque
63
- * (no public type) so it's not part of the public API surface.
64
- *
65
- * Shape: `{ file: string; line: number; col: number }` matching
66
- * `@pyreon/reactivity`'s `SourceLocation`.
67
- */
68
- __sourceLocation?: { file: string; line: number; col: number }
69
- }
70
-
71
- // Internal shape of a signal function — state stored as properties on the
72
- // function object so methods can be shared via assignment (not per-signal closures).
73
- interface SignalFn<T> {
74
- (): T
75
- /** @internal current value */
76
- _v: T
77
- /** @internal subscriber set (lazily allocated by trackSubscriber) */
78
- _s: Set<() => void> | null
79
- /** @internal direct updater set — compiler-emitted DOM updaters (lazily allocated) */
80
- _d: Set<() => void> | null
81
- peek(): T
82
- set(value: T): void
83
- update(fn: (current: T) => T): void
84
- subscribe(listener: () => void): () => void
85
- /** Register a direct updater — lighter than subscribe; O(1) set-based disposal. */
86
- direct(updater: () => void): () => void
87
- label: string | undefined
88
- debug(): SignalDebugInfo<T>
89
- }
90
-
91
- // Shared method implementations — defined once, assigned to every signal.
92
- // Uses `this` binding (signal methods are always called as `signal.method()`).
93
- function _peek(this: SignalFn<unknown>) {
94
- return this._v
95
- }
96
-
97
- function _set(this: SignalFn<unknown>, newValue: unknown) {
98
- if (Object.is(this._v, newValue)) return
99
- if (process.env.NODE_ENV !== 'production')
100
- _countSink.__pyreon_count__?.('reactivity.signalWrite')
101
- const prev = this._v
102
- this._v = newValue
103
- // Dev-only bounded ring buffer of recent writes — attached to error
104
- // reports so a crash carries the causal sequence of signal changes,
105
- // not just the thrown value. Tree-shaken in prod via the gate.
106
- // Deliberately separate from the `isTracing()` path below: that one
107
- // is opt-in (requires an onSignalUpdate listener) and captures a
108
- // stack (expensive); this is always-on in dev and intentionally
109
- // cheap (string preview, no stack).
110
- if (process.env.NODE_ENV !== 'production') {
111
- _recordSignalWrite(this.label, prev, newValue)
112
- _rdRecordFire(this)
113
- }
114
- if (isTracing()) {
115
- // Trace listeners are user-supplied debug code that fires on every
116
- // signal write. A throwing listener here would leave `_v` updated but
117
- // subscribers never notified (state divergence: readers see the new
118
- // value, but no effects run). Trace failures must not corrupt program
119
- // state — wrap in try/catch and route through `_userErrorHandler` so
120
- // the corruption is at least visible. Listeners are removed via the
121
- // disposer returned by `onSignalUpdate`; this catch prevents one bad
122
- // listener from breaking unrelated reactive flow.
123
- try {
124
- _notifyTraceListeners(this as unknown as Signal<unknown>, prev, newValue)
125
- } catch (err) {
126
- if (process.env.NODE_ENV !== 'production') {
127
- // oxlint-disable-next-line no-console
128
- console.error(
129
- '[pyreon] signal trace listener threw — listener is buggy. Subscribers continue uninterrupted.',
130
- err,
131
- )
132
- }
133
- }
134
- }
135
- // Auto-batch the notification chain. Without this, a diamond dependency
136
- // graph (a → b, c → d → effect) fires the apex effect TWICE per write
137
- // because subscribers cascade inline: the first path through `b` reaches
138
- // `effect`, whose read clears `d`'s dirty flag; then `c`'s notification
139
- // re-dirties `d` and re-notifies `effect`. Wrapping the notify chain in
140
- // `batch()` routes cascade-notifications through the pending Set, which
141
- // dedupes on `d.recompute` and on `effect.run`.
142
- //
143
- // The batch is synchronous — observable behaviour is unchanged for the
144
- // common case (subscribers still fire immediately after the write). Only
145
- // the dedup semantics change, which is a bug fix.
146
- //
147
- // Short-circuit when already inside a batch so we don't wrap redundantly.
148
- if (isBatching()) {
149
- if (this._d) notifyDirect(this._d)
150
- if (this._s) notifySubscribers(this._s)
151
- } else {
152
- batch(() => {
153
- if (this._d) notifyDirect(this._d)
154
- if (this._s) notifySubscribers(this._s)
155
- })
156
- }
157
- }
158
-
159
- function _update(this: SignalFn<unknown>, fn: (current: unknown) => unknown) {
160
- _set.call(this, fn(this._v))
161
- }
162
-
163
- function _subscribe(this: SignalFn<unknown>, listener: () => void): () => void {
164
- if (!this._s) this._s = new Set()
165
- this._s.add(listener)
166
- return () => this._s?.delete(listener)
167
- }
168
-
169
- /**
170
- * Register a direct updater — lighter than subscribe().
171
- * Used by compiler-emitted _bindText/_bindDirect for zero-overhead DOM bindings.
172
- *
173
- * Backed by a `Set` (same as `_s`), NOT a flat array. The array form
174
- * disposed by nulling the slot (`arr[idx] = null`) but never compacted —
175
- * so a long-lived signal (theme/locale/auth, or a signal read inside
176
- * `<For>` rows) bound by churning components accumulated one permanent
177
- * dead slot per ever-mounted binding. That is an app-lifetime memory
178
- * leak AND degrades the signal-write hot path: `notifyDirect` iterated
179
- * O(total-ever-registered), not O(live). A Set bounds growth to the live
180
- * set and keeps disposal + iteration O(live); the "Set.delete overhead"
181
- * the array form optimised for is negligible against an unbounded array.
182
- */
183
- function _directFn(this: SignalFn<unknown>, updater: () => void): () => void {
184
- if (!this._d) this._d = new Set()
185
- const set = this._d
186
- set.add(updater)
187
- return () => {
188
- set.delete(updater)
189
- }
190
- }
191
-
192
- /**
193
- * Notify direct updaters — set iteration, batch-aware. Disposed updaters
194
- * are already absent from the set (O(1) delete on disposal).
195
- */
196
- function notifyDirect(updaters: Set<() => void>): void {
197
- if (isBatching()) {
198
- for (const fn of updaters) enqueuePendingNotification(fn)
199
- } else {
200
- for (const fn of updaters) fn()
201
- }
202
- }
203
-
204
- function _debug(this: SignalFn<unknown>): SignalDebugInfo<unknown> {
205
- return {
206
- name: this.label,
207
- value: this._v,
208
- subscriberCount: this._s?.size ?? 0,
209
- }
210
- }
211
-
212
- /**
213
- * Create a reactive signal.
214
- *
215
- * Only 1 closure is allocated (the read function). State is stored as
216
- * properties on the function object (_v, _s) and methods (peek, set,
217
- * update, subscribe) are shared across all signals — not per-signal closures.
218
- */
219
- export function signal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
220
- if (process.env.NODE_ENV !== 'production')
221
- _countSink.__pyreon_count__?.('reactivity.signalCreate')
222
- // The read function is the only per-signal closure.
223
- // It doubles as the SubscriberHost (_s property) for trackSubscriber.
224
- const read = ((...args: unknown[]) => {
225
- if (process.env.NODE_ENV !== 'production' && args.length > 0) {
226
- // oxlint-disable-next-line no-console
227
- console.warn(
228
- '[Pyreon] signal() was called with an argument. ' +
229
- 'Use signal.set(value) or signal.update(fn) to write. ' +
230
- 'signal(value) only reads — the argument is ignored.',
231
- )
232
- }
233
- trackSubscriber(read as SignalFn<T>)
234
- return read._v
235
- }) as unknown as SignalFn<T>
236
-
237
- read._v = initialValue
238
- read._s = null
239
- read._d = null
240
- read.peek = _peek as () => T
241
- read.set = _set as (value: T) => void
242
- read.update = _update as (fn: (current: T) => T) => void
243
- read.subscribe = _subscribe as (listener: () => void) => () => void
244
- read.direct = _directFn as (updater: () => void) => () => void
245
- read.debug = _debug as () => SignalDebugInfo<T>
246
- read.label = options?.name
247
-
248
- if (process.env.NODE_ENV !== 'production') {
249
- // Prefer build-time-injected location (zero runtime cost) over the
250
- // ~2.2µs stack-capture fallback. @pyreon/vite-plugin's
251
- // `injectSignalLocations` rewrites `signal(0)` to
252
- // `signal(0, { __sourceLocation: {...} })` at transform time so most
253
- // dev-mode signals never pay the stack-capture cost.
254
- const loc = options?.__sourceLocation
255
- ? options.__sourceLocation
256
- : _captureCallerLocation(1)
257
- _rdRegister(read, 'signal', read, null, read.label, loc)
258
- }
259
-
260
- return read as unknown as Signal<T>
261
- }