react-external-subject
Version:
Wrap mutable sources into React-safe mutable source
123 lines (102 loc) • 2.61 kB
text/typescript
import {
ExternalSubject,
ExternalSubjectOptions,
ExternalSubjectSubscribe,
ExternalSubjectSynchronize,
UpdateRequest,
} from './types';
function defaultShouldUpdate<T>(a: T, b: T): boolean {
return !Object.is(a, b);
}
interface CacheRef<T> {
value: T;
}
export default function createExternalSubject<T>(
options: ExternalSubjectOptions<T>,
): ExternalSubject<T> {
const shouldUpdate = options.shouldUpdate ?? defaultShouldUpdate;
let cache: CacheRef<T> | undefined;
const listeners = new Set<() => void>();
let request: UpdateRequest | undefined;
const getCachedValue = () => {
if (!cache) {
cache = {
value: options.read(),
};
}
return cache.value;
};
let synchronize: ExternalSubjectSynchronize;
const setSynchronizer = (sync: ExternalSubjectSynchronize) => {
if (!synchronize) {
synchronize = sync;
}
};
const requestUpdate = () => {
if (shouldUpdate(getCachedValue(), options.read())) {
if (request) {
request.alive = false;
}
request = {
promise: Promise.resolve().then(() => new Promise((resolve) => {
if (request?.alive) {
request = undefined;
const onResolve = () => {
if (cache) {
cache.value = options.read();
}
listeners.forEach((listener) => {
listener();
});
resolve();
};
if (synchronize) {
synchronize(onResolve);
} else {
onResolve();
}
}
})),
alive: true,
};
}
};
let unsubscribe: (() => void) | undefined | void;
const subscribe: ExternalSubjectSubscribe = (handler) => {
listeners.add(handler);
if (options.lazySubscribe && options.subscribe && listeners.size === 1) {
const unsub = options.subscribe(requestUpdate);
if (unsub) {
unsubscribe = unsub;
}
}
return () => {
listeners.delete(handler);
if (options.lazySubscribe && unsubscribe) {
unsubscribe();
}
};
};
if (!options.lazySubscribe && options.subscribe) {
unsubscribe = options.subscribe(requestUpdate);
}
const destroy = () => {
if (request) {
request.alive = false;
}
listeners.clear();
if (unsubscribe) {
unsubscribe();
}
};
return {
subscribe,
shouldUpdate,
requestUpdate,
getRequest: () => request,
getCachedValue,
getCurrentValue: options.read,
setSynchronizer,
destroy,
};
}