@reckona/mreact-reactive-core 0.0.65 → 0.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/batch.ts +33 -0
- package/src/cell.ts +65 -0
- package/src/cleanup-scope.ts +19 -0
- package/src/computed.ts +232 -0
- package/src/devtools.ts +39 -0
- package/src/effect.ts +107 -0
- package/src/index.ts +7 -0
- package/src/internal.ts +8 -0
- package/src/runtime-state-public.ts +1 -0
- package/src/runtime-state.ts +17 -0
- package/src/scheduler.ts +130 -0
- package/src/state.ts +40 -0
- package/src/testing.ts +11 -0
- package/src/tracking.ts +166 -0
- package/src/types.ts +7 -0
- package/src/untrack.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reckona/mreact-reactive-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.67",
|
|
4
4
|
"description": "Fine-grained reactive primitives used across mreact.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jsx",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"dist/**/*.js",
|
|
25
25
|
"dist/**/*.js.map",
|
|
26
26
|
"dist/**/*.d.ts",
|
|
27
|
-
"dist/**/*.d.ts.map"
|
|
27
|
+
"dist/**/*.d.ts.map",
|
|
28
|
+
"src/**/*"
|
|
28
29
|
],
|
|
29
30
|
"type": "module",
|
|
30
31
|
"sideEffects": false,
|
package/src/batch.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { runtimeState } from "./state.js";
|
|
2
|
+
import { schedulePendingFlush } from "./scheduler.js";
|
|
3
|
+
import { flushPendingComputed } from "./tracking.js";
|
|
4
|
+
|
|
5
|
+
export function batch<T>(fn: () => T): T {
|
|
6
|
+
runtimeState.batchDepth += 1;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
return fn();
|
|
10
|
+
} finally {
|
|
11
|
+
runtimeState.batchDepth -= 1;
|
|
12
|
+
|
|
13
|
+
if (runtimeState.batchDepth === 0) {
|
|
14
|
+
flushPendingComputed();
|
|
15
|
+
schedulePendingFlush();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function batchAsync<T>(fn: () => Promise<T> | T): Promise<T> {
|
|
21
|
+
runtimeState.batchDepth += 1;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return await fn();
|
|
25
|
+
} finally {
|
|
26
|
+
runtimeState.batchDepth -= 1;
|
|
27
|
+
|
|
28
|
+
if (runtimeState.batchDepth === 0) {
|
|
29
|
+
flushPendingComputed();
|
|
30
|
+
schedulePendingFlush();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cell.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Cell } from "./types.js";
|
|
2
|
+
import type { Source } from "./state.js";
|
|
3
|
+
import { notifySubscribers, trackSource } from "./tracking.js";
|
|
4
|
+
|
|
5
|
+
declare const __MREACT_CLIENT_DEVTOOLS__: boolean | undefined;
|
|
6
|
+
|
|
7
|
+
export function cell<T>(initial: T): Cell<T> {
|
|
8
|
+
let current = initial;
|
|
9
|
+
const source: Source = {
|
|
10
|
+
subscribers: new Set(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
get(): T {
|
|
15
|
+
trackSource(source);
|
|
16
|
+
return current;
|
|
17
|
+
},
|
|
18
|
+
set(next: T | ((prev: T) => T)): void {
|
|
19
|
+
const resolved = typeof next === "function" ? (next as (prev: T) => T)(current) : next;
|
|
20
|
+
|
|
21
|
+
if (Object.is(current, resolved)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
typeof __MREACT_CLIENT_DEVTOOLS__ !== "undefined" &&
|
|
27
|
+
__MREACT_CLIENT_DEVTOOLS__ === false
|
|
28
|
+
) {
|
|
29
|
+
current = resolved;
|
|
30
|
+
} else {
|
|
31
|
+
const devtools = (
|
|
32
|
+
globalThis as typeof globalThis & {
|
|
33
|
+
__mreactDevtools?:
|
|
34
|
+
| { emit?: (event: Record<string, unknown>) => void }
|
|
35
|
+
| undefined;
|
|
36
|
+
}
|
|
37
|
+
).__mreactDevtools;
|
|
38
|
+
const emit = devtools?.emit;
|
|
39
|
+
|
|
40
|
+
if (typeof emit !== "function") {
|
|
41
|
+
current = resolved;
|
|
42
|
+
} else {
|
|
43
|
+
const previous = current;
|
|
44
|
+
current = resolved;
|
|
45
|
+
emit.call(devtools, {
|
|
46
|
+
package: "@reckona/mreact-reactive-core",
|
|
47
|
+
previous,
|
|
48
|
+
subscribers: source.subscribers.size,
|
|
49
|
+
timestamp: Date.now(),
|
|
50
|
+
type: "reactive:cell:set",
|
|
51
|
+
value: resolved,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const singleSubscriber = source.singleSubscriber;
|
|
56
|
+
if (
|
|
57
|
+
singleSubscriber !== undefined &&
|
|
58
|
+
(singleSubscriber.disposed || singleSubscriber.queued)
|
|
59
|
+
) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
notifySubscribers(source);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { runtimeState } from "./state.js";
|
|
2
|
+
|
|
3
|
+
export function withCleanupScope<T>(
|
|
4
|
+
owner: (dispose: () => void) => void,
|
|
5
|
+
run: () => T,
|
|
6
|
+
): T {
|
|
7
|
+
const previousOwner = runtimeState.cleanupOwner;
|
|
8
|
+
runtimeState.cleanupOwner = owner;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
return run();
|
|
12
|
+
} finally {
|
|
13
|
+
runtimeState.cleanupOwner = previousOwner;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function registerCleanup(dispose: () => void): void {
|
|
18
|
+
runtimeState.cleanupOwner?.(dispose);
|
|
19
|
+
}
|
package/src/computed.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { ReactiveComputation, Source } from "./state.js";
|
|
2
|
+
import { schedulePendingFlush } from "./scheduler.js";
|
|
3
|
+
import { runtimeState } from "./state.js";
|
|
4
|
+
import { cleanupDeps, notifySubscribers, trackSource } from "./tracking.js";
|
|
5
|
+
import type { ReadonlyCell } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export type ComputedEquality<T> = (previous: T, next: T) => boolean;
|
|
8
|
+
|
|
9
|
+
export interface ComputedOptions<T> {
|
|
10
|
+
equals?: ComputedEquality<T> | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function computed<T>(
|
|
14
|
+
fn: () => T,
|
|
15
|
+
options?: ComputedOptions<T> | ComputedEquality<T>,
|
|
16
|
+
): ReadonlyCell<T> {
|
|
17
|
+
let hasValue = false;
|
|
18
|
+
let value: T;
|
|
19
|
+
let dirty = true;
|
|
20
|
+
const equals = typeof options === "function" ? options : (options?.equals ?? Object.is);
|
|
21
|
+
|
|
22
|
+
const source: Source = {
|
|
23
|
+
subscribers: new Set(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const computation: ReactiveComputation = {
|
|
27
|
+
id: runtimeState.nextComputationId,
|
|
28
|
+
deps: new Set(),
|
|
29
|
+
disposed: false,
|
|
30
|
+
queued: false,
|
|
31
|
+
markDirty() {
|
|
32
|
+
if (dirty) {
|
|
33
|
+
if (source.subscribers.size === 0 || computation.queued) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dirty = true;
|
|
39
|
+
|
|
40
|
+
if (source.subscribers.size > 0) {
|
|
41
|
+
if (runtimeState.notificationDepth > 0) {
|
|
42
|
+
computation.queued = true;
|
|
43
|
+
runtimeState.pendingComputed.add(computation);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
publishIfChanged();
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
run() {
|
|
51
|
+
publishIfChanged();
|
|
52
|
+
},
|
|
53
|
+
trackSource(source) {
|
|
54
|
+
trackComputedSource(source, computation);
|
|
55
|
+
},
|
|
56
|
+
dispose() {
|
|
57
|
+
if (computation.disposed) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
computation.disposed = true;
|
|
62
|
+
cleanupDeps(computation);
|
|
63
|
+
source.subscribers.clear();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
runtimeState.nextComputationId += 1;
|
|
68
|
+
|
|
69
|
+
function publishIfChanged(): void {
|
|
70
|
+
const previousHasValue = hasValue;
|
|
71
|
+
const previousValue = value;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const nextValue = recompute();
|
|
75
|
+
|
|
76
|
+
if (!previousHasValue || !equals(previousValue, nextValue)) {
|
|
77
|
+
notifySubscribers(source);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
runtimeState.batchDepth += 1;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
notifySubscribers(source);
|
|
84
|
+
} finally {
|
|
85
|
+
runtimeState.batchDepth -= 1;
|
|
86
|
+
|
|
87
|
+
if (runtimeState.batchDepth === 0) {
|
|
88
|
+
schedulePendingFlush();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function recompute(): T {
|
|
95
|
+
if (!dirty && hasValue) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const previousTracker = runtimeState.activeTracker;
|
|
100
|
+
const previousDepsSize = computation.deps.size;
|
|
101
|
+
const nextTrackingVersion = (computation.trackingVersion ?? 0) + 1;
|
|
102
|
+
|
|
103
|
+
computation.trackingAddedDeps = [];
|
|
104
|
+
computation.trackingCount = 0;
|
|
105
|
+
computation.trackingVersion = nextTrackingVersion;
|
|
106
|
+
runtimeState.activeTracker = computation;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const nextValue = fn();
|
|
110
|
+
|
|
111
|
+
const addedDeps = computation.trackingAddedDeps;
|
|
112
|
+
const trackedCount = computation.trackingCount ?? 0;
|
|
113
|
+
|
|
114
|
+
if (trackedCount !== previousDepsSize || (addedDeps?.length ?? 0) > 0) {
|
|
115
|
+
cleanupUntrackedDeps(computation, nextTrackingVersion);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
value = nextValue;
|
|
119
|
+
hasValue = true;
|
|
120
|
+
dirty = false;
|
|
121
|
+
|
|
122
|
+
return value;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
cleanupAddedDeps(computation);
|
|
125
|
+
dirty = true;
|
|
126
|
+
|
|
127
|
+
throw error;
|
|
128
|
+
} finally {
|
|
129
|
+
computation.trackingAddedDeps = undefined;
|
|
130
|
+
computation.trackingCount = undefined;
|
|
131
|
+
computation.trackingVersion = undefined;
|
|
132
|
+
runtimeState.activeTracker = previousTracker;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
get(): T {
|
|
138
|
+
trackSource(source);
|
|
139
|
+
return recompute();
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function trackComputedSource(
|
|
145
|
+
source: Source,
|
|
146
|
+
computation: ReactiveComputation,
|
|
147
|
+
): void {
|
|
148
|
+
const trackingVersion = computation.trackingVersion;
|
|
149
|
+
|
|
150
|
+
if (trackingVersion === undefined) {
|
|
151
|
+
trackSource(source);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (source.trackedBy === computation && source.trackedVersion === trackingVersion) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
source.trackedBy = computation;
|
|
160
|
+
source.trackedVersion = trackingVersion;
|
|
161
|
+
computation.trackingCount = (computation.trackingCount ?? 0) + 1;
|
|
162
|
+
|
|
163
|
+
if (computation.deps.has(source)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const previousSize = source.subscribers.size;
|
|
168
|
+
source.subscribers.add(computation);
|
|
169
|
+
computation.deps.add(source);
|
|
170
|
+
computation.trackingAddedDeps?.push(source);
|
|
171
|
+
|
|
172
|
+
if (previousSize === 0) {
|
|
173
|
+
source.singleSubscriber = computation;
|
|
174
|
+
} else if (source.subscribers.size > 1) {
|
|
175
|
+
source.singleSubscriber = undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function cleanupUntrackedDeps(
|
|
180
|
+
computation: ReactiveComputation,
|
|
181
|
+
trackingVersion: number,
|
|
182
|
+
): void {
|
|
183
|
+
for (const dep of computation.deps) {
|
|
184
|
+
if (dep.trackedBy === computation && dep.trackedVersion === trackingVersion) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!dep.subscribers.delete(computation)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (dep.trackedBy === computation) {
|
|
193
|
+
dep.trackedBy = undefined;
|
|
194
|
+
dep.trackedVersion = undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
computation.deps.delete(dep);
|
|
198
|
+
|
|
199
|
+
if (dep.subscribers.size === 0) {
|
|
200
|
+
dep.singleSubscriber = undefined;
|
|
201
|
+
} else if (dep.subscribers.size === 1) {
|
|
202
|
+
dep.singleSubscriber = dep.subscribers.values().next().value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cleanupAddedDeps(computation: ReactiveComputation): void {
|
|
208
|
+
const addedDeps = computation.trackingAddedDeps;
|
|
209
|
+
|
|
210
|
+
if (addedDeps === undefined) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const dep of addedDeps) {
|
|
215
|
+
if (!dep.subscribers.delete(computation)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (dep.trackedBy === computation) {
|
|
220
|
+
dep.trackedBy = undefined;
|
|
221
|
+
dep.trackedVersion = undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
computation.deps.delete(dep);
|
|
225
|
+
|
|
226
|
+
if (dep.subscribers.size === 0) {
|
|
227
|
+
dep.singleSubscriber = undefined;
|
|
228
|
+
} else if (dep.subscribers.size === 1) {
|
|
229
|
+
dep.singleSubscriber = dep.subscribers.values().next().value;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type DevtoolsEmitter = (event: Record<string, unknown>) => void;
|
|
2
|
+
|
|
3
|
+
export function emitReactiveDevtoolsEvent(event: Record<string, unknown>): void {
|
|
4
|
+
const devtools = currentDevtools();
|
|
5
|
+
const emit = devtools?.emit;
|
|
6
|
+
|
|
7
|
+
if (typeof emit !== "function") {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
emit.call(devtools, {
|
|
12
|
+
package: "@reckona/mreact-reactive-core",
|
|
13
|
+
timestamp: Date.now(),
|
|
14
|
+
...event,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hasReactiveDevtoolsEmitter(): boolean {
|
|
19
|
+
return typeof currentDevtools()?.emit === "function";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function currentDevtoolsEmitter(): DevtoolsEmitter | undefined {
|
|
23
|
+
const devtools = currentDevtools();
|
|
24
|
+
const emit = devtools?.emit;
|
|
25
|
+
|
|
26
|
+
return typeof emit === "function" ? emit.bind(devtools) : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function currentDevtools():
|
|
30
|
+
| { emit?: DevtoolsEmitter | undefined }
|
|
31
|
+
| undefined {
|
|
32
|
+
const devtools = (
|
|
33
|
+
globalThis as typeof globalThis & {
|
|
34
|
+
__mreactDevtools?: { emit?: DevtoolsEmitter } | undefined;
|
|
35
|
+
}
|
|
36
|
+
).__mreactDevtools;
|
|
37
|
+
|
|
38
|
+
return devtools;
|
|
39
|
+
}
|
package/src/effect.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { queueComputation } from "./scheduler.js";
|
|
2
|
+
import { currentDevtoolsEmitter } from "./devtools.js";
|
|
3
|
+
import { registerCleanup } from "./cleanup-scope.js";
|
|
4
|
+
import { runtimeState, type ReactiveComputation } from "./state.js";
|
|
5
|
+
import { cleanupDeps } from "./tracking.js";
|
|
6
|
+
|
|
7
|
+
declare const __MREACT_CLIENT_DEVTOOLS__: boolean | undefined;
|
|
8
|
+
|
|
9
|
+
export function effect(fn: () => void | (() => void)): () => void {
|
|
10
|
+
let cleanup: (() => void) | undefined;
|
|
11
|
+
|
|
12
|
+
const computation: ReactiveComputation = {
|
|
13
|
+
id: runtimeState.nextComputationId,
|
|
14
|
+
deps: new Set(),
|
|
15
|
+
disposed: false,
|
|
16
|
+
queued: false,
|
|
17
|
+
markDirty() {
|
|
18
|
+
queueComputation(computation);
|
|
19
|
+
},
|
|
20
|
+
run() {
|
|
21
|
+
if (computation.disposed) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const previousTracker = runtimeState.activeTracker;
|
|
26
|
+
|
|
27
|
+
if (cleanup !== undefined) {
|
|
28
|
+
const currentCleanup = cleanup;
|
|
29
|
+
currentCleanup();
|
|
30
|
+
cleanup = undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
cleanupDeps(computation);
|
|
34
|
+
runtimeState.activeTracker = computation;
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
typeof __MREACT_CLIENT_DEVTOOLS__ !== "undefined" &&
|
|
38
|
+
__MREACT_CLIENT_DEVTOOLS__ === false
|
|
39
|
+
) {
|
|
40
|
+
try {
|
|
41
|
+
const result = fn();
|
|
42
|
+
cleanup = typeof result === "function" ? result : undefined;
|
|
43
|
+
} finally {
|
|
44
|
+
runtimeState.activeTracker = previousTracker;
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const emit = currentDevtoolsEmitter();
|
|
50
|
+
const startedAt = emit === undefined ? 0 : performanceNow();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = fn();
|
|
54
|
+
cleanup = typeof result === "function" ? result : undefined;
|
|
55
|
+
} finally {
|
|
56
|
+
runtimeState.activeTracker = previousTracker;
|
|
57
|
+
if (emit !== undefined) {
|
|
58
|
+
emit({
|
|
59
|
+
durationMs: performanceNow() - startedAt,
|
|
60
|
+
id: computation.id,
|
|
61
|
+
package: "@reckona/mreact-reactive-core",
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
type: "reactive:effect:run",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
dispose() {
|
|
69
|
+
if (computation.disposed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
computation.disposed = true;
|
|
74
|
+
cleanupDeps(computation);
|
|
75
|
+
|
|
76
|
+
if (cleanup !== undefined) {
|
|
77
|
+
const currentCleanup = cleanup;
|
|
78
|
+
cleanup = undefined;
|
|
79
|
+
currentCleanup();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
runtimeState.nextComputationId += 1;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
computation.run();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
computation.disposed = true;
|
|
90
|
+
cleanupDeps(computation);
|
|
91
|
+
|
|
92
|
+
if (cleanup !== undefined) {
|
|
93
|
+
const currentCleanup = cleanup;
|
|
94
|
+
cleanup = undefined;
|
|
95
|
+
currentCleanup();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
registerCleanup(computation.dispose);
|
|
102
|
+
return computation.dispose;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function performanceNow(): number {
|
|
106
|
+
return typeof performance === "undefined" ? Date.now() : performance.now();
|
|
107
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { Cell, ReadonlyCell } from "./types.js";
|
|
2
|
+
export type { ComputedEquality, ComputedOptions } from "./computed.js";
|
|
3
|
+
export { batch, batchAsync } from "./batch.js";
|
|
4
|
+
export { cell } from "./cell.js";
|
|
5
|
+
export { computed } from "./computed.js";
|
|
6
|
+
export { effect } from "./effect.js";
|
|
7
|
+
export { untrack } from "./untrack.js";
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { Scheduler } from "./scheduler.js";
|
|
2
|
+
export { withCleanupScope } from "./cleanup-scope.js";
|
|
3
|
+
export {
|
|
4
|
+
flushQueuedComputations,
|
|
5
|
+
schedulePendingFlush,
|
|
6
|
+
setScheduler,
|
|
7
|
+
} from "./scheduler.js";
|
|
8
|
+
export { getGlobalRuntimeState } from "./runtime-state.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getGlobalRuntimeState } from "./runtime-state.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type RuntimeGlobal = typeof globalThis & Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export function getGlobalRuntimeState<TState extends object>(
|
|
4
|
+
key: string,
|
|
5
|
+
create: () => TState,
|
|
6
|
+
): TState {
|
|
7
|
+
const global = globalThis as RuntimeGlobal;
|
|
8
|
+
const existing = global[key];
|
|
9
|
+
|
|
10
|
+
if (existing !== undefined) {
|
|
11
|
+
return existing as TState;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const state = create();
|
|
15
|
+
global[key] = state;
|
|
16
|
+
return state;
|
|
17
|
+
}
|
package/src/scheduler.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { runtimeState, type ReactiveComputation } from "./state.js";
|
|
2
|
+
|
|
3
|
+
export interface Scheduler {
|
|
4
|
+
schedule(flush: () => void): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const defaultScheduler: Scheduler = {
|
|
8
|
+
schedule(flush) {
|
|
9
|
+
if (typeof queueMicrotask === "function") {
|
|
10
|
+
queueMicrotask(flush);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
void Promise.resolve().then(flush);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let scheduler = defaultScheduler;
|
|
19
|
+
let queue: ReactiveComputation[] = [];
|
|
20
|
+
let lastQueuedComputationId = -1;
|
|
21
|
+
let queueRequiresSort = false;
|
|
22
|
+
let scheduled = false;
|
|
23
|
+
let flushing = false;
|
|
24
|
+
const maxFlushIterations = 100;
|
|
25
|
+
|
|
26
|
+
export function setScheduler(nextScheduler: Scheduler): () => void {
|
|
27
|
+
const previous = scheduler;
|
|
28
|
+
scheduler = nextScheduler;
|
|
29
|
+
|
|
30
|
+
return () => {
|
|
31
|
+
scheduler = previous;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function queueComputation(computation: ReactiveComputation): void {
|
|
36
|
+
if (computation.disposed || computation.queued) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (computation.id < lastQueuedComputationId) {
|
|
41
|
+
queueRequiresSort = true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
lastQueuedComputationId = computation.id;
|
|
45
|
+
queue.push(computation);
|
|
46
|
+
computation.queued = true;
|
|
47
|
+
|
|
48
|
+
if (runtimeState.batchDepth > 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
schedulePendingFlush();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function schedulePendingFlush(): void {
|
|
56
|
+
if (queue.length === 0 || scheduled || flushing) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
scheduled = true;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
scheduler.schedule(flushQueuedComputations);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
scheduled = false;
|
|
66
|
+
for (const computation of queue) {
|
|
67
|
+
computation.queued = false;
|
|
68
|
+
}
|
|
69
|
+
clearQueue();
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function flushQueuedComputations(): void {
|
|
75
|
+
if (flushing) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
scheduled = false;
|
|
80
|
+
flushing = true;
|
|
81
|
+
let firstError: unknown;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
for (let iteration = 0; queue.length > 0; iteration += 1) {
|
|
85
|
+
if (iteration >= maxFlushIterations) {
|
|
86
|
+
throw new Error("Reactive flush limit exceeded");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const current = takeQueuedComputations();
|
|
90
|
+
|
|
91
|
+
for (const computation of current) {
|
|
92
|
+
computation.queued = false;
|
|
93
|
+
|
|
94
|
+
if (!computation.disposed) {
|
|
95
|
+
try {
|
|
96
|
+
computation.run();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
firstError ??= error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (firstError !== undefined) {
|
|
105
|
+
throw firstError;
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
flushing = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function clearQueue(): void {
|
|
113
|
+
queue = [];
|
|
114
|
+
lastQueuedComputationId = -1;
|
|
115
|
+
queueRequiresSort = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function takeQueuedComputations(): ReactiveComputation[] {
|
|
119
|
+
if (queueRequiresSort) {
|
|
120
|
+
const current = queue.sort((a, b) => a.id - b.id);
|
|
121
|
+
clearQueue();
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const current = queue;
|
|
126
|
+
queue = [];
|
|
127
|
+
lastQueuedComputationId = -1;
|
|
128
|
+
queueRequiresSort = false;
|
|
129
|
+
return current;
|
|
130
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface Source {
|
|
2
|
+
singleSubscriber?: ReactiveComputation | undefined;
|
|
3
|
+
subscribers: Set<ReactiveComputation>;
|
|
4
|
+
trackedBy?: ReactiveComputation | undefined;
|
|
5
|
+
trackedVersion?: number | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ReactiveComputation {
|
|
9
|
+
readonly id: number;
|
|
10
|
+
deps: Set<Source>;
|
|
11
|
+
trackingAddedDeps?: Source[] | undefined;
|
|
12
|
+
trackingCount?: number | undefined;
|
|
13
|
+
trackingVersion?: number | undefined;
|
|
14
|
+
disposed: boolean;
|
|
15
|
+
queued: boolean;
|
|
16
|
+
markDirty(): void;
|
|
17
|
+
run(): void;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
trackSource?(source: Source): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Tracker = ReactiveComputation | null;
|
|
23
|
+
|
|
24
|
+
export const runtimeState: {
|
|
25
|
+
activeTracker: Tracker;
|
|
26
|
+
batchDepth: number;
|
|
27
|
+
cleanupOwner: ((dispose: () => void) => void) | undefined;
|
|
28
|
+
flushingComputed: boolean;
|
|
29
|
+
nextComputationId: number;
|
|
30
|
+
notificationDepth: number;
|
|
31
|
+
pendingComputed: Set<ReactiveComputation>;
|
|
32
|
+
} = {
|
|
33
|
+
activeTracker: null,
|
|
34
|
+
batchDepth: 0,
|
|
35
|
+
cleanupOwner: undefined,
|
|
36
|
+
flushingComputed: false,
|
|
37
|
+
nextComputationId: 0,
|
|
38
|
+
notificationDepth: 0,
|
|
39
|
+
pendingComputed: new Set(),
|
|
40
|
+
};
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { flushQueuedComputations } from "./scheduler.js";
|
|
2
|
+
|
|
3
|
+
export async function flushMicrotasks(): Promise<void> {
|
|
4
|
+
await Promise.resolve();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function flushEffects(): Promise<void> {
|
|
8
|
+
flushQueuedComputations();
|
|
9
|
+
await Promise.resolve();
|
|
10
|
+
flushQueuedComputations();
|
|
11
|
+
}
|
package/src/tracking.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { runtimeState, type ReactiveComputation, type Source } from "./state.js";
|
|
2
|
+
|
|
3
|
+
const maxPendingComputedFlushIterations = 100;
|
|
4
|
+
|
|
5
|
+
export function trackSource(source: Source): void {
|
|
6
|
+
const tracker = runtimeState.activeTracker;
|
|
7
|
+
|
|
8
|
+
if (tracker === null || tracker.disposed) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (tracker.trackSource !== undefined) {
|
|
13
|
+
tracker.trackSource(source);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (source.singleSubscriber === tracker) {
|
|
18
|
+
tracker.deps.add(source);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const previousSize = source.subscribers.size;
|
|
23
|
+
source.subscribers.add(tracker);
|
|
24
|
+
tracker.deps.add(source);
|
|
25
|
+
|
|
26
|
+
if (previousSize === 0) {
|
|
27
|
+
source.singleSubscriber = tracker;
|
|
28
|
+
} else if (source.subscribers.size > 1) {
|
|
29
|
+
source.singleSubscriber = undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function cleanupDeps(computation: ReactiveComputation): void {
|
|
34
|
+
for (const dep of computation.deps) {
|
|
35
|
+
if (!dep.subscribers.delete(computation)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (dep.trackedBy === computation) {
|
|
40
|
+
dep.trackedBy = undefined;
|
|
41
|
+
dep.trackedVersion = undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (dep.subscribers.size === 0) {
|
|
45
|
+
dep.singleSubscriber = undefined;
|
|
46
|
+
} else if (dep.subscribers.size === 1) {
|
|
47
|
+
dep.singleSubscriber = dep.subscribers.values().next().value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
computation.deps.clear();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function notifySubscribers(source: Source): void {
|
|
55
|
+
if (source.subscribers.size === 0) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cachedSingleSubscriber = source.singleSubscriber;
|
|
60
|
+
if (cachedSingleSubscriber !== undefined) {
|
|
61
|
+
if (cachedSingleSubscriber.disposed || cachedSingleSubscriber.queued) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
runtimeState.notificationDepth += 1;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
cachedSingleSubscriber.markDirty();
|
|
69
|
+
} finally {
|
|
70
|
+
runtimeState.notificationDepth -= 1;
|
|
71
|
+
|
|
72
|
+
if (runtimeState.notificationDepth === 0 && runtimeState.batchDepth === 0) {
|
|
73
|
+
flushPendingComputed();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
runtimeState.notificationDepth += 1;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const singleSubscriber =
|
|
83
|
+
source.subscribers.size === 1
|
|
84
|
+
? source.subscribers.values().next().value
|
|
85
|
+
: undefined;
|
|
86
|
+
|
|
87
|
+
if (singleSubscriber !== undefined) {
|
|
88
|
+
if (!singleSubscriber.disposed && !singleSubscriber.queued) {
|
|
89
|
+
singleSubscriber.markDirty();
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
const subscribers = orderedComputations(source.subscribers);
|
|
93
|
+
|
|
94
|
+
for (const subscriber of subscribers) {
|
|
95
|
+
if (!subscriber.disposed && !subscriber.queued) {
|
|
96
|
+
subscriber.markDirty();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
runtimeState.notificationDepth -= 1;
|
|
102
|
+
|
|
103
|
+
if (runtimeState.notificationDepth === 0 && runtimeState.batchDepth === 0) {
|
|
104
|
+
flushPendingComputed();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function flushPendingComputed(): void {
|
|
110
|
+
if (runtimeState.flushingComputed) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
runtimeState.flushingComputed = true;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
for (
|
|
118
|
+
let iteration = 0;
|
|
119
|
+
runtimeState.pendingComputed.size > 0;
|
|
120
|
+
iteration += 1
|
|
121
|
+
) {
|
|
122
|
+
if (iteration >= maxPendingComputedFlushIterations) {
|
|
123
|
+
runtimeState.pendingComputed.clear();
|
|
124
|
+
throw new Error("Reactive computed flush limit exceeded");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const computations =
|
|
128
|
+
runtimeState.pendingComputed.size === 1
|
|
129
|
+
? [runtimeState.pendingComputed.values().next().value as ReactiveComputation]
|
|
130
|
+
: orderedComputations(runtimeState.pendingComputed);
|
|
131
|
+
runtimeState.pendingComputed.clear();
|
|
132
|
+
|
|
133
|
+
for (const computation of computations) {
|
|
134
|
+
computation.queued = false;
|
|
135
|
+
|
|
136
|
+
if (!computation.disposed) {
|
|
137
|
+
computation.run();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
runtimeState.flushingComputed = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function orderedComputations(
|
|
147
|
+
computations: ReadonlySet<ReactiveComputation>,
|
|
148
|
+
): ReactiveComputation[] {
|
|
149
|
+
const ordered: ReactiveComputation[] = [];
|
|
150
|
+
let previousId = -1;
|
|
151
|
+
let monotonic = true;
|
|
152
|
+
|
|
153
|
+
for (const computation of computations) {
|
|
154
|
+
ordered.push(computation);
|
|
155
|
+
|
|
156
|
+
if (computation.id < previousId) {
|
|
157
|
+
monotonic = false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
previousId = computation.id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return monotonic || ordered.length < 2
|
|
164
|
+
? ordered
|
|
165
|
+
: ordered.sort((a, b) => a.id - b.id);
|
|
166
|
+
}
|
package/src/types.ts
ADDED
package/src/untrack.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { runtimeState } from "./state.js";
|
|
2
|
+
|
|
3
|
+
export function untrack<T>(fn: () => T): T {
|
|
4
|
+
const previousTracker = runtimeState.activeTracker;
|
|
5
|
+
runtimeState.activeTracker = null;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
return fn();
|
|
9
|
+
} finally {
|
|
10
|
+
runtimeState.activeTracker = previousTracker;
|
|
11
|
+
}
|
|
12
|
+
}
|