@esportsplus/reactivity 0.38.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bench/ownership.bench.ts +212 -0
- package/build/reactive/array.d.ts +3 -2
- package/build/reactive/array.js +28 -10
- package/build/reactive/index.js +1 -1
- package/build/system.d.ts +2 -1
- package/build/system.js +93 -37
- package/build/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/reactive/array.ts +42 -13
- package/src/reactive/index.ts +1 -1
- package/src/system.ts +134 -43
- package/src/types.ts +5 -0
- package/test/ownership.test.ts +569 -0
- package/test/reactive/array.test.ts +68 -0
- package/test/system.test.ts +2 -2
package/src/reactive/array.ts
CHANGED
|
@@ -158,10 +158,11 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
158
158
|
let dirty = false,
|
|
159
159
|
errors: unknown[] | null = null;
|
|
160
160
|
|
|
161
|
+
// n is fixed: a listener unsubscribed mid-dispatch leaves a null (or, once trimmed, a missing) slot
|
|
161
162
|
for (let i = 0, n = listeners.length; i < n; i++) {
|
|
162
163
|
let listener = listeners[i];
|
|
163
164
|
|
|
164
|
-
if (listener
|
|
165
|
+
if (listener == null) {
|
|
165
166
|
continue;
|
|
166
167
|
}
|
|
167
168
|
|
|
@@ -181,9 +182,7 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
if (dirty) {
|
|
184
|
-
|
|
185
|
-
listeners.pop();
|
|
186
|
-
}
|
|
185
|
+
this.prune(event, listeners);
|
|
187
186
|
}
|
|
188
187
|
|
|
189
188
|
if (errors !== null) {
|
|
@@ -205,7 +204,9 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
205
204
|
write(this._length, 0);
|
|
206
205
|
}
|
|
207
206
|
|
|
208
|
-
|
|
207
|
+
// Returns the unsubscribe. A listener already registered for the event is not added twice, and either
|
|
208
|
+
// registration's unsubscribe removes it.
|
|
209
|
+
on<K extends keyof Events<T>>(event: K, listener: Listener<Events<T>[K]>): VoidFunction {
|
|
209
210
|
let entry = listener as AnyListener<T>,
|
|
210
211
|
listeners = this.listeners[event];
|
|
211
212
|
|
|
@@ -213,30 +214,58 @@ class ReactiveArray<T> extends Array<T> {
|
|
|
213
214
|
this.listeners[event] = [entry];
|
|
214
215
|
}
|
|
215
216
|
else {
|
|
216
|
-
let hole = listeners.length
|
|
217
|
+
let hole = listeners.length,
|
|
218
|
+
registered = false;
|
|
217
219
|
|
|
218
220
|
for (let i = 0, n = hole; i < n; i++) {
|
|
219
221
|
let l = listeners[i];
|
|
220
222
|
|
|
221
223
|
if (l === entry) {
|
|
222
|
-
|
|
224
|
+
registered = true;
|
|
225
|
+
break;
|
|
223
226
|
}
|
|
224
227
|
else if (l === null && hole === n) {
|
|
225
228
|
hole = i;
|
|
226
229
|
}
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
listeners.pop();
|
|
232
|
+
if (!registered) {
|
|
233
|
+
listeners[hole] = entry;
|
|
234
|
+
this.prune(event, listeners);
|
|
233
235
|
}
|
|
234
236
|
}
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
let current = this.listeners[event];
|
|
240
|
+
|
|
241
|
+
if (current === undefined) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let i = current.indexOf(entry);
|
|
246
|
+
|
|
247
|
+
if (i !== -1) {
|
|
248
|
+
current[i] = null;
|
|
249
|
+
this.prune(event, current);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
235
252
|
}
|
|
236
253
|
|
|
237
|
-
once<K extends keyof Events<T>>(event: K, listener: Listener<Events<T>[K]>) {
|
|
254
|
+
once<K extends keyof Events<T>>(event: K, listener: Listener<Events<T>[K]>): VoidFunction {
|
|
238
255
|
listener.once = true;
|
|
239
|
-
|
|
256
|
+
|
|
257
|
+
return this.on(event, listener);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Trims trailing empty slots; an event left with no listeners is deleted, so e.g. sort() skips computing its order
|
|
261
|
+
private prune<K extends keyof Events<T>>(event: K, listeners: (AnyListener<T> | null)[]) {
|
|
262
|
+
while (listeners.length && listeners[listeners.length - 1] === null) {
|
|
263
|
+
listeners.pop();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (listeners.length === 0 && this.listeners[event] === listeners) {
|
|
267
|
+
delete this.listeners[event];
|
|
268
|
+
}
|
|
240
269
|
}
|
|
241
270
|
|
|
242
271
|
pop() {
|
package/src/reactive/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { onCleanup, root } from '@esportsplus/reactivity';
|
|
2
1
|
import { isArray, isObject } from '@esportsplus/utilities';
|
|
3
2
|
import { PACKAGE_NAME } from '~/constants';
|
|
3
|
+
import { onCleanup, root } from '~/system';
|
|
4
4
|
import type { Reactive } from '~/types';
|
|
5
5
|
import { ReactiveArray } from './array';
|
|
6
6
|
import { ReactiveObject } from './object';
|
package/src/system.ts
CHANGED
|
@@ -16,7 +16,8 @@ type Walk = {
|
|
|
16
16
|
prev: Walk | null;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// Async wrapper -> the factory computed it settles, so invalidate() can redirect a refetch to it
|
|
20
|
+
let asyncFactory = new WeakMap<Computed<unknown>, Computed<unknown>>(),
|
|
20
21
|
depth = 0,
|
|
21
22
|
disposeHead: Walk | null = null,
|
|
22
23
|
draining = false,
|
|
@@ -67,36 +68,46 @@ function walkPush(computed: Computed<unknown> | null, link: Link | null, prev: W
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
// Owned children merge with the user cleanups by ownerSlot, so both run in registration order. Each
|
|
72
|
+
// child is taken from the live list head and disowned before its dispose, so a cleanup that disposes
|
|
73
|
+
// a later sibling just shortens the list.
|
|
70
74
|
function cleanup<T>(computed: Computed<T>): void {
|
|
71
|
-
|
|
75
|
+
let child = computed.owned,
|
|
76
|
+
value = computed.cleanup;
|
|
77
|
+
|
|
78
|
+
computed.cleanup = null;
|
|
79
|
+
|
|
80
|
+
if (child === null && typeof value === 'function') {
|
|
81
|
+
value();
|
|
72
82
|
return;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
let errors: unknown[] =
|
|
76
|
-
|
|
85
|
+
let errors: unknown[] | null = null,
|
|
86
|
+
n = value === null ? 0 : typeof value === 'function' ? 1 : value.length;
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
for (let i = 0; i <= n; i++) {
|
|
89
|
+
while ((child = computed.owned) !== null && (i === n || child.ownerSlot <= i)) {
|
|
90
|
+
disown(child);
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
try {
|
|
93
|
+
dispose(child);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
(errors ??= []).push(e);
|
|
97
|
+
}
|
|
86
98
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (let i = 0, n = value.length; i < n; i++) {
|
|
99
|
+
|
|
100
|
+
if (i < n) {
|
|
90
101
|
try {
|
|
91
|
-
value[i]();
|
|
102
|
+
(typeof value === 'function' ? value : value![i])();
|
|
92
103
|
}
|
|
93
104
|
catch (e) {
|
|
94
|
-
errors.push(e);
|
|
105
|
+
(errors ??= []).push(e);
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
if (errors
|
|
110
|
+
if (errors !== null) {
|
|
100
111
|
throw errors.length === 1 ? errors[0] : new AggregateError(errors, `${PACKAGE_NAME}: cleanup produced multiple errors`);
|
|
101
112
|
}
|
|
102
113
|
}
|
|
@@ -134,6 +145,27 @@ function deleteFromHeap<T>(computed: Computed<T>) {
|
|
|
134
145
|
computed.prevHeap = computed;
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
// O(1) removal from the owner's owned list; the head's prevOwned is the tail, as in the heap buckets
|
|
149
|
+
function disown(child: Computed<unknown>) {
|
|
150
|
+
let next = child.nextOwned,
|
|
151
|
+
owner = child.owner!,
|
|
152
|
+
prev = child.prevOwned!;
|
|
153
|
+
|
|
154
|
+
if (owner.owned === child) {
|
|
155
|
+
owner.owned = next;
|
|
156
|
+
|
|
157
|
+
if (next !== null) {
|
|
158
|
+
next.prevOwned = prev;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
prev.nextOwned = next;
|
|
163
|
+
(next ?? owner.owned!).prevOwned = prev;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
child.nextOwned = child.owner = child.prevOwned = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
137
169
|
// N writes to one signal queue it once, so each subscriber is heap-inserted once. A self-linked
|
|
138
170
|
// nextPending marks the tail.
|
|
139
171
|
function drainPending() {
|
|
@@ -313,6 +345,29 @@ function notify<T>(computed: Computed<T>, newState: number) {
|
|
|
313
345
|
}
|
|
314
346
|
}
|
|
315
347
|
|
|
348
|
+
// Child owners (a root with a dispose param, a computed/effect created under an owner) link into the
|
|
349
|
+
// owner's list instead of registering a dispose closure, so an early dispose unlinks in O(1).
|
|
350
|
+
// ownerSlot snapshots how many user cleanups precede the child for cleanup()'s ordering merge.
|
|
351
|
+
function own(owner: Computed<unknown>, child: Computed<unknown>) {
|
|
352
|
+
let head = owner.owned,
|
|
353
|
+
value = owner.cleanup;
|
|
354
|
+
|
|
355
|
+
child.owner = owner;
|
|
356
|
+
child.ownerSlot = value === null ? 0 : typeof value === 'function' ? 1 : value.length;
|
|
357
|
+
|
|
358
|
+
if (head === null) {
|
|
359
|
+
owner.owned = child;
|
|
360
|
+
child.prevOwned = child;
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
let tail = head.prevOwned!;
|
|
364
|
+
|
|
365
|
+
tail.nextOwned = child;
|
|
366
|
+
child.prevOwned = tail;
|
|
367
|
+
head.prevOwned = child;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
316
371
|
// Shared by read()'s tracked pull and peek()'s untracked pull. observer is nulled around update()
|
|
317
372
|
// so a recompute triggered here tracks into the node's own scope, never the caller's. pulled/puller
|
|
318
373
|
// let propagate() skip re-queueing the caller: it receives the fresh value when this returns.
|
|
@@ -371,7 +426,7 @@ function recompute<T>(computed: Computed<T>) {
|
|
|
371
426
|
deleteFromHeap(computed);
|
|
372
427
|
}
|
|
373
428
|
|
|
374
|
-
if (computed.cleanup) {
|
|
429
|
+
if (computed.cleanup !== null || computed.owned !== null) {
|
|
375
430
|
// A failing PREVIOUS generation's teardown must not poison this recompute or the stabilize pass
|
|
376
431
|
try {
|
|
377
432
|
cleanup(computed);
|
|
@@ -727,7 +782,7 @@ function makeAsyncComputed<T>(factory: Computed<Promise<T> | AsyncIterable<T> |
|
|
|
727
782
|
|
|
728
783
|
wrapper.pending = pending;
|
|
729
784
|
|
|
730
|
-
|
|
785
|
+
asyncFactory.set(wrapper as Computed<unknown>, factory as Computed<unknown>);
|
|
731
786
|
wrapper.disposal = stop;
|
|
732
787
|
|
|
733
788
|
return wrapper;
|
|
@@ -755,13 +810,13 @@ function makeComputed<T>(fn: Computed<T>['fn'], eager: boolean = false): Compute
|
|
|
755
810
|
}
|
|
756
811
|
|
|
757
812
|
link(self, observer);
|
|
758
|
-
|
|
813
|
+
own(observer, self as Computed<unknown>);
|
|
759
814
|
}
|
|
760
815
|
else {
|
|
761
816
|
recompute(self);
|
|
762
817
|
|
|
763
818
|
if (scope) {
|
|
764
|
-
|
|
819
|
+
own(scope, self as Computed<unknown>);
|
|
765
820
|
}
|
|
766
821
|
}
|
|
767
822
|
|
|
@@ -782,8 +837,13 @@ function makeNode<T>(fn: Computed<T>['fn']): Computed<T> {
|
|
|
782
837
|
gv: 0,
|
|
783
838
|
height: 0,
|
|
784
839
|
nextHeap: undefined,
|
|
840
|
+
nextOwned: null,
|
|
841
|
+
owned: null,
|
|
842
|
+
owner: null,
|
|
843
|
+
ownerSlot: 0,
|
|
785
844
|
pending: null,
|
|
786
845
|
prevHeap: null as unknown as Computed<unknown>,
|
|
846
|
+
prevOwned: null,
|
|
787
847
|
rv: 0,
|
|
788
848
|
state: STATE_COMPUTED,
|
|
789
849
|
subs: null,
|
|
@@ -824,14 +884,17 @@ const computed = <T>(fn: Computed<T>['fn'], equals: ((a: Settled<T>, b: Settled<
|
|
|
824
884
|
value = self.value;
|
|
825
885
|
|
|
826
886
|
if (isPromise(value) || (value != null && typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function')) {
|
|
827
|
-
// Built
|
|
887
|
+
// Built in a detached root: makeAsyncComputed's polling effect + wrapper take makeComputed's
|
|
828
888
|
// observer-null path (the proven top-level flow) instead of entangling with the enclosing
|
|
829
889
|
// observer's recompute, which deadlocks the scheduler. The probe is already linked+owned by
|
|
830
|
-
// the parent as the factory; the wrapper
|
|
831
|
-
|
|
890
|
+
// the parent as the factory; the wrapper (whose disposal stops the polling effect) is owned
|
|
891
|
+
// by that same owner.
|
|
892
|
+
let owner = o || scope;
|
|
832
893
|
|
|
833
|
-
|
|
834
|
-
|
|
894
|
+
self = root(() => makeAsyncComputed(self as Computed<Promise<unknown> | AsyncIterable<unknown>>));
|
|
895
|
+
|
|
896
|
+
if (owner !== null) {
|
|
897
|
+
own(owner, self);
|
|
835
898
|
}
|
|
836
899
|
}
|
|
837
900
|
|
|
@@ -844,10 +907,10 @@ const computed = <T>(fn: Computed<T>['fn'], equals: ((a: Settled<T>, b: Settled<
|
|
|
844
907
|
// node cannot skip the forced re-run via update()'s clean-graph fast path; an async computed's wrapper
|
|
845
908
|
// redirects to its factory so the promise re-dispatches (a refetch).
|
|
846
909
|
computed.invalidate = <T>(c: Computed<T>): void => {
|
|
847
|
-
let
|
|
910
|
+
let factory = asyncFactory.get(c as Computed<unknown>);
|
|
848
911
|
|
|
849
|
-
if (
|
|
850
|
-
computed.invalidate(
|
|
912
|
+
if (factory) {
|
|
913
|
+
computed.invalidate(factory);
|
|
851
914
|
return;
|
|
852
915
|
}
|
|
853
916
|
|
|
@@ -860,7 +923,8 @@ computed.invalidate = <T>(c: Computed<T>): void => {
|
|
|
860
923
|
// Teardown runs as an iterative LIFO drain, not recursion: a recursive dispose→unlink→dispose cascade
|
|
861
924
|
// overflows the call stack on deep chains. A dispose issued while a drain runs enqueues and returns,
|
|
862
925
|
// so the running drain picks it up. Field-nulling keeps the drain exactly-once, so a double dispose
|
|
863
|
-
// stays a no-op.
|
|
926
|
+
// stays a no-op. Cleanup/disposal errors are collected and rethrown once the drain empties, so a
|
|
927
|
+
// throwing cleanup never strands queued nodes; the try/finally still guarantees `draining` resets.
|
|
864
928
|
const dispose = <T>(computed: Computed<T>): void => {
|
|
865
929
|
// A dispose issued mid-drain only enqueues; the running drain picks it up, so the first node is
|
|
866
930
|
// processed inline (no worklist node) and the pool is touched only for re-entrant deep cascades.
|
|
@@ -871,7 +935,8 @@ const dispose = <T>(computed: Computed<T>): void => {
|
|
|
871
935
|
|
|
872
936
|
draining = true;
|
|
873
937
|
|
|
874
|
-
let
|
|
938
|
+
let errors: unknown[] | null = null,
|
|
939
|
+
node: Computed<unknown> = computed as Computed<unknown>;
|
|
875
940
|
|
|
876
941
|
try {
|
|
877
942
|
for (;;) {
|
|
@@ -885,15 +950,30 @@ const dispose = <T>(computed: Computed<T>): void => {
|
|
|
885
950
|
|
|
886
951
|
node.deps = null;
|
|
887
952
|
|
|
888
|
-
if (node.
|
|
889
|
-
|
|
953
|
+
if (node.owner !== null) {
|
|
954
|
+
disown(node);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (node.cleanup !== null || node.owned !== null) {
|
|
958
|
+
try {
|
|
959
|
+
cleanup(node);
|
|
960
|
+
}
|
|
961
|
+
catch (e) {
|
|
962
|
+
(errors ??= []).push(e);
|
|
963
|
+
}
|
|
890
964
|
}
|
|
891
965
|
|
|
892
966
|
if (node.disposal) {
|
|
893
967
|
let d = node.disposal;
|
|
894
968
|
|
|
895
969
|
node.disposal = null;
|
|
896
|
-
|
|
970
|
+
|
|
971
|
+
try {
|
|
972
|
+
d();
|
|
973
|
+
}
|
|
974
|
+
catch (e) {
|
|
975
|
+
(errors ??= []).push(e);
|
|
976
|
+
}
|
|
897
977
|
}
|
|
898
978
|
|
|
899
979
|
if (disposeHead === null) {
|
|
@@ -907,6 +987,10 @@ const dispose = <T>(computed: Computed<T>): void => {
|
|
|
907
987
|
finally {
|
|
908
988
|
draining = false;
|
|
909
989
|
}
|
|
990
|
+
|
|
991
|
+
if (errors !== null) {
|
|
992
|
+
throw errors.length === 1 ? errors[0] : new AggregateError(errors, `${PACKAGE_NAME}: dispose produced multiple errors`);
|
|
993
|
+
}
|
|
910
994
|
};
|
|
911
995
|
|
|
912
996
|
const effect = <T>(fn: Computed<T>['fn'], apply?: (value: T, prev: T | undefined) => void) => {
|
|
@@ -948,6 +1032,9 @@ const flush = (): void => {
|
|
|
948
1032
|
}
|
|
949
1033
|
};
|
|
950
1034
|
|
|
1035
|
+
// Whether an onCleanup() made now would land on an owner (a running computed/effect or a root's scope)
|
|
1036
|
+
const hasOwner = (): boolean => observer !== null || scope !== null;
|
|
1037
|
+
|
|
951
1038
|
const isComputed = (value: unknown): value is Computed<unknown> => {
|
|
952
1039
|
return isObject(value) && !!((value as unknown as Computed<unknown>).state & STATE_COMPUTED);
|
|
953
1040
|
};
|
|
@@ -1028,11 +1115,11 @@ const read = <T>(node: Signal<T> | Computed<T>): T => {
|
|
|
1028
1115
|
return node.value;
|
|
1029
1116
|
};
|
|
1030
1117
|
|
|
1118
|
+
// Owned before fn runs (nothing can register on the owner meanwhile, so ownerSlot is unchanged): a
|
|
1119
|
+
// root disposed inside its own fn then unlinks instead of being parked on the owner as a dead child.
|
|
1031
1120
|
const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
|
|
1032
|
-
let
|
|
1033
|
-
o = observer,
|
|
1121
|
+
let o = observer,
|
|
1034
1122
|
s = scope,
|
|
1035
|
-
self: Computed<unknown> | null = null,
|
|
1036
1123
|
tracking = fn.length,
|
|
1037
1124
|
value: T;
|
|
1038
1125
|
|
|
@@ -1040,8 +1127,15 @@ const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
|
|
|
1040
1127
|
|
|
1041
1128
|
try {
|
|
1042
1129
|
if (tracking) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1130
|
+
let self = makeNode(noop),
|
|
1131
|
+
owner = o || s;
|
|
1132
|
+
|
|
1133
|
+
if (owner !== null) {
|
|
1134
|
+
own(owner, self);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
scope = self;
|
|
1138
|
+
value = (fn as (dispose: VoidFunction) => T)(() => dispose(self));
|
|
1045
1139
|
}
|
|
1046
1140
|
else {
|
|
1047
1141
|
scope = null;
|
|
@@ -1053,10 +1147,6 @@ const root = <T>(fn: ((dispose: VoidFunction) => T) | (() => T)) => {
|
|
|
1053
1147
|
scope = s;
|
|
1054
1148
|
}
|
|
1055
1149
|
|
|
1056
|
-
if (c) {
|
|
1057
|
-
onCleanup(c);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
1150
|
return value;
|
|
1061
1151
|
};
|
|
1062
1152
|
|
|
@@ -1166,6 +1256,7 @@ export {
|
|
|
1166
1256
|
dispose,
|
|
1167
1257
|
effect,
|
|
1168
1258
|
flush,
|
|
1259
|
+
hasOwner,
|
|
1169
1260
|
isComputed, isSignal,
|
|
1170
1261
|
onCleanup,
|
|
1171
1262
|
peek,
|
package/src/types.ts
CHANGED
|
@@ -13,8 +13,13 @@ interface Computed<T> {
|
|
|
13
13
|
gv: number;
|
|
14
14
|
height: number;
|
|
15
15
|
nextHeap: Computed<unknown> | undefined;
|
|
16
|
+
nextOwned: Computed<unknown> | null;
|
|
17
|
+
owned: Computed<unknown> | null;
|
|
18
|
+
owner: Computed<unknown> | null;
|
|
19
|
+
ownerSlot: number;
|
|
16
20
|
pending: Signal<boolean> | null;
|
|
17
21
|
prevHeap: Computed<unknown>;
|
|
22
|
+
prevOwned: Computed<unknown> | null;
|
|
18
23
|
rv: number;
|
|
19
24
|
state: number;
|
|
20
25
|
subs: Link | null;
|