contextors
Version:
A library for creating memoised "context selector" functions
230 lines (225 loc) • 7.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
// src/index.ts
import {
isContext as isContext2,
useSubscriber
} from "contexto";
import {
useEffect,
useReducer,
useRef,
useState
} from "react";
// src/memoslots.ts
var MemoSlotProvider = class {
constructor() {
this.slots = [];
}
iterator() {
let i = 0;
const { slots } = this;
return {
next() {
if (i >= slots.length)
slots.push({ sourceValues: [], tag: void 0, out: void 0 });
return slots[i++];
}
};
}
};
// src/rawcontextor.ts
import {
isContext
} from "contexto";
var RawContextor = class {
constructor(sources, combiner, isEqual) {
this.sources = sources;
this.combiner = combiner;
this.isEqual = isEqual;
this.multiCache = /* @__PURE__ */ new WeakMap();
// Arbitrary object scoped to the Contextor that can be used to index a WeakMap
this.TerminalCacheKey = {};
this.contexts = /* @__PURE__ */ new Set();
for (const source of sources) {
if (isContext(source))
this.contexts.add(source);
else
source.contexts.forEach((context) => this.contexts.add(context));
}
}
subscribe(subscriber, onChange, tag, opts) {
const { sources } = this;
const unsubscribers = [];
const unsubscribeAll = () => unsubscribers.forEach((unsubscribe) => unsubscribe());
const sourceValues = sources.map(
(source, i) => {
const updateValue = (newValue) => {
sourceValues[i] = newValue;
onChange(this.computeWithCache(sourceValues, tag, opts == null ? void 0 : opts.memoProvider.iterator()));
};
const [initialValue2, unsubscribe] = isContext(source) ? subscriber(source, updateValue) : source.subscribe(subscriber, updateValue, tag, opts);
unsubscribers.push(unsubscribe);
return initialValue2;
}
);
const initialValue = this.computeWithCache(sourceValues, tag, opts == null ? void 0 : opts.memoProvider.iterator());
return [initialValue, unsubscribeAll];
}
computeWithCache(sourceValues, tag, memoSlots) {
return this.isEqual ? this.computeWithMemoiseCache(sourceValues, tag, memoSlots) : this.computeWithMultiCache(sourceValues, tag);
}
computeWithMemoiseCache(sourceValues, tag, memoSlots) {
const memoSlot = memoSlots == null ? void 0 : memoSlots.next();
if ((memoSlot == null ? void 0 : memoSlot.sourceValues) && this.isEqual(
// eslint-disable-line @typescript-eslint/no-non-null-assertion
[sourceValues, tag],
[memoSlot.sourceValues, memoSlot.tag]
))
return memoSlot.out;
const out = this.combiner(...sourceValues, tag);
if (memoSlot) {
memoSlot.sourceValues = sourceValues;
memoSlot.tag = tag;
memoSlot.out = out;
}
return out;
}
computeWithMultiCache(sourceValues, tag) {
let cacheRef = this.multiCache;
const sourceValuesWithTag = [...sourceValues, tag];
for (const source of sourceValuesWithTag) {
if (isObject(source)) {
let nextCacheRef = cacheRef.get(source);
if (nextCacheRef)
cacheRef = nextCacheRef;
else {
nextCacheRef = /* @__PURE__ */ new WeakMap();
cacheRef.set(source, nextCacheRef);
cacheRef = nextCacheRef;
}
}
}
const terminalCache = cacheRef.get(this.TerminalCacheKey);
if (isTerminalCache(terminalCache) && shallowEqual(terminalCache.keys, sourceValuesWithTag)) {
return terminalCache.value;
}
const value = this.combiner(...sourceValues, tag);
cacheRef.set(this.TerminalCacheKey, { keys: sourceValuesWithTag, value });
return value;
}
};
function isContextor(value) {
return value instanceof RawContextor;
}
function isTerminalCache(cache) {
return cache !== void 0 && !(cache instanceof WeakMap);
}
function isObject(value) {
return value instanceof Object;
}
var shallowEqual = (array1, array2) => array1 === array2 || array1.length === array2.length && array1.every((keyComponent, i) => keyComponent === array2[i]);
// src/index.ts
function createContextor(...params) {
const rawParams = [...params];
const lastParam = rawParams.pop();
const [combiner, options] = typeof lastParam === "object" ? [rawParams.pop(), lastParam] : [lastParam, {}];
const sources = Array.isArray(rawParams[0]) ? rawParams[0] : rawParams;
assertValidSources(sources);
return new RawContextor(sources, combiner, options == null ? void 0 : options.isEqual);
}
function assertValidSources(sources) {
if (!sources.every((source) => isContext2(source) || isContextor(source))) {
if (sources.some(isReactContext)) {
throw new Error(
"createContextor received React.Context source, but Contexto.Context source is required"
);
}
const sourceTypes = sources.map(
(source) => typeof source === "function" ? source.toString() : typeof source
).join(", ");
throw new Error(
`createContextor sources must be Context or Contextor, but received the following types: [${sourceTypes}]`
);
}
}
function isReactContext(value) {
return value !== null && typeof value === "object" && "$$typeof" in value && value.$$typeof === Symbol.for("react.context");
}
function contextorReducer(state, action) {
const { value, unsubscribe, subscribe } = state;
switch (action.type) {
case "setValue":
return (
// Only update state if value has changed
action.value !== value ? { value: action.value, unsubscribe, subscribe } : state
);
case "unsetContextor":
unsubscribe == null ? void 0 : unsubscribe();
return { value, subscribe };
case "setContextor":
return __spreadProps(__spreadValues({}, subscribe(action.contextor, action.tag)), { subscribe });
default:
return state;
}
}
function useContextor(contextor, tag) {
const subscriber = useSubscriber();
const [memoProvider] = useState(() => new MemoSlotProvider());
const subscribe = (newContextor) => {
const [initialValue, unsubscribe] = newContextor.subscribe(
subscriber,
(updatedValue) => dispatch({ type: "setValue", value: updatedValue }),
tag,
// nb: tag may be undefined here but only if Tag extends undefined
{ memoProvider }
);
return { value: initialValue, unsubscribe, subscribe };
};
const [{ value: currentValue }, dispatch] = useReducer(
contextorReducer,
contextor,
subscribe
);
useEffectOnUpdate(
() => {
dispatch({ type: "setContextor", contextor, tag });
return () => dispatch({ type: "unsetContextor" });
},
[dispatch, contextor, tag]
);
return currentValue;
}
function useEffectOnUpdate(effect, deps) {
const hasMounted = useRef(false);
useEffect(
() => {
if (hasMounted.current)
return effect();
hasMounted.current = true;
},
[hasMounted, ...deps]
// eslint-disable-line react-hooks/exhaustive-deps
);
}
export {
createContextor,
useContextor
};