UNPKG

tinybase

Version:

A reactive data store and sync engine.

550 lines (537 loc) 17.1 kB
const getTypeOf = (thing) => typeof thing; const EMPTY_STRING = ''; const STRING = getTypeOf(EMPTY_STRING); const id = (key) => EMPTY_STRING + key; const isUndefined = (thing) => thing == void 0; const ifNotUndefined = (value, then, otherwise) => isUndefined(value) ? otherwise?.() : then(value); const isString = (thing) => getTypeOf(thing) == STRING; const isArray = (thing) => Array.isArray(thing); const size = (arrayOrString) => arrayOrString.length; const test = (regex, subject) => regex.test(subject); const arrayEvery = (array, cb) => array.every(cb); const arrayIsEqual = (array1, array2) => size(array1) === size(array2) && arrayEvery(array1, (value1, index) => array2[index] === value1); const arrayIsSorted = (array, sorter) => arrayEvery( array, (value, index) => index == 0 || sorter(array[index - 1], value) <= 0, ); const arraySort = (array, sorter) => array.sort(sorter); const arrayForEach = (array, cb) => array.forEach(cb); const arrayMap = (array, cb) => array.map(cb); const arrayIsEmpty = (array) => size(array) == 0; const arrayReduce = (array, cb, initial) => array.reduce(cb, initial); const arrayPush = (array, ...values) => array.push(...values); const arrayShift = (array) => array.shift(); const collSizeN = (collSizer) => (coll) => arrayReduce(collValues(coll), (total, coll2) => total + collSizer(coll2), 0); const collSize = (coll) => coll?.size ?? 0; const collSize2 = collSizeN(collSize); const collSize3 = collSizeN(collSize2); const collHas = (coll, keyOrValue) => coll?.has(keyOrValue) ?? false; const collIsEmpty = (coll) => isUndefined(coll) || collSize(coll) == 0; const collValues = (coll) => [...(coll?.values() ?? [])]; const collClear = (coll) => coll.clear(); 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 mapKeys = (map) => [...(map?.keys() ?? [])]; 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 getDefinableFunctions = ( store, getDefaultThing, validateRowValue, addListener, callListeners, ) => { const hasRow = store.hasRow; const tableIds = mapNew(); const things = mapNew(); const thingIdListeners = mapNew(); const allRowValues = mapNew(); const allSortKeys = mapNew(); const storeListenerIds = mapNew(); const getStore = () => store; const getThingIds = () => mapKeys(tableIds); const forEachThing = (cb) => mapForEach(things, cb); const hasThing = (id) => collHas(things, id); const getTableId = (id) => mapGet(tableIds, id); const getThing = (id) => mapGet(things, id); const setThing = (id, thing) => mapSet(things, id, thing); const addStoreListeners = (id, andCall, ...listenerIds) => { const set = mapEnsure(storeListenerIds, id, setNew); arrayForEach( listenerIds, (listenerId) => setAdd(set, listenerId) && andCall && store.callListener(listenerId), ); return listenerIds; }; const delStoreListeners = (id, ...listenerIds) => ifNotUndefined(mapGet(storeListenerIds, id), (allListenerIds) => { arrayForEach( arrayIsEmpty(listenerIds) ? collValues(allListenerIds) : listenerIds, (listenerId) => { store.delListener(listenerId); collDel(allListenerIds, listenerId); }, ); if (collIsEmpty(allListenerIds)) { mapSet(storeListenerIds, id); } }); const setDefinition = (id, tableId) => { mapSet(tableIds, id, tableId); if (!collHas(things, id)) { mapSet(things, id, getDefaultThing()); mapSet(allRowValues, id, mapNew()); mapSet(allSortKeys, id, mapNew()); callListeners(thingIdListeners); } }; const setDefinitionAndListen = ( id, tableId, onChanged, getRowValue, getSortKey, ) => { setDefinition(id, tableId); const changedRowValues = mapNew(); const changedSortKeys = mapNew(); const rowValues = mapGet(allRowValues, id); const sortKeys = mapGet(allSortKeys, id); const processRow = (rowId) => { const getCell = (cellId) => store.getCell(tableId, rowId, cellId); const oldRowValue = mapGet(rowValues, rowId); const newRowValue = hasRow(tableId, rowId) ? validateRowValue(getRowValue(getCell, rowId)) : void 0; if ( !( oldRowValue === newRowValue || (isArray(oldRowValue) && isArray(newRowValue) && arrayIsEqual(oldRowValue, newRowValue)) ) ) { mapSet(changedRowValues, rowId, [oldRowValue, newRowValue]); } if (!isUndefined(getSortKey)) { const oldSortKey = mapGet(sortKeys, rowId); const newSortKey = hasRow(tableId, rowId) ? getSortKey(getCell, rowId) : void 0; if (oldSortKey != newSortKey) { mapSet(changedSortKeys, rowId, newSortKey); } } }; const processTable = (force) => { onChanged( () => { collForEach(changedRowValues, ([, newRowValue], rowId) => mapSet(rowValues, rowId, newRowValue), ); collForEach(changedSortKeys, (newSortKey, rowId) => mapSet(sortKeys, rowId, newSortKey), ); }, changedRowValues, changedSortKeys, rowValues, sortKeys, force, ); collClear(changedRowValues); collClear(changedSortKeys); }; mapForEach(rowValues, processRow); if (store.hasTable(tableId)) { arrayForEach(store.getRowIds(tableId), (rowId) => { if (!collHas(rowValues, rowId)) { processRow(rowId); } }); } processTable(true); delStoreListeners(id); addStoreListeners( id, 0, store.addRowListener(tableId, null, (_store, _tableId, rowId) => processRow(rowId), ), store.addTableListener(tableId, () => processTable()), ); }; const delDefinition = (id) => { mapSet(tableIds, id); mapSet(things, id); mapSet(allRowValues, id); mapSet(allSortKeys, id); delStoreListeners(id); callListeners(thingIdListeners); }; const addThingIdsListener = (listener) => addListener(listener, thingIdListeners); const destroy = () => mapForEach(storeListenerIds, delDefinition); return [ getStore, getThingIds, forEachThing, hasThing, getTableId, getThing, setThing, setDefinition, setDefinitionAndListen, delDefinition, addThingIdsListener, destroy, addStoreListeners, delStoreListeners, ]; }; const getRowCellFunction = (getRowCell, defaultCellValue) => isString(getRowCell) ? (getCell) => getCell(getRowCell) : (getRowCell ?? (() => defaultCellValue ?? EMPTY_STRING)); 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); return thing; }; }; const defaultSorter = (sortKey1, sortKey2) => (sortKey1 ?? 0) < (sortKey2 ?? 0) ? -1 : 1; 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 createIndexes = getCreateFunction((store) => { const sliceIdsListeners = mapNew(); const sliceRowIdsListeners = mapNew(); const [addListener, callListeners, delListenerImpl] = getListenerFunctions( () => indexes, ); const [ getStore, getIndexIds, forEachIndexImpl, hasIndex, getTableId, getIndex, setIndex, , setDefinitionAndListen, delDefinition, addIndexIdsListener, destroy, ] = getDefinableFunctions( store, mapNew, (value) => isUndefined(value) ? EMPTY_STRING : isArray(value) ? arrayMap(value, id) : id(value), addListener, callListeners, ); const hasSlice = (indexId, sliceId) => collHas(getIndex(indexId), sliceId); const setIndexDefinition = ( indexId, tableId, getSliceIdOrIds, getSortKey, sliceIdSorter, rowIdSorter = defaultSorter, ) => { const sliceIdArraySorter = isUndefined(sliceIdSorter) ? void 0 : ([id1], [id2]) => sliceIdSorter(id1, id2); setDefinitionAndListen( indexId, tableId, (change, changedSliceIds, changedSortKeys, sliceIds, sortKeys, force) => { let sliceIdsChanged = 0; const changedSlices = setNew(); const unsortedSlices = setNew(); const index = getIndex(indexId); collForEach( changedSliceIds, ([oldSliceIdOrIds, newSliceIdOrIds], rowId) => { const oldSliceIds = setNew(oldSliceIdOrIds); const newSliceIds = setNew(newSliceIdOrIds); collForEach(oldSliceIds, (oldSliceId) => collDel(newSliceIds, oldSliceId) ? collDel(oldSliceIds, oldSliceId) : 0, ); collForEach(oldSliceIds, (oldSliceId) => { setAdd(changedSlices, oldSliceId); ifNotUndefined(mapGet(index, oldSliceId), (oldSlice) => { collDel(oldSlice, rowId); if (collIsEmpty(oldSlice)) { mapSet(index, oldSliceId); sliceIdsChanged = 1; } }); }); collForEach(newSliceIds, (newSliceId) => { setAdd(changedSlices, newSliceId); if (!collHas(index, newSliceId)) { mapSet(index, newSliceId, setNew()); sliceIdsChanged = 1; } setAdd(mapGet(index, newSliceId), rowId); if (!isUndefined(getSortKey)) { setAdd(unsortedSlices, newSliceId); } }); }, ); change(); if (!collIsEmpty(sortKeys)) { if (force) { mapForEach(index, (sliceId) => setAdd(unsortedSlices, sliceId)); } else { mapForEach(changedSortKeys, (rowId) => ifNotUndefined(mapGet(sliceIds, rowId), (sliceId) => setAdd(unsortedSlices, sliceId), ), ); } collForEach(unsortedSlices, (sliceId) => { const rowIdArraySorter = (rowId1, rowId2) => rowIdSorter( mapGet(sortKeys, rowId1), mapGet(sortKeys, rowId2), sliceId, ); const sliceArray = [...mapGet(index, sliceId)]; if (!arrayIsSorted(sliceArray, rowIdArraySorter)) { mapSet( index, sliceId, setNew(arraySort(sliceArray, rowIdArraySorter)), ); setAdd(changedSlices, sliceId); } }); } if (sliceIdsChanged || force) { if (!isUndefined(sliceIdArraySorter)) { const indexArray = [...index]; if (!arrayIsSorted(indexArray, sliceIdArraySorter)) { setIndex( indexId, mapNew(arraySort(indexArray, sliceIdArraySorter)), ); sliceIdsChanged = 1; } } } if (sliceIdsChanged) { callListeners(sliceIdsListeners, [indexId]); } collForEach(changedSlices, (sliceId) => callListeners(sliceRowIdsListeners, [indexId, sliceId]), ); }, getRowCellFunction(getSliceIdOrIds), ifNotUndefined(getSortKey, getRowCellFunction), ); return indexes; }; const forEachIndex = (indexCallback) => forEachIndexImpl((indexId, slices) => indexCallback(indexId, (sliceCallback) => forEachSliceImpl(indexId, sliceCallback, slices), ), ); const forEachSlice = (indexId, sliceCallback) => forEachSliceImpl(indexId, sliceCallback, getIndex(indexId)); const forEachSliceImpl = (indexId, sliceCallback, slices) => { const tableId = getTableId(indexId); collForEach(slices, (rowIds, sliceId) => sliceCallback(sliceId, (rowCallback) => collForEach(rowIds, (rowId) => rowCallback(rowId, (cellCallback) => store.forEachCell(tableId, rowId, cellCallback), ), ), ), ); }; const delIndexDefinition = (indexId) => { delDefinition(indexId); return indexes; }; const getSliceIds = (indexId) => mapKeys(getIndex(indexId)); const getSliceRowIds = (indexId, sliceId) => collValues(mapGet(getIndex(indexId), sliceId)); const addSliceIdsListener = (indexId, listener) => addListener(listener, sliceIdsListeners, [indexId]); const addSliceRowIdsListener = (indexId, sliceId, listener) => addListener(listener, sliceRowIdsListeners, [indexId, sliceId]); const delListener = (listenerId) => { delListenerImpl(listenerId); return indexes; }; const getListenerStats = () => ({ sliceIds: collSize2(sliceIdsListeners), sliceRowIds: collSize3(sliceRowIdsListeners), }); const indexes = { setIndexDefinition, delIndexDefinition, getStore, getIndexIds, forEachIndex, forEachSlice, hasIndex, hasSlice, getTableId, getSliceIds, getSliceRowIds, addIndexIdsListener, addSliceIdsListener, addSliceRowIdsListener, delListener, destroy, getListenerStats, }; return objFreeze(indexes); }); export {createIndexes};