@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
@@ -1,22 +1,5 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { NAMESPACE, TYPES } from './constants.js';
3
- const COMPOUND_OPERATORS = new Map([
4
- [ts.SyntaxKind.AmpersandAmpersandEqualsToken, '&&'],
5
- [ts.SyntaxKind.AmpersandEqualsToken, '&'],
6
- [ts.SyntaxKind.AsteriskAsteriskEqualsToken, '**'],
7
- [ts.SyntaxKind.AsteriskEqualsToken, '*'],
8
- [ts.SyntaxKind.BarBarEqualsToken, '||'],
9
- [ts.SyntaxKind.BarEqualsToken, '|'],
10
- [ts.SyntaxKind.CaretEqualsToken, '^'],
11
- [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken, '>>'],
12
- [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken, '>>>'],
13
- [ts.SyntaxKind.LessThanLessThanEqualsToken, '<<'],
14
- [ts.SyntaxKind.MinusEqualsToken, '-'],
15
- [ts.SyntaxKind.PercentEqualsToken, '%'],
16
- [ts.SyntaxKind.PlusEqualsToken, '+'],
17
- [ts.SyntaxKind.QuestionQuestionEqualsToken, '??'],
18
- [ts.SyntaxKind.SlashEqualsToken, '/']
19
- ]);
2
+ import { COMPOUND_OPERATORS, NAMESPACE, TYPES } from './constants.js';
20
3
  function inScope(reference, binding) {
21
4
  let current = reference;
22
5
  while (current) {
@@ -27,9 +10,34 @@ function inScope(reference, binding) {
27
10
  }
28
11
  return false;
29
12
  }
13
+ function isScope(node) {
14
+ return ts.isArrowFunction(node) ||
15
+ ts.isBlock(node) ||
16
+ ts.isCatchClause(node) ||
17
+ ts.isForInStatement(node) ||
18
+ ts.isForOfStatement(node) ||
19
+ ts.isForStatement(node) ||
20
+ ts.isFunctionDeclaration(node) ||
21
+ ts.isFunctionExpression(node) ||
22
+ ts.isSourceFile(node);
23
+ }
24
+ function scopeOf(node) {
25
+ let current = node.parent, depth = 0, scope = node.getSourceFile();
26
+ while (current) {
27
+ if (isScope(current)) {
28
+ if (scope === node.getSourceFile() && !ts.isSourceFile(current)) {
29
+ scope = current;
30
+ }
31
+ depth++;
32
+ }
33
+ current = current.parent;
34
+ }
35
+ return { depth, scope };
36
+ }
30
37
  function visit(ctx, node) {
31
38
  if (ctx.isReactiveCall(node)) {
32
39
  let call = node;
40
+ ctx.calls.push(call);
33
41
  if (call.arguments.length > 0) {
34
42
  let arg = call.arguments[0], classification = TYPES.Signal;
35
43
  if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
@@ -37,7 +45,7 @@ function visit(ctx, node) {
37
45
  }
38
46
  else {
39
47
  let unwrapped = arg;
40
- while (ts.isAsExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped) || ts.isTypeAssertionExpression(unwrapped)) {
48
+ while (ts.isAsExpression(unwrapped) || ts.isParenthesizedExpression(unwrapped) || ts.isTypeAssertion(unwrapped)) {
41
49
  unwrapped = unwrapped.expression;
42
50
  }
43
51
  if (ts.isArrayLiteralExpression(unwrapped) || ts.isObjectLiteralExpression(unwrapped)) {
@@ -48,7 +56,7 @@ function visit(ctx, node) {
48
56
  generate: () => `${NAMESPACE}.reactive`,
49
57
  node: call.expression
50
58
  });
51
- ts.forEachChild(node, n => visit(ctx, n));
59
+ node.forEachChild(n => visit(ctx, n));
52
60
  return;
53
61
  }
54
62
  }
@@ -64,25 +72,9 @@ function visit(ctx, node) {
64
72
  varname = call.parent.left.text;
65
73
  }
66
74
  if (varname) {
67
- let current = call.parent, scope;
68
- while (current) {
69
- if (ts.isArrowFunction(current) ||
70
- ts.isBlock(current) ||
71
- ts.isForInStatement(current) ||
72
- ts.isForOfStatement(current) ||
73
- ts.isForStatement(current) ||
74
- ts.isFunctionDeclaration(current) ||
75
- ts.isFunctionExpression(current) ||
76
- ts.isSourceFile(current)) {
77
- scope = current;
78
- }
79
- current = current.parent;
80
- }
81
- if (!scope) {
82
- scope = call.getSourceFile();
83
- }
75
+ let { depth, scope } = scopeOf(call);
84
76
  ctx.bindings.set(varname, classification);
85
- ctx.scopedBindings.push({ name: varname, scope, type: classification });
77
+ ctx.scopedBindings.push({ depth, name: varname, scope, type: classification });
86
78
  }
87
79
  ctx.replacements.push({
88
80
  generate: () => classification === TYPES.Computed
@@ -97,13 +89,13 @@ function visit(ctx, node) {
97
89
  node.parent &&
98
90
  !(ts.isVariableDeclaration(node.parent) && node.parent.name === node)) {
99
91
  if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
100
- ts.forEachChild(node, n => visit(ctx, n));
92
+ node.forEachChild(n => visit(ctx, n));
101
93
  return;
102
94
  }
103
95
  let bindings = ctx.scopedBindings, binding, name = node.text;
104
96
  for (let i = 0, n = bindings.length; i < n; i++) {
105
97
  let b = bindings[i];
106
- if (b.name === name && inScope(node, b)) {
98
+ if (b.name === name && (!binding || b.depth >= binding.depth) && inScope(node, b)) {
107
99
  binding = b;
108
100
  }
109
101
  }
@@ -178,11 +170,12 @@ function visit(ctx, node) {
178
170
  }
179
171
  }
180
172
  }
181
- ts.forEachChild(node, n => visit(ctx, n));
173
+ node.forEachChild(n => visit(ctx, n));
182
174
  }
183
175
  export default (sourceFile, bindings, isReactiveCall) => {
184
176
  let ctx = {
185
177
  bindings,
178
+ calls: [],
186
179
  isReactiveCall,
187
180
  replacements: [],
188
181
  scopedBindings: [],
@@ -190,5 +183,5 @@ export default (sourceFile, bindings, isReactiveCall) => {
190
183
  tmpCounter: 0
191
184
  };
192
185
  visit(ctx, sourceFile);
193
- return ctx.replacements;
186
+ return { calls: ctx.calls, replacements: ctx.replacements };
194
187
  };
@@ -1,3 +1,5 @@
1
+ import type { ts } from '@esportsplus/typescript';
1
2
  import { TYPES } from './constants.js';
2
3
  type Bindings = Map<string, TYPES>;
3
- export type { Bindings };
4
+ type IsReactiveCall = (node: ts.Node) => node is ts.CallExpression;
5
+ export type { Bindings, IsReactiveCall };
@@ -3,11 +3,11 @@ declare const PACKAGE_NAME = "@esportsplus/reactivity";
3
3
  declare const REACTIVE_ARRAY: unique symbol;
4
4
  declare const REACTIVE_OBJECT: unique symbol;
5
5
  declare const SIGNAL: unique symbol;
6
+ declare const STABILIZER_DEFERRED = 4;
6
7
  declare const STABILIZER_IDLE = 0;
7
8
  declare const STABILIZER_RESCHEDULE = 1;
8
9
  declare const STABILIZER_RUNNING = 2;
9
10
  declare const STABILIZER_SCHEDULED = 3;
10
- declare const STATE_NONE = 0;
11
11
  declare const STATE_CHECK: number;
12
12
  declare const STATE_DIRTY: number;
13
13
  declare const STATE_RECOMPUTING: number;
@@ -16,4 +16,4 @@ declare const STATE_COMPUTED: number;
16
16
  declare const STATE_ERROR: number;
17
17
  declare const STATE_EFFECT: number;
18
18
  declare const STATE_NOTIFY_MASK: number;
19
- export { COMPUTED, PACKAGE_NAME, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NONE, STATE_NOTIFY_MASK, STATE_RECOMPUTING };
19
+ export { COMPUTED, PACKAGE_NAME, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL, STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING };
@@ -3,11 +3,11 @@ const PACKAGE_NAME = '@esportsplus/reactivity';
3
3
  const REACTIVE_ARRAY = Symbol('reactivity.reactive.array');
4
4
  const REACTIVE_OBJECT = Symbol('reactivity.reactive.object');
5
5
  const SIGNAL = Symbol('reactivity.signal');
6
+ const STABILIZER_DEFERRED = 4;
6
7
  const STABILIZER_IDLE = 0;
7
8
  const STABILIZER_RESCHEDULE = 1;
8
9
  const STABILIZER_RUNNING = 2;
9
10
  const STABILIZER_SCHEDULED = 3;
10
- const STATE_NONE = 0;
11
11
  const STATE_CHECK = 1 << 0;
12
12
  const STATE_DIRTY = 1 << 1;
13
13
  const STATE_RECOMPUTING = 1 << 2;
@@ -16,4 +16,4 @@ const STATE_COMPUTED = 1 << 4;
16
16
  const STATE_ERROR = 1 << 5;
17
17
  const STATE_EFFECT = 1 << 6;
18
18
  const STATE_NOTIFY_MASK = (STATE_CHECK | STATE_DIRTY);
19
- export { COMPUTED, PACKAGE_NAME, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NONE, STATE_NOTIFY_MASK, STATE_RECOMPUTING };
19
+ export { COMPUTED, PACKAGE_NAME, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL, STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING };
@@ -40,7 +40,7 @@ type Listeners<T> = {
40
40
  declare class ReactiveArray<T> extends Array<T> {
41
41
  private _length;
42
42
  listeners: Listeners<T>;
43
- constructor(...items: T[]);
43
+ constructor(items?: readonly T[]);
44
44
  static get [Symbol.species](): ArrayConstructor;
45
45
  get $length(): number;
46
46
  set $length(value: number);
@@ -1,5 +1,5 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
- import { REACTIVE_ARRAY } from '../constants.js';
2
+ import { PACKAGE_NAME, REACTIVE_ARRAY } from '../constants.js';
3
3
  import { read, signal, write } from '../system.js';
4
4
  import { isReactiveObject } from './object.js';
5
5
  function dispose(value) {
@@ -10,9 +10,13 @@ function dispose(value) {
10
10
  class ReactiveArray extends Array {
11
11
  _length;
12
12
  listeners = {};
13
- constructor(...items) {
14
- super(...items);
15
- this._length = signal(items.length);
13
+ constructor(items) {
14
+ super();
15
+ let n = items ? items.length : 0;
16
+ for (let i = 0; i < n; i++) {
17
+ this[i] = items[i];
18
+ }
19
+ this._length = signal(n);
16
20
  }
17
21
  static get [Symbol.species]() {
18
22
  return Array;
@@ -37,6 +41,7 @@ class ReactiveArray extends Array {
37
41
  write(this._length, i + 1);
38
42
  }
39
43
  this.dispatch('set', { index: i, item: value });
44
+ dispose(prev);
40
45
  }
41
46
  clear() {
42
47
  this.dispose();
@@ -69,7 +74,7 @@ class ReactiveArray extends Array {
69
74
  if (!listeners) {
70
75
  return;
71
76
  }
72
- let dirty = false;
77
+ let dirty = false, errors = null;
73
78
  for (let i = 0, n = listeners.length; i < n; i++) {
74
79
  let listener = listeners[i];
75
80
  if (listener === null) {
@@ -82,9 +87,10 @@ class ReactiveArray extends Array {
82
87
  listeners[i] = null;
83
88
  }
84
89
  }
85
- catch {
90
+ catch (e) {
86
91
  dirty = true;
87
92
  listeners[i] = null;
93
+ (errors ??= []).push(e);
88
94
  }
89
95
  }
90
96
  if (dirty) {
@@ -92,6 +98,14 @@ class ReactiveArray extends Array {
92
98
  listeners.pop();
93
99
  }
94
100
  }
101
+ if (errors !== null) {
102
+ let error = errors.length === 1
103
+ ? errors[0]
104
+ : new AggregateError(errors, `${PACKAGE_NAME}: dispatch produced multiple errors`);
105
+ queueMicrotask(() => {
106
+ throw error;
107
+ });
108
+ }
95
109
  }
96
110
  dispose() {
97
111
  while (this.length) {
@@ -169,20 +183,17 @@ class ReactiveArray extends Array {
169
183
  super.sort(fn);
170
184
  let buckets = new Map(), order = new Array(n);
171
185
  for (let i = 0; i < n; i++) {
172
- let value = before[i], list = buckets.get(value);
173
- if (!list) {
174
- buckets.set(value, [i]);
186
+ let value = before[i], bucket = buckets.get(value);
187
+ if (bucket === undefined) {
188
+ buckets.set(value, { indices: [i], next: 0 });
175
189
  }
176
190
  else {
177
- list.push(i);
191
+ bucket.indices.push(i);
178
192
  }
179
193
  }
180
194
  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
- }
195
+ let bucket = buckets.get(this[i]);
196
+ order[i] = bucket.indices[bucket.next++];
186
197
  }
187
198
  this.dispatch('sort', { order });
188
199
  }
@@ -4,25 +4,16 @@ import { PACKAGE_NAME } from '../constants.js';
4
4
  import { ReactiveArray } from './array.js';
5
5
  import { ReactiveObject } from './object.js';
6
6
  function reactive(input) {
7
- let dispose = false, value = root(() => {
8
- let response;
7
+ let value = root(() => {
9
8
  if (isObject(input)) {
10
- response = new ReactiveObject(input);
9
+ return new ReactiveObject(input);
11
10
  }
12
- else if (isArray(input)) {
13
- response = new ReactiveArray(...input);
14
- }
15
- if (response) {
16
- if (root.disposables) {
17
- dispose = true;
18
- }
19
- return response;
11
+ if (isArray(input)) {
12
+ return new ReactiveArray(input);
20
13
  }
21
14
  throw new Error(`${PACKAGE_NAME}: 'reactive' received invalid input - ${JSON.stringify(input)}`);
22
15
  });
23
- if (dispose) {
24
- onCleanup(() => value.dispose());
25
- }
16
+ onCleanup(() => value.dispose());
26
17
  return value;
27
18
  }
28
19
  export default reactive;
@@ -46,7 +46,7 @@ class ReactiveObject {
46
46
  });
47
47
  }
48
48
  [REACTIVE_ARRAY](value) {
49
- let node = new ReactiveArray(...value);
49
+ let node = new ReactiveArray(value);
50
50
  (this.disposers ??= []).push(() => node.dispose());
51
51
  return node;
52
52
  }
package/build/system.d.ts CHANGED
@@ -1,25 +1,22 @@
1
1
  import { Computed, ComputedResult, Settled, Signal } from './types.js';
2
2
  declare const batch: <T>(fn: () => T) => T;
3
- declare const computed: {
4
- <T>(fn: Computed<T>["fn"], equals?: ((a: Settled<T>, b: Settled<T>) => boolean) | null): ComputedResult<T>;
5
- invalidate<T>(c: Computed<T>): void;
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>["fn"], apply?: (value: T, prev: T | undefined) => void) => () => void;
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 const root: {
16
- <T>(fn: ((dispose: VoidFunction) => T) | (() => T)): T;
17
- disposables: number;
18
- };
19
- declare const signal: {
20
- <T>(value: T, equals?: ((a: T, b: T) => boolean) | null): Signal<T>;
21
- selector<T>(node: Signal<T>, key: T): boolean;
22
- };
15
+ declare const root: <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => T;
16
+ declare function signal<T>(value: T, equals?: ((a: T, b: T) => boolean) | null): Signal<T>;
17
+ declare namespace signal {
18
+ var selector: <T>(node: Signal<T>, key: T) => boolean;
19
+ }
23
20
  declare const untrack: <T>(fn: () => T) => T;
24
21
  declare const write: <T>(signal: Signal<T>, value: T) => void;
25
22
  export { batch, computed, dispose, effect, flush, isComputed, isSignal, onCleanup, peek, read, root, signal, untrack, write };