@pyreon/reactivity 0.1.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/src/debug.ts ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @pyreon/reactivity debug utilities.
3
+ *
4
+ * Development-only tools for tracing signal updates, inspecting reactive
5
+ * graphs, and understanding why DOM nodes re-render.
6
+ *
7
+ * All utilities are tree-shakeable — they compile away in production builds
8
+ * when unused.
9
+ */
10
+
11
+ import type { Signal, SignalDebugInfo } from "./signal"
12
+
13
+ // ─── Signal update tracing ───────────────────────────────────────────────────
14
+
15
+ interface SignalUpdateEvent {
16
+ /** The signal that changed */
17
+ signal: Signal<unknown>
18
+ /** Signal name (from options or label) */
19
+ name: string | undefined
20
+ /** Previous value */
21
+ prev: unknown
22
+ /** New value */
23
+ next: unknown
24
+ /** Stack trace at the point of the .set() / .update() call */
25
+ stack: string
26
+ /** Timestamp */
27
+ timestamp: number
28
+ }
29
+
30
+ type SignalUpdateListener = (event: SignalUpdateEvent) => void
31
+
32
+ let _traceListeners: SignalUpdateListener[] | null = null
33
+
34
+ /**
35
+ * Register a listener that fires on every signal write.
36
+ * Returns a dispose function.
37
+ *
38
+ * @example
39
+ * const dispose = onSignalUpdate(e => {
40
+ * console.log(`${e.name ?? 'anonymous'}: ${e.prev} → ${e.next}`)
41
+ * })
42
+ */
43
+ export function onSignalUpdate(listener: SignalUpdateListener): () => void {
44
+ if (!_traceListeners) _traceListeners = []
45
+ _traceListeners.push(listener)
46
+ return () => {
47
+ if (!_traceListeners) return
48
+ _traceListeners = _traceListeners.filter((l) => l !== listener)
49
+ if (_traceListeners.length === 0) _traceListeners = null
50
+ }
51
+ }
52
+
53
+ /** @internal — called from signal.set() when tracing is active */
54
+ export function _notifyTraceListeners(sig: Signal<unknown>, prev: unknown, next: unknown): void {
55
+ if (!_traceListeners) return
56
+ const event: SignalUpdateEvent = {
57
+ signal: sig,
58
+ name: sig.label,
59
+ prev,
60
+ next,
61
+ stack: new Error().stack ?? "",
62
+ timestamp: performance.now(),
63
+ }
64
+ for (const l of _traceListeners) l(event)
65
+ }
66
+
67
+ /** Check if any trace listeners are active (fast path for signal.set) */
68
+ export function isTracing(): boolean {
69
+ return _traceListeners !== null
70
+ }
71
+
72
+ // ─── why() — trace which signal caused a re-run ──────────────────────────────
73
+
74
+ let _whyActive = false
75
+ let _whyLog: { name: string | undefined; prev: unknown; next: unknown }[] = []
76
+
77
+ /**
78
+ * Trace the next signal update. Logs which signals fire and what changed.
79
+ * Call before triggering a state change to see what updates and why.
80
+ *
81
+ * @example
82
+ * why()
83
+ * count.set(5)
84
+ * // Console: [pyreon:why] "count": 3 → 5 (2 subscribers)
85
+ */
86
+ export function why(): void {
87
+ if (_whyActive) return
88
+ _whyActive = true
89
+ _whyLog = []
90
+
91
+ const dispose = onSignalUpdate((e) => {
92
+ const _subCount = (e.signal as unknown as { _s: Set<unknown> | null })._s?.size ?? 0
93
+ const _name = e.name ? `"${e.name}"` : "(anonymous signal)"
94
+
95
+ console.log(
96
+ `[pyreon:why] ${_name}: ${JSON.stringify(e.prev)} → ${JSON.stringify(e.next)} (${_subCount} subscriber${_subCount === 1 ? "" : "s"})`,
97
+ )
98
+ _whyLog.push({ name: e.name, prev: e.prev, next: e.next })
99
+ })
100
+
101
+ // Auto-dispose after the current microtask (captures the synchronous batch)
102
+ queueMicrotask(() => {
103
+ dispose()
104
+ if (_whyLog.length === 0) {
105
+ console.log("[pyreon:why] No signal updates detected")
106
+ }
107
+ _whyActive = false
108
+ _whyLog = []
109
+ })
110
+ }
111
+
112
+ // ─── inspectSignal — rich console output ─────────────────────────────────────
113
+
114
+ /**
115
+ * Print a signal's current state to the console in a readable format.
116
+ *
117
+ * @example
118
+ * const count = signal(42, { name: "count" })
119
+ * inspectSignal(count)
120
+ * // Console:
121
+ * // 🔍 Signal "count"
122
+ * // value: 42
123
+ * // subscribers: 3
124
+ */
125
+ export function inspectSignal<T>(sig: Signal<T>): SignalDebugInfo<T> {
126
+ const info = sig.debug()
127
+
128
+ console.group(`🔍 Signal ${info.name ? `"${info.name}"` : "(anonymous)"}`)
129
+ console.log("value:", info.value)
130
+ console.log("subscribers:", info.subscriberCount)
131
+ console.groupEnd()
132
+
133
+ return info
134
+ }
package/src/effect.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { getCurrentScope } from "./scope"
2
+ import { cleanupEffect, setDepsCollector, withTracking } from "./tracking"
3
+
4
+ export interface Effect {
5
+ dispose(): void
6
+ }
7
+
8
+ // Global error handler — called for unhandled errors thrown inside effects.
9
+ // Defaults to console.error so silent failures are never swallowed.
10
+ let _errorHandler: (err: unknown) => void = (err) => {
11
+ console.error("[pyreon] Unhandled effect error:", err)
12
+ }
13
+
14
+ export function setErrorHandler(fn: (err: unknown) => void): void {
15
+ _errorHandler = fn
16
+ }
17
+
18
+ // biome-ignore lint/suspicious/noConfusingVoidType: void is intentional — callbacks that return nothing must be assignable
19
+ export function effect(fn: () => (() => void) | void): Effect {
20
+ // Capture the scope at creation time — remains correct during future re-runs
21
+ // even after setCurrentScope(null) has been called post-setup.
22
+ const scope = getCurrentScope()
23
+ let disposed = false
24
+ let isFirstRun = true
25
+ let cleanup: (() => void) | undefined
26
+
27
+ const runCleanup = () => {
28
+ if (typeof cleanup === "function") {
29
+ try {
30
+ cleanup()
31
+ } catch (err) {
32
+ _errorHandler(err)
33
+ }
34
+ cleanup = undefined
35
+ }
36
+ }
37
+
38
+ const run = () => {
39
+ if (disposed) return
40
+ // Run previous cleanup before re-running
41
+ runCleanup()
42
+ // Clean up previous subscriptions before re-running (dynamic dep tracking)
43
+ cleanupEffect(run)
44
+ try {
45
+ cleanup = withTracking(run, fn) || undefined
46
+ } catch (err) {
47
+ _errorHandler(err)
48
+ }
49
+ // Notify scope after each reactive re-run (not the initial synchronous run)
50
+ // so onUpdate hooks fire after the DOM has settled.
51
+ if (!isFirstRun) scope?.notifyEffectRan()
52
+ isFirstRun = false
53
+ }
54
+
55
+ run()
56
+
57
+ const e: Effect = {
58
+ dispose() {
59
+ runCleanup()
60
+ disposed = true
61
+ cleanupEffect(run)
62
+ },
63
+ }
64
+
65
+ // Auto-register with the active EffectScope (if any)
66
+ getCurrentScope()?.add(e)
67
+
68
+ return e
69
+ }
70
+
71
+ /**
72
+ * Lightweight effect for DOM render bindings.
73
+ *
74
+ * Differences from `effect()`:
75
+ * - No EffectScope registration (caller owns the dispose lifecycle)
76
+ * - No error handler (errors propagate naturally)
77
+ * - No onUpdate notification
78
+ * - Deps stored in a local array instead of the global WeakMap — faster
79
+ * creation and disposal (~200ns saved per effect vs WeakMap path)
80
+ *
81
+ * Returns a dispose function (not an Effect object — saves 1 allocation).
82
+ */
83
+ /**
84
+ * Static-dep binding — compiler helper for template expressions.
85
+ *
86
+ * Like renderEffect but assumes dependencies never change (true for all
87
+ * compiler-emitted template bindings like `_tpl()` text/attribute updates).
88
+ *
89
+ * Tracks dependencies only on the first run. Re-runs skip cleanup, re-tracking,
90
+ * and tracking context save/restore entirely — just calls `fn()` directly.
91
+ *
92
+ * Per re-run savings vs renderEffect:
93
+ * - No deps iteration + Set.delete (cleanup)
94
+ * - No setDepsCollector + withTracking (re-registration)
95
+ * - Signal reads hit `if (activeEffect)` null check → instant return
96
+ */
97
+ export function _bind(fn: () => void): () => void {
98
+ const deps: Set<() => void>[] = []
99
+ let disposed = false
100
+
101
+ const run = () => {
102
+ if (disposed) return
103
+ fn()
104
+ }
105
+
106
+ // First run: track deps so we know what to unsubscribe on dispose
107
+ setDepsCollector(deps)
108
+ withTracking(run, fn)
109
+ setDepsCollector(null)
110
+
111
+ const dispose = () => {
112
+ if (disposed) return
113
+ disposed = true
114
+ for (const s of deps) s.delete(run)
115
+ deps.length = 0
116
+ }
117
+
118
+ // Auto-register with scope so template bindings are disposed during teardown
119
+ getCurrentScope()?.add({ dispose })
120
+
121
+ return dispose
122
+ }
123
+
124
+ export function renderEffect(fn: () => void): () => void {
125
+ const deps: Set<() => void>[] = []
126
+ let disposed = false
127
+
128
+ const run = () => {
129
+ if (disposed) return
130
+ // Clean up old subscriptions
131
+ for (const s of deps) s.delete(run)
132
+ deps.length = 0
133
+ // Track with fast collector — pushes to our local deps array
134
+ setDepsCollector(deps)
135
+ withTracking(run, fn)
136
+ setDepsCollector(null)
137
+ }
138
+
139
+ run()
140
+
141
+ const dispose = () => {
142
+ if (disposed) return
143
+ disposed = true
144
+ for (const s of deps) s.delete(run)
145
+ deps.length = 0
146
+ }
147
+
148
+ // Auto-register with scope so render effects are disposed during teardown
149
+ getCurrentScope()?.add({ dispose })
150
+
151
+ return dispose
152
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
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 { _bind, type Effect, effect, renderEffect, setErrorHandler } from "./effect"
9
+ export { reconcile } from "./reconcile"
10
+ export { createResource, type Resource } from "./resource"
11
+ export { EffectScope, effectScope, getCurrentScope, setCurrentScope } from "./scope"
12
+ export { type Signal, type SignalDebugInfo, type SignalOptions, signal } from "./signal"
13
+ export { createStore, isStore } from "./store"
14
+ export { runUntracked } from "./tracking"
15
+ export { type WatchOptions, watch } from "./watch"
@@ -0,0 +1,98 @@
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
+ export function reconcile<T extends object>(source: T, target: T): void {
27
+ _reconcileInner(source, target, new WeakSet())
28
+ }
29
+
30
+ function _reconcileInner(source: object, target: object, seen: WeakSet<object>): void {
31
+ if (seen.has(source)) return // circular reference — stop recursion
32
+ seen.add(source)
33
+ if (Array.isArray(source) && Array.isArray(target)) {
34
+ _reconcileArray(source as unknown[], target as unknown[], seen)
35
+ } else {
36
+ _reconcileObject(source as AnyObject, target as AnyObject, seen)
37
+ }
38
+ }
39
+
40
+ function _reconcileArray(source: unknown[], target: unknown[], seen: WeakSet<object>): void {
41
+ const targetLen = target.length
42
+ const sourceLen = source.length
43
+
44
+ // Update / add entries
45
+ for (let i = 0; i < sourceLen; i++) {
46
+ const sv = source[i]
47
+ const tv = (target as unknown[])[i]
48
+
49
+ if (
50
+ i < targetLen &&
51
+ sv !== null &&
52
+ typeof sv === "object" &&
53
+ tv !== null &&
54
+ typeof tv === "object"
55
+ ) {
56
+ // Both sides are objects — recurse
57
+ _reconcileInner(sv as object, tv as object, seen)
58
+ } else {
59
+ // Scalar or new entry — write directly (signal will skip if equal via Object.is)
60
+ ;(target as unknown[])[i] = sv
61
+ }
62
+ }
63
+
64
+ // Trim excess entries
65
+ if (targetLen > sourceLen) {
66
+ target.length = sourceLen
67
+ }
68
+ }
69
+
70
+ function _reconcileObject(source: AnyObject, target: AnyObject, seen: WeakSet<object>): void {
71
+ const sourceKeys = Object.keys(source)
72
+ const targetKeys = new Set(Object.keys(target))
73
+
74
+ for (const key of sourceKeys) {
75
+ const sv = source[key]
76
+ const tv = target[key]
77
+
78
+ if (sv !== null && typeof sv === "object" && tv !== null && typeof tv === "object") {
79
+ if (isStore(tv)) {
80
+ // Both objects — recurse into the store node
81
+ _reconcileInner(sv as object, tv as object, seen)
82
+ } else {
83
+ // Target is a raw object (not yet proxied) — just assign
84
+ target[key] = sv
85
+ }
86
+ } else {
87
+ // Scalar: assign (store proxy's set trap skips if Object.is equal)
88
+ target[key] = sv
89
+ }
90
+
91
+ targetKeys.delete(key)
92
+ }
93
+
94
+ // Remove keys that no longer exist in source
95
+ for (const key of targetKeys) {
96
+ delete target[key]
97
+ }
98
+ }
@@ -0,0 +1,66 @@
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
+
17
+ /**
18
+ * Async data primitive. Fetches data reactively whenever `source()` changes.
19
+ *
20
+ * @example
21
+ * const userId = signal(1)
22
+ * const user = createResource(userId, (id) => fetchUser(id))
23
+ * // user.data() — the fetched user (undefined while loading)
24
+ * // user.loading() — true while in flight
25
+ * // user.error() — last error
26
+ */
27
+ export function createResource<T, P>(
28
+ source: () => P,
29
+ fetcher: (param: P) => Promise<T>,
30
+ ): Resource<T> {
31
+ const data = signal<T | undefined>(undefined)
32
+ const loading = signal(false)
33
+ const error = signal<unknown>(undefined)
34
+ let requestId = 0
35
+
36
+ const doFetch = (param: P) => {
37
+ const id = ++requestId
38
+ loading.set(true)
39
+ error.set(undefined)
40
+ fetcher(param)
41
+ .then((result) => {
42
+ if (id !== requestId) return
43
+ data.set(result)
44
+ loading.set(false)
45
+ })
46
+ .catch((err: unknown) => {
47
+ if (id !== requestId) return
48
+ error.set(err)
49
+ loading.set(false)
50
+ })
51
+ }
52
+
53
+ effect(() => {
54
+ const param = source()
55
+ runUntracked(() => doFetch(param))
56
+ })
57
+
58
+ return {
59
+ data,
60
+ loading,
61
+ error,
62
+ refetch() {
63
+ runUntracked(() => doFetch(source()))
64
+ },
65
+ }
66
+ }
package/src/scope.ts ADDED
@@ -0,0 +1,80 @@
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 }[] = []
6
+ private _active = true
7
+ private _updateHooks: (() => void)[] = []
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) this._effects.push(e)
13
+ }
14
+
15
+ /**
16
+ * Temporarily re-activate this scope so effects created inside `fn` are
17
+ * auto-tracked and will be disposed when the scope stops.
18
+ * Used to ensure effects created in `onMount` callbacks belong to their
19
+ * component's scope rather than leaking as global effects.
20
+ */
21
+ runInScope<T>(fn: () => T): T {
22
+ const prev = _currentScope
23
+ _currentScope = this
24
+ try {
25
+ return fn()
26
+ } finally {
27
+ _currentScope = prev
28
+ }
29
+ }
30
+
31
+ /** Register a callback to run after any reactive update in this scope. */
32
+ addUpdateHook(fn: () => void): void {
33
+ this._updateHooks.push(fn)
34
+ }
35
+
36
+ /**
37
+ * Called by effects after each non-initial re-run.
38
+ * Schedules onUpdate hooks via microtask so all synchronous effects settle first.
39
+ */
40
+ notifyEffectRan(): void {
41
+ if (!this._active || this._updateHooks.length === 0 || this._updatePending) return
42
+ this._updatePending = true
43
+ queueMicrotask(() => {
44
+ this._updatePending = false
45
+ if (!this._active) return
46
+ for (const fn of this._updateHooks) {
47
+ try {
48
+ fn()
49
+ } catch (err) {
50
+ console.error("[pyreon] onUpdate hook error:", err)
51
+ }
52
+ }
53
+ })
54
+ }
55
+
56
+ /** Dispose all tracked effects. */
57
+ stop(): void {
58
+ if (!this._active) return
59
+ for (const e of this._effects) e.dispose()
60
+ this._effects = []
61
+ this._updateHooks = []
62
+ this._updatePending = false
63
+ this._active = false
64
+ }
65
+ }
66
+
67
+ let _currentScope: EffectScope | null = null
68
+
69
+ export function getCurrentScope(): EffectScope | null {
70
+ return _currentScope
71
+ }
72
+
73
+ export function setCurrentScope(scope: EffectScope | null): void {
74
+ _currentScope = scope
75
+ }
76
+
77
+ /** Create a new EffectScope. */
78
+ export function effectScope(): EffectScope {
79
+ return new EffectScope()
80
+ }
package/src/signal.ts ADDED
@@ -0,0 +1,125 @@
1
+ import { _notifyTraceListeners, isTracing } from "./debug"
2
+ import { notifySubscribers, trackSubscriber } from "./tracking"
3
+
4
+ export interface SignalDebugInfo<T> {
5
+ /** Signal name (set via options or inferred) */
6
+ name: string | undefined
7
+ /** Current value (same as peek()) */
8
+ value: T
9
+ /** Number of active subscribers */
10
+ subscriberCount: number
11
+ }
12
+
13
+ export interface Signal<T> {
14
+ (): T
15
+ /** Read the current value WITHOUT registering a reactive dependency. */
16
+ peek(): T
17
+ set(value: T): void
18
+ update(fn: (current: T) => T): void
19
+ /**
20
+ * Subscribe a static listener directly — no effect overhead (no withTracking,
21
+ * no cleanupEffect, no effectDeps WeakMap). Use when the dependency is fixed
22
+ * and dynamic re-tracking is not needed.
23
+ * Returns a disposer that removes the subscription.
24
+ */
25
+ subscribe(listener: () => void): () => void
26
+ /** Debug name — useful for devtools and logging. */
27
+ label: string | undefined
28
+ /** Returns a snapshot of the signal's debug info (value, name, subscriber count). */
29
+ debug(): SignalDebugInfo<T>
30
+ }
31
+
32
+ export interface SignalOptions {
33
+ /** Debug name for this signal — shows up in devtools and debug() output. */
34
+ name?: string
35
+ }
36
+
37
+ // Internal shape of a signal function — state stored as properties on the
38
+ // function object so methods can be shared via assignment (not per-signal closures).
39
+ interface SignalFn<T> {
40
+ (): T
41
+ /** @internal current value */
42
+ _v: T
43
+ /** @internal subscriber set (lazily allocated by trackSubscriber) */
44
+ _s: Set<() => void> | null
45
+ /** @internal debug name */
46
+ _n: string | undefined
47
+ peek(): T
48
+ set(value: T): void
49
+ update(fn: (current: T) => T): void
50
+ subscribe(listener: () => void): () => void
51
+ label: string | undefined
52
+ debug(): SignalDebugInfo<T>
53
+ }
54
+
55
+ // Shared method implementations — defined once, assigned to every signal.
56
+ // Uses `this` binding (signal methods are always called as `signal.method()`).
57
+ function _peek(this: SignalFn<unknown>) {
58
+ return this._v
59
+ }
60
+
61
+ function _set(this: SignalFn<unknown>, newValue: unknown) {
62
+ if (Object.is(this._v, newValue)) return
63
+ const prev = this._v
64
+ this._v = newValue
65
+ if (isTracing()) _notifyTraceListeners(this as unknown as Signal<unknown>, prev, newValue)
66
+ if (this._s) notifySubscribers(this._s)
67
+ }
68
+
69
+ function _update(this: SignalFn<unknown>, fn: (current: unknown) => unknown) {
70
+ _set.call(this, fn(this._v))
71
+ }
72
+
73
+ function _subscribe(this: SignalFn<unknown>, listener: () => void): () => void {
74
+ if (!this._s) this._s = new Set()
75
+ this._s.add(listener)
76
+ return () => this._s?.delete(listener)
77
+ }
78
+
79
+ function _debug(this: SignalFn<unknown>): SignalDebugInfo<unknown> {
80
+ return {
81
+ name: this._n,
82
+ value: this._v,
83
+ subscriberCount: this._s?.size ?? 0,
84
+ }
85
+ }
86
+
87
+ // label getter/setter — maps to _n for devtools-friendly access
88
+ const _labelDescriptor: PropertyDescriptor = {
89
+ get(this: SignalFn<unknown>) {
90
+ return this._n
91
+ },
92
+ set(this: SignalFn<unknown>, v: string | undefined) {
93
+ this._n = v
94
+ },
95
+ enumerable: false,
96
+ configurable: true,
97
+ }
98
+
99
+ /**
100
+ * Create a reactive signal.
101
+ *
102
+ * Only 1 closure is allocated (the read function). State is stored as
103
+ * properties on the function object (_v, _s) and methods (peek, set,
104
+ * update, subscribe) are shared across all signals — not per-signal closures.
105
+ */
106
+ export function signal<T>(initialValue: T, options?: SignalOptions): Signal<T> {
107
+ // The read function is the only per-signal closure.
108
+ // It doubles as the SubscriberHost (_s property) for trackSubscriber.
109
+ const read = (() => {
110
+ trackSubscriber(read as SignalFn<T>)
111
+ return read._v
112
+ }) as unknown as SignalFn<T>
113
+
114
+ read._v = initialValue
115
+ read._s = null
116
+ read._n = options?.name
117
+ read.peek = _peek as () => T
118
+ read.set = _set as (value: T) => void
119
+ read.update = _update as (fn: (current: T) => T) => void
120
+ read.subscribe = _subscribe as (listener: () => void) => () => void
121
+ read.debug = _debug as () => SignalDebugInfo<T>
122
+ Object.defineProperty(read, "label", _labelDescriptor)
123
+
124
+ return read as unknown as Signal<T>
125
+ }