@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,182 @@
|
|
|
1
|
+
export const enum EventSubscriberState {
|
|
2
|
+
Active = 1 << 0,
|
|
3
|
+
Disposed = 1 << 1,
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class EventSource<T> {
|
|
7
|
+
dispatchDepth = 0;
|
|
8
|
+
head: EventSubscriber<T> | null = null;
|
|
9
|
+
tail: EventSubscriber<T> | null = null;
|
|
10
|
+
pendingHead: EventSubscriber<T> | null = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EventSubscriber<T> {
|
|
14
|
+
fn: (value: T) => void;
|
|
15
|
+
next: EventSubscriber<T> | null;
|
|
16
|
+
prev: EventSubscriber<T> | null;
|
|
17
|
+
state: number;
|
|
18
|
+
unlinkNext: EventSubscriber<T> | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type EventBoundary = <T>(fn: () => T) => T;
|
|
22
|
+
|
|
23
|
+
const EVENT_SUBSCRIBER_OWNER = Symbol("EventSubscriber.owner");
|
|
24
|
+
|
|
25
|
+
type OwnedEventSubscriber<T> = EventSubscriber<T> & {
|
|
26
|
+
[EVENT_SUBSCRIBER_OWNER]?: EventSource<T> | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function getSubscriberOwner<T>(
|
|
30
|
+
subscriber: EventSubscriber<T>,
|
|
31
|
+
): EventSource<T> | null | undefined {
|
|
32
|
+
return (subscriber as OwnedEventSubscriber<T>)[EVENT_SUBSCRIBER_OWNER];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setSubscriberOwner<T>(
|
|
36
|
+
subscriber: EventSubscriber<T>,
|
|
37
|
+
source: EventSource<T> | null,
|
|
38
|
+
): void {
|
|
39
|
+
(subscriber as OwnedEventSubscriber<T>)[EVENT_SUBSCRIBER_OWNER] = source;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function identityBoundary<T>(fn: () => T): T {
|
|
43
|
+
return fn();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function appendSubscriber<T>(
|
|
47
|
+
source: EventSource<T>,
|
|
48
|
+
subscriber: EventSubscriber<T>,
|
|
49
|
+
): void {
|
|
50
|
+
if ((subscriber.state & EventSubscriberState.Active) === 0) return;
|
|
51
|
+
|
|
52
|
+
const owner = getSubscriberOwner(subscriber);
|
|
53
|
+
if (owner !== undefined && owner !== null) return;
|
|
54
|
+
|
|
55
|
+
const tail = source.tail;
|
|
56
|
+
subscriber.prev = tail;
|
|
57
|
+
subscriber.next = null;
|
|
58
|
+
subscriber.unlinkNext = null;
|
|
59
|
+
setSubscriberOwner(subscriber, source);
|
|
60
|
+
|
|
61
|
+
if (tail === null) {
|
|
62
|
+
source.head = source.tail = subscriber;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tail.next = subscriber;
|
|
67
|
+
source.tail = subscriber;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unlinkSubscriber<T>(
|
|
71
|
+
source: EventSource<T>,
|
|
72
|
+
subscriber: EventSubscriber<T>,
|
|
73
|
+
): void {
|
|
74
|
+
const prev = subscriber.prev;
|
|
75
|
+
const next = subscriber.next;
|
|
76
|
+
|
|
77
|
+
if (prev === null) source.head = next;
|
|
78
|
+
else prev.next = next;
|
|
79
|
+
|
|
80
|
+
if (next === null) source.tail = prev;
|
|
81
|
+
else next.prev = prev;
|
|
82
|
+
|
|
83
|
+
subscriber.prev = null;
|
|
84
|
+
subscriber.next = null;
|
|
85
|
+
subscriber.unlinkNext = null;
|
|
86
|
+
setSubscriberOwner(subscriber, null);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function enqueuePendingRemoval<T>(
|
|
90
|
+
source: EventSource<T>,
|
|
91
|
+
subscriber: EventSubscriber<T>,
|
|
92
|
+
): void {
|
|
93
|
+
if ((subscriber.state & EventSubscriberState.Disposed) !== 0) return;
|
|
94
|
+
|
|
95
|
+
subscriber.state |= EventSubscriberState.Disposed;
|
|
96
|
+
subscriber.unlinkNext = source.pendingHead;
|
|
97
|
+
source.pendingHead = subscriber;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function flushPendingRemovals<T>(source: EventSource<T>): void {
|
|
101
|
+
let node = source.pendingHead;
|
|
102
|
+
source.pendingHead = null;
|
|
103
|
+
|
|
104
|
+
while (node !== null) {
|
|
105
|
+
const next = node.unlinkNext;
|
|
106
|
+
node.unlinkNext = null;
|
|
107
|
+
|
|
108
|
+
unlinkSubscriber(source, node);
|
|
109
|
+
node = next;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function removeSubscriber<T>(
|
|
114
|
+
source: EventSource<T>,
|
|
115
|
+
subscriber: EventSubscriber<T>,
|
|
116
|
+
): void {
|
|
117
|
+
if (getSubscriberOwner(subscriber) !== source) return;
|
|
118
|
+
if ((subscriber.state & EventSubscriberState.Active) === 0) return;
|
|
119
|
+
|
|
120
|
+
subscriber.state &= ~EventSubscriberState.Active;
|
|
121
|
+
|
|
122
|
+
if (source.dispatchDepth !== 0) {
|
|
123
|
+
enqueuePendingRemoval(source, subscriber);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
subscriber.state |= EventSubscriberState.Disposed;
|
|
128
|
+
unlinkSubscriber(source, subscriber);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function subscribeEvent<T>(
|
|
132
|
+
source: EventSource<T>,
|
|
133
|
+
fn: (value: T) => void,
|
|
134
|
+
): () => void {
|
|
135
|
+
const subscriber: EventSubscriber<T> = {
|
|
136
|
+
fn,
|
|
137
|
+
next: null,
|
|
138
|
+
prev: null,
|
|
139
|
+
state: EventSubscriberState.Active,
|
|
140
|
+
unlinkNext: null,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
appendSubscriber(source, subscriber);
|
|
144
|
+
|
|
145
|
+
return () => {
|
|
146
|
+
removeSubscriber(source, subscriber);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function emitEvent<T>(
|
|
151
|
+
source: EventSource<T>,
|
|
152
|
+
value: T,
|
|
153
|
+
boundary: EventBoundary = identityBoundary,
|
|
154
|
+
): void {
|
|
155
|
+
boundary(() => {
|
|
156
|
+
const end = source.tail;
|
|
157
|
+
if (end === null) return;
|
|
158
|
+
|
|
159
|
+
++source.dispatchDepth;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
let node = source.head;
|
|
163
|
+
|
|
164
|
+
while (node !== null) {
|
|
165
|
+
const current = node;
|
|
166
|
+
const next = current === end ? null : current.next;
|
|
167
|
+
|
|
168
|
+
if ((current.state & EventSubscriberState.Active) !== 0) {
|
|
169
|
+
current.fn(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
node = next;
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
--source.dispatchDepth;
|
|
176
|
+
|
|
177
|
+
if (source.dispatchDepth === 0 && source.pendingHead !== null) {
|
|
178
|
+
flushPendingRemovals(source);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReactiveNode as RuntimeReactiveNode,
|
|
3
|
+
PRODUCER_INITIAL_STATE,
|
|
4
|
+
WATCHER_INITIAL_STATE,
|
|
5
|
+
CONSUMER_INITIAL_STATE,
|
|
6
|
+
} from "@reflex/runtime";
|
|
7
|
+
import type { ReactiveNode } from "@reflex/runtime";
|
|
8
|
+
import { EventSource as RuntimeEventSource } from "./event";
|
|
9
|
+
|
|
10
|
+
export const UNINITIALIZED = Symbol("UNINITIALIZED") as unknown;
|
|
11
|
+
|
|
12
|
+
export const createSignalNode = <T>(payload: T) => {
|
|
13
|
+
return new RuntimeReactiveNode<T>(
|
|
14
|
+
payload,
|
|
15
|
+
/*TODO: replace with undefined*/ null,
|
|
16
|
+
PRODUCER_INITIAL_STATE,
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const createSource = <T>(): RuntimeEventSource<T> => {
|
|
21
|
+
return new RuntimeEventSource<T>();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const createResourceStateNode = () => {
|
|
25
|
+
return new RuntimeReactiveNode<number>(
|
|
26
|
+
0,
|
|
27
|
+
/*TODO: replace with undefined*/ null,
|
|
28
|
+
PRODUCER_INITIAL_STATE,
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const createAccumulator = <T>(payload: T): ReactiveNode<T> => {
|
|
33
|
+
return new RuntimeReactiveNode(
|
|
34
|
+
payload,
|
|
35
|
+
/*TODO: replace with undefined*/ null,
|
|
36
|
+
PRODUCER_INITIAL_STATE,
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const createComputedNode = <T>(fn: () => T) => {
|
|
41
|
+
return new RuntimeReactiveNode<T>(undefined as T, fn, CONSUMER_INITIAL_STATE);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const createWatcherNode = (compute: EffectFn): ReactiveNode => {
|
|
45
|
+
return new RuntimeReactiveNode(undefined, compute, WATCHER_INITIAL_STATE);
|
|
46
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createExecutionContext, setDefaultContext } from "@reflex/runtime";
|
|
2
|
+
import type { ExecutionContext, EngineHooks } from "@reflex/runtime";
|
|
3
|
+
import { subscribeEvent } from "./event";
|
|
4
|
+
import { createSource } from "./factory";
|
|
5
|
+
import { EventDispatcher } from "../policy";
|
|
6
|
+
import type { EffectStrategy } from "../policy/scheduler";
|
|
7
|
+
import {
|
|
8
|
+
createEffectScheduler,
|
|
9
|
+
resolveEffectSchedulerMode,
|
|
10
|
+
} from "../policy/scheduler";
|
|
11
|
+
|
|
12
|
+
export interface RuntimeOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Optional low-level runtime hooks forwarded to the execution context.
|
|
15
|
+
*
|
|
16
|
+
* These hooks are composed with Reflex's scheduler integration rather than
|
|
17
|
+
* replacing it.
|
|
18
|
+
*/
|
|
19
|
+
hooks?: EngineHooks;
|
|
20
|
+
/**
|
|
21
|
+
* Controls when invalidated effects are executed.
|
|
22
|
+
*
|
|
23
|
+
* - `"flush"` queues reruns until `rt.flush()` is called.
|
|
24
|
+
* - `"sab"` keeps lazy enqueue semantics but stabilizes effects after the
|
|
25
|
+
* outermost `rt.batch()` exits.
|
|
26
|
+
* - `"eager"` flushes reruns automatically.
|
|
27
|
+
*
|
|
28
|
+
* @default "flush"
|
|
29
|
+
*/
|
|
30
|
+
effectStrategy?: EffectStrategy;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createRuntimeInfrastructure(options?: RuntimeOptions) {
|
|
34
|
+
const executionContext = createExecutionContext(options?.hooks);
|
|
35
|
+
const scheduler = createEffectScheduler(
|
|
36
|
+
resolveEffectSchedulerMode(options?.effectStrategy),
|
|
37
|
+
executionContext,
|
|
38
|
+
);
|
|
39
|
+
const dispatcher = new EventDispatcher(scheduler.batch);
|
|
40
|
+
|
|
41
|
+
executionContext.setRuntimeHooks(
|
|
42
|
+
scheduler.enqueue,
|
|
43
|
+
scheduler.runtimeNotifySettled,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
executionContext.resetState();
|
|
47
|
+
setDefaultContext(executionContext);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
scheduler,
|
|
51
|
+
dispatcher,
|
|
52
|
+
executionContext,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Push-based event stream that allows observers to subscribe to future values.
|
|
58
|
+
*
|
|
59
|
+
* `Event` is the read-only view of an event source. It does not expose
|
|
60
|
+
* mutation, only observation.
|
|
61
|
+
*
|
|
62
|
+
* @typeParam T - Event payload type.
|
|
63
|
+
*/
|
|
64
|
+
export interface Event<T> {
|
|
65
|
+
/**
|
|
66
|
+
* Registers a callback for future event deliveries.
|
|
67
|
+
*
|
|
68
|
+
* @param fn - Callback invoked for each emitted value.
|
|
69
|
+
*
|
|
70
|
+
* @returns Destructor that unsubscribes `fn`.
|
|
71
|
+
*/
|
|
72
|
+
subscribe(fn: (value: T) => void): Destructor;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Mutable event source created by `Runtime.event()`.
|
|
77
|
+
*
|
|
78
|
+
* @typeParam T - Event payload type.
|
|
79
|
+
*/
|
|
80
|
+
export interface EventSource<T> extends Event<T> {
|
|
81
|
+
/**
|
|
82
|
+
* Emits a value to current subscribers using the runtime dispatcher.
|
|
83
|
+
*
|
|
84
|
+
* Nested emits are queued after the current delivery completes, preserving
|
|
85
|
+
* FIFO ordering across sources created by the same runtime.
|
|
86
|
+
*
|
|
87
|
+
* @param value - Event payload to deliver.
|
|
88
|
+
*/
|
|
89
|
+
emit(value: T): void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Connected Reflex runtime returned by `createRuntime()`.
|
|
94
|
+
*
|
|
95
|
+
* The runtime owns the event dispatcher, effect scheduler, and execution
|
|
96
|
+
* context used by the top-level Reflex primitives.
|
|
97
|
+
*/
|
|
98
|
+
export interface Runtime {
|
|
99
|
+
batch<T>(fn: () => T): T;
|
|
100
|
+
/**
|
|
101
|
+
* Creates a new mutable event source associated with this runtime.
|
|
102
|
+
*
|
|
103
|
+
* @typeParam T - Event payload type.
|
|
104
|
+
*
|
|
105
|
+
* @returns Event source with `emit(value)` and `subscribe(fn)`.
|
|
106
|
+
*/
|
|
107
|
+
event<T>(): EventSource<T>;
|
|
108
|
+
/**
|
|
109
|
+
* Flushes queued effect re-runs immediately.
|
|
110
|
+
*
|
|
111
|
+
* In the default `"flush"` strategy, call this after writes when you want
|
|
112
|
+
* scheduled effects to observe the latest stable snapshot. In `"sab"` and
|
|
113
|
+
* `"eager"` it remains available as an explicit synchronization escape hatch.
|
|
114
|
+
*/
|
|
115
|
+
flush(): void;
|
|
116
|
+
/**
|
|
117
|
+
* Underlying execution context used by this runtime.
|
|
118
|
+
*
|
|
119
|
+
* Most application code does not need this. It exists for low-level
|
|
120
|
+
* integrations, tests, and diagnostics.
|
|
121
|
+
*/
|
|
122
|
+
readonly ctx: ExecutionContext;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates and installs the active Reflex runtime.
|
|
127
|
+
*
|
|
128
|
+
* `createRuntime` wires together an execution context, effect scheduler, and
|
|
129
|
+
* event dispatcher, then makes that context the default runtime used by the
|
|
130
|
+
* top-level Reflex primitives exported from this package.
|
|
131
|
+
*
|
|
132
|
+
* @param options - Optional runtime configuration:
|
|
133
|
+
* - `effectStrategy` controls whether invalidated effects flush on
|
|
134
|
+
* `rt.flush()`, stabilize after the outermost batch, or run automatically.
|
|
135
|
+
* - `hooks` installs low-level runtime hooks that are composed with Reflex's
|
|
136
|
+
* scheduler integration.
|
|
137
|
+
*
|
|
138
|
+
* @returns Connected runtime with event creation, flushing, and context
|
|
139
|
+
* access.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* const rt = createRuntime();
|
|
144
|
+
* const ticks = rt.event<number>();
|
|
145
|
+
* const [count, setCount] = signal(0);
|
|
146
|
+
*
|
|
147
|
+
* ticks.subscribe((value) => {
|
|
148
|
+
* setCount((current) => current + value);
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* effect(() => {
|
|
152
|
+
* console.log(count());
|
|
153
|
+
* });
|
|
154
|
+
*
|
|
155
|
+
* ticks.emit(1);
|
|
156
|
+
* rt.flush();
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* - Call this once during app startup or per test case to establish the active
|
|
161
|
+
* runtime.
|
|
162
|
+
* - Creating a new runtime replaces the previously active default context.
|
|
163
|
+
* - `rt.flush()` is primarily for scheduled effects; signals and computed
|
|
164
|
+
* reads stay current without it.
|
|
165
|
+
* - Event sources created by `rt.event()` share one dispatcher and preserve
|
|
166
|
+
* FIFO delivery order.
|
|
167
|
+
*/
|
|
168
|
+
export function createRuntime(options?: RuntimeOptions): Runtime {
|
|
169
|
+
const { scheduler, dispatcher, executionContext } =
|
|
170
|
+
createRuntimeInfrastructure(options);
|
|
171
|
+
return {
|
|
172
|
+
ctx: executionContext,
|
|
173
|
+
batch: scheduler.batch,
|
|
174
|
+
|
|
175
|
+
event<T>() {
|
|
176
|
+
const source = createSource();
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
subscribe(fn: (value: T) => void) {
|
|
180
|
+
return subscribeEvent(source, fn);
|
|
181
|
+
},
|
|
182
|
+
emit(value: T) {
|
|
183
|
+
dispatcher.emit(source, value);
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
flush: scheduler.flush,
|
|
188
|
+
};
|
|
189
|
+
}
|