@esportsplus/reactivity 0.33.0 → 0.36.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.
Files changed (45) hide show
  1. package/README.md +9 -7
  2. package/bench/reactive/array.bench.ts +17 -17
  3. package/build/compiler/array.d.ts +2 -2
  4. package/build/compiler/array.js +12 -54
  5. package/build/compiler/constants.d.ts +4 -4
  6. package/build/compiler/constants.js +20 -4
  7. package/build/compiler/index.js +25 -38
  8. package/build/compiler/object.d.ts +2 -2
  9. package/build/compiler/object.js +10 -21
  10. package/build/compiler/plugins/vite.d.ts +4 -2
  11. package/build/compiler/primitives.d.ts +6 -2
  12. package/build/compiler/primitives.js +35 -42
  13. package/build/compiler/types.d.ts +3 -1
  14. package/build/constants.d.ts +2 -2
  15. package/build/constants.js +2 -2
  16. package/build/reactive/array.d.ts +1 -1
  17. package/build/reactive/array.js +26 -15
  18. package/build/reactive/index.js +5 -14
  19. package/build/reactive/object.js +1 -1
  20. package/build/system.d.ts +10 -13
  21. package/build/system.js +105 -83
  22. package/build/types.d.ts +2 -7
  23. package/package.json +5 -5
  24. package/pnpm-workspace.yaml +3 -0
  25. package/src/compiler/array.ts +14 -64
  26. package/src/compiler/constants.ts +21 -5
  27. package/src/compiler/index.ts +39 -70
  28. package/src/compiler/object.ts +12 -29
  29. package/src/compiler/primitives.ts +55 -53
  30. package/src/compiler/types.ts +4 -1
  31. package/src/constants.ts +4 -4
  32. package/src/reactive/array.ts +33 -17
  33. package/src/reactive/index.ts +5 -17
  34. package/src/reactive/object.ts +1 -1
  35. package/src/system.ts +138 -101
  36. package/src/types.ts +2 -9
  37. package/test/async-computed.test.ts +2 -2
  38. package/test/compiler/compiler.test.ts +40 -23
  39. package/test/effects.test.ts +37 -1
  40. package/test/lib/uncaught.ts +26 -0
  41. package/test/reactive/array.test.ts +169 -99
  42. package/test/reactive/nested.test.ts +14 -14
  43. package/test/reactive/objects.test.ts +1 -1
  44. package/test/reactive/reactive.test.ts +49 -0
  45. package/test/system.test.ts +130 -22
@@ -9,27 +9,7 @@ import object from './object';
9
9
  import primitives from './primitives';
10
10
 
11
11
 
12
- type FindRemainingContext = {
13
- checker: ts.TypeChecker;
14
- replacements: ReplacementIntent[];
15
- sourceFile: ts.SourceFile;
16
- transformedNodes: Set<ts.Node>;
17
- };
18
-
19
-
20
- function findRemainingCalls(
21
- checker: ts.TypeChecker,
22
- sourceFile: ts.SourceFile,
23
- transformedNodes: Set<ts.Node>
24
- ): ReplacementIntent[] {
25
- let ctx: FindRemainingContext = { checker, replacements: [], sourceFile, transformedNodes };
26
-
27
- visit(ctx, sourceFile);
28
-
29
- return ctx.replacements;
30
- }
31
-
32
- function isReactiveCallExpression(checker: ts.TypeChecker, node: ts.Node): node is ts.CallExpression {
12
+ function isReactiveCallExpression(checker: ts.Checker, node: ts.Node): node is ts.CallExpression {
33
13
  if (!ts.isCallExpression(node)) {
34
14
  return false;
35
15
  }
@@ -49,38 +29,13 @@ function isReactiveCallExpression(checker: ts.TypeChecker, node: ts.Node): node
49
29
  return false;
50
30
  }
51
31
 
52
- function hasReactiveCalls(checker: ts.TypeChecker, node: ts.Node): boolean {
53
- if (isReactiveCallExpression(checker, node)) {
54
- return true;
55
- }
56
-
57
- let found = false;
58
-
59
- ts.forEachChild(node, child => {
60
- if (!found && hasReactiveCalls(checker, child)) {
61
- found = true;
62
- }
63
- });
64
-
65
- return found;
66
- }
67
-
68
- function visit(ctx: FindRemainingContext, node: ts.Node): void {
69
- if (isReactiveCallExpression(ctx.checker, node) && !ctx.transformedNodes.has(node) && !ctx.transformedNodes.has(node.expression)) {
70
- ctx.replacements.push({
71
- generate: () => `${NAMESPACE}.reactive(${node.arguments.map(a => a.getText(ctx.sourceFile)).join(', ')})`,
72
- node
73
- });
74
- }
75
-
76
- ts.forEachChild(node, n => visit(ctx, n));
77
- }
78
-
79
32
 
80
33
  export default {
81
34
  patterns: ['reactive(', 'reactive<'],
82
35
  transform: (ctx: TransformContext) => {
83
- if (!ctx.checker || !hasReactiveCalls(ctx.checker, ctx.sourceFile)) {
36
+ let checker = ctx.checker;
37
+
38
+ if (!checker) {
84
39
  return {};
85
40
  }
86
41
 
@@ -89,36 +44,50 @@ export default {
89
44
  imports: [] as ImportIntent[],
90
45
  prepend: [] as string[],
91
46
  replacements: [] as ReplacementIntent[]
92
- };
47
+ },
48
+ isReactiveCall = (node: ts.Node): node is ts.CallExpression => isReactiveCallExpression(checker, node),
49
+ sourceFile = ctx.sourceFile;
93
50
 
94
- // Run primitives transform first (tracks bindings for signal/computed)
95
- intents.replacements.push(
96
- ...primitives(ctx.sourceFile, bindings, (node: ts.Node) => isReactiveCallExpression(ctx.checker!, node))
97
- );
51
+ // Run primitives transform first (tracks bindings for signal/computed, collects every call)
52
+ let { calls, replacements } = primitives(sourceFile, bindings, isReactiveCall);
98
53
 
99
- // Run object transform
100
- let { prepend, replacements } = object(ctx.sourceFile, bindings, ctx.checker);
54
+ if (calls.length === 0) {
55
+ return {};
56
+ }
101
57
 
102
- intents.prepend.push(...prepend);
103
58
  intents.replacements.push(...replacements);
104
59
 
60
+ // Run object transform
61
+ let objects = object(sourceFile, bindings, isReactiveCall);
62
+
63
+ intents.prepend.push(...objects.prepend);
64
+ intents.replacements.push(...objects.replacements);
65
+
105
66
  // Run array transform separately ( avoid race conditions )
106
- intents.replacements.push(...array(ctx.sourceFile, bindings, ctx.checker));
107
-
108
- // Find remaining reactive() calls that weren't transformed and replace with namespace version
109
- intents.replacements.push(
110
- ...findRemainingCalls(ctx.checker, ctx.sourceFile, new Set(intents.replacements.map(r => r.node)))
111
- );
112
-
113
- // Build import intent
114
- if (intents.replacements.length > 0 || intents.prepend.length > 0) {
115
- intents.imports.push({
116
- namespace: NAMESPACE,
117
- package: PACKAGE_NAME,
118
- remove: [ENTRYPOINT]
67
+ intents.replacements.push(...array(sourceFile, bindings, isReactiveCall));
68
+
69
+ // Calls no transform claimed fall through to the runtime reactive()
70
+ let transformed = new Set(intents.replacements.map(r => r.node));
71
+
72
+ for (let i = 0, n = calls.length; i < n; i++) {
73
+ let call = calls[i];
74
+
75
+ if (transformed.has(call) || transformed.has(call.expression)) {
76
+ continue;
77
+ }
78
+
79
+ intents.replacements.push({
80
+ generate: () => `${NAMESPACE}.reactive(${call.arguments.map(a => a.getText(sourceFile)).join(', ')})`,
81
+ node: call
119
82
  });
120
83
  }
121
84
 
85
+ intents.imports.push({
86
+ namespace: NAMESPACE,
87
+ package: PACKAGE_NAME,
88
+ remove: [ENTRYPOINT]
89
+ });
90
+
122
91
  return intents;
123
92
  }
124
93
  };
@@ -1,8 +1,8 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { code, imports, uid } from '@esportsplus/typescript/compiler';
2
+ import { code, uid } from '@esportsplus/typescript/compiler';
3
3
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
4
- import { ENTRYPOINT, NAMESPACE, PACKAGE_NAME, TYPES } from './constants';
5
- import type { Bindings } from './types';
4
+ import { NAMESPACE, TYPES } from './constants';
5
+ import type { Bindings, IsReactiveCall } from './types';
6
6
 
7
7
 
8
8
  interface AnalyzedProperty {
@@ -28,7 +28,7 @@ interface ReactiveObjectCall {
28
28
  interface VisitContext {
29
29
  bindings: Bindings;
30
30
  calls: ReactiveObjectCall[];
31
- checker: ts.TypeChecker | undefined;
31
+ isReactiveCall: IsReactiveCall;
32
32
  sourceFile: ts.SourceFile;
33
33
  }
34
34
 
@@ -51,11 +51,11 @@ function analyzeProperty(prop: ts.ObjectLiteralElementLike, sourceFile: ts.Sourc
51
51
  value = unwrapped,
52
52
  valueText = value.getText(sourceFile);
53
53
 
54
- while (ts.isAsExpression(unwrapped) || ts.isTypeAssertionExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
54
+ while (ts.isAsExpression(unwrapped) || ts.isTypeAssertion(unwrapped) || ts.isParenthesizedExpression(unwrapped)) {
55
55
  unwrapped = unwrapped.expression;
56
56
  }
57
57
 
58
- if (ts.isAsExpression(value) || ts.isTypeAssertionExpression(value)) {
58
+ if (ts.isAsExpression(value) || ts.isTypeAssertion(value)) {
59
59
  let type = (value as ts.AsExpression).type;
60
60
 
61
61
  if (
@@ -216,24 +216,8 @@ function isStaticValue(node: ts.Node): boolean {
216
216
  (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand));
217
217
  }
218
218
 
219
- function isReactiveCall(checker: ts.TypeChecker | undefined, node: ts.Node): node is ts.CallExpression {
220
- if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) {
221
- return false;
222
- }
223
-
224
- let expr = node.expression;
225
-
226
- // Use checker to verify symbol origin (handles re-exports)
227
- if (checker) {
228
- return imports.includes(checker, expr, PACKAGE_NAME, ENTRYPOINT);
229
- }
230
-
231
- // Fallback without checker: match by name only
232
- return expr.text === ENTRYPOINT;
233
- }
234
-
235
219
  function visit(ctx: VisitContext, node: ts.Node): void {
236
- if (isReactiveCall(ctx.checker, node)) {
220
+ if (ctx.isReactiveCall(node)) {
237
221
  let arg = node.arguments[0];
238
222
 
239
223
  if (arg && ts.isObjectLiteralExpression(arg)) {
@@ -243,21 +227,20 @@ function visit(ctx: VisitContext, node: ts.Node): void {
243
227
 
244
228
  if (node.parent && ts.isVariableDeclaration(node.parent) && ts.isIdentifier(node.parent.name)) {
245
229
  varname = node.parent.name.text;
246
- ctx.bindings.set(varname, TYPES.Object);
247
230
  }
248
231
 
249
232
  for (let i = 0, n = props.length; i < n; i++) {
250
233
  let prop = props[i];
251
234
 
252
235
  if (ts.isSpreadAssignment(prop)) {
253
- ts.forEachChild(node, n => visit(ctx, n));
236
+ node.forEachChild(n => visit(ctx, n));
254
237
  return;
255
238
  }
256
239
 
257
240
  let analyzed = analyzeProperty(prop, ctx.sourceFile);
258
241
 
259
242
  if (!analyzed) {
260
- ts.forEachChild(node, n => visit(ctx, n));
243
+ node.forEachChild(n => visit(ctx, n));
261
244
  return;
262
245
  }
263
246
 
@@ -278,15 +261,15 @@ function visit(ctx: VisitContext, node: ts.Node): void {
278
261
  }
279
262
  }
280
263
 
281
- ts.forEachChild(node, n => visit(ctx, n));
264
+ node.forEachChild(n => visit(ctx, n));
282
265
  }
283
266
 
284
267
 
285
- export default (sourceFile: ts.SourceFile, bindings: Bindings, checker?: ts.TypeChecker): ObjectTransformResult => {
268
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): ObjectTransformResult => {
286
269
  let ctx: VisitContext = {
287
270
  bindings,
288
271
  calls: [],
289
- checker,
272
+ isReactiveCall,
290
273
  sourceFile
291
274
  };
292
275
 
@@ -1,10 +1,11 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
3
- import { NAMESPACE, TYPES } from './constants';
4
- import type { Bindings } from './types';
3
+ import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants';
4
+ import type { Bindings, IsReactiveCall } from './types';
5
5
 
6
6
 
7
7
  interface ScopeBinding {
8
+ depth: number;
8
9
  name: string;
9
10
  scope: ts.Node;
10
11
  type: TYPES;
@@ -12,31 +13,18 @@ interface ScopeBinding {
12
13
 
13
14
  interface TransformContext {
14
15
  bindings: Bindings;
15
- isReactiveCall: (node: ts.Node) => boolean;
16
+ calls: ts.CallExpression[];
17
+ isReactiveCall: IsReactiveCall;
16
18
  replacements: ReplacementIntent[];
17
19
  scopedBindings: ScopeBinding[];
18
20
  sourceFile: ts.SourceFile;
19
21
  tmpCounter: number;
20
22
  }
21
23
 
22
-
23
- const COMPOUND_OPERATORS = new Map<ts.SyntaxKind, string>([
24
- [ts.SyntaxKind.AmpersandAmpersandEqualsToken, '&&'],
25
- [ts.SyntaxKind.AmpersandEqualsToken, '&'],
26
- [ts.SyntaxKind.AsteriskAsteriskEqualsToken, '**'],
27
- [ts.SyntaxKind.AsteriskEqualsToken, '*'],
28
- [ts.SyntaxKind.BarBarEqualsToken, '||'],
29
- [ts.SyntaxKind.BarEqualsToken, '|'],
30
- [ts.SyntaxKind.CaretEqualsToken, '^'],
31
- [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, '>>'],
32
- [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, '>>>'],
33
- [ts.SyntaxKind.LessThanLessThanEqualsToken, '<<'],
34
- [ts.SyntaxKind.MinusEqualsToken, '-'],
35
- [ts.SyntaxKind.PercentEqualsToken, '%'],
36
- [ts.SyntaxKind.PlusEqualsToken, '+'],
37
- [ts.SyntaxKind.QuestionQuestionEqualsToken, '??'],
38
- [ts.SyntaxKind.SlashEqualsToken, '/']
39
- ]);
24
+ type PrimitivesTransformResult = {
25
+ calls: ts.CallExpression[];
26
+ replacements: ReplacementIntent[];
27
+ };
40
28
 
41
29
 
42
30
  function inScope(reference: ts.Node, binding: ScopeBinding): boolean {
@@ -53,9 +41,44 @@ function inScope(reference: ts.Node, binding: ScopeBinding): boolean {
53
41
  return false;
54
42
  }
55
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
+
56
77
  function visit(ctx: TransformContext, node: ts.Node): void {
57
78
  if (ctx.isReactiveCall(node)) {
58
- let call = node as ts.CallExpression;
79
+ let call = node;
80
+
81
+ ctx.calls.push(call);
59
82
 
60
83
  if (call.arguments.length > 0) {
61
84
  let arg = call.arguments[0],
@@ -67,7 +90,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
67
90
  else {
68
91
  let unwrapped = arg;
69
92
 
70
- while (ts.isAsExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped) || ts.isTypeAssertionExpression(unwrapped)) {
93
+ while (ts.isAsExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped) || ts.isTypeAssertion(unwrapped)) {
71
94
  unwrapped = unwrapped.expression;
72
95
  }
73
96
 
@@ -80,7 +103,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
80
103
  generate: () => `${NAMESPACE}.reactive`,
81
104
  node: call.expression
82
105
  });
83
- ts.forEachChild(node, n => visit(ctx, n));
106
+ node.forEachChild(n => visit(ctx, n));
84
107
  return;
85
108
  }
86
109
  }
@@ -101,32 +124,10 @@ function visit(ctx: TransformContext, node: ts.Node): void {
101
124
  }
102
125
 
103
126
  if (varname) {
104
- let current = call.parent,
105
- scope;
106
-
107
- while (current) {
108
- if (
109
- ts.isArrowFunction(current) ||
110
- ts.isBlock(current) ||
111
- ts.isForInStatement(current) ||
112
- ts.isForOfStatement(current) ||
113
- ts.isForStatement(current) ||
114
- ts.isFunctionDeclaration(current) ||
115
- ts.isFunctionExpression(current) ||
116
- ts.isSourceFile(current)
117
- ) {
118
- scope = current;
119
- }
120
-
121
- current = current.parent;
122
- }
123
-
124
- if (!scope) {
125
- scope = call.getSourceFile();
126
- }
127
+ let { depth, scope } = scopeOf(call);
127
128
 
128
129
  ctx.bindings.set(varname, classification);
129
- ctx.scopedBindings.push({ name: varname, scope, type: classification });
130
+ ctx.scopedBindings.push({ depth, name: varname, scope, type: classification });
130
131
  }
131
132
 
132
133
  // Replace just the 'reactive' identifier with the appropriate namespace function
@@ -148,7 +149,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
148
149
  !(ts.isVariableDeclaration(node.parent) && node.parent.name === node)
149
150
  ) {
150
151
  if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
151
- ts.forEachChild(node, n => visit(ctx, n));
152
+ node.forEachChild(n => visit(ctx, n));
152
153
  return;
153
154
  }
154
155
 
@@ -159,7 +160,7 @@ function visit(ctx: TransformContext, node: ts.Node): void {
159
160
  for (let i = 0, n = bindings.length; i < n; i++) {
160
161
  let b = bindings[i];
161
162
 
162
- if (b.name === name && inScope(node, b)) {
163
+ if (b.name === name && (!binding || b.depth >= binding.depth) && inScope(node, b)) {
163
164
  binding = b;
164
165
  }
165
166
  }
@@ -251,13 +252,14 @@ function visit(ctx: TransformContext, node: ts.Node): void {
251
252
  }
252
253
  }
253
254
 
254
- ts.forEachChild(node, n => visit(ctx, n));
255
+ node.forEachChild(n => visit(ctx, n));
255
256
  }
256
257
 
257
258
 
258
- export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (node: ts.Node) => boolean) => {
259
+ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: IsReactiveCall): PrimitivesTransformResult => {
259
260
  let ctx: TransformContext = {
260
261
  bindings,
262
+ calls: [],
261
263
  isReactiveCall,
262
264
  replacements: [],
263
265
  scopedBindings: [],
@@ -267,5 +269,5 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: (
267
269
 
268
270
  visit(ctx, sourceFile);
269
271
 
270
- return ctx.replacements;
272
+ return { calls: ctx.calls, replacements: ctx.replacements };
271
273
  };
@@ -1,7 +1,10 @@
1
+ import type { ts } from '@esportsplus/typescript';
1
2
  import { TYPES } from './constants';
2
3
 
3
4
 
4
5
  type Bindings = Map<string, TYPES>;
5
6
 
7
+ type IsReactiveCall = (node: ts.Node) => node is ts.CallExpression;
6
8
 
7
- export type { Bindings };
9
+
10
+ export type { Bindings, IsReactiveCall };
package/src/constants.ts CHANGED
@@ -8,6 +8,8 @@ const REACTIVE_OBJECT = Symbol('reactivity.reactive.object');
8
8
 
9
9
  const SIGNAL = Symbol('reactivity.signal');
10
10
 
11
+ const STABILIZER_DEFERRED = 4;
12
+
11
13
  const STABILIZER_IDLE = 0;
12
14
 
13
15
  const STABILIZER_RESCHEDULE = 1;
@@ -16,8 +18,6 @@ const STABILIZER_RUNNING = 2;
16
18
 
17
19
  const STABILIZER_SCHEDULED = 3;
18
20
 
19
- const STATE_NONE = 0;
20
-
21
21
  const STATE_CHECK = 1 << 0;
22
22
 
23
23
  const STATE_DIRTY = 1 << 1;
@@ -40,6 +40,6 @@ export {
40
40
  PACKAGE_NAME,
41
41
  REACTIVE_ARRAY, REACTIVE_OBJECT,
42
42
  SIGNAL,
43
- STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
44
- STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NONE, STATE_NOTIFY_MASK, STATE_RECOMPUTING
43
+ STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED,
44
+ STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING
45
45
  };
@@ -1,5 +1,5 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
- import { REACTIVE_ARRAY } from '~/constants';
2
+ import { PACKAGE_NAME, REACTIVE_ARRAY } from '~/constants';
3
3
  import { read, signal, write } from '~/system';
4
4
  import type { Signal } from '~/types';
5
5
  import { isReactiveObject } from './object';
@@ -63,9 +63,16 @@ class ReactiveArray<T> extends Array<T> {
63
63
  listeners: Listeners<T> = {};
64
64
 
65
65
 
66
- constructor(...items: T[]) {
67
- super(...items);
68
- this._length = signal(items.length);
66
+ constructor(items?: readonly T[]) {
67
+ super();
68
+
69
+ let n = items ? items.length : 0;
70
+
71
+ for (let i = 0; i < n; i++) {
72
+ this[i] = items![i];
73
+ }
74
+
75
+ this._length = signal(n);
69
76
  }
70
77
 
71
78
 
@@ -103,6 +110,7 @@ class ReactiveArray<T> extends Array<T> {
103
110
  }
104
111
 
105
112
  this.dispatch('set', { index: i, item: value });
113
+ dispose(prev);
106
114
  }
107
115
 
108
116
 
@@ -147,7 +155,8 @@ class ReactiveArray<T> extends Array<T> {
147
155
  return;
148
156
  }
149
157
 
150
- let dirty = false;
158
+ let dirty = false,
159
+ errors: unknown[] | null = null;
151
160
 
152
161
  for (let i = 0, n = listeners.length; i < n; i++) {
153
162
  let listener = listeners[i];
@@ -164,9 +173,10 @@ class ReactiveArray<T> extends Array<T> {
164
173
  listeners[i] = null;
165
174
  }
166
175
  }
167
- catch {
176
+ catch (e) {
168
177
  dirty = true;
169
178
  listeners[i] = null;
179
+ (errors ??= []).push(e);
170
180
  }
171
181
  }
172
182
 
@@ -175,6 +185,16 @@ class ReactiveArray<T> extends Array<T> {
175
185
  listeners.pop();
176
186
  }
177
187
  }
188
+
189
+ if (errors !== null) {
190
+ let error = errors.length === 1
191
+ ? errors[0]
192
+ : new AggregateError(errors, `${PACKAGE_NAME}: dispatch produced multiple errors`);
193
+
194
+ queueMicrotask(() => {
195
+ throw error;
196
+ });
197
+ }
178
198
  }
179
199
 
180
200
  dispose() {
@@ -278,29 +298,25 @@ class ReactiveArray<T> extends Array<T> {
278
298
 
279
299
  super.sort(fn);
280
300
 
281
- let buckets = new Map<T, number[]>(),
301
+ let buckets = new Map<T, { indices: number[]; next: number }>(),
282
302
  order = new Array(n);
283
303
 
284
304
  for (let i = 0; i < n; i++) {
285
305
  let value = before[i],
286
- list = buckets.get(value);
306
+ bucket = buckets.get(value);
287
307
 
288
- if (!list) {
289
- buckets.set(value, [i]);
308
+ if (bucket === undefined) {
309
+ buckets.set(value, { indices: [i], next: 0 });
290
310
  }
291
311
  else {
292
- list.push(i);
312
+ bucket.indices.push(i);
293
313
  }
294
314
  }
295
315
 
296
316
  for (let i = 0; i < n; i++) {
297
- let list = buckets.get(this[i])!;
317
+ let bucket = buckets.get(this[i])!;
298
318
 
299
- order[i] = list.length === 1 ? list[0] : list[list.length - 1];
300
-
301
- if (list.length > 1) {
302
- list.pop();
303
- }
319
+ order[i] = bucket.indices[bucket.next++];
304
320
  }
305
321
 
306
322
  this.dispatch('sort', { order });
@@ -18,31 +18,19 @@ function reactive<T extends unknown[]>(input: T): Reactive<T>;
18
18
  function reactive<T extends Record<PropertyKey, unknown>>(input: Guard<T>): Reactive<T>;
19
19
  function reactive<T>(input: T): Reactive<T>;
20
20
  function reactive<T>(input: T): Reactive<T> {
21
- let dispose = false,
22
- value = root(() => {
23
- let response: Reactive<T> | undefined;
24
-
21
+ let value = root(() => {
25
22
  if (isObject(input)) {
26
- response = new ReactiveObject(input) as unknown as Reactive<T>;
27
- }
28
- else if (isArray(input)) {
29
- response = new ReactiveArray(...input) as unknown as Reactive<T>;
23
+ return new ReactiveObject(input) as unknown as Reactive<T>;
30
24
  }
31
25
 
32
- if (response) {
33
- if (root.disposables) {
34
- dispose = true;
35
- }
36
-
37
- return response;
26
+ if (isArray(input)) {
27
+ return new ReactiveArray(input) as unknown as Reactive<T>;
38
28
  }
39
29
 
40
30
  throw new Error(`${PACKAGE_NAME}: 'reactive' received invalid input - ${JSON.stringify(input)}`);
41
31
  });
42
32
 
43
- if (dispose) {
44
- onCleanup(() => (value as unknown as { dispose: VoidFunction }).dispose());
45
- }
33
+ onCleanup(() => (value as unknown as { dispose: VoidFunction }).dispose());
46
34
 
47
35
  return value;
48
36
  }
@@ -67,7 +67,7 @@ class ReactiveObject<T extends Record<PropertyKey, unknown>> {
67
67
  }
68
68
 
69
69
  protected [REACTIVE_ARRAY]<U>(value: U[]): ReactiveArray<U> {
70
- let node = new ReactiveArray(...value);
70
+ let node = new ReactiveArray(value);
71
71
 
72
72
  (this.disposers ??= []).push( () => node.dispose() );
73
73