tinybase
Version:
A reactive data store and sync engine.
434 lines (421 loc) • 13.6 kB
JavaScript
const EMPTY_STRING = '';
const isUndefined = (thing) => thing == void 0;
const ifNotUndefined = (value, then, otherwise) =>
isUndefined(value) ? otherwise?.() : then(value);
const isArray = (thing) => Array.isArray(thing);
const size = (arrayOrString) => arrayOrString.length;
const test = (regex, subject) => regex.test(subject);
const arrayHas = (array, value) => array.includes(value);
const arrayForEach = (array, cb) => array.forEach(cb);
const arrayIsEmpty = (array) => size(array) == 0;
const arrayReduce = (array, cb, initial) => array.reduce(cb, initial);
const arrayClear = (array, to) => array.splice(0, to);
const arrayPush = (array, ...values) => array.push(...values);
const arrayPop = (array) => array.pop();
const arrayUnshift = (array, ...values) => array.unshift(...values);
const arrayShift = (array) => array.shift();
const setOrDelCell = (store, tableId, rowId, cellId, cell) =>
isUndefined(cell)
? store.delCell(tableId, rowId, cellId, true)
: store.setCell(tableId, rowId, cellId, cell);
const setOrDelValue = (store, valueId, value) =>
isUndefined(value) ? store.delValue(valueId) : store.setValue(valueId, value);
const collSizeN = (collSizer) => (coll) =>
arrayReduce(collValues(coll), (total, coll2) => total + collSizer(coll2), 0);
const collSize = (coll) => coll?.size ?? 0;
const collSize2 = collSizeN(collSize);
const collHas = (coll, keyOrValue) => coll?.has(keyOrValue) ?? false;
const collIsEmpty = (coll) => isUndefined(coll) || collSize(coll) == 0;
const collValues = (coll) => [...(coll?.values() ?? [])];
const collForEach = (coll, cb) => coll?.forEach(cb);
const collDel = (coll, keyOrValue) => coll?.delete(keyOrValue);
const object = Object;
const objFreeze = object.freeze;
const mapNew = (entries) => new Map(entries);
const mapGet = (map, key) => map?.get(key);
const mapForEach = (map, cb) =>
collForEach(map, (value, key) => cb(key, value));
const mapSet = (map, key, value) =>
isUndefined(value) ? (collDel(map, key), map) : map?.set(key, value);
const mapEnsure = (map, key, getDefaultValue, hadExistingValue) => {
if (!collHas(map, key)) {
mapSet(map, key, getDefaultValue());
} else {
hadExistingValue?.(mapGet(map, key));
}
return mapGet(map, key);
};
const visitTree = (node, path, ensureLeaf, pruneLeaf, p = 0) =>
ifNotUndefined(
(ensureLeaf ? mapEnsure : mapGet)(
node,
path[p],
p > size(path) - 2 ? ensureLeaf : mapNew,
),
(nodeOrLeaf) => {
if (p > size(path) - 2) {
if (pruneLeaf?.(nodeOrLeaf)) {
mapSet(node, path[p]);
}
return nodeOrLeaf;
}
const leaf = visitTree(nodeOrLeaf, path, ensureLeaf, pruneLeaf, p + 1);
if (collIsEmpty(nodeOrLeaf)) {
mapSet(node, path[p]);
}
return leaf;
},
);
const setNew = (entryOrEntries) =>
new Set(
isArray(entryOrEntries) || isUndefined(entryOrEntries)
? entryOrEntries
: [entryOrEntries],
);
const setAdd = (set, value) => set?.add(value);
const getCreateFunction = (getFunction, initFunction) => {
const thingsByStore = /* @__PURE__ */ new WeakMap();
return (store) => {
if (!thingsByStore.has(store)) {
thingsByStore.set(store, getFunction(store));
}
const thing = thingsByStore.get(store);
initFunction?.(thing);
return thing;
};
};
const INTEGER = /^\d+$/;
const getPoolFunctions = () => {
const pool = [];
let nextId = 0;
return [
(reuse) => (reuse ? arrayShift(pool) : null) ?? EMPTY_STRING + nextId++,
(id) => {
if (test(INTEGER, id) && size(pool) < 1e3) {
arrayPush(pool, id);
}
},
];
};
const getWildcardedLeaves = (deepIdSet, path = [EMPTY_STRING]) => {
const leaves = [];
const deep = (node, p) =>
p == size(path)
? arrayPush(leaves, node)
: path[p] === null
? collForEach(node, (node2) => deep(node2, p + 1))
: arrayForEach([path[p], null], (id) => deep(mapGet(node, id), p + 1));
deep(deepIdSet, 0);
return leaves;
};
const getListenerFunctions = (getThing) => {
let thing;
const [getId, releaseId] = getPoolFunctions();
const allListeners = mapNew();
const addListener = (
listener,
idSetNode,
path,
pathGetters = [],
extraArgsGetter = () => [],
) => {
thing ??= getThing();
const id = getId(1);
mapSet(allListeners, id, [
listener,
idSetNode,
path,
pathGetters,
extraArgsGetter,
]);
setAdd(visitTree(idSetNode, path ?? [EMPTY_STRING], setNew), id);
return id;
};
const callListeners = (idSetNode, ids, ...extraArgs) =>
arrayForEach(getWildcardedLeaves(idSetNode, ids), (set) =>
collForEach(set, (id) =>
mapGet(allListeners, id)[0](thing, ...(ids ?? []), ...extraArgs),
),
);
const delListener = (id) =>
ifNotUndefined(mapGet(allListeners, id), ([, idSetNode, idOrNulls]) => {
visitTree(idSetNode, idOrNulls ?? [EMPTY_STRING], void 0, (idSet) => {
collDel(idSet, id);
return collIsEmpty(idSet) ? 1 : 0;
});
mapSet(allListeners, id);
releaseId(id);
return idOrNulls;
});
const callListener = (id) =>
ifNotUndefined(
mapGet(allListeners, id),
([listener, , path = [], pathGetters, extraArgsGetter]) => {
const callWithIds = (...ids) => {
const index = size(ids);
if (index == size(path)) {
listener(thing, ...ids, ...extraArgsGetter(ids));
} else if (isUndefined(path[index])) {
arrayForEach(pathGetters[index]?.(...ids) ?? [], (id2) =>
callWithIds(...ids, id2),
);
} else {
callWithIds(...ids, path[index]);
}
};
callWithIds();
},
);
return [addListener, callListeners, delListener, callListener];
};
const createCheckpoints = getCreateFunction(
(store) => {
let backwardIdsSize = 100;
let currentId;
let cellsDelta = mapNew();
let valuesDelta = mapNew();
let listening = 1;
let nextCheckpointId;
let checkpointsChanged;
const checkpointIdsListeners = mapNew();
const checkpointListeners = mapNew();
const [addListener, callListeners, delListenerImpl] = getListenerFunctions(
() => checkpoints,
);
const deltas = mapNew();
const labels = mapNew();
const backwardIds = [];
const forwardIds = [];
const updateStore = (oldOrNew, checkpointId) => {
listening = 0;
store.transaction(() => {
const [cellsDelta2, valuesDelta2] = mapGet(deltas, checkpointId);
collForEach(cellsDelta2, (table, tableId) =>
collForEach(table, (row, rowId) =>
collForEach(row, (oldNew, cellId) =>
setOrDelCell(store, tableId, rowId, cellId, oldNew[oldOrNew]),
),
),
);
collForEach(valuesDelta2, (oldNew, valueId) =>
setOrDelValue(store, valueId, oldNew[oldOrNew]),
);
});
listening = 1;
};
const clearCheckpointId = (checkpointId) => {
mapSet(deltas, checkpointId);
mapSet(labels, checkpointId);
callListeners(checkpointListeners, [checkpointId]);
};
const clearCheckpointIds = (checkpointIds, to) =>
arrayForEach(
arrayClear(checkpointIds, to ?? size(checkpointIds)),
clearCheckpointId,
);
const trimBackwardsIds = () =>
clearCheckpointIds(backwardIds, size(backwardIds) - backwardIdsSize);
const storeChanged = () =>
ifNotUndefined(currentId, () => {
arrayPush(backwardIds, currentId);
trimBackwardsIds();
clearCheckpointIds(forwardIds);
currentId = void 0;
checkpointsChanged = 1;
});
const storeUnchanged = () => {
currentId = arrayPop(backwardIds);
checkpointsChanged = 1;
};
let cellListenerId;
let valueListenerId;
const addCheckpointImpl = (label = EMPTY_STRING) => {
if (isUndefined(currentId)) {
currentId = EMPTY_STRING + nextCheckpointId++;
mapSet(deltas, currentId, [cellsDelta, valuesDelta]);
setCheckpoint(currentId, label);
cellsDelta = mapNew();
valuesDelta = mapNew();
checkpointsChanged = 1;
}
return currentId;
};
const goBackwardImpl = () => {
if (!arrayIsEmpty(backwardIds)) {
arrayUnshift(forwardIds, addCheckpointImpl());
updateStore(0, currentId);
currentId = arrayPop(backwardIds);
checkpointsChanged = 1;
}
};
const goForwardImpl = () => {
if (!arrayIsEmpty(forwardIds)) {
arrayPush(backwardIds, currentId);
currentId = arrayShift(forwardIds);
updateStore(1, currentId);
checkpointsChanged = 1;
}
};
const callListenersIfChanged = () => {
if (checkpointsChanged) {
callListeners(checkpointIdsListeners);
checkpointsChanged = 0;
}
};
const setSize = (size2) => {
backwardIdsSize = size2;
trimBackwardsIds();
return checkpoints;
};
const addCheckpoint = (label) => {
const id = addCheckpointImpl(label);
callListenersIfChanged();
return id;
};
const setCheckpoint = (checkpointId, label) => {
if (
hasCheckpoint(checkpointId) &&
mapGet(labels, checkpointId) !== label
) {
mapSet(labels, checkpointId, label);
callListeners(checkpointListeners, [checkpointId]);
}
return checkpoints;
};
const getStore = () => store;
const getCheckpointIds = () => [
[...backwardIds],
currentId,
[...forwardIds],
];
const forEachCheckpoint = (checkpointCallback) =>
mapForEach(labels, checkpointCallback);
const hasCheckpoint = (checkpointId) => collHas(deltas, checkpointId);
const getCheckpoint = (checkpointId) => mapGet(labels, checkpointId);
const goBackward = () => {
goBackwardImpl();
callListenersIfChanged();
return checkpoints;
};
const goForward = () => {
goForwardImpl();
callListenersIfChanged();
return checkpoints;
};
const goTo = (checkpointId) => {
const action = arrayHas(backwardIds, checkpointId)
? goBackwardImpl
: arrayHas(forwardIds, checkpointId)
? goForwardImpl
: null;
while (!isUndefined(action) && checkpointId != currentId) {
action();
}
callListenersIfChanged();
return checkpoints;
};
const addCheckpointIdsListener = (listener) =>
addListener(listener, checkpointIdsListeners);
const addCheckpointListener = (checkpointId, listener) =>
addListener(listener, checkpointListeners, [checkpointId]);
const delListener = (listenerId) => {
delListenerImpl(listenerId);
return checkpoints;
};
const clear = () => {
clearCheckpointIds(backwardIds);
clearCheckpointIds(forwardIds);
if (!isUndefined(currentId)) {
clearCheckpointId(currentId);
}
currentId = void 0;
nextCheckpointId = 0;
addCheckpoint();
return checkpoints;
};
const clearForward = () => {
if (!arrayIsEmpty(forwardIds)) {
clearCheckpointIds(forwardIds);
callListeners(checkpointIdsListeners);
}
return checkpoints;
};
const destroy = () => {
store.delListener(cellListenerId);
store.delListener(valueListenerId);
};
const getListenerStats = () => ({
checkpointIds: collSize2(checkpointIdsListeners),
checkpoint: collSize2(checkpointListeners),
});
const _registerListeners = () => {
cellListenerId = store.addCellListener(
null,
null,
null,
(_store, tableId, rowId, cellId, newCell, oldCell) => {
if (listening) {
storeChanged();
const table = mapEnsure(cellsDelta, tableId, mapNew);
const row = mapEnsure(table, rowId, mapNew);
const oldNew = mapEnsure(row, cellId, () => [oldCell, void 0]);
oldNew[1] = newCell;
if (
oldNew[0] === newCell &&
collIsEmpty(mapSet(row, cellId)) &&
collIsEmpty(mapSet(table, rowId)) &&
collIsEmpty(mapSet(cellsDelta, tableId))
) {
storeUnchanged();
}
callListenersIfChanged();
}
},
);
valueListenerId = store.addValueListener(
null,
(_store, valueId, newValue, oldValue) => {
if (listening) {
storeChanged();
const oldNew = mapEnsure(valuesDelta, valueId, () => [
oldValue,
void 0,
]);
oldNew[1] = newValue;
if (
oldNew[0] === newValue &&
collIsEmpty(mapSet(valuesDelta, valueId))
) {
storeUnchanged();
}
callListenersIfChanged();
}
},
);
};
const checkpoints = {
setSize,
addCheckpoint,
setCheckpoint,
getStore,
getCheckpointIds,
forEachCheckpoint,
hasCheckpoint,
getCheckpoint,
goBackward,
goForward,
goTo,
addCheckpointIdsListener,
addCheckpointListener,
delListener,
clear,
clearForward,
destroy,
getListenerStats,
_registerListeners,
};
return objFreeze(checkpoints.clear());
},
(checkpoints) => checkpoints._registerListeners(),
);
export {createCheckpoints};