@esportsplus/reactivity 0.30.3 → 0.31.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/build/constants.d.ts +2 -1
- package/build/constants.js +2 -1
- package/build/reactive/object.js +7 -9
- package/build/system.d.ts +2 -1
- package/build/system.js +31 -20
- package/build/types.d.ts +2 -3
- package/package.json +1 -1
- package/src/constants.ts +3 -1
- package/src/reactive/object.ts +12 -12
- package/src/system.ts +42 -28
- package/src/types.ts +2 -8
- package/tests/array.ts +249 -0
- package/tests/async-computed.ts +195 -0
- package/tests/bench/array.ts +65 -0
- package/tests/bench/reactive-object.ts +54 -0
- package/tests/bench/system.ts +88 -1
- package/tests/compiler.ts +326 -0
- package/tests/reactive.ts +210 -0
- package/tests/system.ts +359 -0
- package/tests/tsconfig.json +17 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
4
|
+
import { NAMESPACE } from '~/compiler/constants';
|
|
5
|
+
import type { Bindings } from '~/compiler/types';
|
|
6
|
+
import array from '~/compiler/array';
|
|
7
|
+
import object from '~/compiler/object';
|
|
8
|
+
import primitives from '~/compiler/primitives';
|
|
9
|
+
import pipeline from '~/compiler/index';
|
|
10
|
+
import tscPlugin from '~/compiler/plugins/tsc';
|
|
11
|
+
import vitePlugin from '~/compiler/plugins/vite';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function applyIntents(code: string, sourceFile: ts.SourceFile, intents: ReplacementIntent[]): string {
|
|
15
|
+
let sorted = [...intents].sort((a, b) => b.node.getStart(sourceFile) - a.node.getStart(sourceFile));
|
|
16
|
+
|
|
17
|
+
for (let i = 0, n = sorted.length; i < n; i++) {
|
|
18
|
+
let intent = sorted[i],
|
|
19
|
+
end = intent.node.getEnd(),
|
|
20
|
+
start = intent.node.getStart(sourceFile);
|
|
21
|
+
|
|
22
|
+
code = code.slice(0, start) + intent.generate(sourceFile) + code.slice(end);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return code;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isReactiveCall(node: ts.Node): boolean {
|
|
29
|
+
return ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'reactive';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parse(code: string): ts.SourceFile {
|
|
33
|
+
return ts.createSourceFile('test.ts', code, ts.ScriptTarget.Latest, true);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function transformPrimitives(code: string): { bindings: Bindings; output: string } {
|
|
37
|
+
let bindings: Bindings = new Map(),
|
|
38
|
+
sourceFile = parse(code),
|
|
39
|
+
intents = primitives(sourceFile, bindings, isReactiveCall);
|
|
40
|
+
|
|
41
|
+
return { bindings, output: applyIntents(code, sourceFile, intents) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function transformArray(code: string, bindings?: Bindings): { bindings: Bindings; output: string } {
|
|
45
|
+
let b: Bindings = bindings ?? new Map(),
|
|
46
|
+
sourceFile = parse(code),
|
|
47
|
+
intents = array(sourceFile, b, undefined);
|
|
48
|
+
|
|
49
|
+
return { bindings: b, output: applyIntents(code, sourceFile, intents) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function transformObject(code: string): { bindings: Bindings; output: string; prepend: string[] } {
|
|
53
|
+
let bindings: Bindings = new Map(),
|
|
54
|
+
sourceFile = parse(code),
|
|
55
|
+
result = object(sourceFile, bindings, undefined);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
bindings,
|
|
59
|
+
output: applyIntents(code, sourceFile, result.replacements),
|
|
60
|
+
prepend: result.prepend
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
describe('primitives transform', () => {
|
|
66
|
+
it('transforms reactive(0) to signal', () => {
|
|
67
|
+
let { output } = transformPrimitives('let x = reactive(0);');
|
|
68
|
+
|
|
69
|
+
expect(output).toContain(`${NAMESPACE}.signal(0)`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('transforms reactive(() => expr) to computed', () => {
|
|
73
|
+
let { output } = transformPrimitives('let x = reactive(0); let d = reactive(() => x * 2);');
|
|
74
|
+
|
|
75
|
+
expect(output).toContain(`${NAMESPACE}.computed(() =>`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('transforms reads to namespace read', () => {
|
|
79
|
+
let { output } = transformPrimitives('let x = reactive(0); console.log(x);');
|
|
80
|
+
|
|
81
|
+
expect(output).toContain(`${NAMESPACE}.read(x)`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('transforms simple assignment to write', () => {
|
|
85
|
+
let { output } = transformPrimitives('let x = reactive(0); x = 5;');
|
|
86
|
+
|
|
87
|
+
expect(output).toContain(`${NAMESPACE}.write(x, 5)`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('transforms compound assignment += to write', () => {
|
|
91
|
+
let { output } = transformPrimitives('let x = reactive(0); x += 5;');
|
|
92
|
+
|
|
93
|
+
expect(output).toContain(`${NAMESPACE}.write(x, x.value + 5)`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('transforms postfix x++ in statement to write', () => {
|
|
97
|
+
let { output } = transformPrimitives('let x = reactive(0); x++;');
|
|
98
|
+
|
|
99
|
+
expect(output).toContain(`${NAMESPACE}.write(x, x.value + 1)`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('transforms prefix ++x in expression', () => {
|
|
103
|
+
let { output } = transformPrimitives('let x = reactive(0); let y = ++x;');
|
|
104
|
+
|
|
105
|
+
expect(output).toContain(`(${NAMESPACE}.write(x, x.value + 1), x.value)`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('transforms postfix x++ in expression with temp variable', () => {
|
|
109
|
+
let { output } = transformPrimitives('let x = reactive(0); let y = x++;');
|
|
110
|
+
|
|
111
|
+
expect(output).toContain(`((_t0) => (${NAMESPACE}.write(x, _t0 + 1), _t0))(x.value)`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('transforms reads in nested functions within scope', () => {
|
|
115
|
+
let { output } = transformPrimitives('let x = reactive(0); function fn() { return x; }');
|
|
116
|
+
|
|
117
|
+
// The x inside fn IS within the reactive binding scope, so it gets transformed
|
|
118
|
+
expect(output).toContain(`${NAMESPACE}.read(x)`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('transforms dynamic expression to namespace reactive', () => {
|
|
122
|
+
let { output } = transformPrimitives('let x = reactive(someCall());');
|
|
123
|
+
|
|
124
|
+
expect(output).toContain(`${NAMESPACE}.reactive(someCall())`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('tracks bindings for signal type', () => {
|
|
128
|
+
let { bindings } = transformPrimitives('let x = reactive(0);');
|
|
129
|
+
|
|
130
|
+
// TYPES.Signal = 3
|
|
131
|
+
expect(bindings.get('x')).toBe(3);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('tracks bindings for computed type', () => {
|
|
135
|
+
let { bindings } = transformPrimitives('let x = reactive(0); let d = reactive(() => x * 2);');
|
|
136
|
+
|
|
137
|
+
// TYPES.Computed = 1
|
|
138
|
+
expect(bindings.get('d')).toBe(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('transforms prefix --x in statement', () => {
|
|
142
|
+
let { output } = transformPrimitives('let x = reactive(0); --x;');
|
|
143
|
+
|
|
144
|
+
expect(output).toContain(`${NAMESPACE}.write(x, x.value - 1)`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('transforms compound assignment -= to write', () => {
|
|
148
|
+
let { output } = transformPrimitives('let x = reactive(0); x -= 3;');
|
|
149
|
+
|
|
150
|
+
expect(output).toContain(`${NAMESPACE}.write(x, x.value - 3)`);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
describe('object transform', () => {
|
|
156
|
+
it('transforms reactive object with signal field', () => {
|
|
157
|
+
let { output, prepend } = transformObject('let obj = reactive({ count: 0 });');
|
|
158
|
+
|
|
159
|
+
expect(prepend.length).toBe(1);
|
|
160
|
+
expect(prepend[0]).toContain(`extends ${NAMESPACE}.ReactiveObject`);
|
|
161
|
+
expect(prepend[0]).toContain(`${NAMESPACE}.read(this.#count)`);
|
|
162
|
+
expect(prepend[0]).toContain(`${NAMESPACE}.write(this.#count`);
|
|
163
|
+
expect(output).toContain('new ');
|
|
164
|
+
expect(output).not.toContain('reactive(');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('transforms reactive object with array field', () => {
|
|
168
|
+
let { prepend } = transformObject('let obj = reactive({ items: [1, 2, 3] });');
|
|
169
|
+
|
|
170
|
+
expect(prepend.length).toBe(1);
|
|
171
|
+
expect(prepend[0]).toContain(`${NAMESPACE}.REACTIVE_ARRAY`);
|
|
172
|
+
expect(prepend[0]).toContain('get items()');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('transforms reactive object with computed field', () => {
|
|
176
|
+
let { prepend } = transformObject('let obj = reactive({ doubled: () => 2 });');
|
|
177
|
+
|
|
178
|
+
expect(prepend.length).toBe(1);
|
|
179
|
+
expect(prepend[0]).toContain(`${NAMESPACE}.COMPUTED`);
|
|
180
|
+
expect(prepend[0]).toContain(`${NAMESPACE}.read(this.#doubled)`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('transforms reactive object with mixed properties', () => {
|
|
184
|
+
let { output, prepend } = transformObject(
|
|
185
|
+
'let obj = reactive({ count: 0, items: [1], doubled: () => 2 });'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(prepend.length).toBe(1);
|
|
189
|
+
expect(prepend[0]).toContain(`${NAMESPACE}.SIGNAL`);
|
|
190
|
+
expect(prepend[0]).toContain(`${NAMESPACE}.REACTIVE_ARRAY`);
|
|
191
|
+
expect(prepend[0]).toContain(`${NAMESPACE}.COMPUTED`);
|
|
192
|
+
expect(output).toContain('new ');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('does not transform object with spread assignment', () => {
|
|
196
|
+
let { output, prepend } = transformObject('let obj = reactive({ ...base, count: 0 });');
|
|
197
|
+
|
|
198
|
+
expect(prepend.length).toBe(0);
|
|
199
|
+
expect(output).toContain('reactive(');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('preserves type parameter', () => {
|
|
203
|
+
let { output } = transformObject('let obj = reactive<MyType>({ count: 0 });');
|
|
204
|
+
|
|
205
|
+
expect(output).toContain('<MyType>');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('tracks object binding', () => {
|
|
209
|
+
let { bindings } = transformObject('let obj = reactive({ count: 0 });');
|
|
210
|
+
|
|
211
|
+
// TYPES.Object = 2
|
|
212
|
+
expect(bindings.get('obj')).toBe(2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('tracks nested array bindings', () => {
|
|
216
|
+
let { bindings } = transformObject('let obj = reactive({ items: [1, 2, 3] });');
|
|
217
|
+
|
|
218
|
+
// TYPES.Array = 0
|
|
219
|
+
expect(bindings.get('obj.items')).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
describe('array transform', () => {
|
|
225
|
+
it('transforms reactive([1,2,3]) to ReactiveArray', () => {
|
|
226
|
+
let { output } = transformArray('let arr = reactive([1, 2, 3]);');
|
|
227
|
+
|
|
228
|
+
expect(output).toContain(`new ${NAMESPACE}.ReactiveArray`);
|
|
229
|
+
expect(output).toContain('...[1, 2, 3]');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('transforms reactive([] as Type[]) to typed ReactiveArray', () => {
|
|
233
|
+
let { output } = transformArray('let arr = reactive([] as number[]);');
|
|
234
|
+
|
|
235
|
+
expect(output).toContain(`new ${NAMESPACE}.ReactiveArray<number>()`);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('transforms arr.length read to arr.$length', () => {
|
|
239
|
+
let bindings: Bindings = new Map();
|
|
240
|
+
|
|
241
|
+
bindings.set('arr', 0); // TYPES.Array = 0
|
|
242
|
+
let { output } = transformArray('let x = arr.length;', bindings);
|
|
243
|
+
|
|
244
|
+
expect(output).toContain('arr.$length');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('transforms arr.length = n to arr.$length = n', () => {
|
|
248
|
+
let bindings: Bindings = new Map();
|
|
249
|
+
|
|
250
|
+
bindings.set('arr', 0);
|
|
251
|
+
let { output } = transformArray('arr.length = 5;', bindings);
|
|
252
|
+
|
|
253
|
+
expect(output).toContain('arr.$length = 5');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('transforms arr.length += n to arr.$length = arr.length + n', () => {
|
|
257
|
+
let bindings: Bindings = new Map();
|
|
258
|
+
|
|
259
|
+
bindings.set('arr', 0);
|
|
260
|
+
let { output } = transformArray('arr.length += 3;', bindings);
|
|
261
|
+
|
|
262
|
+
expect(output).toContain('arr.$length = arr.length + 3');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('transforms arr[i] = value to arr.$set(i, value)', () => {
|
|
266
|
+
let bindings: Bindings = new Map();
|
|
267
|
+
|
|
268
|
+
bindings.set('arr', 0);
|
|
269
|
+
let { output } = transformArray('arr[0] = 42;', bindings);
|
|
270
|
+
|
|
271
|
+
expect(output).toContain('arr.$set(');
|
|
272
|
+
expect(output).toContain('42');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('tracks reactive array binding from reactive call', () => {
|
|
276
|
+
let { bindings } = transformArray('let arr = reactive([1, 2, 3]);');
|
|
277
|
+
|
|
278
|
+
// TYPES.Array = 0
|
|
279
|
+
expect(bindings.get('arr')).toBe(0);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('tracks alias binding from reactive array', () => {
|
|
283
|
+
let bindings: Bindings = new Map();
|
|
284
|
+
|
|
285
|
+
bindings.set('a', 0);
|
|
286
|
+
transformArray('let b = a;', bindings);
|
|
287
|
+
|
|
288
|
+
expect(bindings.get('b')).toBe(0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('tracks typed parameter as ReactiveArray', () => {
|
|
292
|
+
let bindings: Bindings = new Map();
|
|
293
|
+
|
|
294
|
+
transformArray('function fn(arr: ReactiveArray) { return arr; }', bindings);
|
|
295
|
+
|
|
296
|
+
expect(bindings.get('arr')).toBe(0);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('transforms empty array', () => {
|
|
300
|
+
let { output } = transformArray('let arr = reactive([] as string[]);');
|
|
301
|
+
|
|
302
|
+
expect(output).toContain(`new ${NAMESPACE}.ReactiveArray<string>()`);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
describe('index transform', () => {
|
|
308
|
+
it('exports patterns array', () => {
|
|
309
|
+
expect(pipeline.patterns).toEqual(['reactive(', 'reactive<']);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('has transform function', () => {
|
|
313
|
+
expect(typeof pipeline.transform).toBe('function');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
describe('plugins', () => {
|
|
319
|
+
it('tsc plugin is defined', () => {
|
|
320
|
+
expect(tscPlugin).toBeDefined();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('vite plugin is defined', () => {
|
|
324
|
+
expect(vitePlugin).toBeDefined();
|
|
325
|
+
});
|
|
326
|
+
});
|
package/tests/reactive.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { computed, effect, onCleanup, read, root, signal, write } from '~/system';
|
|
3
|
+
import { SIGNAL } from '~/constants';
|
|
3
4
|
import { ReactiveObject, isReactiveObject } from '~/reactive/object';
|
|
4
5
|
import reactive from '~/reactive/index';
|
|
5
6
|
import { ReactiveArray } from '~/reactive/array';
|
|
@@ -108,6 +109,215 @@ describe('ReactiveObject', () => {
|
|
|
108
109
|
obj.dispose();
|
|
109
110
|
});
|
|
110
111
|
});
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
describe('async computed', () => {
|
|
115
|
+
it('initial value undefined, resolves to correct value', async () => {
|
|
116
|
+
let s = signal(42),
|
|
117
|
+
obj = new ReactiveObject({
|
|
118
|
+
data: () => Promise.resolve(read(s))
|
|
119
|
+
}) as any;
|
|
120
|
+
|
|
121
|
+
expect(obj.data).toBeUndefined();
|
|
122
|
+
|
|
123
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
124
|
+
|
|
125
|
+
expect(obj.data).toBe(42);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('updates when dependency changes — new promise resolves', async () => {
|
|
129
|
+
let s = signal('hello'),
|
|
130
|
+
obj = new ReactiveObject({
|
|
131
|
+
data: () => Promise.resolve(read(s))
|
|
132
|
+
}) as any;
|
|
133
|
+
|
|
134
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
135
|
+
|
|
136
|
+
expect(obj.data).toBe('hello');
|
|
137
|
+
|
|
138
|
+
write(s, 'world');
|
|
139
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
140
|
+
|
|
141
|
+
expect(obj.data).toBe('world');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('race condition — rapid changes, only latest promise writes', async () => {
|
|
145
|
+
let s = signal(1),
|
|
146
|
+
resolvers: ((v: number) => void)[] = [],
|
|
147
|
+
obj = new ReactiveObject({
|
|
148
|
+
data: () => new Promise<number>((resolve) => {
|
|
149
|
+
resolvers.push(resolve);
|
|
150
|
+
read(s);
|
|
151
|
+
})
|
|
152
|
+
}) as any;
|
|
153
|
+
|
|
154
|
+
expect(obj.data).toBeUndefined();
|
|
155
|
+
|
|
156
|
+
// Trigger second computation
|
|
157
|
+
write(s, 2);
|
|
158
|
+
await Promise.resolve();
|
|
159
|
+
await Promise.resolve();
|
|
160
|
+
|
|
161
|
+
// Trigger third computation
|
|
162
|
+
write(s, 3);
|
|
163
|
+
await Promise.resolve();
|
|
164
|
+
await Promise.resolve();
|
|
165
|
+
|
|
166
|
+
// Resolve first promise (stale — should be ignored)
|
|
167
|
+
resolvers[0](100);
|
|
168
|
+
await Promise.resolve();
|
|
169
|
+
|
|
170
|
+
expect(obj.data).toBeUndefined();
|
|
171
|
+
|
|
172
|
+
// Resolve second promise (stale — should be ignored)
|
|
173
|
+
resolvers[1](200);
|
|
174
|
+
await Promise.resolve();
|
|
175
|
+
|
|
176
|
+
expect(obj.data).toBeUndefined();
|
|
177
|
+
|
|
178
|
+
// Resolve latest promise — should write
|
|
179
|
+
resolvers[2](300);
|
|
180
|
+
await Promise.resolve();
|
|
181
|
+
|
|
182
|
+
expect(obj.data).toBe(300);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('dispose prevents new computations but in-flight resolves still write', async () => {
|
|
186
|
+
let s = signal(1),
|
|
187
|
+
resolver: ((v: number) => void) | null = null,
|
|
188
|
+
obj = new ReactiveObject({
|
|
189
|
+
data: () => new Promise<number>((resolve) => {
|
|
190
|
+
resolver = resolve;
|
|
191
|
+
read(s);
|
|
192
|
+
})
|
|
193
|
+
}) as any;
|
|
194
|
+
|
|
195
|
+
expect(obj.data).toBeUndefined();
|
|
196
|
+
|
|
197
|
+
obj.dispose();
|
|
198
|
+
|
|
199
|
+
// After dispose, dependency changes don't spawn new effects
|
|
200
|
+
write(s, 2);
|
|
201
|
+
await Promise.resolve();
|
|
202
|
+
|
|
203
|
+
// Resolve the original in-flight promise
|
|
204
|
+
resolver!(999);
|
|
205
|
+
await Promise.resolve();
|
|
206
|
+
|
|
207
|
+
// In-flight promise still wrote (version matched)
|
|
208
|
+
expect(obj.data).toBe(999);
|
|
209
|
+
|
|
210
|
+
// But no further computations occur from new dependency changes
|
|
211
|
+
write(s, 3);
|
|
212
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
213
|
+
|
|
214
|
+
expect(obj.data).toBe(999);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
describe('null/undefined properties', () => {
|
|
220
|
+
it('creates reactive signal for null property', async () => {
|
|
221
|
+
let obj = new ReactiveObject({ key: null }) as any,
|
|
222
|
+
values: (null | number)[] = [];
|
|
223
|
+
|
|
224
|
+
expect(obj.key).toBeNull();
|
|
225
|
+
|
|
226
|
+
effect(() => {
|
|
227
|
+
values.push(obj.key);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(values).toEqual([null]);
|
|
231
|
+
|
|
232
|
+
obj.key = 42;
|
|
233
|
+
await Promise.resolve();
|
|
234
|
+
|
|
235
|
+
expect(values).toEqual([null, 42]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('creates reactive signal for undefined property', async () => {
|
|
239
|
+
let obj = new ReactiveObject({ key: undefined }) as any,
|
|
240
|
+
values: (undefined | string)[] = [];
|
|
241
|
+
|
|
242
|
+
expect(obj.key).toBeUndefined();
|
|
243
|
+
|
|
244
|
+
effect(() => {
|
|
245
|
+
values.push(obj.key);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(values).toEqual([undefined]);
|
|
249
|
+
|
|
250
|
+
obj.key = 'hello';
|
|
251
|
+
await Promise.resolve();
|
|
252
|
+
|
|
253
|
+
expect(values).toEqual([undefined, 'hello']);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
describe('enumeration', () => {
|
|
259
|
+
it('Object.keys includes all defined property names', () => {
|
|
260
|
+
let keys = Object.keys(new ReactiveObject({ a: 1, b: 'two', c: null }));
|
|
261
|
+
|
|
262
|
+
expect(keys).toContain('a');
|
|
263
|
+
expect(keys).toContain('b');
|
|
264
|
+
expect(keys).toContain('c');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('for...in iterates all defined properties', () => {
|
|
268
|
+
let obj = new ReactiveObject({ x: 10, y: 20, z: 30 }),
|
|
269
|
+
keys: string[] = [];
|
|
270
|
+
|
|
271
|
+
for (let key in obj) {
|
|
272
|
+
keys.push(key);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
expect(keys).toContain('x');
|
|
276
|
+
expect(keys).toContain('y');
|
|
277
|
+
expect(keys).toContain('z');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
describe('[SIGNAL] protected method', () => {
|
|
283
|
+
it('subclass creates reactive field via [SIGNAL]', async () => {
|
|
284
|
+
class Counter extends ReactiveObject<Record<string, never>> {
|
|
285
|
+
private _count: ReturnType<typeof signal<number>>;
|
|
286
|
+
|
|
287
|
+
constructor(initial: number) {
|
|
288
|
+
super(null);
|
|
289
|
+
this._count = this[SIGNAL](initial);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
get count() {
|
|
293
|
+
return read(this._count);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
set count(value: number) {
|
|
297
|
+
write(this._count, value);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let counter = new Counter(0),
|
|
302
|
+
values: number[] = [];
|
|
303
|
+
|
|
304
|
+
effect(() => {
|
|
305
|
+
values.push(counter.count);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(values).toEqual([0]);
|
|
309
|
+
|
|
310
|
+
counter.count = 5;
|
|
311
|
+
await Promise.resolve();
|
|
312
|
+
|
|
313
|
+
expect(values).toEqual([0, 5]);
|
|
314
|
+
|
|
315
|
+
counter.count = 10;
|
|
316
|
+
await Promise.resolve();
|
|
317
|
+
|
|
318
|
+
expect(values).toEqual([0, 5, 10]);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
111
321
|
});
|
|
112
322
|
|
|
113
323
|
|