Skip to content

Commit f5343a3

Browse files
committed
test: generic tests for tansu, svelte, angular and a new simple library
1 parent ccb0249 commit f5343a3

File tree

11 files changed

+1001
-0
lines changed

11 files changed

+1001
-0
lines changed

test/adapters/all.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { angularFramework } from './angular';
2+
import { simpleFramework } from './simple';
3+
import { svelteFramework } from './svelte';
4+
import { tansuFramework } from './tansu';
5+
import { wrapAdapterWithComputed } from '../generic/wrapAdapter';
6+
import { wrapAdapterWithForceInteropAPI } from '../generic/forceInteropAPI';
7+
8+
export const allFrameworks = [angularFramework, simpleFramework, svelteFramework, tansuFramework];
9+
export const wrappedFrameworks = (() => {
10+
const res = [];
11+
for (const base of allFrameworks) {
12+
if (base.interop === true) {
13+
res.push(wrapAdapterWithForceInteropAPI(base));
14+
}
15+
for (const wrapper of allFrameworks) {
16+
// only create wrappers between compatible frameworks:
17+
if (base.interop === wrapper.interop) {
18+
res.push(wrapAdapterWithComputed(base, wrapper));
19+
}
20+
}
21+
}
22+
return res;
23+
})();
24+
export const allFrameworksAndWrappers = [...allFrameworks, ...wrappedFrameworks];

test/adapters/angular.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { computed, signal } from '@angular/core';
2+
import { createWatch } from '@angular/core/primitives/signals';
3+
import { noopWithBuild, type ReactiveFramework } from './type';
4+
import { afterBatch } from '../../src/interop';
5+
import { batch } from './simple';
6+
7+
export const angularFramework: ReactiveFramework = {
8+
name: 'angular',
9+
interop: 'angular',
10+
signal: (initialValue) => {
11+
const s = signal(initialValue);
12+
return {
13+
write: (v) => s.set(v),
14+
read: () => s(),
15+
};
16+
},
17+
computed: (fn) => {
18+
const c = computed(fn);
19+
return {
20+
read: () => c(),
21+
};
22+
},
23+
effect,
24+
withBatch: batch,
25+
withBuild: noopWithBuild,
26+
};
27+
28+
function effect(effectFn: () => void): () => void {
29+
let scheduled = false;
30+
let alive = true;
31+
const fn = () => {
32+
scheduled = false;
33+
if (alive) {
34+
w.run();
35+
}
36+
};
37+
const w = createWatch(
38+
effectFn,
39+
() => {
40+
if (!scheduled && alive) {
41+
scheduled = true;
42+
afterBatch(fn);
43+
}
44+
},
45+
true
46+
);
47+
w.run();
48+
return () => {
49+
alive = false;
50+
w.destroy();
51+
};
52+
}

test/adapters/simple.ts

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
import {
2+
afterBatch,
3+
beginBatch,
4+
getActiveConsumer,
5+
setActiveConsumer,
6+
type Consumer,
7+
type Signal as InteropSignal,
8+
type Watcher,
9+
} from '../../src/interop';
10+
import { noopWithBuild, type ReactiveFramework } from './type';
11+
12+
abstract class BaseSignal<T> {
13+
protected _version = 0;
14+
protected abstract _getValue(): T;
15+
private _watchers: { notify: () => void; dirty: boolean }[] = [];
16+
17+
protected abstract _start(): void;
18+
protected abstract _stop(): void;
19+
20+
protected _isStarted() {
21+
return this._watchers.length > 0;
22+
}
23+
24+
watchSignal(notify: () => void): Watcher {
25+
const object = { notify, dirty: true };
26+
let version = -1;
27+
let started = false;
28+
const res: Watcher = {
29+
isUpToDate: () => !object.dirty,
30+
isStarted: () => started,
31+
update: () => {
32+
if (object.dirty) {
33+
if (started) {
34+
object.dirty = false;
35+
}
36+
this._update();
37+
const changed = this._version !== version;
38+
version = this._version;
39+
return changed;
40+
}
41+
return false;
42+
},
43+
start: () => {
44+
if (!started) {
45+
this._watchers.push(object);
46+
started = true;
47+
if (this._watchers.length === 1) {
48+
this._start();
49+
}
50+
}
51+
},
52+
stop: () => {
53+
object.dirty = true;
54+
if (started) {
55+
const index = this._watchers.indexOf(object);
56+
if (index !== -1) {
57+
this._watchers.splice(index, 1);
58+
started = false;
59+
if (this._watchers.length === 0) {
60+
this._stop();
61+
}
62+
}
63+
}
64+
},
65+
};
66+
return res;
67+
}
68+
69+
get(): T {
70+
this._update();
71+
getActiveConsumer()?.addProducer(this);
72+
return this._getValue();
73+
}
74+
75+
protected _update() {}
76+
77+
protected _markWatchersDirty() {
78+
for (const watcher of this._watchers) {
79+
if (!watcher.dirty) {
80+
watcher.dirty = true;
81+
const notify = watcher.notify;
82+
notify();
83+
}
84+
}
85+
}
86+
}
87+
88+
class Signal<T> extends BaseSignal<T> implements InteropSignal {
89+
constructor(private _value: T) {
90+
super();
91+
}
92+
93+
protected override _start(): void {}
94+
protected override _stop(): void {}
95+
96+
protected override _getValue(): T {
97+
return this._value;
98+
}
99+
100+
set(value: T) {
101+
if (!Object.is(value, this._value)) {
102+
const endBatch = beginBatch();
103+
let queueError;
104+
try {
105+
this._version++;
106+
this._value = value;
107+
this._markWatchersDirty();
108+
} finally {
109+
queueError = endBatch();
110+
}
111+
if (queueError) {
112+
throw queueError.error;
113+
}
114+
}
115+
}
116+
}
117+
118+
const ERROR_VALUE: any = Symbol('error');
119+
120+
class Computed<T> extends BaseSignal<T> implements Consumer {
121+
private _computing = false;
122+
private _dirty = true;
123+
private _error: any = null;
124+
private _value: T = ERROR_VALUE;
125+
private _depIndex = 0;
126+
private _dependencies: {
127+
signal: InteropSignal;
128+
watcher: Watcher;
129+
changed: boolean;
130+
}[] = [];
131+
132+
constructor(private _fn: () => T) {
133+
super();
134+
this._markDirty = this._markDirty.bind(this);
135+
}
136+
137+
private _markDirty() {
138+
this._dirty = true;
139+
this._markWatchersDirty();
140+
}
141+
142+
protected override _start(): void {
143+
this._dirty = true;
144+
for (const dep of this._dependencies) {
145+
dep.watcher.start();
146+
}
147+
}
148+
149+
protected override _stop(): void {
150+
for (const dep of this._dependencies) {
151+
dep.watcher.stop();
152+
}
153+
}
154+
155+
addProducer(signal: InteropSignal) {
156+
const index = this._depIndex;
157+
const curDep = this._dependencies[index];
158+
let dep = curDep;
159+
if (curDep?.signal !== signal) {
160+
const watcher = signal.watchSignal(this._markDirty);
161+
dep = { signal, watcher, changed: true };
162+
this._dependencies[index] = dep;
163+
if (curDep) {
164+
this._dependencies.push(curDep);
165+
}
166+
if (this._isStarted()) {
167+
dep.watcher.start();
168+
}
169+
}
170+
dep.watcher.update();
171+
dep.changed = false;
172+
this._depIndex++;
173+
}
174+
175+
protected override _getValue(): T {
176+
const value = this._value;
177+
if (value === ERROR_VALUE) {
178+
throw this._error;
179+
}
180+
return value;
181+
}
182+
183+
private _areDependenciesUpToDate() {
184+
if (this._version === 0) {
185+
return false;
186+
}
187+
for (let i = 0; i < this._depIndex; i++) {
188+
const dep = this._dependencies[i];
189+
if (dep.changed) {
190+
return false;
191+
}
192+
if (dep.watcher.update()) {
193+
dep.changed = true;
194+
return false;
195+
}
196+
}
197+
return true;
198+
}
199+
200+
protected override _update(): void {
201+
if (this._computing) {
202+
throw new Error('Circular dependency detected');
203+
}
204+
if (this._dirty || !this._isStarted()) {
205+
let value;
206+
let error;
207+
const prevConsumer = getActiveConsumer();
208+
this._computing = true;
209+
try {
210+
if (this._areDependenciesUpToDate()) {
211+
return;
212+
}
213+
this._depIndex = 0;
214+
setActiveConsumer(this);
215+
const fn = this._fn;
216+
value = fn();
217+
setActiveConsumer(null);
218+
const depIndex = this._depIndex;
219+
const dependencies = this._dependencies;
220+
while (dependencies.length > depIndex) {
221+
dependencies.pop()!.watcher.stop();
222+
}
223+
error = null;
224+
} catch (e) {
225+
value = ERROR_VALUE;
226+
error = e;
227+
} finally {
228+
this._dirty = false;
229+
this._computing = false;
230+
setActiveConsumer(prevConsumer);
231+
}
232+
if (!Object.is(value, this._value) || !Object.is(error, this._error)) {
233+
this._version++;
234+
this._value = value;
235+
this._error = error;
236+
}
237+
}
238+
}
239+
}
240+
241+
export const signal = <T>(value: T): Signal<T> => new Signal(value);
242+
export const computed = <T>(fn: () => T): Computed<T> => new Computed(fn);
243+
export const effect = <T>(fn: () => T): (() => void) => {
244+
let alive = true;
245+
const c = new Computed(fn);
246+
const watcher = c.watchSignal(() => {
247+
if (alive) {
248+
afterBatch(update);
249+
}
250+
});
251+
const update = () => {
252+
if (alive) {
253+
watcher.update();
254+
}
255+
};
256+
watcher.start();
257+
watcher.update();
258+
return () => {
259+
alive = false;
260+
watcher.stop();
261+
};
262+
};
263+
export const batch = <T>(fn: () => T): T => {
264+
let res;
265+
let queueError;
266+
const endBatch = beginBatch();
267+
try {
268+
res = fn();
269+
} finally {
270+
queueError = endBatch();
271+
}
272+
if (queueError) {
273+
throw queueError.error;
274+
}
275+
return res;
276+
};
277+
278+
export const simpleFramework: ReactiveFramework = {
279+
name: 'simple',
280+
interop: true,
281+
signal: (initialValue) => {
282+
const s = signal(initialValue);
283+
return {
284+
write: (v) => s.set(v),
285+
read: () => s.get(),
286+
};
287+
},
288+
computed: (fn) => {
289+
const c = computed(fn);
290+
return {
291+
read: () => c.get(),
292+
};
293+
},
294+
effect,
295+
withBatch: batch,
296+
withBuild: noopWithBuild,
297+
};

0 commit comments

Comments
 (0)