Skip to content

Commit 7e21959

Browse files
committed
feat: watch function
1 parent 37ec893 commit 7e21959

File tree

10 files changed

+130
-15
lines changed

10 files changed

+130
-15
lines changed

src/index.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { RawStoreWithOnUse } from './internal/storeWithOnUse';
2424
import { RawStoreWritable } from './internal/storeWritable';
2525
import { noop } from './internal/subscribeConsumer';
2626
import { untrack } from './internal/untrack';
27+
import { WatcherConsumer } from './internal/watch';
2728
import type {
2829
AsyncDeriveFn,
2930
AsyncDeriveOptions,
@@ -41,15 +42,17 @@ import type {
4142
UnsubscribeObject,
4243
Unsubscriber,
4344
Updater,
45+
Watcher,
4446
Writable,
4547
WritableSignal,
4648
} from './types';
4749

48-
export { batch } from './internal/batch';
50+
import { batch } from './internal/batch';
4951
export { equal } from './internal/equal';
5052
export { symbolObservable } from './internal/exposeRawStores';
5153
export { untrack } from './internal/untrack';
5254
export type * from './types';
55+
export { batch };
5356

5457
/**
5558
* Returns a wrapper (for the given store) which only exposes the {@link ReadableSignal} interface.
@@ -509,3 +512,35 @@ export function computed<T>(
509512
): ReadableSignal<T> {
510513
return exposeRawStore(applyStoreOptions(new RawStoreComputed(fn), options));
511514
}
515+
516+
/**
517+
* Creates a watcher on a store and returns it.
518+
*
519+
* @remarks
520+
*
521+
* A watcher calls synchronously its notify function when any store in the set of transitive dependencies of the watched store is changing.
522+
*
523+
* Note that the notify function must not read or write any store. It should not do any heavy task, it usually only schedules some work to be done later.
524+
* It is called even inside {@link batch}.
525+
*
526+
* A watcher is initially created in the dirty state.
527+
*
528+
* When a watcher is in the dirty state, the notify function is not called until the {@link Watcher.update|update} method is called.
529+
*
530+
* The {@link Watcher.update|update} method clears the dirty state and updates the watched store, allowing the notify function to be called the next
531+
* time any store in the set of transitive dependencies of the watched store changes.
532+
*
533+
* When a watcher is no longer needed, it should be destroyed by calling its {@link Watcher.destroy|destroy} method.
534+
*
535+
* @param store - store to watch
536+
* @param notify - function that will be called synchronously when any store in the set of transitive dependencies of the watched store is changing
537+
* @returns watcher object
538+
*/
539+
export function watch(store: StoreInput<any>, notify: () => void): Watcher {
540+
const watcherConsumer = new WatcherConsumer(getRawStore(store), notify);
541+
return {
542+
isDirty: watcherConsumer.isDirty.bind(watcherConsumer),
543+
update: watcherConsumer.update.bind(watcherConsumer),
544+
destroy: watcherConsumer.destroy.bind(watcherConsumer),
545+
};
546+
}

src/internal/batch.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import type { SubscribeConsumer } from './subscribeConsumer';
2-
3-
export const subscribersQueue: SubscribeConsumer<any, any>[] = [];
1+
export const subscribersQueue: { process(): void }[] = [];
42
let willProcessQueue = false;
53

64
/**
@@ -56,7 +54,7 @@ export const batch = <T>(fn: () => T): T => {
5654
while (subscribersQueue.length > 0) {
5755
const consumer = subscribersQueue.shift()!;
5856
try {
59-
consumer.notify();
57+
consumer.process();
6058
} catch (e) {
6159
// an error in one consumer should not impact others
6260
if (success) {

src/internal/store.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export interface RawStore<T, Link extends BaseLink<T> = BaseLink<T>>
2929
unregisterConsumer(link: Link): void;
3030
updateValue(): void;
3131
isLinkUpToDate(link: Link): boolean;
32-
updateLink(link: Link): T;
32+
updateLink(link: Link): void;
33+
readValue(): T;
3334
}
3435

3536
export const updateLinkProducerValue = <T>(link: BaseLink<T>): void => {

src/internal/storeComputed.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ export class RawStoreComputed<T>
6262
if (producer.flags & RawStoreFlags.HAS_VISIBLE_ONUSE) {
6363
this.flags |= RawStoreFlags.HAS_VISIBLE_ONUSE;
6464
}
65-
return producer.updateLink(link);
65+
producer.updateLink(link);
66+
return producer.readValue();
6667
}
6768

6869
override startUse(): void {

src/internal/storeConst.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export class RawStoreConst<T> implements RawStore<T, BaseLink<T>> {
2121
isLinkUpToDate(_link: BaseLink<T>): boolean {
2222
return true;
2323
}
24-
updateLink(_link: BaseLink<T>): T {
24+
updateLink(_link: BaseLink<T>): void {}
25+
readValue(): T {
2526
return this.value;
2627
}
2728
get(): T {

src/internal/storeDerived.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ abstract class RawStoreDerived<T, S extends StoresInput>
8282
override recompute(): void {
8383
try {
8484
this.callCleanUpFn();
85-
const values = this.producerLinks!.map((link) => link.producer.updateLink(link));
85+
const values = this.producerLinks!.map((link) => {
86+
const producer = link.producer;
87+
producer.updateLink(link);
88+
return producer.readValue();
89+
});
8690
this.cleanUpFn = normalizeUnsubscribe(this.derive(this.arrayMode ? values : values[0]));
8791
} catch (error) {
8892
this.error = error;

src/internal/storeWritable.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,9 @@ export class RawStoreWritable<T> implements RawStore<T, ProducerConsumerLink<T>>
6464
return res;
6565
}
6666

67-
updateLink(link: ProducerConsumerLink<T>): T {
67+
updateLink(link: ProducerConsumerLink<T>): void {
6868
link.value = this.value;
6969
link.version = this.version;
70-
return this.readValue();
7170
}
7271

7372
registerConsumer(link: ProducerConsumerLink<T>): ProducerConsumerLink<T> {

src/internal/subscribeConsumer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class SubscribeConsumer<T, Link extends BaseLink<T>> implements Consumer
2828
constructor(producer: RawStore<T, Link>, subscriber: Subscriber<T>) {
2929
this.subscriber = toSubscriberObject(subscriber);
3030
this.link = producer.registerConsumer(producer.newLink(this));
31-
this.notify(true);
31+
this.process(true);
3232
}
3333

3434
unsubscribe(): void {
@@ -46,7 +46,7 @@ export class SubscribeConsumer<T, Link extends BaseLink<T>> implements Consumer
4646
}
4747
}
4848

49-
notify(first = false): void {
49+
process(first = false): void {
5050
this.dirtyCount--;
5151
if (this.dirtyCount === 0 && this.subscriber !== noopSubscriber) {
5252
const link = this.link;
@@ -55,9 +55,9 @@ export class SubscribeConsumer<T, Link extends BaseLink<T>> implements Consumer
5555
if (producer.isLinkUpToDate(link) && !first) {
5656
this.subscriber.resume();
5757
} else {
58+
producer.updateLink(link);
5859
// note that the following line can throw
59-
const value = producer.updateLink(link);
60-
this.subscriber.next(value);
60+
this.subscriber.next(producer.readValue());
6161
}
6262
}
6363
}

src/internal/watch.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { Watcher } from '../types';
2+
import { updateLinkProducerValue, type BaseLink, type Consumer, type RawStore } from './store';
3+
import { noop } from './subscribeConsumer';
4+
5+
export class WatcherConsumer<T, Link extends BaseLink<T>> implements Consumer, Watcher {
6+
dirty = false;
7+
link: Link | undefined;
8+
constructor(
9+
producer: RawStore<T, Link>,
10+
private notifyFn: () => void
11+
) {
12+
this.link = producer.registerConsumer(producer.newLink(this));
13+
}
14+
15+
isDirty(): boolean {
16+
return this.dirty;
17+
}
18+
19+
markDirty(): void {
20+
if (!this.dirty) {
21+
this.dirty = true;
22+
const notifyFn = this.notifyFn;
23+
notifyFn();
24+
}
25+
}
26+
27+
update(): boolean {
28+
if (this.dirty) {
29+
this.dirty = false;
30+
const link = this.link!;
31+
const producer = link.producer;
32+
updateLinkProducerValue(link);
33+
if (producer.isLinkUpToDate(link)) {
34+
return false;
35+
}
36+
producer.updateLink(link);
37+
return true;
38+
}
39+
return false;
40+
}
41+
42+
destroy(): void {
43+
const link = this.link;
44+
if (link) {
45+
this.link = undefined;
46+
this.notifyFn = noop;
47+
this.dirty = false;
48+
link.producer.unregisterConsumer(link);
49+
}
50+
}
51+
}

src/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,28 @@ export type AsyncDeriveFn<T, S> = (
260260
export interface AsyncDeriveOptions<T, S> extends Omit<StoreOptions<T>, 'onUse'> {
261261
derive: AsyncDeriveFn<T, S>;
262262
}
263+
264+
/**
265+
* Watcher interface.
266+
*/
267+
export interface Watcher {
268+
/**
269+
* Whether the watcher is dirty (i.e. the {@link Watcher.update|update} method needs to be called before notify can be called).
270+
*/
271+
isDirty(): boolean;
272+
273+
/**
274+
* Clears the dirty state and updates the watched store.
275+
* @returns true if the value of the watched store changed (since the last call of update) or false if it stayed the same.
276+
*/
277+
update(): boolean;
278+
279+
/**
280+
* Destroys the watcher.
281+
*
282+
* @remarks
283+
*
284+
* After a watcher is destroyed it should no longer be used and its notify function will no longer be called.
285+
*/
286+
destroy(): void;
287+
}

0 commit comments

Comments
 (0)