@esportsplus/reactivity 0.39.2 → 0.41.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 (53) hide show
  1. package/README.md +12 -6
  2. package/bench/compiler.bench.ts +115 -0
  3. package/bench/kairo.bench.ts +1 -1
  4. package/bench/reactive/array.bench.ts +1 -0
  5. package/bench/reactive/reactive-object.bench.ts +3 -2
  6. package/build/compiler/array.js +28 -25
  7. package/build/compiler/bindings.d.ts +3 -1
  8. package/build/compiler/bindings.js +32 -9
  9. package/build/compiler/index.d.ts +3 -2
  10. package/build/compiler/index.js +21 -13
  11. package/build/compiler/object.js +12 -13
  12. package/build/compiler/plugins/vite.d.ts +13 -4
  13. package/build/compiler/primitives.js +9 -9
  14. package/build/constants.d.ts +2 -1
  15. package/build/constants.js +2 -1
  16. package/build/index.d.ts +1 -0
  17. package/build/reactive/array.d.ts +40 -1
  18. package/build/reactive/array.js +82 -16
  19. package/build/reactive/index.d.ts +1 -1
  20. package/build/reactive/object.d.ts +3 -3
  21. package/build/reactive/object.js +1 -1
  22. package/build/system.d.ts +3 -2
  23. package/build/system.js +10 -5
  24. package/oxlintrc.json +3 -0
  25. package/package.json +15 -7
  26. package/pnpm-workspace.yaml +4 -7
  27. package/src/compiler/array.ts +30 -27
  28. package/src/compiler/bindings.ts +49 -10
  29. package/src/compiler/index.ts +37 -20
  30. package/src/compiler/object.ts +14 -16
  31. package/src/compiler/primitives.ts +13 -10
  32. package/src/constants.ts +4 -1
  33. package/src/index.ts +1 -0
  34. package/src/reactive/array.ts +133 -22
  35. package/src/reactive/index.ts +3 -3
  36. package/src/reactive/object.ts +4 -4
  37. package/src/system.ts +20 -5
  38. package/test/compiler/__snapshots__/output.test.ts.snap +157 -0
  39. package/test/compiler/compiler.test.ts +51 -9
  40. package/test/compiler/composition.test.ts +110 -0
  41. package/test/compiler/coverage.test.ts +22 -22
  42. package/test/compiler/origins.test.ts +156 -0
  43. package/test/compiler/output.test.ts +116 -0
  44. package/test/ownership.test.ts +100 -1
  45. package/test/reactive/array.test.ts +189 -8
  46. package/test/reactive/reactive.test.ts +4 -4
  47. package/test/system.test.ts +3 -4
  48. package/test/untrack.test.ts +1 -1
  49. package/tsconfig.build.json +3 -0
  50. package/tsconfig.json +2 -2
  51. package/vitest.config.ts +2 -2
  52. package/bench/tsconfig.json +0 -16
  53. package/test/tsconfig.json +0 -16
@@ -15,5 +15,6 @@ declare const STATE_IN_HEAP: number;
15
15
  declare const STATE_COMPUTED: number;
16
16
  declare const STATE_ERROR: number;
17
17
  declare const STATE_EFFECT: number;
18
+ declare const STATE_DISPOSED: number;
18
19
  declare const STATE_NOTIFY_MASK: number;
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 };
20
+ 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_DISPOSED, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING };
@@ -15,5 +15,6 @@ const STATE_IN_HEAP = 1 << 3;
15
15
  const STATE_COMPUTED = 1 << 4;
16
16
  const STATE_ERROR = 1 << 5;
17
17
  const STATE_EFFECT = 1 << 6;
18
+ const STATE_DISPOSED = 1 << 7;
18
19
  const STATE_NOTIFY_MASK = (STATE_CHECK | STATE_DIRTY);
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 };
20
+ 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_DISPOSED, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING };
package/build/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { isPromise } from '@esportsplus/utilities';
2
2
  export { COMPUTED, REACTIVE_ARRAY, REACTIVE_OBJECT, SIGNAL } from './constants.js';
3
3
  export { default as reactive, ReactiveArray, ReactiveObject } from './reactive/index.js';
4
+ export type { ArrayOp } from './reactive/array.js';
4
5
  export * from './system.js';
5
6
  export * from './types.js';
@@ -1,4 +1,37 @@
1
+ type ArrayOp<T> = {
2
+ items: T[];
3
+ op: 'concat';
4
+ } | {
5
+ deleteCount: number;
6
+ items: T[];
7
+ op: 'splice';
8
+ start: number;
9
+ } | {
10
+ items: T[];
11
+ op: 'push';
12
+ } | {
13
+ index: number;
14
+ item: T;
15
+ op: 'set';
16
+ } | {
17
+ items: T[];
18
+ op: 'unshift';
19
+ } | {
20
+ op: 'clear';
21
+ } | {
22
+ item: T;
23
+ op: 'pop';
24
+ } | {
25
+ op: 'reverse';
26
+ } | {
27
+ item: T;
28
+ op: 'shift';
29
+ } | {
30
+ op: 'sort';
31
+ order: number[];
32
+ };
1
33
  type Events<T> = {
34
+ batch: ArrayOp<T>[];
2
35
  clear: undefined;
3
36
  concat: {
4
37
  items: T[];
@@ -39,12 +72,18 @@ type Listeners<T> = {
39
72
  };
40
73
  declare class ReactiveArray<T> extends Array<T> {
41
74
  private _length;
75
+ private batched;
76
+ private delivering;
42
77
  listeners: Listeners<T>;
43
78
  constructor(items?: readonly T[]);
44
79
  static get [Symbol.species](): ArrayConstructor;
45
80
  get $length(): number;
46
81
  set $length(value: number);
47
82
  $set(i: number, value: T): void;
83
+ private batch;
84
+ private prune;
85
+ private record;
86
+ private unbatch;
48
87
  clear(): void;
49
88
  concat(...items: ConcatArray<T>[]): ReactiveArray<T>;
50
89
  concat(...items: (T | ConcatArray<T>)[]): ReactiveArray<T>;
@@ -52,7 +91,6 @@ declare class ReactiveArray<T> extends Array<T> {
52
91
  dispose(): void;
53
92
  on<K extends keyof Events<T>>(event: K, listener: Listener<Events<T>[K]>): VoidFunction;
54
93
  once<K extends keyof Events<T>>(event: K, listener: Listener<Events<T>[K]>): VoidFunction;
55
- private prune;
56
94
  pop(): T | undefined;
57
95
  push(...items: T[]): number;
58
96
  reverse(): this;
@@ -62,3 +100,4 @@ declare class ReactiveArray<T> extends Array<T> {
62
100
  unshift(...items: T[]): number;
63
101
  }
64
102
  export { ReactiveArray };
103
+ export type { ArrayOp };
@@ -1,6 +1,6 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
2
  import { PACKAGE_NAME, REACTIVE_ARRAY } from '../constants.js';
3
- import { read, signal, write } from '../system.js';
3
+ import { effect, read, root, signal, write } from '../system.js';
4
4
  import { isReactiveObject } from './object.js';
5
5
  function dispose(value) {
6
6
  if (isReactiveObject(value)) {
@@ -9,6 +9,8 @@ function dispose(value) {
9
9
  }
10
10
  class ReactiveArray extends Array {
11
11
  _length;
12
+ batched = null;
13
+ delivering = false;
12
14
  listeners = {};
13
15
  constructor(items) {
14
16
  super();
@@ -41,12 +43,71 @@ class ReactiveArray extends Array {
41
43
  write(this._length, i + 1);
42
44
  }
43
45
  this.dispatch('set', { index: i, item: value });
46
+ this.record({ index: i, item: value, op: 'set' });
44
47
  dispose(prev);
45
48
  }
49
+ batch() {
50
+ let tick = signal(0);
51
+ this.batched = {
52
+ ops: null,
53
+ stop: root(() => effect(() => {
54
+ read(tick);
55
+ let batched = this.batched, ops = batched?.ops;
56
+ if (!ops) {
57
+ return;
58
+ }
59
+ batched.ops = null;
60
+ this.delivering = true;
61
+ try {
62
+ root(() => this.dispatch('batch', ops));
63
+ }
64
+ finally {
65
+ this.delivering = false;
66
+ }
67
+ if (this.listeners.batch === undefined) {
68
+ queueMicrotask(() => {
69
+ if (this.listeners.batch === undefined) {
70
+ this.unbatch();
71
+ }
72
+ });
73
+ }
74
+ })),
75
+ tick
76
+ };
77
+ }
78
+ prune(event, listeners) {
79
+ while (listeners.length && listeners[listeners.length - 1] === null) {
80
+ listeners.pop();
81
+ }
82
+ if (listeners.length === 0 && this.listeners[event] === listeners) {
83
+ delete this.listeners[event];
84
+ }
85
+ }
86
+ record(op) {
87
+ let batched = this.batched;
88
+ if (batched === null) {
89
+ return;
90
+ }
91
+ if (op.op === 'clear' || batched.ops === null) {
92
+ batched.ops = [op];
93
+ }
94
+ else {
95
+ batched.ops.push(op);
96
+ }
97
+ write(batched.tick, batched.tick.value + 1);
98
+ }
99
+ unbatch() {
100
+ let batched = this.batched;
101
+ if (batched !== null) {
102
+ this.batched = null;
103
+ batched.stop();
104
+ }
105
+ }
46
106
  clear() {
47
107
  this.dispose();
48
108
  write(this._length, 0);
49
109
  this.dispatch('clear');
110
+ this.record({ op: 'clear' });
50
111
  }
51
112
  concat(...items) {
52
113
  let added = [];
@@ -66,6 +127,7 @@ class ReactiveArray extends Array {
66
127
  if (added.length) {
67
128
  write(this._length, this.length);
68
129
  this.dispatch('concat', { items: added });
130
+ this.record({ items: added, op: 'concat' });
69
131
  }
70
132
  return this;
71
133
  }
@@ -96,6 +158,9 @@ class ReactiveArray extends Array {
96
158
  if (dirty) {
97
159
  this.prune(event, listeners);
98
160
  }
161
+ if (event === 'batch' && !this.delivering && this.listeners.batch === undefined) {
162
+ this.unbatch();
163
+ }
99
164
  if (errors !== null) {
100
165
  let error = errors.length === 1
101
166
  ? errors[0]
@@ -113,6 +178,9 @@ class ReactiveArray extends Array {
113
178
  }
114
179
  on(event, listener) {
115
180
  let entry = listener, listeners = this.listeners[event];
181
+ if (event === 'batch' && this.batched === null) {
182
+ this.batch();
183
+ }
116
184
  if (listeners === undefined) {
117
185
  this.listeners[event] = [entry];
118
186
  }
@@ -143,20 +211,15 @@ class ReactiveArray extends Array {
143
211
  current[i] = null;
144
212
  this.prune(event, current);
145
213
  }
214
+ if (event === 'batch' && !this.delivering && this.listeners.batch === undefined) {
215
+ this.unbatch();
216
+ }
146
217
  };
147
218
  }
148
219
  once(event, listener) {
149
220
  listener.once = true;
150
221
  return this.on(event, listener);
151
222
  }
152
- prune(event, listeners) {
153
- while (listeners.length && listeners[listeners.length - 1] === null) {
154
- listeners.pop();
155
- }
156
- if (listeners.length === 0 && this.listeners[event] === listeners) {
157
- delete this.listeners[event];
158
- }
159
- }
160
223
  pop() {
161
224
  if (this.length === 0) {
162
225
  return undefined;
@@ -165,6 +228,7 @@ class ReactiveArray extends Array {
165
228
  dispose(item);
166
229
  write(this._length, this.length);
167
230
  this.dispatch('pop', { item });
231
+ this.record({ item, op: 'pop' });
168
232
  return item;
169
233
  }
170
234
  push(...items) {
@@ -174,11 +238,13 @@ class ReactiveArray extends Array {
174
238
  let length = super.push(...items);
175
239
  write(this._length, length);
176
240
  this.dispatch('push', { items });
241
+ this.record({ items, op: 'push' });
177
242
  return length;
178
243
  }
179
244
  reverse() {
180
245
  super.reverse();
181
246
  this.dispatch('reverse');
247
+ this.record({ op: 'reverse' });
182
248
  return this;
183
249
  }
184
250
  shift() {
@@ -189,10 +255,11 @@ class ReactiveArray extends Array {
189
255
  dispose(item);
190
256
  write(this._length, this.length);
191
257
  this.dispatch('shift', { item });
258
+ this.record({ item, op: 'shift' });
192
259
  return item;
193
260
  }
194
261
  sort(fn) {
195
- if (this.listeners.sort === undefined) {
262
+ if (this.listeners.sort === undefined && this.batched === null) {
196
263
  super.sort(fn);
197
264
  }
198
265
  else {
@@ -216,6 +283,7 @@ class ReactiveArray extends Array {
216
283
  order[i] = bucket.indices[bucket.next++];
217
284
  }
218
285
  this.dispatch('sort', { order });
286
+ this.record({ op: 'sort', order });
219
287
  }
220
288
  return this;
221
289
  }
@@ -226,12 +294,9 @@ class ReactiveArray extends Array {
226
294
  for (let i = 0, n = removed.length; i < n; i++) {
227
295
  dispose(removed[i]);
228
296
  }
229
- let index = Math.trunc(start) || 0;
230
- this.dispatch('splice', {
231
- deleteCount: removed.length,
232
- items,
233
- start: index < 0 ? Math.max(length + index, 0) : Math.min(index, length)
234
- });
297
+ let index = Math.trunc(start) || 0, resolved = index < 0 ? Math.max(length + index, 0) : Math.min(index, length);
298
+ this.dispatch('splice', { deleteCount: removed.length, items, start: resolved });
299
+ this.record({ deleteCount: removed.length, items, op: 'splice', start: resolved });
235
300
  }
236
301
  return removed;
237
302
  }
@@ -242,6 +307,7 @@ class ReactiveArray extends Array {
242
307
  let length = super.unshift(...items);
243
308
  write(this._length, length);
244
309
  this.dispatch('unshift', { items });
310
+ this.record({ items, op: 'unshift' });
245
311
  return length;
246
312
  }
247
313
  }
@@ -1,6 +1,6 @@
1
- import type { Reactive } from '../types.js';
2
1
  import { ReactiveArray } from './array.js';
3
2
  import { ReactiveObject } from './object.js';
3
+ import type { Reactive } from '../types.js';
4
4
  type Guard<T> = T extends Record<PropertyKey, unknown> ? T extends {
5
5
  dispose: unknown;
6
6
  } ? {
@@ -1,12 +1,12 @@
1
1
  import { COMPUTED, REACTIVE_ARRAY, SIGNAL } from '../constants.js';
2
- import { Computed } from '../types.js';
3
2
  import { ReactiveArray } from './array.js';
3
+ import type { Computed } from '../types.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): import("../types.js").ComputedResult<ReturnType<T>>;
7
+ protected [COMPUTED]<T extends Computed<ReturnType<T>>['fn']>(value: T): import("../index.js").ComputedResult<ReturnType<T>>;
8
8
  protected [REACTIVE_ARRAY]<U>(value: U[]): ReactiveArray<U>;
9
- protected [SIGNAL]<T>(value: T): import("../types.js").Signal<T>;
9
+ protected [SIGNAL]<T>(value: T): import("../index.js").Signal<T>;
10
10
  dispose(): void;
11
11
  }
12
12
  declare const isReactiveObject: (value: unknown) => value is ReactiveObject<Record<PropertyKey, unknown>>;
@@ -58,7 +58,7 @@ class ReactiveObject {
58
58
  if (!disposers) {
59
59
  return;
60
60
  }
61
- while (disposer = disposers.pop()) {
61
+ while ((disposer = disposers.pop())) {
62
62
  disposer();
63
63
  }
64
64
  }
package/build/system.d.ts CHANGED
@@ -13,12 +13,13 @@ declare const isSignal: (value: unknown) => value is Signal<unknown>;
13
13
  declare const onCleanup: (fn: VoidFunction) => typeof fn;
14
14
  declare const peek: <T>(node: Signal<T> | Computed<T>) => T;
15
15
  declare const read: <T>(node: Signal<T> | Computed<T>) => T;
16
- declare const root: <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => T;
16
+ declare const owner: () => Computed<unknown> | null;
17
+ declare const root: <T>(fn: ((dispose: VoidFunction) => T) | (() => T), parent?: Computed<unknown> | null) => T;
17
18
  declare function signal<T>(value: T, equals?: ((a: T, b: T) => boolean) | null): Signal<T>;
18
19
  declare namespace signal {
19
20
  var selector: <T>(node: Signal<T>, key: T) => boolean;
20
21
  }
21
22
  declare const untrack: <T>(fn: () => T) => T;
22
23
  declare const write: <T>(signal: Signal<T>, value: T) => void;
23
- export { batch, computed, dispose, effect, flush, hasOwner, isComputed, isSignal, onCleanup, peek, read, root, signal, untrack, write };
24
+ export { batch, computed, dispose, effect, flush, hasOwner, isComputed, isSignal, onCleanup, owner, peek, read, root, signal, untrack, write };
24
25
  export type { Computed, Signal };
package/build/system.js CHANGED
@@ -1,4 +1,4 @@
1
- import { PACKAGE_NAME, 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 } from './constants.js';
1
+ import { PACKAGE_NAME, SIGNAL, STABILIZER_DEFERRED, STABILIZER_IDLE, STABILIZER_RESCHEDULE, STABILIZER_RUNNING, STABILIZER_SCHEDULED, STATE_CHECK, STATE_COMPUTED, STATE_DIRTY, STATE_DISPOSED, STATE_EFFECT, STATE_ERROR, STATE_IN_HEAP, STATE_NOTIFY_MASK, STATE_RECOMPUTING } from './constants.js';
2
2
  import { isObject, isPromise } from '@esportsplus/utilities';
3
3
  let asyncFactory = new WeakMap(), depth = 0, disposeHead = null, draining = false, heap = new Array(64), heap_i = 0, heap_n = 0, linkPoolHead = null, microtask = queueMicrotask, notified = false, observer = null, pendingHead = null, pulled = null, puller = null, scope = null, stabilizer = STABILIZER_IDLE, version = 0, walkPoolHead = null, writes = 0;
4
4
  function noop() {
@@ -623,6 +623,7 @@ const dispose = (computed) => {
623
623
  try {
624
624
  for (;;) {
625
625
  deleteFromHeap(node);
626
+ node.state |= STATE_DISPOSED;
626
627
  let dep = node.deps;
627
628
  while (dep) {
628
629
  dep = unlink(dep);
@@ -751,12 +752,16 @@ const read = (node) => {
751
752
  }
752
753
  return node.value;
753
754
  };
754
- const root = (fn) => {
755
+ const owner = () => observer || scope;
756
+ const root = (fn, parent) => {
757
+ if (parent && parent.state & STATE_DISPOSED) {
758
+ throw new Error(`${PACKAGE_NAME}: root() was given a disposed owner`);
759
+ }
755
760
  let o = observer, s = scope, tracking = fn.length, value;
756
761
  observer = null;
757
762
  try {
758
763
  if (tracking) {
759
- let self = makeNode(noop), owner = o || s;
764
+ let self = makeNode(noop), owner = parent === undefined ? o || s : parent;
760
765
  if (owner !== null) {
761
766
  own(owner, self);
762
767
  }
@@ -764,7 +769,7 @@ const root = (fn) => {
764
769
  value = fn(() => dispose(self));
765
770
  }
766
771
  else {
767
- scope = null;
772
+ scope = parent ?? null;
768
773
  value = fn();
769
774
  }
770
775
  }
@@ -854,4 +859,4 @@ const write = (signal, value) => {
854
859
  }
855
860
  schedule();
856
861
  };
857
- export { batch, computed, dispose, effect, flush, hasOwner, isComputed, isSignal, onCleanup, peek, read, root, signal, untrack, write };
862
+ export { batch, computed, dispose, effect, flush, hasOwner, isComputed, isSignal, onCleanup, owner, peek, read, root, signal, untrack, write };
package/oxlintrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["./node_modules/@esportsplus/typescript/oxlintrc.json"]
3
+ }
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
+ "@esportsplus/typescript": "^0.39.0",
4
5
  "@esportsplus/utilities": "^0.28.6"
5
6
  },
6
7
  "devDependencies": {
7
- "@esportsplus/typescript": "^0.38.0",
8
- "@types/node": "^26.4.1",
9
- "vite": "^8.2.2",
10
- "vitest": "^5.0.0"
8
+ "@types/node": "^26.6.6",
9
+ "oxlint": "^1.90.0",
10
+ "oxlint-tsgolint": "^7.0.2005",
11
+ "vite": "^8.3.5",
12
+ "vitest": "^5.1.0"
13
+ },
14
+ "engines": {
15
+ "node": ">=26"
11
16
  },
12
17
  "exports": {
13
18
  ".": {
@@ -36,13 +41,16 @@
36
41
  },
37
42
  "type": "module",
38
43
  "types": "build/index.d.ts",
39
- "version": "0.39.2",
44
+ "version": "0.41.0",
40
45
  "scripts": {
41
46
  "agent:bench": "vitest bench --run",
42
- "agent:test": "tsc --noEmit && vitest run",
47
+ "agent:test": "pnpm typecheck && vitest run",
43
48
  "bench": "vitest bench --run",
44
- "build": "tsc",
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "lint": "oxlint -c oxlintrc.json --type-aware --deny-warnings",
45
51
  "test": "vitest run",
52
+ "typecheck": "tsc --noEmit",
53
+ "verify": "pnpm build && pnpm typecheck && pnpm lint && pnpm test",
46
54
  "-": "-"
47
55
  }
48
56
  }
@@ -1,10 +1,7 @@
1
1
  allowBuilds:
2
2
  esbuild: true
3
3
  minimumReleaseAgeExclude:
4
- - '@esportsplus/utilities@0.28.1 || 0.28.3 || 0.28.4 || 0.28.6'
5
- - '@esportsplus/typescript@0.31.0 || 0.32.0 || 0.33.0 || 0.33.2 || 0.33.4 || 0.34.1 || 0.34.3 || 0.34.4 || 0.36.0 || 0.36.1 || 0.37.0 || 0.38.0'
6
- - tsc-alias@1.9.4
7
- - '@vitest/mocker@5.0.0'
8
- - '@vitest/spy@5.0.0'
9
- - vitest@5.0.0
10
- - '@esportsplus/cli-passthrough@0.0.18'
4
+ - '@esportsplus/*'
5
+ - '@vitest/mocker@5.1.0'
6
+ - '@vitest/spy@5.1.0'
7
+ - vitest@5.1.0
@@ -68,10 +68,10 @@ function declare(ctx: { isReactiveCall: IsReactiveCall; replacements: Replacemen
68
68
  let typeParam = elementType ? `<${elementType}>` : '';
69
69
 
70
70
  ctx.replacements.push({
71
- node,
72
- generate: (sf) => expression.elements.length > 0
73
- ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${expression.getText(sf)})`
74
- : ` new ${NAMESPACE}.ReactiveArray${typeParam}()`
71
+ generate: (_sourceFile, text) => expression.elements.length > 0
72
+ ? ` new ${NAMESPACE}.ReactiveArray${typeParam}(${text(expression)})`
73
+ : ` new ${NAMESPACE}.ReactiveArray${typeParam}()`,
74
+ node
75
75
  });
76
76
  }
77
77
  }
@@ -126,10 +126,10 @@ function length(access: ts.PropertyAccessExpression, replacements: ReplacementIn
126
126
  let op = COMPOUND_OPERATORS.get(parent.operatorToken.kind);
127
127
 
128
128
  replacements.push({
129
- node: parent,
130
- generate: (sf) => op === undefined
131
- ? `${expr.getText(sf)}.$length = ${parent.right.getText(sf)}`
132
- : `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} ${parent.right.getText(sf)}`
129
+ generate: (_sourceFile, text) => op === undefined
130
+ ? `${text(expr)}.$length = ${text(parent.right)}`
131
+ : `${text(expr)}.$length = ${text(expr)}.length ${op} ${text(parent.right)}`,
132
+ node: parent
133
133
  });
134
134
  }
135
135
  // arr.length++ / arr.length-- / ++arr.length / --arr.length
@@ -141,24 +141,22 @@ function length(access: ts.PropertyAccessExpression, replacements: ReplacementIn
141
141
  let op = parent.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
142
142
 
143
143
  replacements.push({
144
- node: parent,
145
- generate: (sf) => `${expr.getText(sf)}.$length = ${expr.getText(sf)}.length ${op} 1`
144
+ generate: (_sourceFile, text) => `${text(expr)}.$length = ${text(expr)}.length ${op} 1`,
145
+ node: parent
146
146
  });
147
147
  }
148
148
  // Read-only: arr.length -> arr.$length
149
149
  else {
150
150
  replacements.push({
151
- node: access,
152
- generate: (sf) => `${expr.getText(sf)}.$length`
151
+ generate: (_sourceFile, text) => `${text(expr)}.$length`,
152
+ node: access
153
153
  });
154
154
  }
155
155
  }
156
156
 
157
157
  // An element write must notify, so it goes through $set; operands are evaluated once
158
158
  function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): void {
159
- let parent = access.parent!,
160
- array = (sf: ts.SourceFile) => access.expression.getText(sf),
161
- index = (sf: ts.SourceFile) => access.argumentExpression.getText(sf);
159
+ let parent = access.parent!;
162
160
 
163
161
  if (ts.isDeleteExpression(parent)) {
164
162
  ctx.failures.push({ message: 'an element of a reactive array cannot be deleted; use splice()', node: parent });
@@ -170,10 +168,10 @@ function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): v
170
168
  right = parent.right;
171
169
 
172
170
  ctx.replacements.push({
173
- node: parent,
174
- generate: (sf) => op === undefined
175
- ? `${array(sf)}.$set(${index(sf)}, ${right.getText(sf)})`
176
- : `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${right.getText(sf)})))(${array(sf)}, ${index(sf)})`
171
+ generate: (_sourceFile, text) => op === undefined
172
+ ? `${text(access.expression)}.$set(${text(access.argumentExpression)}, ${text(right)})`
173
+ : `((_a, _k) => _a.$set(_k, _a[_k] ${op} (${text(right)})))(${text(access.expression)}, ${text(access.argumentExpression)})`,
174
+ node: parent
177
175
  });
178
176
 
179
177
  return;
@@ -183,10 +181,15 @@ function write(access: ts.ElementAccessExpression, ctx: ArrayTransformResult): v
183
181
  op = unary.operator === ts.SyntaxKind.PlusPlusToken ? '+' : '-';
184
182
 
185
183
  ctx.replacements.push({
186
- node: unary,
187
- generate: (sf) => ts.isPrefixUnaryExpression(unary)
188
- ? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array(sf)}, ${index(sf)})`
189
- : `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array(sf)}, ${index(sf)}, ${array(sf)}[${index(sf)}])`
184
+ generate: (_sourceFile, text) => {
185
+ let array = text(access.expression),
186
+ index = text(access.argumentExpression);
187
+
188
+ return ts.isPrefixUnaryExpression(unary)
189
+ ? `((_a, _k) => (_a.$set(_k, _a[_k] ${op} 1), _a[_k]))(${array}, ${index})`
190
+ : `((_a, _k, _v) => (_a.$set(_k, _v ${op} 1), _v))(${array}, ${index}, ${array}[${index}])`;
191
+ },
192
+ node: unary
190
193
  });
191
194
  }
192
195
 
@@ -234,10 +237,10 @@ export default (sourceFile: ts.SourceFile, bindings: Bindings, isReactiveCall: I
234
237
  elements = scope.unwrap(assignment.right) as ts.ArrayLiteralExpression;
235
238
 
236
239
  result.replacements.push({
237
- node: assignment,
238
- generate: (sf) => elements.elements.length > 0
239
- ? `${name}.splice(0, ${name}.length, ...${elements.getText(sf)})`
240
- : `${name}.splice(0, ${name}.length)`
240
+ generate: (_sourceFile, text) => elements.elements.length > 0
241
+ ? `${name}.splice(0, ${name}.length, ...${text(elements)})`
242
+ : `${name}.splice(0, ${name}.length)`,
243
+ node: assignment
241
244
  });
242
245
  }
243
246
 
@@ -1,5 +1,5 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { references } from '@esportsplus/typescript/compiler';
2
+ import { checker, references } from '@esportsplus/typescript/compiler';
3
3
  import type { Origin } from '@esportsplus/typescript/compiler';
4
4
  import { ENTRYPOINT, PACKAGE_NAME, TYPES } from './constants';
5
5
  import type { Bindings } from './types';
@@ -7,7 +7,9 @@ import type { Bindings } from './types';
7
7
 
8
8
  type Assignment = { call: ts.CallExpression; target: ts.Identifier };
9
9
 
10
- // Per program snapshot: answers about declarations and types hold for the snapshot's lifetime
10
+ // Per program snapshot: answers about declarations and types hold for the snapshot's lifetime. `arrays`
11
+ // and `kinds` hold answers built from queries the shared checker does not memoize (getPropertyOfType,
12
+ // getNonNullableType, isArrayType), so dropping them costs a round-trip per array candidate or read
11
13
  type Caches = {
12
14
  arrays: Map<number, boolean>;
13
15
  assignments: Map<string, Map<string, Assignment[]>>;
@@ -204,13 +206,19 @@ const classify = (bindings: Bindings, call: ts.CallExpression): TYPES | null =>
204
206
  return type ? kindOf(bindings.checker, type) : null;
205
207
  };
206
208
 
207
- const create = (checker: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile): Bindings => ({
208
- checker,
209
- entries: new Set(references.exported(checker, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
210
- origins: references.origins(checker, program, sourceFile),
211
- program,
212
- sourceFile
213
- });
209
+ // Memoized per program (an already memoized checker is kept as is), so batched and repeated queries
210
+ // share one cache whoever hands the checker in
211
+ const create = (queries: ts.Checker, program: ts.Program, sourceFile: ts.SourceFile): Bindings => {
212
+ let memoized = checker.cached(queries, program);
213
+
214
+ return {
215
+ checker: memoized,
216
+ entries: new Set(references.exported(memoized, program, PACKAGE_NAME, ENTRYPOINT).map(references.key)),
217
+ origins: references.origins(memoized, program, sourceFile),
218
+ program,
219
+ sourceFile
220
+ };
221
+ };
214
222
 
215
223
  // Whether a callee expression denotes `reactive`: `reactive`, an alias, `ns.reactive`, `ns['reactive']`
216
224
  const denotes = (bindings: Bindings, expression: ts.Expression): boolean => {
@@ -293,6 +301,37 @@ const origin = (bindings: Bindings, node: ts.Node): Origin | null => {
293
301
  return bindings.origins.get(node) ?? references.origin(bindings.checker, bindings.program, node);
294
302
  };
295
303
 
304
+ // Types of the arguments classify() reads from the checker, requested in one round-trip; the
305
+ // memoized checker then answers each classify() from that request
306
+ const prefetch = (bindings: Bindings, calls: ts.CallExpression[]): void => {
307
+ let nodes: ts.Node[] = [];
308
+
309
+ for (let i = 0, n = calls.length; i < n; i++) {
310
+ let arg = calls[i].arguments[0];
311
+
312
+ if (!arg || calls[i].arguments.length !== 1 || ts.isSpreadElement(arg)) {
313
+ continue;
314
+ }
315
+
316
+ let value = unwrap(arg);
317
+
318
+ if (
319
+ ts.isArrayLiteralExpression(value) ||
320
+ ts.isArrowFunction(value) ||
321
+ ts.isFunctionExpression(value) ||
322
+ ts.isObjectLiteralExpression(value)
323
+ ) {
324
+ continue;
325
+ }
326
+
327
+ nodes.push(arg);
328
+ }
329
+
330
+ if (nodes.length > 0) {
331
+ bindings.checker.getTypeAtLocation(nodes);
332
+ }
333
+ };
334
+
296
335
 
297
336
  // The expression a value comes from, without parentheses, `!` and type assertions
298
337
  const unwrap = (expression: ts.Expression): ts.Expression => {
@@ -300,4 +339,4 @@ const unwrap = (expression: ts.Expression): ts.Expression => {
300
339
  };
301
340
 
302
341
 
303
- export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, unwrap };
342
+ export default { arrays, classify, create, denotes, holds, isReactiveCall, kind, origin, prefetch, unwrap };