UNPKG

tinybase

Version:

A reactive data store and sync engine.

434 lines (421 loc) 13.6 kB
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};