@esportsplus/reactivity 0.37.0 → 0.37.1
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/bench/cellx.bench.ts +4 -4
- package/bench/dynamic.bench.ts +10 -9
- package/bench/kairo.bench.ts +28 -33
- package/bench/molbench.bench.ts +4 -4
- package/bench/reactive/array.bench.ts +163 -163
- package/bench/reactive/reactive-object.bench.ts +47 -45
- package/bench/system.bench.ts +263 -263
- package/build/compiler/array.js +68 -73
- package/build/compiler/bindings.d.ts +19 -0
- package/build/compiler/bindings.js +65 -0
- package/build/compiler/index.js +15 -6
- package/build/compiler/object.js +10 -6
- package/build/compiler/primitives.js +137 -138
- package/build/compiler/types.d.ts +7 -2
- package/build/reactive/array.js +19 -12
- package/build/system.js +1 -1
- package/package.json +2 -2
- package/pnpm-workspace.yaml +2 -4
- package/src/compiler/array.ts +86 -89
- package/src/compiler/bindings.ts +100 -0
- package/src/compiler/index.ts +22 -9
- package/src/compiler/object.ts +12 -7
- package/src/compiler/primitives.ts +190 -187
- package/src/compiler/types.ts +7 -2
- package/src/reactive/array.ts +27 -14
- package/src/system.ts +3 -1
- package/test/async-iterable.test.ts +42 -0
- package/test/compiler/compiler.test.ts +115 -45
- package/test/reactive/array.test.ts +54 -14
- package/vitest.config.ts +2 -2
|
@@ -2,79 +2,26 @@ import { ts } from '@esportsplus/typescript';
|
|
|
2
2
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
3
3
|
import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants';
|
|
4
4
|
import type { Bindings, IsReactiveCall } from './types';
|
|
5
|
+
import scope from './bindings';
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type: TYPES;
|
|
12
|
-
}
|
|
8
|
+
type PrimitivesTransformResult = {
|
|
9
|
+
calls: ts.CallExpression[];
|
|
10
|
+
replacements: ReplacementIntent[];
|
|
11
|
+
};
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
type TransformContext = {
|
|
15
14
|
bindings: Bindings;
|
|
16
15
|
calls: ts.CallExpression[];
|
|
17
16
|
isReactiveCall: IsReactiveCall;
|
|
18
17
|
replacements: ReplacementIntent[];
|
|
19
|
-
scopedBindings: ScopeBinding[];
|
|
20
|
-
sourceFile: ts.SourceFile;
|
|
21
18
|
tmpCounter: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type PrimitivesTransformResult = {
|
|
25
|
-
calls: ts.CallExpression[];
|
|
26
|
-
replacements: ReplacementIntent[];
|
|
27
19
|
};
|
|
28
20
|
|
|
29
21
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
while (current) {
|
|
34
|
-
if (current === binding.scope) {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
current = current.parent;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isScope(node: ts.Node): boolean {
|
|
45
|
-
return ts.isArrowFunction(node) ||
|
|
46
|
-
ts.isBlock(node) ||
|
|
47
|
-
ts.isCatchClause(node) ||
|
|
48
|
-
ts.isForInStatement(node) ||
|
|
49
|
-
ts.isForOfStatement(node) ||
|
|
50
|
-
ts.isForStatement(node) ||
|
|
51
|
-
ts.isFunctionDeclaration(node) ||
|
|
52
|
-
ts.isFunctionExpression(node) ||
|
|
53
|
-
ts.isSourceFile(node);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Innermost enclosing scope plus its nesting depth, so shadowed names resolve to the closest binding
|
|
57
|
-
function scopeOf(node: ts.Node): { depth: number; scope: ts.Node } {
|
|
58
|
-
let current: ts.Node | undefined = node.parent,
|
|
59
|
-
depth = 0,
|
|
60
|
-
scope: ts.Node = node.getSourceFile();
|
|
61
|
-
|
|
62
|
-
while (current) {
|
|
63
|
-
if (isScope(current)) {
|
|
64
|
-
if (scope === node.getSourceFile() && !ts.isSourceFile(current)) {
|
|
65
|
-
scope = current;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
depth++;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
current = current.parent;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return { depth, scope };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function visit(ctx: TransformContext, node: ts.Node): void {
|
|
22
|
+
// Pass 1: record every reactive() call and the variable each signal/computed is bound to, before
|
|
23
|
+
// any reference is rewritten — a use that precedes its declaration in source order still resolves
|
|
24
|
+
function declare(ctx: TransformContext, node: ts.Node): void {
|
|
78
25
|
if (ctx.isReactiveCall(node)) {
|
|
79
26
|
let call = node;
|
|
80
27
|
|
|
@@ -99,160 +46,217 @@ function visit(ctx: TransformContext, node: ts.Node): void {
|
|
|
99
46
|
}
|
|
100
47
|
// Dynamic expression - use runtime reactive via namespace
|
|
101
48
|
else if (ts.isCallExpression(unwrapped) || ts.isIdentifier(unwrapped)) {
|
|
49
|
+
classification = null;
|
|
102
50
|
ctx.replacements.push({
|
|
103
51
|
generate: () => `${NAMESPACE}.reactive`,
|
|
104
52
|
node: call.expression
|
|
105
53
|
});
|
|
106
|
-
node.forEachChild(n => visit(ctx, n));
|
|
107
|
-
return;
|
|
108
54
|
}
|
|
109
55
|
}
|
|
110
56
|
|
|
111
|
-
if (classification) {
|
|
112
|
-
let
|
|
57
|
+
if (classification !== null) {
|
|
58
|
+
let target = targetOf(call),
|
|
59
|
+
type = classification;
|
|
113
60
|
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
else if (
|
|
118
|
-
call.parent &&
|
|
119
|
-
ts.isBinaryExpression(call.parent) &&
|
|
120
|
-
call.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
121
|
-
ts.isIdentifier(call.parent.left)
|
|
122
|
-
) {
|
|
123
|
-
varname = call.parent.left.text;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (varname) {
|
|
127
|
-
let { depth, scope } = scopeOf(call);
|
|
128
|
-
|
|
129
|
-
ctx.bindings.set(varname, classification);
|
|
130
|
-
ctx.scopedBindings.push({ depth, name: varname, scope, type: classification });
|
|
61
|
+
if (target) {
|
|
62
|
+
scope.declare(ctx.bindings, target, type);
|
|
131
63
|
}
|
|
132
64
|
|
|
133
65
|
// Replace just the 'reactive' identifier with the appropriate namespace function
|
|
134
66
|
ctx.replacements.push({
|
|
135
|
-
generate: () =>
|
|
67
|
+
generate: () => type === TYPES.Computed
|
|
136
68
|
? `${NAMESPACE}.computed`
|
|
137
69
|
: `${NAMESPACE}.signal`,
|
|
138
70
|
node: call.expression
|
|
139
71
|
});
|
|
140
|
-
|
|
141
|
-
// Continue visiting children - inner identifiers will get their own ReplacementIntents
|
|
142
72
|
}
|
|
143
73
|
}
|
|
144
74
|
}
|
|
145
75
|
|
|
76
|
+
node.forEachChild(child => declare(ctx, child));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A name slot rather than a value reference: a declaration's or member's own name, a property
|
|
80
|
+
// name (`o.count`, `{ count: 1 }`, `{ count: local } = o`), or a label. A shorthand property's
|
|
81
|
+
// name doubles as a read of the variable, so it is not a name slot. (TS 7's `isDeclarationName`
|
|
82
|
+
// only tests the node kind, so it is true for every identifier and cannot be used here.)
|
|
83
|
+
function isName(node: ts.Identifier, parent: ts.Node): boolean {
|
|
84
|
+
if (ts.isShorthandPropertyAssignment(parent)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let slots = parent as { label?: ts.Node; name?: ts.Node; propertyName?: ts.Node };
|
|
89
|
+
|
|
90
|
+
return slots.name === node || slots.propertyName === node || slots.label === node;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Destructuring assignment targets (`({ count } = o)`, `[count] = xs`) write through a pattern
|
|
94
|
+
// that has no per-signal rewrite, so they are left untouched
|
|
95
|
+
function isPatternTarget(node: ts.Node): boolean {
|
|
96
|
+
let current = node;
|
|
97
|
+
|
|
98
|
+
while (
|
|
99
|
+
current.parent &&
|
|
100
|
+
(
|
|
101
|
+
ts.isShorthandPropertyAssignment(current.parent) ||
|
|
102
|
+
ts.isPropertyAssignment(current.parent) ||
|
|
103
|
+
ts.isSpreadAssignment(current.parent) ||
|
|
104
|
+
ts.isSpreadElement(current.parent) ||
|
|
105
|
+
ts.isObjectLiteralExpression(current.parent) ||
|
|
106
|
+
ts.isArrayLiteralExpression(current.parent)
|
|
107
|
+
)
|
|
108
|
+
) {
|
|
109
|
+
current = current.parent;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let parent = current.parent;
|
|
113
|
+
|
|
114
|
+
if (current === node || !parent) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && parent.left === current) ||
|
|
119
|
+
((ts.isForOfStatement(parent) || ts.isForInStatement(parent)) && parent.initializer === current);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function reference(ctx: TransformContext, node: ts.Identifier): void {
|
|
123
|
+
let parent = node.parent;
|
|
124
|
+
|
|
125
|
+
if (!parent || ts.isExportSpecifier(parent) || ts.isImportSpecifier(parent) || isName(node, parent)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let binding = scope.resolve(ctx.bindings, node);
|
|
130
|
+
|
|
131
|
+
if (!binding || binding.type === TYPES.Array) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
146
135
|
if (
|
|
147
|
-
ts.
|
|
148
|
-
|
|
149
|
-
|
|
136
|
+
(ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && parent.left === node && ctx.isReactiveCall(parent.right)) ||
|
|
137
|
+
(ts.isTypeOfExpression(parent) && parent.expression === node) ||
|
|
138
|
+
isPatternTarget(node)
|
|
150
139
|
) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
155
142
|
|
|
156
|
-
|
|
157
|
-
binding,
|
|
158
|
-
name = node.text;
|
|
143
|
+
let name = node.text;
|
|
159
144
|
|
|
160
|
-
|
|
161
|
-
|
|
145
|
+
if (ts.isShorthandPropertyAssignment(parent)) {
|
|
146
|
+
ctx.replacements.push({
|
|
147
|
+
generate: () => `${name}: ${NAMESPACE}.read(${name})`,
|
|
148
|
+
node: parent
|
|
149
|
+
});
|
|
162
150
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let writeCtx: 'compound' | 'increment' | 'simple' | undefined;
|
|
155
|
+
|
|
156
|
+
if (ts.isBinaryExpression(parent) && parent.left === node) {
|
|
157
|
+
let op = parent.operatorToken.kind;
|
|
158
|
+
|
|
159
|
+
if (op === ts.SyntaxKind.EqualsToken) {
|
|
160
|
+
writeCtx = 'simple';
|
|
161
|
+
}
|
|
162
|
+
else if (COMPOUND_OPERATORS.has(op)) {
|
|
163
|
+
writeCtx = 'compound';
|
|
166
164
|
}
|
|
165
|
+
}
|
|
166
|
+
else if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
|
|
167
|
+
let op = parent.operator;
|
|
167
168
|
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
!(
|
|
173
|
-
ts.isBinaryExpression(parent) &&
|
|
174
|
-
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
175
|
-
ctx.isReactiveCall(parent.right)
|
|
176
|
-
) &&
|
|
177
|
-
!(ts.isTypeOfExpression(parent) && parent.expression === node)
|
|
178
|
-
) {
|
|
179
|
-
let writeCtx;
|
|
180
|
-
|
|
181
|
-
if (ts.isBinaryExpression(parent) && parent.left === node) {
|
|
182
|
-
let op = parent.operatorToken.kind;
|
|
183
|
-
|
|
184
|
-
if (op === ts.SyntaxKind.EqualsToken) {
|
|
185
|
-
writeCtx = 'simple';
|
|
186
|
-
}
|
|
187
|
-
else if (COMPOUND_OPERATORS.has(op)) {
|
|
188
|
-
writeCtx = 'compound';
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
else if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
|
|
192
|
-
let op = parent.operator;
|
|
169
|
+
if (op === ts.SyntaxKind.MinusMinusToken || op === ts.SyntaxKind.PlusPlusToken) {
|
|
170
|
+
writeCtx = 'increment';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
193
173
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
174
|
+
if (!writeCtx) {
|
|
175
|
+
ctx.replacements.push({
|
|
176
|
+
generate: () => `${NAMESPACE}.read(${name})`,
|
|
177
|
+
node
|
|
178
|
+
});
|
|
198
179
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
generate: () => `((${tmp}) => (${NAMESPACE}.write(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
|
|
239
|
-
node: parent
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
ctx.replacements.push({
|
|
247
|
-
generate: () => `${NAMESPACE}.read(${name})`,
|
|
248
|
-
node
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (binding.type === TYPES.Computed) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (writeCtx === 'simple' && ts.isBinaryExpression(parent)) {
|
|
188
|
+
let right = parent.right;
|
|
189
|
+
|
|
190
|
+
ctx.replacements.push({
|
|
191
|
+
generate: (sf) => `${NAMESPACE}.write(${name}, ${right.getText(sf)})`,
|
|
192
|
+
node: parent
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else if (writeCtx === 'compound' && ts.isBinaryExpression(parent)) {
|
|
196
|
+
let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind) ?? '+',
|
|
197
|
+
right = parent.right;
|
|
198
|
+
|
|
199
|
+
ctx.replacements.push({
|
|
200
|
+
generate: (sf) => `${NAMESPACE}.write(${name}, ${name}.value ${op} ${right.getText(sf)})`,
|
|
201
|
+
node: parent
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else if (writeCtx === 'increment') {
|
|
205
|
+
let unary = parent as ts.PostfixUnaryExpression | ts.PrefixUnaryExpression,
|
|
206
|
+
delta = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+ 1' : '- 1';
|
|
207
|
+
|
|
208
|
+
if (ts.isExpressionStatement(unary.parent)) {
|
|
209
|
+
ctx.replacements.push({
|
|
210
|
+
generate: () => `${NAMESPACE}.write(${name}, ${name}.value ${delta})`,
|
|
211
|
+
node: unary
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
else if (ts.isPrefixUnaryExpression(unary)) {
|
|
215
|
+
ctx.replacements.push({
|
|
216
|
+
generate: () => `(${NAMESPACE}.write(${name}, ${name}.value ${delta}), ${name}.value)`,
|
|
217
|
+
node: unary
|
|
218
|
+
});
|
|
252
219
|
}
|
|
220
|
+
else {
|
|
221
|
+
let tmp = `_t${ctx.tmpCounter++}`;
|
|
222
|
+
|
|
223
|
+
ctx.replacements.push({
|
|
224
|
+
generate: () => `((${tmp}) => (${NAMESPACE}.write(${name}, ${tmp} ${delta}), ${tmp}))(${name}.value)`,
|
|
225
|
+
node: unary
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Pass 2: rewrite reads and writes of every signal/computed binding. Type positions never read a
|
|
232
|
+
// value, so they are not walked at all.
|
|
233
|
+
function rewrite(ctx: TransformContext, node: ts.Node): void {
|
|
234
|
+
if (ts.isTypeNode(node)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (ts.isIdentifier(node)) {
|
|
239
|
+
reference(ctx, node);
|
|
240
|
+
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
node.forEachChild(child => rewrite(ctx, child));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The variable a reactive() call initializes: `let x = reactive(...)` or `x = reactive(...)`
|
|
248
|
+
function targetOf(call: ts.CallExpression): ts.Identifier | null {
|
|
249
|
+
let parent = call.parent;
|
|
250
|
+
|
|
251
|
+
if (parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
252
|
+
return parent.name;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (parent && ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(parent.left)) {
|
|
256
|
+
return parent.left;
|
|
253
257
|
}
|
|
254
258
|
|
|
255
|
-
|
|
259
|
+
return null;
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
|
|
@@ -262,12 +266,11 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: I
|
|
|
262
266
|
calls: [],
|
|
263
267
|
isReactiveCall,
|
|
264
268
|
replacements: [],
|
|
265
|
-
scopedBindings: [],
|
|
266
|
-
sourceFile,
|
|
267
269
|
tmpCounter: 0
|
|
268
270
|
};
|
|
269
271
|
|
|
270
|
-
|
|
272
|
+
declare(ctx, sourceFile);
|
|
273
|
+
rewrite(ctx, sourceFile);
|
|
271
274
|
|
|
272
275
|
return { calls: ctx.calls, replacements: ctx.replacements };
|
|
273
276
|
};
|
package/src/compiler/types.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { TYPES } from './constants';
|
|
2
|
+
import type { TYPES } from './constants';
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
type Bindings =
|
|
5
|
+
type Bindings = {
|
|
6
|
+
checker: ts.Checker;
|
|
7
|
+
names: Set<string>;
|
|
8
|
+
paths: Map<ts.Symbol, Set<string>>;
|
|
9
|
+
symbols: Map<ts.Symbol, TYPES>;
|
|
10
|
+
};
|
|
6
11
|
|
|
7
12
|
type IsReactiveCall = (node: ts.Node) => node is ts.CallExpression;
|
|
8
13
|
|
package/src/reactive/array.ts
CHANGED
|
@@ -240,14 +240,16 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
pop() {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (item !== undefined) {
|
|
246
|
-
dispose(item);
|
|
247
|
-
write(this._length, this.length);
|
|
248
|
-
this.dispatch('pop', { item });
|
|
243
|
+
if (this.length === 0) {
|
|
244
|
+
return undefined;
|
|
249
245
|
}
|
|
250
246
|
|
|
247
|
+
let item = super.pop() as T;
|
|
248
|
+
|
|
249
|
+
dispose(item);
|
|
250
|
+
write(this._length, this.length);
|
|
251
|
+
this.dispatch('pop', { item });
|
|
252
|
+
|
|
251
253
|
return item;
|
|
252
254
|
}
|
|
253
255
|
|
|
@@ -273,14 +275,16 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
shift() {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (item !== undefined) {
|
|
279
|
-
dispose(item);
|
|
280
|
-
write(this._length, this.length);
|
|
281
|
-
this.dispatch('shift', { item });
|
|
278
|
+
if (this.length === 0) {
|
|
279
|
+
return undefined;
|
|
282
280
|
}
|
|
283
281
|
|
|
282
|
+
let item = super.shift() as T;
|
|
283
|
+
|
|
284
|
+
dispose(item);
|
|
285
|
+
write(this._length, this.length);
|
|
286
|
+
this.dispatch('shift', { item });
|
|
287
|
+
|
|
284
288
|
return item;
|
|
285
289
|
}
|
|
286
290
|
|
|
@@ -326,7 +330,8 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
326
330
|
}
|
|
327
331
|
|
|
328
332
|
splice(start: number, deleteCount: number = this.length, ...items: T[]) {
|
|
329
|
-
let
|
|
333
|
+
let length = this.length,
|
|
334
|
+
removed = super.splice(start, deleteCount, ...items);
|
|
330
335
|
|
|
331
336
|
if (items.length > 0 || removed.length > 0) {
|
|
332
337
|
write(this._length, this.length);
|
|
@@ -335,7 +340,15 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
335
340
|
dispose(removed[i]);
|
|
336
341
|
}
|
|
337
342
|
|
|
338
|
-
|
|
343
|
+
// Listeners receive the position and count Array.prototype.splice actually applied
|
|
344
|
+
// (negative, fractional and out-of-range arguments resolved), never the raw arguments
|
|
345
|
+
let index = Math.trunc(start) || 0;
|
|
346
|
+
|
|
347
|
+
this.dispatch('splice', {
|
|
348
|
+
deleteCount: removed.length,
|
|
349
|
+
items,
|
|
350
|
+
start: index < 0 ? Math.max(length + index, 0) : Math.min(index, length)
|
|
351
|
+
});
|
|
339
352
|
}
|
|
340
353
|
|
|
341
354
|
return removed;
|
package/src/system.ts
CHANGED
|
@@ -680,8 +680,10 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
680
680
|
else if (result != null && typeof (result as AsyncIterable<T>)[Symbol.asyncIterator] === 'function') {
|
|
681
681
|
let it = (result as AsyncIterable<T>)[Symbol.asyncIterator]();
|
|
682
682
|
|
|
683
|
+
// Disposal leaves no consumer to observe a failing iterator cleanup; left unhandled, the
|
|
684
|
+
// rejection would crash hosts that treat unhandled rejections as fatal (Node's default)
|
|
683
685
|
onCleanup(() => {
|
|
684
|
-
it.return?.();
|
|
686
|
+
it.return?.().then(undefined, () => {});
|
|
685
687
|
});
|
|
686
688
|
|
|
687
689
|
let step = (r: IteratorResult<T>) => {
|
|
@@ -218,4 +218,46 @@ describe('asyncComputed AsyncIterable support', () => {
|
|
|
218
218
|
|
|
219
219
|
expect(() => read(node)).toThrow('iterable step boom');
|
|
220
220
|
});
|
|
221
|
+
|
|
222
|
+
it('a rejecting return() during disposal is not an unhandled rejection', async () => {
|
|
223
|
+
let unhandled: unknown[] = [],
|
|
224
|
+
listener = (reason: unknown) => {
|
|
225
|
+
unhandled.push(reason);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
process.on('unhandledRejection', listener);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
let iterable = {
|
|
232
|
+
[Symbol.asyncIterator]: () => ({
|
|
233
|
+
next: () => new Promise<IteratorResult<number>>(() => {}),
|
|
234
|
+
return: () => Promise.reject(new Error('cleanup boom'))
|
|
235
|
+
})
|
|
236
|
+
},
|
|
237
|
+
node!: Computed<number | undefined>,
|
|
238
|
+
stopRoot!: VoidFunction;
|
|
239
|
+
|
|
240
|
+
root((dispose) => {
|
|
241
|
+
stopRoot = dispose;
|
|
242
|
+
node = computed(() => iterable);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
let keeper = effect(() => {
|
|
246
|
+
read(node);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await tick();
|
|
250
|
+
|
|
251
|
+
keeper();
|
|
252
|
+
stopRoot();
|
|
253
|
+
|
|
254
|
+
await tick();
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
256
|
+
|
|
257
|
+
expect(unhandled).toEqual([]);
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
process.off('unhandledRejection', listener);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
221
263
|
});
|