@esportsplus/reactivity 0.32.0 → 0.34.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/README.md +30 -0
- package/build/compiler/array.d.ts +1 -1
- package/build/compiler/array.js +2 -2
- package/build/compiler/index.js +2 -2
- package/build/compiler/object.d.ts +1 -1
- package/build/compiler/object.js +5 -5
- package/build/compiler/plugins/vite.d.ts +4 -2
- package/build/compiler/primitives.d.ts +1 -1
- package/build/compiler/primitives.js +4 -4
- package/build/reactive/array.d.ts +3 -2
- package/build/reactive/array.js +36 -28
- package/build/reactive/object.d.ts +1 -1
- package/build/system.d.ts +14 -14
- package/build/system.js +26 -9
- package/build/types.d.ts +7 -1
- package/package.json +6 -6
- package/pnpm-workspace.yaml +2 -0
- package/src/compiler/array.ts +5 -5
- package/src/compiler/index.ts +6 -6
- package/src/compiler/object.ts +8 -8
- package/src/compiler/primitives.ts +4 -4
- package/src/reactive/array.ts +51 -39
- package/src/system.ts +35 -12
- package/src/types.ts +9 -0
- package/test/async-computed.test.ts +87 -1
- package/test/compiler/compiler.test.ts +2 -1
- package/test/effects.test.ts +62 -0
- package/test/errors.test.ts +6 -6
- package/test/system.test.ts +10 -10
- package/.claude/CHANGELOG.md +0 -43
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @esportsplus/reactivity
|
|
2
2
|
|
|
3
|
+
Project doc bundle: [docs/index.md](./docs/index.md) — rejected dead-ends, skip reasons, completed-work index.
|
|
4
|
+
|
|
3
5
|
A fine-grained reactivity system with compile-time transformations. Write reactive code with natural JavaScript syntax while the compiler generates optimized signal-based code.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
@@ -313,6 +315,34 @@ All standard array methods (`push`, `pop`, `shift`, `unshift`, `splice`, `sort`,
|
|
|
313
315
|
| `splice` | `{ start, deleteCount, items }` | Array was spliced |
|
|
314
316
|
| `unshift` | `{ items: T[] }` | Items were unshifted |
|
|
315
317
|
|
|
318
|
+
## Architecture
|
|
319
|
+
|
|
320
|
+
- `src/system.ts` — the core signal/computed/effect engine: height-bucketed heap scheduler,
|
|
321
|
+
generation-version dependency dedup (read-version dedup), and iterative (non-recursive)
|
|
322
|
+
notify/update/dispose walks. Also supports async (Promise/AsyncIterable) computeds and
|
|
323
|
+
per-key selector signals (`signal.selector`).
|
|
324
|
+
- `src/constants.ts` — shared state bitmasks and symbol constants consumed by every other module.
|
|
325
|
+
- `src/types.ts` — shared `Signal`/`Computed`/`Link`/`Reactive` types.
|
|
326
|
+
- `src/reactive/index.ts`, `src/reactive/array.ts`, `src/reactive/object.ts` — the runtime
|
|
327
|
+
`reactive()` dispatch and the `ReactiveObject`/`ReactiveArray` classes it builds on.
|
|
328
|
+
- `src/compiler/*` — the build-time transform pipeline (`constants.ts`, `primitives.ts`,
|
|
329
|
+
`object.ts`, `array.ts`, `index.ts`) plus the `plugins/tsc.ts` and `plugins/vite.ts`
|
|
330
|
+
entrypoints that wire it into a build (see Transformer Plugins above).
|
|
331
|
+
|
|
332
|
+
## Development
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
pnpm install
|
|
336
|
+
pnpm build # tsc
|
|
337
|
+
pnpm test # vitest run
|
|
338
|
+
pnpm bench # vitest bench --run
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`agent:test` (`tsc --noEmit && vitest run`) and `agent:bench` (`vitest bench --run`) are the
|
|
342
|
+
CI-facing aliases of the same build/test/bench commands.
|
|
343
|
+
|
|
316
344
|
## License
|
|
317
345
|
|
|
318
346
|
MIT
|
|
347
|
+
|
|
348
|
+
<!-- claude-code:readme-source-hash: 189167a17e9be8c6 -->
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
3
3
|
import type { Bindings } from './types.js';
|
|
4
|
-
declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker) => ReplacementIntent[];
|
|
5
4
|
export default _default;
|
|
5
|
+
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Checker): ReplacementIntent[];
|
package/build/compiler/array.js
CHANGED
|
@@ -160,7 +160,7 @@ function visit(ctx, node) {
|
|
|
160
160
|
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
161
161
|
ts.isIdentifier(node.left)) {
|
|
162
162
|
let name = node.left.text, right = node.right;
|
|
163
|
-
while (ts.isAsExpression(right) || ts.
|
|
163
|
+
while (ts.isAsExpression(right) || ts.isTypeAssertion(right)) {
|
|
164
164
|
right = right.expression;
|
|
165
165
|
}
|
|
166
166
|
if (ctx.bindings.get(name) === TYPES.Array && ts.isArrayLiteralExpression(right)) {
|
|
@@ -172,7 +172,7 @@ function visit(ctx, node) {
|
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
-
|
|
175
|
+
node.forEachChild(n => visit(ctx, n));
|
|
176
176
|
}
|
|
177
177
|
export default (sourceFile, bindings, checker) => {
|
|
178
178
|
let ctx = {
|
package/build/compiler/index.js
CHANGED
|
@@ -27,7 +27,7 @@ function hasReactiveCalls(checker, node) {
|
|
|
27
27
|
return true;
|
|
28
28
|
}
|
|
29
29
|
let found = false;
|
|
30
|
-
|
|
30
|
+
node.forEachChild(child => {
|
|
31
31
|
if (!found && hasReactiveCalls(checker, child)) {
|
|
32
32
|
found = true;
|
|
33
33
|
}
|
|
@@ -41,7 +41,7 @@ function visit(ctx, node) {
|
|
|
41
41
|
node
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
node.forEachChild(n => visit(ctx, n));
|
|
45
45
|
}
|
|
46
46
|
export default {
|
|
47
47
|
patterns: ['reactive(', 'reactive<'],
|
|
@@ -5,5 +5,5 @@ type ObjectTransformResult = {
|
|
|
5
5
|
prepend: string[];
|
|
6
6
|
replacements: ReplacementIntent[];
|
|
7
7
|
};
|
|
8
|
-
declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker) => ObjectTransformResult;
|
|
9
8
|
export default _default;
|
|
9
|
+
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Checker): ObjectTransformResult;
|
package/build/compiler/object.js
CHANGED
|
@@ -13,10 +13,10 @@ function analyzeProperty(prop, sourceFile) {
|
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
let unwrapped = prop.initializer, value = unwrapped, valueText = value.getText(sourceFile);
|
|
16
|
-
while (ts.isAsExpression(unwrapped) || ts.
|
|
16
|
+
while (ts.isAsExpression(unwrapped) || ts.isTypeAssertion(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
|
|
17
17
|
unwrapped = unwrapped.expression;
|
|
18
18
|
}
|
|
19
|
-
if (ts.isAsExpression(value) || ts.
|
|
19
|
+
if (ts.isAsExpression(value) || ts.isTypeAssertion(value)) {
|
|
20
20
|
let type = value.type;
|
|
21
21
|
if (ts.isArrayTypeNode(type) ||
|
|
22
22
|
(ts.isTypeReferenceNode(type) &&
|
|
@@ -161,12 +161,12 @@ function visit(ctx, node) {
|
|
|
161
161
|
for (let i = 0, n = props.length; i < n; i++) {
|
|
162
162
|
let prop = props[i];
|
|
163
163
|
if (ts.isSpreadAssignment(prop)) {
|
|
164
|
-
|
|
164
|
+
node.forEachChild(n => visit(ctx, n));
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
167
|
let analyzed = analyzeProperty(prop, ctx.sourceFile);
|
|
168
168
|
if (!analyzed) {
|
|
169
|
-
|
|
169
|
+
node.forEachChild(n => visit(ctx, n));
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
172
|
properties.push(analyzed);
|
|
@@ -183,7 +183,7 @@ function visit(ctx, node) {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
node.forEachChild(n => visit(ctx, n));
|
|
187
187
|
}
|
|
188
188
|
export default (sourceFile, bindings, checker) => {
|
|
189
189
|
let ctx = {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
declare const _default: ({ root }?: {
|
|
2
2
|
root?: string;
|
|
3
3
|
}) => {
|
|
4
|
+
closeBundle: () => void;
|
|
5
|
+
closeWatcher: () => void;
|
|
4
6
|
configResolved: (config: unknown) => void;
|
|
5
|
-
enforce:
|
|
7
|
+
enforce: 'pre';
|
|
6
8
|
name: string;
|
|
7
9
|
transform: (code: string, id: string) => {
|
|
8
10
|
code: string;
|
|
9
|
-
map:
|
|
11
|
+
map: import("@esportsplus/typescript/compiler").SourceMapV3;
|
|
10
12
|
} | null;
|
|
11
13
|
watchChange: (id: string) => void;
|
|
12
14
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
3
3
|
import type { Bindings } from './types.js';
|
|
4
|
-
declare const _default: (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (node: ts.Node) => boolean) => ReplacementIntent[];
|
|
5
4
|
export default _default;
|
|
5
|
+
declare function _default(sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (node: ts.Node) => boolean): ReplacementIntent[];
|
|
@@ -37,7 +37,7 @@ function visit(ctx, node) {
|
|
|
37
37
|
}
|
|
38
38
|
else {
|
|
39
39
|
let unwrapped = arg;
|
|
40
|
-
while (ts.isAsExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped) || ts.
|
|
40
|
+
while (ts.isAsExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped) || ts.isTypeAssertion(unwrapped)) {
|
|
41
41
|
unwrapped = unwrapped.expression;
|
|
42
42
|
}
|
|
43
43
|
if (ts.isArrayLiteralExpression(unwrapped) || ts.isObjectLiteralExpression(unwrapped)) {
|
|
@@ -48,7 +48,7 @@ function visit(ctx, node) {
|
|
|
48
48
|
generate: () => `${NAMESPACE}.reactive`,
|
|
49
49
|
node: call.expression
|
|
50
50
|
});
|
|
51
|
-
|
|
51
|
+
node.forEachChild(n => visit(ctx, n));
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -97,7 +97,7 @@ function visit(ctx, node) {
|
|
|
97
97
|
node.parent &&
|
|
98
98
|
!(ts.isVariableDeclaration(node.parent) && node.parent.name === node)) {
|
|
99
99
|
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
|
|
100
|
-
|
|
100
|
+
node.forEachChild(n => visit(ctx, n));
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
let bindings = ctx.scopedBindings, binding, name = node.text;
|
|
@@ -178,7 +178,7 @@ function visit(ctx, node) {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
-
|
|
181
|
+
node.forEachChild(n => visit(ctx, n));
|
|
182
182
|
}
|
|
183
183
|
export default (sourceFile, bindings, isReactiveCall) => {
|
|
184
184
|
let ctx = {
|
|
@@ -41,6 +41,9 @@ declare class ReactiveArray<T> extends Array<T> {
|
|
|
41
41
|
private _length;
|
|
42
42
|
listeners: Listeners<T>;
|
|
43
43
|
constructor(...items: T[]);
|
|
44
|
+
static get [Symbol.species](): ArrayConstructor;
|
|
45
|
+
get $length(): number;
|
|
46
|
+
set $length(value: number);
|
|
44
47
|
$set(i: number, value: T): void;
|
|
45
48
|
clear(): void;
|
|
46
49
|
concat(...items: ConcatArray<T>[]): ReactiveArray<T>;
|
|
@@ -56,7 +59,5 @@ declare class ReactiveArray<T> extends Array<T> {
|
|
|
56
59
|
sort(fn?: (a: T, b: T) => number): this;
|
|
57
60
|
splice(start: number, deleteCount?: number, ...items: T[]): T[];
|
|
58
61
|
unshift(...items: T[]): number;
|
|
59
|
-
get $length(): number;
|
|
60
|
-
set $length(value: number);
|
|
61
62
|
}
|
|
62
63
|
export { ReactiveArray };
|
package/build/reactive/array.js
CHANGED
|
@@ -14,6 +14,18 @@ class ReactiveArray extends Array {
|
|
|
14
14
|
super(...items);
|
|
15
15
|
this._length = signal(items.length);
|
|
16
16
|
}
|
|
17
|
+
static get [Symbol.species]() {
|
|
18
|
+
return Array;
|
|
19
|
+
}
|
|
20
|
+
get $length() {
|
|
21
|
+
return read(this._length);
|
|
22
|
+
}
|
|
23
|
+
set $length(value) {
|
|
24
|
+
if (value > this.length) {
|
|
25
|
+
throw Error(`@esportsplus/reactivity: cannot set length to a value larger than the current length, use splice instead.`);
|
|
26
|
+
}
|
|
27
|
+
this.splice(value, this.length);
|
|
28
|
+
}
|
|
17
29
|
$set(i, value) {
|
|
18
30
|
let prev = this[i];
|
|
19
31
|
if (prev === value) {
|
|
@@ -146,29 +158,34 @@ class ReactiveArray extends Array {
|
|
|
146
158
|
return item;
|
|
147
159
|
}
|
|
148
160
|
sort(fn) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
let value = before[i], list = buckets.get(value);
|
|
157
|
-
if (!list) {
|
|
158
|
-
buckets.set(value, [i]);
|
|
161
|
+
if (this.listeners.sort === undefined) {
|
|
162
|
+
super.sort(fn);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
let n = this.length, before = new Array(n);
|
|
166
|
+
for (let i = 0; i < n; i++) {
|
|
167
|
+
before[i] = this[i];
|
|
159
168
|
}
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
super.sort(fn);
|
|
170
|
+
let buckets = new Map(), order = new Array(n);
|
|
171
|
+
for (let i = 0; i < n; i++) {
|
|
172
|
+
let value = before[i], list = buckets.get(value);
|
|
173
|
+
if (!list) {
|
|
174
|
+
buckets.set(value, [i]);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
list.push(i);
|
|
178
|
+
}
|
|
162
179
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
180
|
+
for (let i = 0; i < n; i++) {
|
|
181
|
+
let list = buckets.get(this[i]);
|
|
182
|
+
order[i] = list.length === 1 ? list[0] : list[list.length - 1];
|
|
183
|
+
if (list.length > 1) {
|
|
184
|
+
list.pop();
|
|
185
|
+
}
|
|
169
186
|
}
|
|
187
|
+
this.dispatch('sort', { order });
|
|
170
188
|
}
|
|
171
|
-
this.dispatch('sort', { order });
|
|
172
189
|
return this;
|
|
173
190
|
}
|
|
174
191
|
splice(start, deleteCount = this.length, ...items) {
|
|
@@ -191,15 +208,6 @@ class ReactiveArray extends Array {
|
|
|
191
208
|
this.dispatch('unshift', { items });
|
|
192
209
|
return length;
|
|
193
210
|
}
|
|
194
|
-
get $length() {
|
|
195
|
-
return read(this._length);
|
|
196
|
-
}
|
|
197
|
-
set $length(value) {
|
|
198
|
-
if (value > this.length) {
|
|
199
|
-
throw Error(`@esportsplus/reactivity: cannot set length to a value larger than the current length, use splice instead.`);
|
|
200
|
-
}
|
|
201
|
-
this.splice(value, this.length);
|
|
202
|
-
}
|
|
203
211
|
}
|
|
204
212
|
Object.defineProperty(ReactiveArray.prototype, REACTIVE_ARRAY, { value: true });
|
|
205
213
|
export { ReactiveArray };
|
|
@@ -4,7 +4,7 @@ import { ReactiveArray } from './array.js';
|
|
|
4
4
|
declare class ReactiveObject<T extends Record<PropertyKey, unknown>> {
|
|
5
5
|
protected disposers: VoidFunction[] | null;
|
|
6
6
|
constructor(data: T | null);
|
|
7
|
-
protected [COMPUTED]<T extends Computed<ReturnType<T>>['fn']>(value: T):
|
|
7
|
+
protected [COMPUTED]<T extends Computed<ReturnType<T>>['fn']>(value: T): import("../types.js").ComputedResult<ReturnType<T>>;
|
|
8
8
|
protected [REACTIVE_ARRAY]<U>(value: U[]): ReactiveArray<U>;
|
|
9
9
|
protected [SIGNAL]<T>(value: T): import("../types.js").Signal<T>;
|
|
10
10
|
dispose(): void;
|
package/build/system.d.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { Computed, Settled, Signal } from './types.js';
|
|
1
|
+
import { Computed, ComputedResult, Settled, Signal } from './types.js';
|
|
2
2
|
declare const batch: <T>(fn: () => T) => T;
|
|
3
|
-
declare
|
|
4
|
-
|
|
5
|
-
invalidate<T>(c: Computed<T>)
|
|
6
|
-
}
|
|
3
|
+
declare function computed<T>(fn: Computed<T>['fn'], equals?: ((a: Settled<T>, b: Settled<T>) => boolean) | null): ComputedResult<T>;
|
|
4
|
+
declare namespace computed {
|
|
5
|
+
var invalidate: <T>(c: Computed<T>) => void;
|
|
6
|
+
}
|
|
7
7
|
declare const dispose: <T>(computed: Computed<T>) => void;
|
|
8
|
-
declare const effect: <T>(fn: Computed<T>[
|
|
8
|
+
declare const effect: <T>(fn: Computed<T>['fn'], apply?: (value: T, prev: T | undefined) => void) => () => void;
|
|
9
9
|
declare const flush: () => void;
|
|
10
10
|
declare const isComputed: (value: unknown) => value is Computed<unknown>;
|
|
11
11
|
declare const isSignal: (value: unknown) => value is Signal<unknown>;
|
|
12
12
|
declare const onCleanup: (fn: VoidFunction) => typeof fn;
|
|
13
13
|
declare const peek: <T>(node: Signal<T> | Computed<T>) => T;
|
|
14
14
|
declare const read: <T>(node: Signal<T> | Computed<T>) => T;
|
|
15
|
-
declare
|
|
16
|
-
|
|
17
|
-
disposables: number;
|
|
18
|
-
}
|
|
19
|
-
declare
|
|
20
|
-
|
|
21
|
-
selector<T>(node: Signal<T>, key: T)
|
|
22
|
-
}
|
|
15
|
+
declare function root<T>(fn: ((dispose: VoidFunction) => T) | (() => T)): T;
|
|
16
|
+
declare namespace root {
|
|
17
|
+
var disposables: number;
|
|
18
|
+
}
|
|
19
|
+
declare function signal<T>(value: T, equals?: ((a: T, b: T) => boolean) | null): Signal<T>;
|
|
20
|
+
declare namespace signal {
|
|
21
|
+
var selector: <T>(node: Signal<T>, key: T) => boolean;
|
|
22
|
+
}
|
|
23
23
|
declare const untrack: <T>(fn: () => T) => T;
|
|
24
24
|
declare const write: <T>(signal: Signal<T>, value: T) => void;
|
|
25
25
|
export { batch, computed, dispose, effect, flush, isComputed, isSignal, onCleanup, peek, read, root, signal, untrack, write };
|
package/build/system.js
CHANGED
|
@@ -412,18 +412,23 @@ function update(root) {
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
function makeAsyncComputed(factory) {
|
|
415
|
-
let error = signal(undefined), node = signal(undefined), v = 0;
|
|
415
|
+
let error = signal(undefined), node = signal(undefined), pending = signal(false), v = 0;
|
|
416
416
|
let stop = effect(() => {
|
|
417
417
|
let fail = (e) => {
|
|
418
418
|
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
419
419
|
write(error, e === undefined ? new Error('reactivity: async computed rejected with undefined') : e);
|
|
420
|
+
write(pending, false);
|
|
420
421
|
}
|
|
421
422
|
}, id = ++v, result = read(factory);
|
|
422
423
|
if (isPromise(result)) {
|
|
424
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
425
|
+
write(pending, true);
|
|
426
|
+
}
|
|
423
427
|
result.then((value) => {
|
|
424
428
|
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
425
429
|
write(error, undefined);
|
|
426
430
|
write(node, value);
|
|
431
|
+
write(pending, false);
|
|
427
432
|
}
|
|
428
433
|
}, fail);
|
|
429
434
|
}
|
|
@@ -439,14 +444,22 @@ function makeAsyncComputed(factory) {
|
|
|
439
444
|
if (!r.done) {
|
|
440
445
|
write(error, undefined);
|
|
441
446
|
write(node, r.value);
|
|
447
|
+
write(pending, false);
|
|
442
448
|
it.next().then(step, fail);
|
|
443
449
|
}
|
|
450
|
+
else {
|
|
451
|
+
write(pending, false);
|
|
452
|
+
}
|
|
444
453
|
};
|
|
454
|
+
if (id === v && !(factory.state & (STATE_IN_HEAP | STATE_NOTIFY_MASK))) {
|
|
455
|
+
write(pending, true);
|
|
456
|
+
}
|
|
445
457
|
untrack(() => it.next()).then(step, fail);
|
|
446
458
|
}
|
|
447
459
|
else {
|
|
448
460
|
write(error, undefined);
|
|
449
461
|
write(node, result);
|
|
462
|
+
write(pending, false);
|
|
450
463
|
}
|
|
451
464
|
});
|
|
452
465
|
let wrapper = makeComputed(() => {
|
|
@@ -456,6 +469,7 @@ function makeAsyncComputed(factory) {
|
|
|
456
469
|
}
|
|
457
470
|
return read(node);
|
|
458
471
|
});
|
|
472
|
+
wrapper.pending = pending;
|
|
459
473
|
asyncMeta.set(wrapper, { factory: factory });
|
|
460
474
|
wrapper.disposal = stop;
|
|
461
475
|
return wrapper;
|
|
@@ -574,15 +588,14 @@ const dispose = (computed) => {
|
|
|
574
588
|
draining = false;
|
|
575
589
|
}
|
|
576
590
|
};
|
|
577
|
-
const effect = (fn,
|
|
578
|
-
let
|
|
591
|
+
const effect = (fn, apply) => {
|
|
592
|
+
let prev;
|
|
593
|
+
let c = makeComputed(apply
|
|
579
594
|
? (o) => {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
onError(e);
|
|
585
|
-
}
|
|
595
|
+
let v = fn(o);
|
|
596
|
+
untrack(() => apply(v, prev));
|
|
597
|
+
prev = v;
|
|
598
|
+
return v;
|
|
586
599
|
}
|
|
587
600
|
: fn);
|
|
588
601
|
c.state |= STATE_EFFECT;
|
|
@@ -681,9 +694,12 @@ root.disposables = 0;
|
|
|
681
694
|
const signal = (value, equals = null) => {
|
|
682
695
|
return {
|
|
683
696
|
equals: equals,
|
|
697
|
+
key: undefined,
|
|
684
698
|
keys: null,
|
|
685
699
|
nextPending: null,
|
|
700
|
+
parent: undefined,
|
|
686
701
|
rv: 0,
|
|
702
|
+
state: 0,
|
|
687
703
|
subs: null,
|
|
688
704
|
subsTail: null,
|
|
689
705
|
type: SIGNAL,
|
|
@@ -703,6 +719,7 @@ signal.selector = (node, key) => {
|
|
|
703
719
|
nextPending: null,
|
|
704
720
|
parent: node,
|
|
705
721
|
rv: 0,
|
|
722
|
+
state: 0,
|
|
706
723
|
subs: null,
|
|
707
724
|
subsTail: null,
|
|
708
725
|
type: SIGNAL,
|
package/build/types.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ interface Computed<T> {
|
|
|
19
19
|
subsTail: Link | null;
|
|
20
20
|
value: T;
|
|
21
21
|
}
|
|
22
|
+
type ComputedResult<T> = T extends Promise<any> | AsyncIterable<any> ? Computed<Settled<T>> & {
|
|
23
|
+
pending: Signal<boolean>;
|
|
24
|
+
} : Computed<Settled<T>>;
|
|
22
25
|
interface Link {
|
|
23
26
|
dep: Signal<unknown> | Computed<unknown>;
|
|
24
27
|
nextDep: Link | null;
|
|
@@ -42,9 +45,12 @@ type SelectorSignal<T> = Signal<boolean> & {
|
|
|
42
45
|
type Settled<T> = T extends Promise<infer U> ? Awaited<U> | undefined : T extends AsyncIterable<infer U> ? U | undefined : T;
|
|
43
46
|
type Signal<T> = {
|
|
44
47
|
equals: ((a: unknown, b: unknown) => boolean) | null;
|
|
48
|
+
key: unknown;
|
|
45
49
|
keys: Map<T, SelectorSignal<T>> | null;
|
|
46
50
|
nextPending: Signal<unknown> | null;
|
|
51
|
+
parent: Signal<unknown> | undefined;
|
|
47
52
|
rv: number;
|
|
53
|
+
state: number;
|
|
48
54
|
subs: Link | null;
|
|
49
55
|
subsTail: Link | null;
|
|
50
56
|
type: typeof SIGNAL;
|
|
@@ -55,4 +61,4 @@ interface TransformResult {
|
|
|
55
61
|
code: string;
|
|
56
62
|
sourceFile: ts.SourceFile;
|
|
57
63
|
}
|
|
58
|
-
export type { Computed, Link, Reactive, SelectorSignal, Settled, Signal, TransformResult };
|
|
64
|
+
export type { Computed, ComputedResult, Link, Reactive, SelectorSignal, Settled, Signal, TransformResult };
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "ICJR",
|
|
3
3
|
"dependencies": {
|
|
4
|
-
"@esportsplus/utilities": "^0.
|
|
4
|
+
"@esportsplus/utilities": "^0.28.0"
|
|
5
5
|
},
|
|
6
6
|
"devDependencies": {
|
|
7
|
-
"@esportsplus/typescript": "^0.
|
|
8
|
-
"@types/node": "^
|
|
9
|
-
"vite": "^8.
|
|
10
|
-
"vitest": "^4.1.
|
|
7
|
+
"@esportsplus/typescript": "^0.31.0",
|
|
8
|
+
"@types/node": "^26.1.1",
|
|
9
|
+
"vite": "^8.1.5",
|
|
10
|
+
"vitest": "^4.1.10"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"type": "module",
|
|
38
38
|
"types": "build/index.d.ts",
|
|
39
|
-
"version": "0.
|
|
39
|
+
"version": "0.34.0",
|
|
40
40
|
"scripts": {
|
|
41
41
|
"agent:bench": "vitest bench --run",
|
|
42
42
|
"agent:test": "tsc --noEmit && vitest run",
|
package/pnpm-workspace.yaml
CHANGED
package/src/compiler/array.ts
CHANGED
|
@@ -7,13 +7,13 @@ import type { Bindings } from './types';
|
|
|
7
7
|
|
|
8
8
|
type VisitContext = {
|
|
9
9
|
bindings: Bindings;
|
|
10
|
-
checker: ts.
|
|
10
|
+
checker: ts.Checker | undefined;
|
|
11
11
|
replacements: ReplacementIntent[];
|
|
12
12
|
sourceFile: ts.SourceFile;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
function isReactiveCall(checker: ts.
|
|
16
|
+
function isReactiveCall(checker: ts.Checker | undefined, node: ts.Node): node is ts.CallExpression {
|
|
17
17
|
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
@@ -220,7 +220,7 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
220
220
|
right = node.right;
|
|
221
221
|
|
|
222
222
|
// Unwrap "as" expressions: arr = [] as Type[]
|
|
223
|
-
while (ts.isAsExpression(right) || ts.
|
|
223
|
+
while (ts.isAsExpression(right) || ts.isTypeAssertion(right)) {
|
|
224
224
|
right = right.expression;
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -234,11 +234,11 @@ function visit(ctx: VisitContext, node: ts.Node): void {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
node.forEachChild(n => visit(ctx, n));
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
|
|
241
|
-
export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.
|
|
241
|
+
export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.Checker): ReplacementIntent[] => {
|
|
242
242
|
let ctx: VisitContext = {
|
|
243
243
|
bindings,
|
|
244
244
|
checker,
|
package/src/compiler/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import primitives from './primitives';
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
type FindRemainingContext = {
|
|
13
|
-
checker: ts.
|
|
13
|
+
checker: ts.Checker;
|
|
14
14
|
replacements: ReplacementIntent[];
|
|
15
15
|
sourceFile: ts.SourceFile;
|
|
16
16
|
transformedNodes: Set<ts.Node>;
|
|
@@ -18,7 +18,7 @@ type FindRemainingContext = {
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
function findRemainingCalls(
|
|
21
|
-
checker: ts.
|
|
21
|
+
checker: ts.Checker,
|
|
22
22
|
sourceFile: ts.SourceFile,
|
|
23
23
|
transformedNodes: Set<ts.Node>
|
|
24
24
|
): ReplacementIntent[] {
|
|
@@ -29,7 +29,7 @@ function findRemainingCalls(
|
|
|
29
29
|
return ctx.replacements;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function isReactiveCallExpression(checker: ts.
|
|
32
|
+
function isReactiveCallExpression(checker: ts.Checker, node: ts.Node): node is ts.CallExpression {
|
|
33
33
|
if (!ts.isCallExpression(node)) {
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
@@ -49,14 +49,14 @@ function isReactiveCallExpression(checker: ts.TypeChecker, node: ts.Node): node
|
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function hasReactiveCalls(checker: ts.
|
|
52
|
+
function hasReactiveCalls(checker: ts.Checker, node: ts.Node): boolean {
|
|
53
53
|
if (isReactiveCallExpression(checker, node)) {
|
|
54
54
|
return true;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
let found = false;
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
node.forEachChild(child => {
|
|
60
60
|
if (!found && hasReactiveCalls(checker, child)) {
|
|
61
61
|
found = true;
|
|
62
62
|
}
|
|
@@ -73,7 +73,7 @@ function visit(ctx: FindRemainingContext, node: ts.Node): void {
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
node.forEachChild(n => visit(ctx, n));
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
|