Skip to content

Commit

Permalink
Transforming RawStore into an interface
Browse files Browse the repository at this point in the history
  • Loading branch information
divdavem committed Nov 12, 2024
1 parent f6b344b commit 4c45668
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 298 deletions.
5 changes: 3 additions & 2 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ import {
writable,
} from './index';
import { rawStoreSymbol } from './internal/exposeRawStores';
import { RawStoreFlags, type RawStore } from './internal/store';
import { RawStoreFlags } from './internal/store';
import { flushUnused } from './internal/storeTrackingUsage';
import type { RawStoreWritable } from './internal/storeWritable';

const expectCorrectlyCleanedUp = <T>(store: StoreInput<T>) => {
const rawStore = (store as any)[rawStoreSymbol] as RawStore<T>;
const rawStore = (store as any)[rawStoreSymbol] as RawStoreWritable<T>;
expect(rawStore.consumerLinks?.length ?? 0).toBe(0);
expect(rawStore.flags & RawStoreFlags.START_USE_CALLED).toBeFalsy();
};
Expand Down
23 changes: 13 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
rawStoreSymbol,
symbolObservable,
} from './internal/exposeRawStores';
import { noop, RawStore } from './internal/store';
import { RawStoreComputed } from './internal/storeComputed';
import { RawStoreConst } from './internal/storeConst';
import {
Expand All @@ -22,6 +21,8 @@ import {
RawStoreSyncDerived,
} from './internal/storeDerived';
import { RawStoreWithOnUse } from './internal/storeWithOnUse';
import { RawStoreWritable } from './internal/storeWritable';
import { noop } from './internal/subscribeConsumer';
import { untrack } from './internal/untrack';
import type {
AsyncDeriveFn,
Expand Down Expand Up @@ -183,18 +184,20 @@ export abstract class Store<T> implements Readable<T> {
*/
constructor(value: T) {
let rawStore;
if (value instanceof RawStore) {
if (value instanceof RawStoreWritable) {
rawStore = value;
} else {
const onUse = this.onUse;
rawStore = onUse ? new RawStoreWithOnUse(value, onUse.bind(this)) : new RawStore(value);
rawStore = onUse
? new RawStoreWithOnUse(value, onUse.bind(this))
: new RawStoreWritable(value);
rawStore.equalFn = (a, b) => this.equal(a, b);
}
this[rawStoreSymbol] = rawStore;
}

/** @internal */
[rawStoreSymbol]: RawStore<T>;
[rawStoreSymbol]: RawStoreWritable<T>;

/**
* Compares two values and returns true if they are equal.
Expand Down Expand Up @@ -246,11 +249,11 @@ export abstract class Store<T> implements Readable<T> {
* @param value - value to be used as the new state of a store.
*/
protected set(value: T): void {
getRawStore(this).set(value);
this[rawStoreSymbol].set(value);
}

get(): T {
return getRawStore(this).get();
return this[rawStoreSymbol].get();
}

/**
Expand All @@ -260,7 +263,7 @@ export abstract class Store<T> implements Readable<T> {
* @param updater - a function that takes the current state as an argument and returns the new state.
*/
protected update(updater: Updater<T>): void {
getRawStore(this).update(updater);
this[rawStoreSymbol].update(updater);
}

/**
Expand Down Expand Up @@ -294,7 +297,7 @@ export abstract class Store<T> implements Readable<T> {
* @param subscriber - see {@link SubscribableStore.subscribe}
*/
subscribe(subscriber: Subscriber<T>): UnsubscribeFunction & UnsubscribeObject {
return getRawStore(this).subscribe(subscriber);
return this[rawStoreSymbol].subscribe(subscriber);
}

[symbolObservable](): this {
Expand All @@ -308,7 +311,7 @@ const createStoreWithOnUse = <T>(initValue: T, onUse: OnUseFn<T>) => {
return store;
};

const applyStoreOptions = <T, S extends RawStore<T>>(
const applyStoreOptions = <T, S extends RawStoreWritable<T>>(
store: S,
options?: Omit<StoreOptions<T>, 'onUse'>
): S => {
Expand Down Expand Up @@ -381,7 +384,7 @@ export function writable<T>(value: T, options?: StoreOptions<T> | OnUseFn<T>): W
}
const onUse = options?.onUse;
const store = applyStoreOptions(
onUse ? createStoreWithOnUse(value, onUse) : new RawStore(value),
onUse ? createStoreWithOnUse(value, onUse) : new RawStoreWritable(value),
options
);
const res = exposeRawStore(store) as any;
Expand Down
4 changes: 2 additions & 2 deletions src/internal/batch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SubscribeConsumer } from './store';
import type { SubscribeConsumer } from './subscribeConsumer';

export const subscribersQueue: SubscribeConsumer<any>[] = [];
export const subscribersQueue: SubscribeConsumer<any, any>[] = [];
let willProcessQueue = false;

/**
Expand Down
260 changes: 22 additions & 238 deletions src/internal/store.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,9 @@
import type {
SignalStore,
SubscribableStore,
Subscriber,
SubscriberObject,
UnsubscribeFunction,
UnsubscribeObject,
Updater,
} from '../types';
import { batch, subscribersQueue } from './batch';
import { equal } from './equal';
import { activeConsumer } from './untrack';

export let notificationPhase = false;

export const checkNotInNotificationPhase = (): void => {
if (notificationPhase) {
throw new Error('Reading or writing a signal is forbidden during the notification phase.');
}
};

export let epoch = 0;
import type { SignalStore, SubscribableStore } from '../types';

export interface Consumer {
markDirty(): void;
}

export interface ProducerConsumerLink<T> {
value: T;
version: number;
producer: RawStore<T>;
indexInProducer: number;
consumer: Consumer | null;
skipMarkDirty: boolean;
}

export const updateLinkProducerValue = <T>(link: ProducerConsumerLink<T>): void => {
try {
link.skipMarkDirty = true;
link.producer.updateValue();
} finally {
link.skipMarkDirty = false;
}
};

export const isLinkUpToDate = <T>(link: ProducerConsumerLink<T>): boolean => {
const producer = link.producer;
if (link.version === producer.version) {
return true;
}
if (link.version === producer.version - 1 || link.version < 0) {
return false;
}
if (!producer.equalCache) {
producer.equalCache = {};
}
let res = producer.equalCache[link.version];
if (res === undefined) {
res = producer.equal(link.value, producer.value);
producer.equalCache[link.version] = res;
}
return res;
};

export const updateLink = <T>(link: ProducerConsumerLink<T>): T => {
const producer = link.producer;
link.value = producer.value;
link.version = producer.version;
return producer.readValue();
};

export const enum RawStoreFlags {
NONE = 0,
// the following flags are used in RawStoreTrackingUsage and derived classes
Expand All @@ -81,179 +16,28 @@ export const enum RawStoreFlags {
DIRTY = 1 << 5,
}

export class RawStore<T> implements SignalStore<T>, SubscribableStore<T> {
constructor(public value: T) {}
flags = RawStoreFlags.NONE;
version = 0;
equalFn = equal<T>;
equalCache: Record<number, boolean> | null = null;
consumerLinks: null | ProducerConsumerLink<T>[] = null;

newLink(consumer: Consumer | null): ProducerConsumerLink<T> {
return {
version: -1,
value: undefined as any,
producer: this,
indexInProducer: 0,
consumer,
skipMarkDirty: false,
};
}

registerConsumer(link: ProducerConsumerLink<T>): ProducerConsumerLink<T> {
let consumerLinks = this.consumerLinks;
if (!consumerLinks) {
consumerLinks = [];
this.consumerLinks = consumerLinks;
}
const indexInProducer = consumerLinks.length;
link.indexInProducer = indexInProducer;
consumerLinks[indexInProducer] = link;
return link;
}

unregisterConsumer(link: ProducerConsumerLink<T>): void {
const consumerLinks = this.consumerLinks;
const index = link.indexInProducer;
/* v8 ignore next 3 */
if (consumerLinks?.[index] !== link) {
throw new Error('assert failed: invalid indexInProducer');
}
// swap with the last item to avoid shifting the array
const lastConsumerLink = consumerLinks.pop()!;
const isLast = link === lastConsumerLink;
if (!isLast) {
consumerLinks[index] = lastConsumerLink;
lastConsumerLink.indexInProducer = index;
} else if (index === 0) {
this.checkUnused();
}
}

checkUnused(): void {}
updateValue(): void {}

equal(a: T, b: T): boolean {
const equalFn = this.equalFn;
return equalFn(a, b);
}

protected increaseEpoch(): void {
epoch++;
this.markConsumersDirty();
}

set(newValue: T): void {
checkNotInNotificationPhase();
const same = this.equal(this.value, newValue);
if (!same) {
batch(() => {
this.value = newValue;
this.version++;
this.equalCache = null;
this.increaseEpoch();
});
}
}

update(updater: Updater<T>): void {
this.set(updater(this.value));
}

markConsumersDirty(): void {
const prevNotificationPhase = notificationPhase;
notificationPhase = true;
try {
const consumerLinks = this.consumerLinks;
if (consumerLinks) {
for (let i = 0, l = consumerLinks.length; i < l; i++) {
const link = consumerLinks[i];
if (link.skipMarkDirty) continue;
link.consumer?.markDirty?.();
}
}
} finally {
notificationPhase = prevNotificationPhase;
}
}

get(): T {
checkNotInNotificationPhase();
if (activeConsumer) {
return activeConsumer.addProducer(this);
} else {
return this.readValue();
}
}

readValue(): T {
return this.value;
}

subscribe(subscriber: Subscriber<T>): UnsubscribeFunction & UnsubscribeObject {
checkNotInNotificationPhase();
const subscription = new SubscribeConsumer(this, subscriber);
const unsubscriber = () => subscription.unsubscribe();
unsubscriber.unsubscribe = unsubscriber;
return unsubscriber;
}
export interface BaseLink<T> {
producer: RawStore<T, BaseLink<T>>;
skipMarkDirty?: boolean;
}

export const noop = (): void => {};

const bind = <T>(object: T | null | undefined, fnName: keyof T) => {
const fn = object ? object[fnName] : null;
return typeof fn === 'function' ? fn.bind(object) : noop;
};

const noopSubscriber: SubscriberObject<any> = {
next: noop,
pause: noop,
resume: noop,
};

export const toSubscriberObject = <T>(subscriber: Subscriber<T>): SubscriberObject<T> => ({
next: typeof subscriber === 'function' ? subscriber.bind(null) : bind(subscriber, 'next'),
pause: bind(subscriber, 'pause'),
resume: bind(subscriber, 'resume'),
});

export class SubscribeConsumer<T> implements Consumer {
link: ProducerConsumerLink<T>;
subscriber: SubscriberObject<T>;
dirtyCount = 1;
constructor(producer: RawStore<T>, subscriber: Subscriber<T>) {
this.subscriber = toSubscriberObject(subscriber);
this.link = producer.registerConsumer(producer.newLink(this));
this.notify();
}

unsubscribe(): void {
if (this.subscriber !== noopSubscriber) {
this.subscriber = noopSubscriber;
this.link.producer.unregisterConsumer(this.link);
}
}

markDirty(): void {
this.dirtyCount++;
subscribersQueue.push(this);
if (this.dirtyCount === 1) {
this.subscriber.pause();
}
}
export interface RawStore<T, Link extends BaseLink<T> = BaseLink<T>>
extends SignalStore<T>,
SubscribableStore<T> {
flags: RawStoreFlags;
newLink(consumer: Consumer): Link;
registerConsumer(link: Link): Link;
unregisterConsumer(link: Link): void;
updateValue(): void;
isLinkUpToDate(link: Link): boolean;
updateLink(link: Link): T;
}

notify(): void {
this.dirtyCount--;
if (this.dirtyCount === 0 && this.subscriber !== noopSubscriber) {
updateLinkProducerValue(this.link);
if (isLinkUpToDate(this.link)) {
this.subscriber.resume();
} else {
// note that the following line can throw
const value = updateLink(this.link);
this.subscriber.next(value);
}
}
export const updateLinkProducerValue = <T>(link: BaseLink<T>): void => {
try {
link.skipMarkDirty = true;
link.producer.updateValue();
} finally {
link.skipMarkDirty = false;
}
}
};
Loading

0 comments on commit 4c45668

Please sign in to comment.