@volynets/reflex 0.1.1 → 0.1.2
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/README.md +16 -15
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/unstable/index.cjs +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/unstable/index.js +1 -1
- package/dist/globals.d.ts +333 -32
- package/dist/unstable/index.d.ts +195 -0
- package/package.json +10 -1
- package/src/api/derived.ts +90 -0
- package/src/api/effect.ts +120 -0
- package/src/api/event.ts +257 -0
- package/src/api/index.ts +4 -0
- package/src/api/signal.ts +68 -0
- package/src/globals.d.ts +169 -0
- package/src/index.ts +6 -0
- package/src/infra/event.ts +182 -0
- package/src/infra/factory.ts +46 -0
- package/src/infra/index.ts +2 -0
- package/src/infra/runtime.ts +189 -0
- package/src/policy/SCHEDULER_SEMANTICS.md +389 -0
- package/src/policy/event_dispatcher.ts +39 -0
- package/src/policy/index.ts +1 -0
- package/src/policy/scheduler/index.ts +6 -0
- package/src/policy/scheduler/scheduler.constants.ts +17 -0
- package/src/policy/scheduler/scheduler.core.ts +165 -0
- package/src/policy/scheduler/scheduler.infra.ts +37 -0
- package/src/policy/scheduler/scheduler.queue.ts +74 -0
- package/src/policy/scheduler/scheduler.types.ts +54 -0
- package/src/policy/scheduler/variants/index.ts +3 -0
- package/src/policy/scheduler/variants/scheduler.eager.ts +46 -0
- package/src/policy/scheduler/variants/scheduler.flush.ts +35 -0
- package/src/policy/scheduler/variants/scheduler.sab.ts +37 -0
- package/src/unstable/index.ts +4 -0
- package/src/unstable/resource.ts +505 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readConsumerEager, readConsumerLazy } from "@reflex/runtime";
|
|
2
|
+
import { createComputedNode } from "../infra";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a lazy derived accessor.
|
|
6
|
+
*
|
|
7
|
+
* `computed` runs `fn` only when the returned accessor is read. During that
|
|
8
|
+
* evaluation it tracks the reactive values that `fn` touches, caches the
|
|
9
|
+
* result, and reuses the cached value for subsequent clean reads.
|
|
10
|
+
*
|
|
11
|
+
* @typeParam T - Derived value type.
|
|
12
|
+
*
|
|
13
|
+
* @param fn - Pure synchronous computation that derives a value from reactive
|
|
14
|
+
* reads.
|
|
15
|
+
*
|
|
16
|
+
* @returns Tracked accessor that returns the latest derived value.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* createRuntime();
|
|
21
|
+
*
|
|
22
|
+
* const [count, setCount] = signal(1);
|
|
23
|
+
* const doubled = computed(() => count() * 2);
|
|
24
|
+
*
|
|
25
|
+
* console.log(doubled()); // 2
|
|
26
|
+
*
|
|
27
|
+
* setCount(2);
|
|
28
|
+
*
|
|
29
|
+
* console.log(doubled()); // 4
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* - `fn` does not run until the first read.
|
|
34
|
+
* - Dependencies are tracked dynamically on each execution, so branch changes
|
|
35
|
+
* automatically update the dependency set.
|
|
36
|
+
* - Dirty computeds recompute on demand when read again.
|
|
37
|
+
* - Reading a computed does not require `rt.flush()`; `flush()` is only for
|
|
38
|
+
* scheduled effects.
|
|
39
|
+
* - Keep `fn` pure and synchronous.
|
|
40
|
+
*
|
|
41
|
+
* @see memo
|
|
42
|
+
* @see effect
|
|
43
|
+
*/
|
|
44
|
+
export function computed<T>(fn: () => T): Accessor<T> {
|
|
45
|
+
const node = createComputedNode(fn);
|
|
46
|
+
return readConsumerLazy.bind(null, node) as Accessor<T>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a computed accessor and warms it eagerly once.
|
|
51
|
+
*
|
|
52
|
+
* `memo` has the same dependency tracking and caching semantics as
|
|
53
|
+
* `computed()`, but it performs one eager read immediately after creation.
|
|
54
|
+
* This is useful when you want the initial value materialized up front while
|
|
55
|
+
* still interacting with a normal accessor afterward.
|
|
56
|
+
*
|
|
57
|
+
* @typeParam T - Derived value type.
|
|
58
|
+
*
|
|
59
|
+
* @param fn - Pure synchronous computation that derives a value from reactive
|
|
60
|
+
* reads.
|
|
61
|
+
*
|
|
62
|
+
* @returns Tracked accessor that returns the latest memoized value.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* createRuntime();
|
|
67
|
+
*
|
|
68
|
+
* const [price, setPrice] = signal(100);
|
|
69
|
+
* const total = memo(() => price() * 1.2);
|
|
70
|
+
*
|
|
71
|
+
* console.log(total()); // 120
|
|
72
|
+
*
|
|
73
|
+
* setPrice(200);
|
|
74
|
+
*
|
|
75
|
+
* console.log(total()); // 240
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* - `memo(fn)` is equivalent to `computed(fn)` plus one immediate warm-up read.
|
|
80
|
+
* - After the warm-up, clean reads reuse the cached value exactly like
|
|
81
|
+
* `computed()`.
|
|
82
|
+
* - Later invalidations still follow normal computed semantics.
|
|
83
|
+
*
|
|
84
|
+
* @see computed
|
|
85
|
+
*/
|
|
86
|
+
export function memo<T>(fn: () => T): Accessor<T> {
|
|
87
|
+
const node = createComputedNode(fn);
|
|
88
|
+
readConsumerEager(node);
|
|
89
|
+
return readConsumerLazy.bind(null, node) as Accessor<T>;
|
|
90
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
disposeWatcher,
|
|
3
|
+
getDefaultContext,
|
|
4
|
+
ReactiveNodeState,
|
|
5
|
+
runWatcher,
|
|
6
|
+
} from "@reflex/runtime";
|
|
7
|
+
import type { ReactiveNode } from "@reflex/runtime";
|
|
8
|
+
import type { UNINITIALIZED } from "../infra/factory";
|
|
9
|
+
import { createWatcherNode } from "../infra/factory";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Marks an effect watcher node as scheduled.
|
|
13
|
+
*
|
|
14
|
+
* This is a low-level helper used by scheduler integrations and tests to set
|
|
15
|
+
* the runtime's scheduled flag on a watcher node.
|
|
16
|
+
*/
|
|
17
|
+
export function effectScheduled(
|
|
18
|
+
node: ReactiveNode<typeof UNINITIALIZED | Destructor>,
|
|
19
|
+
) {
|
|
20
|
+
node.state |= ReactiveNodeState.Scheduled;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clears the scheduled flag from an effect watcher node.
|
|
25
|
+
*
|
|
26
|
+
* This is a low-level helper used by scheduler integrations and tests to mark
|
|
27
|
+
* a watcher as no longer queued for execution.
|
|
28
|
+
*/
|
|
29
|
+
export function effectUnscheduled(
|
|
30
|
+
node: ReactiveNode<typeof UNINITIALIZED | Destructor>,
|
|
31
|
+
) {
|
|
32
|
+
node.state &= ~ReactiveNodeState.Scheduled;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Callback used to register cleanup produced by nested helpers with an
|
|
37
|
+
* enclosing effect scope.
|
|
38
|
+
*/
|
|
39
|
+
export type EffectCleanupRegistrar = (cleanup: Destructor) => void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Runs `fn` with a temporary cleanup registrar installed on the active runtime
|
|
43
|
+
* context.
|
|
44
|
+
*
|
|
45
|
+
* Helpers that allocate resources during `fn` can forward their teardown to
|
|
46
|
+
* `registrar`, allowing the surrounding effect or integration to dispose them
|
|
47
|
+
* automatically.
|
|
48
|
+
*
|
|
49
|
+
* @typeParam T - Return type of `fn`.
|
|
50
|
+
*
|
|
51
|
+
* @param registrar - Cleanup registrar to expose during `fn`, or `null` to run
|
|
52
|
+
* without one.
|
|
53
|
+
* @param fn - Callback executed with the temporary registrar installed.
|
|
54
|
+
*
|
|
55
|
+
* @returns The value returned by `fn`.
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* - The registrar is scoped to the duration of `fn`.
|
|
59
|
+
* - This is a low-level integration helper. Most application code should use
|
|
60
|
+
* `effect()` directly.
|
|
61
|
+
*/
|
|
62
|
+
export function withEffectCleanupRegistrar<T>(
|
|
63
|
+
registrar: EffectCleanupRegistrar | null,
|
|
64
|
+
fn: () => T,
|
|
65
|
+
): T {
|
|
66
|
+
const context = getDefaultContext();
|
|
67
|
+
return context.withCleanupRegistrar(registrar, fn);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a reactive effect.
|
|
72
|
+
*
|
|
73
|
+
* `effect` runs `fn` immediately, tracks any reactive values read during that
|
|
74
|
+
* run, and schedules re-execution when those dependencies change.
|
|
75
|
+
*
|
|
76
|
+
* @param fn - Effect body. It may return a cleanup function that runs before
|
|
77
|
+
* the next execution and when the effect is disposed.
|
|
78
|
+
*
|
|
79
|
+
* @returns Destructor that disposes the effect and runs the latest cleanup, if
|
|
80
|
+
* present.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const rt = createRuntime();
|
|
85
|
+
* const [count, setCount] = signal(0);
|
|
86
|
+
*
|
|
87
|
+
* const stop = effect(() => {
|
|
88
|
+
* console.log(count());
|
|
89
|
+
* });
|
|
90
|
+
*
|
|
91
|
+
* setCount(1);
|
|
92
|
+
* rt.flush();
|
|
93
|
+
*
|
|
94
|
+
* stop();
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* - The first run happens synchronously during `effect()` creation.
|
|
99
|
+
* - With the default runtime strategy, later re-runs are queued until
|
|
100
|
+
* `rt.flush()`.
|
|
101
|
+
* - With `createRuntime({ effectStrategy: "sab" })`, invalidations stay lazy
|
|
102
|
+
* during propagation but auto-deliver after the outermost `rt.batch()`.
|
|
103
|
+
* - With `createRuntime({ effectStrategy: "eager" })`, invalidations flush
|
|
104
|
+
* automatically.
|
|
105
|
+
* - Reads performed inside cleanup do not become dependencies of the next run.
|
|
106
|
+
* - Disposing the returned scope prevents future re-runs.
|
|
107
|
+
*
|
|
108
|
+
* @see createRuntime
|
|
109
|
+
* @see computed
|
|
110
|
+
* @see memo
|
|
111
|
+
*/
|
|
112
|
+
export function effect(fn: EffectFn): Destructor {
|
|
113
|
+
const context = getDefaultContext();
|
|
114
|
+
const node = createWatcherNode(fn);
|
|
115
|
+
runWatcher(node);
|
|
116
|
+
|
|
117
|
+
const dispose = disposeWatcher.bind(null, node) as Destructor;
|
|
118
|
+
context.registerWatcherCleanup(dispose);
|
|
119
|
+
return dispose;
|
|
120
|
+
}
|
package/src/api/event.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
disposeNodeEvent,
|
|
3
|
+
isDisposedNode,
|
|
4
|
+
readProducer,
|
|
5
|
+
writeProducer,
|
|
6
|
+
} from "@reflex/runtime";
|
|
7
|
+
import type { Event } from "../infra";
|
|
8
|
+
import { createAccumulator } from "../infra";
|
|
9
|
+
|
|
10
|
+
type EventValue<E extends Event<unknown>> =
|
|
11
|
+
E extends Event<infer T> ? T : never;
|
|
12
|
+
|
|
13
|
+
function createEvent<T>(
|
|
14
|
+
subscribe: (fn: (value: T) => void) => Destructor,
|
|
15
|
+
): Event<T> {
|
|
16
|
+
return { subscribe };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Subscribes to the first value from `source`, then unsubscribes automatically.
|
|
21
|
+
*
|
|
22
|
+
* The subscription is disposed before `fn` runs, so nested emits triggered from
|
|
23
|
+
* inside `fn` will not deliver a second time to the same callback.
|
|
24
|
+
*/
|
|
25
|
+
export function subscribeOnce<T>(
|
|
26
|
+
source: Event<T>,
|
|
27
|
+
fn: (value: T) => void,
|
|
28
|
+
): Destructor {
|
|
29
|
+
let active = true;
|
|
30
|
+
let unsubscribe: Destructor | undefined;
|
|
31
|
+
let unsubscribePending = false;
|
|
32
|
+
|
|
33
|
+
const dispose = () => {
|
|
34
|
+
if (!active) return;
|
|
35
|
+
|
|
36
|
+
active = false;
|
|
37
|
+
|
|
38
|
+
const stop = unsubscribe;
|
|
39
|
+
if (stop === undefined) {
|
|
40
|
+
unsubscribePending = true;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
unsubscribe = undefined;
|
|
45
|
+
stop();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
unsubscribe = source.subscribe((value) => {
|
|
49
|
+
if (!active) return;
|
|
50
|
+
|
|
51
|
+
dispose();
|
|
52
|
+
fn(value);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (unsubscribePending) {
|
|
56
|
+
const stop = unsubscribe;
|
|
57
|
+
unsubscribe = undefined;
|
|
58
|
+
stop?.();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return dispose;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Projects each event value from `source` into a new event stream.
|
|
66
|
+
*/
|
|
67
|
+
export function map<T, U>(
|
|
68
|
+
source: Event<T>,
|
|
69
|
+
project: (value: T) => U,
|
|
70
|
+
): Event<U> {
|
|
71
|
+
return createEvent((fn) =>
|
|
72
|
+
source.subscribe((value) => {
|
|
73
|
+
fn(project(value));
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Forwards only the values from `source` that satisfy `predicate`.
|
|
80
|
+
*/
|
|
81
|
+
export function filter<T, S extends T>(
|
|
82
|
+
source: Event<T>,
|
|
83
|
+
predicate: (value: T) => value is S,
|
|
84
|
+
): Event<S>;
|
|
85
|
+
export function filter<T>(
|
|
86
|
+
source: Event<T>,
|
|
87
|
+
predicate: (value: T) => boolean,
|
|
88
|
+
): Event<T>;
|
|
89
|
+
export function filter<T>(
|
|
90
|
+
source: Event<T>,
|
|
91
|
+
predicate: (value: T) => boolean,
|
|
92
|
+
): Event<T> {
|
|
93
|
+
return createEvent((fn) =>
|
|
94
|
+
source.subscribe((value) => {
|
|
95
|
+
if (predicate(value)) {
|
|
96
|
+
fn(value);
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Merges multiple event sources into one event stream.
|
|
104
|
+
*
|
|
105
|
+
* The resulting event preserves the delivery order defined by the upstream
|
|
106
|
+
* sources and their runtime dispatcher.
|
|
107
|
+
*/
|
|
108
|
+
export function merge<const Sources extends readonly Event<unknown>[]>(
|
|
109
|
+
...sources: Sources
|
|
110
|
+
): Event<EventValue<Sources[number]>> {
|
|
111
|
+
return createEvent((fn) => {
|
|
112
|
+
const unsubscribers = sources.map((source) =>
|
|
113
|
+
source.subscribe((value) => {
|
|
114
|
+
fn(value as EventValue<Sources[number]>);
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return () => {
|
|
119
|
+
for (const unsubscribe of unsubscribers) {
|
|
120
|
+
unsubscribe();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates an accumulator derived from an event stream.
|
|
128
|
+
*
|
|
129
|
+
* `scan` listens to `source` and applies `reducer` to the current accumulated
|
|
130
|
+
* state and each incoming event value. The result becomes the next stored state.
|
|
131
|
+
*
|
|
132
|
+
* It is analogous to `Array.prototype.reduce`, but for a stream of events over time.
|
|
133
|
+
*
|
|
134
|
+
* @typeParam T - Event payload type.
|
|
135
|
+
* @typeParam A - Accumulator state type.
|
|
136
|
+
*
|
|
137
|
+
* @param source - Event source to subscribe to.
|
|
138
|
+
* @param seed - Initial accumulator state used before the first event arrives.
|
|
139
|
+
* @param reducer - Pure function that receives the current accumulated state and
|
|
140
|
+
* the next event value, and returns the next accumulated state.
|
|
141
|
+
*
|
|
142
|
+
* @returns A tuple:
|
|
143
|
+
* - `read` - accessor that returns the current accumulated state.
|
|
144
|
+
* - `dispose` - destructor that unsubscribes from the source and disposes the internal node.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* const rt = createRuntime();
|
|
149
|
+
* const increments = rt.event<number>();
|
|
150
|
+
*
|
|
151
|
+
* const [total, dispose] = scan(increments, 0, (acc, value) => acc + value);
|
|
152
|
+
*
|
|
153
|
+
* increments.emit(1);
|
|
154
|
+
* increments.emit(2);
|
|
155
|
+
*
|
|
156
|
+
* console.log(total()); // 3
|
|
157
|
+
*
|
|
158
|
+
* dispose();
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @remarks
|
|
162
|
+
* - `seed` is used as the initial state until the first event is delivered.
|
|
163
|
+
* - `reducer` should be pure and synchronous.
|
|
164
|
+
* - `reducer` should derive the next state only from the previous accumulated
|
|
165
|
+
* state and the current event value.
|
|
166
|
+
* - The accumulated value is updated only in response to `source` events.
|
|
167
|
+
* - Do not read signals, computeds, or other reactive values inside
|
|
168
|
+
* `reducer`. `scan` does not track reactive dependencies read there.
|
|
169
|
+
* - If you need to combine event-driven state with reactive state, first
|
|
170
|
+
* derive the accumulator with `scan`, then combine it outside via
|
|
171
|
+
* `computed()`.
|
|
172
|
+
* - To stop receiving updates and release subscriptions, call `dispose`.
|
|
173
|
+
*
|
|
174
|
+
* @see hold
|
|
175
|
+
*/
|
|
176
|
+
export function scan<T, A>(
|
|
177
|
+
source: Event<T>,
|
|
178
|
+
seed: A,
|
|
179
|
+
reducer: (acc: A, value: T) => A,
|
|
180
|
+
): [read: Accessor<A>, dispose: Destructor] {
|
|
181
|
+
return createScan(source, seed, reducer);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Stores the latest value emitted by an event source.
|
|
186
|
+
*
|
|
187
|
+
* `hold` is a specialized form of {@link scan} that replaces the current state
|
|
188
|
+
* with each new event value.
|
|
189
|
+
*
|
|
190
|
+
* @typeParam T - Event payload type.
|
|
191
|
+
*
|
|
192
|
+
* @param source - Event source to subscribe to.
|
|
193
|
+
* @param initial - Initial value returned before the first event arrives.
|
|
194
|
+
*
|
|
195
|
+
* @returns A tuple:
|
|
196
|
+
* - `read` - accessor that returns the latest observed event value.
|
|
197
|
+
* - `dispose` - destructor that unsubscribes from the source and disposes the internal node.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* const rt = createRuntime();
|
|
202
|
+
* const updates = rt.event<string>();
|
|
203
|
+
*
|
|
204
|
+
* const [latest, dispose] = hold(updates, "idle");
|
|
205
|
+
*
|
|
206
|
+
* console.log(latest()); // "idle"
|
|
207
|
+
*
|
|
208
|
+
* updates.emit("loading");
|
|
209
|
+
* console.log(latest()); // "loading"
|
|
210
|
+
*
|
|
211
|
+
* updates.emit("done");
|
|
212
|
+
* console.log(latest()); // "done"
|
|
213
|
+
*
|
|
214
|
+
* dispose();
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* @remarks
|
|
218
|
+
* - `initial` is returned until the first event is delivered.
|
|
219
|
+
* - Equivalent to:
|
|
220
|
+
* `scan(source, initial, (_, value) => value)`
|
|
221
|
+
*
|
|
222
|
+
* @see scan
|
|
223
|
+
*/
|
|
224
|
+
export function hold<T>(
|
|
225
|
+
source: Event<T>,
|
|
226
|
+
initial: T,
|
|
227
|
+
): [read: Accessor<T>, dispose: Destructor] {
|
|
228
|
+
return createScan(source, initial, (_, value) => value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createScan<T, A>(
|
|
232
|
+
source: Event<T>,
|
|
233
|
+
seed: A,
|
|
234
|
+
reducer: (acc: A, value: T) => A,
|
|
235
|
+
): [read: Accessor<A>, dispose: Destructor] {
|
|
236
|
+
const node = createAccumulator(seed);
|
|
237
|
+
let current = seed;
|
|
238
|
+
const accessor = () => (isDisposedNode(node) ? current : readProducer(node));
|
|
239
|
+
|
|
240
|
+
let unsubscribe: Destructor | undefined = source.subscribe((value: T) => {
|
|
241
|
+
/* c8 ignore start -- disposal unsubscribes before a queued delivery can reach this callback */
|
|
242
|
+
if (isDisposedNode(node)) return;
|
|
243
|
+
/* c8 ignore stop */
|
|
244
|
+
current = reducer(current, value);
|
|
245
|
+
writeProducer(node, current);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
function dispose(): void {
|
|
249
|
+
disposeNodeEvent(node);
|
|
250
|
+
|
|
251
|
+
const stop = unsubscribe;
|
|
252
|
+
unsubscribe = undefined;
|
|
253
|
+
stop?.();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return [accessor, dispose];
|
|
257
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readProducer, writeProducer } from "@reflex/runtime";
|
|
2
|
+
import { createSignalNode } from "../infra";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates writable reactive state.
|
|
6
|
+
*
|
|
7
|
+
* `signal` returns a tuple containing a tracked read accessor and a setter.
|
|
8
|
+
* Reading the accessor inside `computed()`, `memo()`, or `effect()` registers
|
|
9
|
+
* a dependency. Writing through the setter updates the stored value
|
|
10
|
+
* synchronously and invalidates downstream reactive consumers only when the
|
|
11
|
+
* value actually changes.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam T - Signal value type.
|
|
14
|
+
*
|
|
15
|
+
* @param initialValue - Initial signal value returned until a later write
|
|
16
|
+
* replaces it.
|
|
17
|
+
* @param options - Optional development diagnostics. `options.name` is used
|
|
18
|
+
* only in development builds when formatting setter error messages.
|
|
19
|
+
*
|
|
20
|
+
* @returns A readonly tuple:
|
|
21
|
+
* - `value` - tracked accessor that returns the current signal value.
|
|
22
|
+
* - `setValue` - setter that accepts either a direct value or an updater
|
|
23
|
+
* function receiving the previous value. The setter returns the committed
|
|
24
|
+
* next value.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* createRuntime();
|
|
29
|
+
*
|
|
30
|
+
* const [count, setCount] = signal(0);
|
|
31
|
+
*
|
|
32
|
+
* console.log(count()); // 0
|
|
33
|
+
*
|
|
34
|
+
* setCount(1);
|
|
35
|
+
* setCount((prev) => prev + 1);
|
|
36
|
+
*
|
|
37
|
+
* console.log(count()); // 2
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* - Reads are synchronous and always return the latest committed value.
|
|
42
|
+
* - Same-value writes do not invalidate downstream computed values or effects.
|
|
43
|
+
* - Calling `setValue()` with no argument is only valid when `T` includes
|
|
44
|
+
* `undefined`.
|
|
45
|
+
* - In typical app code, call `createRuntime()` during setup before building
|
|
46
|
+
* the rest of the reactive graph.
|
|
47
|
+
*
|
|
48
|
+
* @see computed
|
|
49
|
+
* @see memo
|
|
50
|
+
* @see effect
|
|
51
|
+
*/
|
|
52
|
+
export function signal<T>(initialValue: T): readonly [Accessor<T>, Setter<T>] {
|
|
53
|
+
const node = createSignalNode(initialValue);
|
|
54
|
+
|
|
55
|
+
function set(input: SetInput<T>) {
|
|
56
|
+
const payload = node.payload;
|
|
57
|
+
const next =
|
|
58
|
+
typeof input === "function"
|
|
59
|
+
? (input as (prev: T) => T)(payload as T)
|
|
60
|
+
: input;
|
|
61
|
+
writeProducer(node, next);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
readProducer.bind(null, node) as Accessor<T>,
|
|
66
|
+
set as Setter<T>,
|
|
67
|
+
] as const;
|
|
68
|
+
}
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
declare const __DEV__: boolean;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cleanup function returned from an effect.
|
|
5
|
+
*/
|
|
6
|
+
type Destructor = () => void;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Effect callback.
|
|
10
|
+
* May return a cleanup function.
|
|
11
|
+
*/
|
|
12
|
+
type EffectFn = () => void | Destructor;
|
|
13
|
+
|
|
14
|
+
type AnyFn = (...args: unknown[]) => unknown;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Direct value that can be assigned via `.set(value)`.
|
|
18
|
+
* Function values are excluded to avoid ambiguity with updater functions.
|
|
19
|
+
*/
|
|
20
|
+
type DirectValue<T> = Exclude<T, AnyFn>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Functional updater.
|
|
24
|
+
*/
|
|
25
|
+
type Updater<T> = (prev: T) => T;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Accepted input for writable reactive values.
|
|
29
|
+
*/
|
|
30
|
+
type SetInput<T> = DirectValue<T> | Updater<T>;
|
|
31
|
+
|
|
32
|
+
interface RequiredSetter<T> {
|
|
33
|
+
(value: SetInput<T>): T;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface OptionalSetter<T> {
|
|
37
|
+
(): T;
|
|
38
|
+
(value: SetInput<T>): T;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* If T includes undefined, calling `set()` with no arguments is allowed.
|
|
43
|
+
*/
|
|
44
|
+
type Setter<T> = undefined extends T ? OptionalSetter<T> : RequiredSetter<T>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Nominal brand helper for semantically distinct reactive primitives.
|
|
48
|
+
*/
|
|
49
|
+
interface Brand<K extends string> {
|
|
50
|
+
readonly __brand?: K;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Callable tracked read.
|
|
55
|
+
*/
|
|
56
|
+
interface Accessor<T> {
|
|
57
|
+
(): T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Property-based read.
|
|
62
|
+
*/
|
|
63
|
+
interface ValueReadable<T> {
|
|
64
|
+
readonly value: T;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Untracked read.
|
|
69
|
+
*/
|
|
70
|
+
interface Peekable<T> {
|
|
71
|
+
peek(): T;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Writable capability.
|
|
76
|
+
*/
|
|
77
|
+
interface Writable<T> {
|
|
78
|
+
set: Setter<T>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface Disposable {
|
|
82
|
+
(): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Standard readable reactive value.
|
|
87
|
+
*/
|
|
88
|
+
interface Readable<T> extends Accessor<T>, ValueReadable<T> {}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Readable value with untracked read.
|
|
92
|
+
*/
|
|
93
|
+
interface PeekableReadable<T> extends Readable<T>, Peekable<T> {}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Writable signal-like value.
|
|
97
|
+
*/
|
|
98
|
+
interface WritableReadable<T> extends Readable<T>, Writable<T> {}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Writable signal-like value with untracked read.
|
|
102
|
+
*/
|
|
103
|
+
interface PeekableWritableReadable<T>
|
|
104
|
+
extends WritableReadable<T>,
|
|
105
|
+
Peekable<T> {}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Mutable signal.
|
|
109
|
+
*/
|
|
110
|
+
interface Signal<T> extends PeekableWritableReadable<T>, Brand<"signal"> {}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Computed reactive value.
|
|
114
|
+
*/
|
|
115
|
+
interface Computed<T> extends PeekableReadable<T>, Brand<"computed"> {}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Memoized derived value.
|
|
119
|
+
*/
|
|
120
|
+
interface Memo<T> extends PeekableReadable<T>, Brand<"memo"> {}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Derived reactive value.
|
|
124
|
+
*/
|
|
125
|
+
interface Derived<T> extends PeekableReadable<T>, Brand<"derived"> {}
|
|
126
|
+
|
|
127
|
+
interface Effect<T> extends Readable<T>, Brand<"effect">, Disposable {}
|
|
128
|
+
|
|
129
|
+
interface Scan<T> extends Readable<T>, Brand<"scan">, Disposable {}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Push-based realtime source.
|
|
133
|
+
*/
|
|
134
|
+
interface Realtime<T> extends PeekableWritableReadable<T>, Brand<"realtime"> {
|
|
135
|
+
subscribe(cb: () => void): () => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Async iterable stream source.
|
|
140
|
+
*/
|
|
141
|
+
interface Stream<T> extends PeekableWritableReadable<T>, Brand<"stream"> {
|
|
142
|
+
[Symbol.asyncIterator](): AsyncIterator<T>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Common readonly view over reactive values.
|
|
147
|
+
*/
|
|
148
|
+
type ReadableLike<T> =
|
|
149
|
+
| Signal<T>
|
|
150
|
+
| Computed<T>
|
|
151
|
+
| Memo<T>
|
|
152
|
+
| Derived<T>
|
|
153
|
+
| Realtime<T>
|
|
154
|
+
| Stream<T>;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Common writable view over reactive values.
|
|
158
|
+
*/
|
|
159
|
+
type WritableLike<T> = Signal<T> | Realtime<T> | Stream<T>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract value type from a reactive value.
|
|
163
|
+
*/
|
|
164
|
+
type ValueOf<T> =
|
|
165
|
+
T extends Accessor<infer V>
|
|
166
|
+
? V
|
|
167
|
+
: T extends ValueReadable<infer V>
|
|
168
|
+
? V
|
|
169
|
+
: never;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
|
2
|
+
/// <reference path="./globals.d.ts" />
|
|
3
|
+
|
|
4
|
+
export { signal, computed, memo, effect, withEffectCleanupRegistrar } from "./api";
|
|
5
|
+
export { subscribeOnce, map, filter, merge, scan, hold } from "./api";
|
|
6
|
+
export { createRuntime } from "./infra";
|