Skip to content

Commit 47d75c9

Browse files
committed
feat: experimental interoperability implementation
1 parent 61d082b commit 47d75c9

18 files changed

+1089
-110
lines changed

src/index.spec.ts

Lines changed: 301 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Component, Injectable, inject } from '@angular/core';
33
import { TestBed } from '@angular/core/testing';
44
import { BehaviorSubject, from } from 'rxjs';
55
import { writable as svelteWritable } from 'svelte/store';
6-
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
6+
import { afterAll, afterEach, beforeAll, describe, expect, it, test, vi } from 'vitest';
77
import type {
88
OnUseArgument,
99
Readable,
@@ -30,10 +30,11 @@ import {
3030
untrack,
3131
writable,
3232
} from './index';
33-
import { rawStoreSymbol } from './internal/exposeRawStores';
34-
import { RawStoreFlags } from './internal/store';
3533
import { flushUnused } from './internal/asyncFlush';
34+
import { RawStoreFlags, rawStoreSymbol } from './internal/store';
3635
import type { RawStoreWritable } from './internal/storeWritable';
36+
import { setActiveConsumer, type Signal, type Watcher } from './interop';
37+
import * as mySample from './sample';
3738

3839
const expectCorrectlyCleanedUp = <T>(store: StoreInput<T>) => {
3940
const rawStore = (store as any)[rawStoreSymbol] as RawStoreWritable<T>;
@@ -3565,4 +3566,301 @@ describe('stores', () => {
35653566
expectCorrectlyCleanedUp(doubleDoubleDoubleStore);
35663567
});
35673568
});
3569+
3570+
describe('watch', () => {
3571+
it('should work', () => {
3572+
const onUseCalls: { onUnused: number }[] = [];
3573+
const store = writable(0, {
3574+
onUse: () => {
3575+
const call = { onUnused: 0 };
3576+
onUseCalls.push(call);
3577+
return () => {
3578+
call.onUnused++;
3579+
};
3580+
},
3581+
});
3582+
const notify = vi.fn();
3583+
const watcher = store.watchSignal(notify);
3584+
watcher.start();
3585+
expect(watcher.isUpToDate()).toBe(false);
3586+
expect(onUseCalls.length).toBe(0);
3587+
expect(watcher.update()).toBe(true);
3588+
expect(onUseCalls.length).toBe(1);
3589+
expect(onUseCalls[0].onUnused).toBe(0);
3590+
expect(watcher.isUpToDate()).toBe(true);
3591+
expect(watcher.get()).toBe(0);
3592+
expect(notify).not.toHaveBeenCalled();
3593+
store.set(1);
3594+
expect(notify).toHaveBeenCalledOnce();
3595+
notify.mockClear();
3596+
expect(watcher.isUpToDate()).toBe(false);
3597+
store.set(2);
3598+
expect(notify).not.toHaveBeenCalled();
3599+
expect(watcher.update()).toBe(true);
3600+
expect(watcher.isUpToDate()).toBe(true);
3601+
expect(watcher.get()).toBe(2);
3602+
expect(notify).not.toHaveBeenCalled();
3603+
store.set(3);
3604+
expect(notify).toHaveBeenCalledOnce();
3605+
expect(watcher.isUpToDate()).toBe(false);
3606+
notify.mockClear();
3607+
store.set(4);
3608+
store.set(2);
3609+
expect(notify).not.toHaveBeenCalled();
3610+
expect(watcher.update()).toBe(false);
3611+
expect(watcher.isUpToDate()).toBe(true);
3612+
expect(watcher.update()).toBe(false);
3613+
expect(watcher.get()).toBe(2);
3614+
watcher.stop();
3615+
expect(() => watcher.get()).toThrowError('invalid watcher state');
3616+
});
3617+
});
3618+
3619+
describe('fromWatch', () => {
3620+
it('should work', () => {
3621+
let notify: () => void;
3622+
let isUpdated = true;
3623+
let value = 0;
3624+
const watcher = {
3625+
isUpToDate: vi.fn(() => {
3626+
throw new Error('unexpected call to isUpToDate');
3627+
}),
3628+
isStarted: vi.fn(() => {
3629+
throw new Error('unexpected call to isStarted');
3630+
}),
3631+
update: vi.fn(() => isUpdated),
3632+
get: vi.fn(() => value),
3633+
start: vi.fn(),
3634+
stop: vi.fn(),
3635+
} satisfies Watcher<number>;
3636+
const watchFn = vi.fn((notifyFn: () => void) => {
3637+
notify = notifyFn;
3638+
return watcher;
3639+
}) satisfies (notify: () => void) => Watcher<number>;
3640+
const clearMocks = () => {
3641+
watchFn.mockClear();
3642+
watcher.update.mockClear();
3643+
watcher.get.mockClear();
3644+
watcher.stop.mockClear();
3645+
};
3646+
const store = asReadable({ watchSignal: watchFn });
3647+
expect(watchFn).toHaveBeenCalledOnce();
3648+
expect(store.get()).toBe(0);
3649+
expect(watchFn).toHaveBeenCalledOnce();
3650+
expect(watcher.update).toHaveBeenCalledOnce();
3651+
expect(watcher.get).toHaveBeenCalledOnce();
3652+
expect(watcher.stop).toHaveBeenCalledOnce();
3653+
clearMocks();
3654+
const values: number[] = [];
3655+
const unsubscribe = store.subscribe((value) => {
3656+
values.push(value);
3657+
});
3658+
expect(values).toEqual([0]);
3659+
expect(watcher.update).toHaveBeenCalledOnce();
3660+
expect(watcher.get).toHaveBeenCalledOnce();
3661+
expect(watchFn).not.toHaveBeenCalled();
3662+
expect(watcher.stop).not.toHaveBeenCalled();
3663+
clearMocks();
3664+
isUpdated = false;
3665+
batch(() => {
3666+
notify!();
3667+
});
3668+
expect(watcher.update).toHaveBeenCalledOnce();
3669+
expect(watchFn).not.toHaveBeenCalled();
3670+
expect(watcher.get).not.toHaveBeenCalled();
3671+
expect(watcher.stop).not.toHaveBeenCalled();
3672+
clearMocks();
3673+
expect(values).toEqual([0]);
3674+
isUpdated = true;
3675+
value = 2;
3676+
batch(() => {
3677+
notify!();
3678+
});
3679+
expect(watcher.update).toHaveBeenCalledOnce();
3680+
expect(watcher.get).toHaveBeenCalledOnce();
3681+
expect(watcher.stop).not.toHaveBeenCalled();
3682+
expect(watchFn).not.toHaveBeenCalled();
3683+
clearMocks();
3684+
expect(values).toEqual([0, 2]);
3685+
isUpdated = true;
3686+
watcher.get.mockImplementation(() => {
3687+
throw new Error('myerror');
3688+
});
3689+
expect(() => {
3690+
batch(() => {
3691+
notify!();
3692+
});
3693+
}).toThrowError('myerror');
3694+
expect(watcher.update).toHaveBeenCalledOnce();
3695+
expect(watcher.get).toHaveBeenCalledOnce();
3696+
expect(watcher.stop).not.toHaveBeenCalled();
3697+
expect(watchFn).not.toHaveBeenCalled();
3698+
clearMocks();
3699+
expect(() => {
3700+
store.get();
3701+
}).toThrowError('myerror');
3702+
expect(values).toEqual([0, 2]);
3703+
expect(watcher.update).not.toHaveBeenCalled();
3704+
expect(watcher.get).not.toHaveBeenCalled();
3705+
expect(watcher.stop).not.toHaveBeenCalled();
3706+
expect(watchFn).not.toHaveBeenCalled();
3707+
unsubscribe();
3708+
expect(watcher.stop).toHaveBeenCalledOnce();
3709+
expect(watcher.update).not.toHaveBeenCalled();
3710+
expect(watcher.get).not.toHaveBeenCalled();
3711+
expect(watchFn).not.toHaveBeenCalled();
3712+
expect(watcher.isUpToDate).not.toHaveBeenCalled();
3713+
});
3714+
});
3715+
3716+
describe('watch / fromWatch', () => {
3717+
it('should work to convert back and forth a basic writable', () => {
3718+
const store = writable(0);
3719+
const otherStore = asReadable({ watchSignal: (notify) => store.watchSignal(notify) });
3720+
expect(otherStore()).toBe(0);
3721+
store.set(1);
3722+
expect(otherStore()).toBe(1);
3723+
store.set(2);
3724+
expect(otherStore()).toBe(2);
3725+
3726+
const values: number[] = [];
3727+
const unsubscribe = otherStore.subscribe((value) => {
3728+
values.push(value);
3729+
});
3730+
expect(values).toEqual([2]);
3731+
store.set(3);
3732+
expect(values).toEqual([2, 3]);
3733+
expect(otherStore()).toBe(3);
3734+
batch(() => {
3735+
store.set(4);
3736+
expect(otherStore()).toBe(4);
3737+
store.set(5);
3738+
store.set(3);
3739+
});
3740+
expect(values).toEqual([2, 3]);
3741+
unsubscribe();
3742+
});
3743+
3744+
it('should work to convert back and forth a basic Store', () => {
3745+
class MyStore extends Store<number> {
3746+
increase() {
3747+
this.update((value) => value + 1);
3748+
}
3749+
}
3750+
const store = new MyStore(0);
3751+
3752+
const otherStore = asReadable({ watchSignal: (notify) => store.watchSignal(notify) });
3753+
expect(otherStore()).toBe(0);
3754+
store.increase();
3755+
expect(otherStore()).toBe(1);
3756+
store.increase();
3757+
expect(otherStore()).toBe(2);
3758+
3759+
const values: number[] = [];
3760+
const unsubscribe = otherStore.subscribe((value) => {
3761+
values.push(value);
3762+
});
3763+
expect(values).toEqual([2]);
3764+
store.increase();
3765+
expect(values).toEqual([2, 3]);
3766+
expect(otherStore()).toBe(3);
3767+
batch(() => {
3768+
store.increase();
3769+
expect(otherStore()).toBe(4);
3770+
store.increase();
3771+
store.increase();
3772+
});
3773+
expect(values).toEqual([2, 3, 6]);
3774+
unsubscribe();
3775+
});
3776+
3777+
it('should work to convert back and forth a computed', () => {
3778+
const store = writable(0);
3779+
const doubleStore = computed(() => store() * 2);
3780+
const otherStore = asReadable({
3781+
watchSignal: (notify) => doubleStore.watchSignal(notify),
3782+
});
3783+
expect(otherStore()).toBe(0);
3784+
store.set(1);
3785+
expect(otherStore()).toBe(2);
3786+
store.set(2);
3787+
expect(otherStore()).toBe(4);
3788+
3789+
const values: number[] = [];
3790+
const unsubscribe = otherStore.subscribe((value) => {
3791+
values.push(value);
3792+
});
3793+
expect(values).toEqual([4]);
3794+
store.set(3);
3795+
expect(values).toEqual([4, 6]);
3796+
expect(otherStore()).toBe(6);
3797+
batch(() => {
3798+
store.set(4);
3799+
expect(otherStore()).toBe(8);
3800+
store.set(5);
3801+
store.set(3);
3802+
});
3803+
expect(values).toEqual([4, 6]);
3804+
unsubscribe();
3805+
});
3806+
});
3807+
3808+
describe('interop computed', () => {
3809+
test('should work with a computed from another library', () => {
3810+
const a = writable(0);
3811+
const notify = vi.fn();
3812+
const watchers: Watcher<any>[] = [];
3813+
const consumer = {
3814+
addProducer: vi.fn(<T>(signal: Signal<T>) => {
3815+
const watcher = signal.watchSignal(notify);
3816+
watchers.push(watcher);
3817+
watcher.start();
3818+
watcher.update();
3819+
}),
3820+
};
3821+
const computeValue = () => {
3822+
const prevConsumer = setActiveConsumer(consumer);
3823+
try {
3824+
return 2 * a();
3825+
} finally {
3826+
setActiveConsumer(prevConsumer);
3827+
}
3828+
};
3829+
expect(computeValue()).toBe(0);
3830+
expect(consumer.addProducer).toHaveBeenCalledOnce();
3831+
expect(notify).not.toHaveBeenCalled();
3832+
a.set(1);
3833+
expect(notify).toHaveBeenCalledOnce();
3834+
expect(watchers).toHaveLength(1);
3835+
});
3836+
3837+
test('should work with a store from another library', () => {
3838+
const a = mySample.signal(0);
3839+
const myComputed = computed(() => a.get());
3840+
console.log('before myComputed()');
3841+
expect(myComputed()).toBe(0);
3842+
console.log('before a.set(1)');
3843+
a.set(1);
3844+
console.log('before myComputed()');
3845+
expect(myComputed()).toBe(1);
3846+
});
3847+
3848+
test('should work with a store from another library (live)', () => {
3849+
const a = mySample.signal(0);
3850+
const myComputed = computed(() => a.get());
3851+
const values: number[] = [];
3852+
console.log('before subscribe()');
3853+
myComputed.subscribe((value) => {
3854+
values.push(value);
3855+
});
3856+
expect(values).toEqual([0]);
3857+
console.log('before myComputed()');
3858+
expect(myComputed()).toBe(0);
3859+
console.log('before a.set(1)');
3860+
a.set(1);
3861+
expect(values).toEqual([0, 1]);
3862+
console.log('before myComputed()');
3863+
expect(myComputed()).toBe(1);
3864+
});
3865+
});
35683866
});

src/index.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@
66
*/
77

88
import { equal } from './internal/equal';
9-
import {
10-
exposeRawStore,
11-
getRawStore,
12-
rawStoreSymbol,
13-
symbolObservable,
14-
} from './internal/exposeRawStores';
9+
import { exposeRawStore, getRawStore, symbolObservable } from './internal/exposeRawStores';
1510
import { RawStoreComputed } from './internal/storeComputed';
1611
import { RawStoreConst } from './internal/storeConst';
1712
import {
@@ -24,6 +19,7 @@ import { RawStoreWithOnUse } from './internal/storeWithOnUse';
2419
import { RawStoreWritable } from './internal/storeWritable';
2520
import { noop } from './internal/subscribeConsumer';
2621
import { untrack } from './internal/untrack';
22+
import { watchRawStore } from './internal/watch';
2723
import type {
2824
AsyncDeriveFn,
2925
AsyncDeriveOptions,
@@ -45,6 +41,9 @@ import type {
4541
WritableSignal,
4642
} from './types';
4743

44+
import { rawStoreSymbol } from './internal/store';
45+
import { type Watcher } from './interop';
46+
4847
export { batch } from './internal/batch';
4948
export { equal } from './internal/equal';
5049
export { symbolObservable } from './internal/exposeRawStores';
@@ -303,6 +302,10 @@ export abstract class Store<T> implements Readable<T> {
303302
[symbolObservable](): this {
304303
return this;
305304
}
305+
306+
watchSignal(notify: () => void): Watcher<T> {
307+
return watchRawStore(this[rawStoreSymbol], notify);
308+
}
306309
}
307310

308311
const createStoreWithOnUse = <T>(initValue: T, onUse: OnUseFn<T>) => {

0 commit comments

Comments
 (0)