mutable-store
Version:
a mutable state management library for javascript
169 lines (149 loc) • 4.04 kB
text/typescript
export function getProps(obj: Record<string, any>) {
const props = new Set();
const builtins = [
Object.prototype,
Array.prototype,
Function.prototype,
String.prototype,
Number.prototype,
Boolean.prototype,
Symbol.prototype,
Date.prototype,
RegExp.prototype,
Map.prototype,
Set.prototype,
WeakMap.prototype,
WeakSet.prototype,
Promise.prototype,
Error.prototype,
];
while (obj && !builtins.includes(obj)) {
for (const key of Reflect.ownKeys(obj)) {
if (key !== "constructor") {
props.add(key);
}
}
obj = Object.getPrototypeOf(obj);
}
return Array.from(props);
}
function makePropsReadOnly<T extends Record<string, any>>(
obj: T,
keys: (keyof T)[]
) {
for (const key of keys) {
if (obj.hasOwnProperty(key)) {
Object.defineProperty(obj, key, {
writable: false,
configurable: false,
enumerable: true, // or false depending on your needs
});
} else {
console.warn(`Property "${String(key)}" does not exist on the object.`);
}
}
return obj;
}
export type TMutableStore<T> = T & {
subscribe: (fn: () => void) => () => void;
___thisIsAMutableStore___: true;
___version___: number;
};
export default function createMutableStore<T extends Record<string, any>>(
mutableState: T
): TMutableStore<T> {
if (mutableState === null || typeof mutableState !== 'object') {
throw new Error("mutableState must be an object");
}
const props: (keyof T)[] = getProps(mutableState) as (keyof T)[];
if (props.includes("subscribe")) {
throw Error(
"subscribe is a reserved keyword for the store, please use another name for your property"
);
}
makePropsReadOnly(
mutableState,
props.filter(
(item) =>
typeof mutableState[item] === "function" ||
mutableState[item]?.___thisIsAMutableStore___
)
);
const subscriptions = new Set<() => void>();
const internalStoreUnSubs = new Set<() => void>();
const getInternalStores = () =>
props
.filter((item) => mutableState[item].___thisIsAMutableStore___)
.map((item) => mutableState[item]);
const subscribe = (fn: () => void) => {
subscriptions.add(fn);
return () => {
subscriptions.delete(fn);
};
};
function callSubs() {
try {
subscriptions.forEach((fn) => fn());
} catch (e) {
console.error(e);
}
}
// auto subscribe to internal stores
function autoSubscribeToInternalStores() {
getInternalStores().forEach((item) =>
internalStoreUnSubs.add(
item.subscribe(() => {
callSubs();
})
)
);
}
autoSubscribeToInternalStores();
props.forEach((prop) => {
if (
typeof mutableState[prop] === "function" &&
prop.toString().startsWith("set_")
) {
const originalMethod = mutableState[prop];
// Replace the method with our wrapped version
Object.defineProperty(mutableState, prop, {
value: function (this: T, ...args: any[]) {
const result = originalMethod.apply(this, args);
setTimeout(() => {
callSubs();
autoSubscribeToInternalStores();
}, 0);
return result;
},
writable: false,
configurable: false,
enumerable: true
});
}
});
// Add our internal properties first
Object.defineProperties(mutableState, {
subscribe: {
value: subscribe,
writable: false,
configurable: false,
enumerable: false
},
___thisIsAMutableStore___: {
value: true,
writable: false,
configurable: false,
enumerable: false
},
___version___: {
value: 1,
writable: false,
configurable: false,
enumerable: false
}
});
// Now make the object non-extensible after all modifications
Object.preventExtensions(mutableState);
return mutableState as unknown as TMutableStore<T>;
}
export const Store = createMutableStore;