UNPKG

tinybase

Version:

A reactive data store and sync engine.

817 lines (805 loc) 26 kB
const getTypeOf = (thing) => typeof thing; const EMPTY_STRING = ''; const STRING = getTypeOf(EMPTY_STRING); const BOOLEAN = getTypeOf(true); const NUMBER = getTypeOf(0); const FUNCTION = getTypeOf(getTypeOf); const SUM = 'sum'; const AVG = 'avg'; const MIN = 'min'; const MAX = 'max'; const LISTENER = 'Listener'; const RESULT = 'Result'; const GET = 'get'; const ADD = 'add'; const IDS = 'Ids'; const TABLE = 'Table'; const ROW = 'Row'; const ROW_COUNT = ROW + 'Count'; const ROW_IDS = ROW + IDS; const SORTED_ROW_IDS = 'Sorted' + ROW + IDS; const CELL = 'Cell'; const CELL_IDS = CELL + IDS; const math = Math; const mathMax = math.max; const mathMin = math.min; const isFiniteNumber = isFinite; const isUndefined = (thing) => thing == void 0; const ifNotUndefined = (value, then, otherwise) => isUndefined(value) ? otherwise?.() : then(value); const isTypeStringOrBoolean = (type) => type == STRING || type == BOOLEAN; const isFunction = (thing) => getTypeOf(thing) == FUNCTION; const isArray = (thing) => Array.isArray(thing); const slice = (arrayOrString, start, end) => arrayOrString.slice(start, end); const size = (arrayOrString) => arrayOrString.length; const getUndefined = () => void 0; const arrayEvery = (array, cb) => array.every(cb); const arrayIsEqual = (array1, array2) => size(array1) === size(array2) && arrayEvery(array1, (value1, index) => array2[index] === value1); const arrayForEach = (array, cb) => array.forEach(cb); const arrayMap = (array, cb) => array.map(cb); const arraySum = (array) => arrayReduce(array, (i, j) => i + j, 0); const arrayIsEmpty = (array) => size(array) == 0; const arrayReduce = (array, cb, initial) => array.reduce(cb, initial); const arrayPush = (array, ...values) => array.push(...values); const collSize = (coll) => coll?.size ?? 0; 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 objEntries = object.entries; const objFreeze = object.freeze; const objNew = (entries = []) => object.fromEntries(entries); const objToArray = (obj, cb) => arrayMap(objEntries(obj), ([id, value]) => cb(value, id)); const objMap = (obj, cb) => objNew(objToArray(obj, (value, id) => [id, cb(value, id)])); 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 numericAggregators = /* @__PURE__ */ mapNew([ [ AVG, [ (numbers, length) => arraySum(numbers) / length, (metric, add, length) => metric + (add - metric) / (length + 1), (metric, remove, length) => metric + (metric - remove) / (length - 1), (metric, add, remove, length) => metric + (add - remove) / length, ], ], [ MAX, [ (numbers) => mathMax(...numbers), (metric, add) => mathMax(add, metric), (metric, remove) => (remove == metric ? void 0 : metric), (metric, add, remove) => remove == metric ? void 0 : mathMax(add, metric), ], ], [ MIN, [ (numbers) => mathMin(...numbers), (metric, add) => mathMin(add, metric), (metric, remove) => (remove == metric ? void 0 : metric), (metric, add, remove) => remove == metric ? void 0 : mathMin(add, metric), ], ], [ SUM, [ (numbers) => arraySum(numbers), (metric, add) => metric + add, (metric, remove) => metric - remove, (metric, add, remove) => metric - remove + add, ], ], ]); const getAggregateValue = ( aggregateValue, oldLength, newValues, changedValues, aggregators, force = false, ) => { if (collIsEmpty(newValues)) { return void 0; } const [aggregate, aggregateAdd, aggregateRemove, aggregateReplace] = aggregators; force ||= isUndefined(aggregateValue); collForEach(changedValues, ([oldValue, newValue]) => { if (!force) { aggregateValue = isUndefined(oldValue) ? aggregateAdd?.(aggregateValue, newValue, oldLength++) : isUndefined(newValue) ? aggregateRemove?.(aggregateValue, oldValue, oldLength--) : aggregateReplace?.(aggregateValue, newValue, oldValue, oldLength); force ||= isUndefined(aggregateValue); } }); return force ? aggregate(collValues(newValues), collSize(newValues)) : aggregateValue; }; const getCellOrValueType = (cellOrValue) => { const type = getTypeOf(cellOrValue); return isTypeStringOrBoolean(type) || (type == NUMBER && isFiniteNumber(cellOrValue)) ? type : void 0; }; const setOrDelCell = (store, tableId, rowId, cellId, cell) => isUndefined(cell) ? store.delCell(tableId, rowId, cellId, true) : store.setCell(tableId, rowId, cellId, cell); 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 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 createQueries = getCreateFunction((store) => { const createStore = store.createStore; const preStore = createStore(); const resultStore = createStore(); const preStoreListenerIds = mapNew(); const { addListener, callListeners, delListener: delListenerImpl, } = resultStore; const [ getStore, getQueryIds, forEachQuery, hasQuery, getTableId, , , setDefinition, , delDefinition, addQueryIdsListenerImpl, destroy, addStoreListeners, delStoreListeners, ] = getDefinableFunctions( store, () => true, getUndefined, addListener, callListeners, ); const addPreStoreListener = (preStore2, queryId, ...listenerIds) => arrayForEach(listenerIds, (listenerId) => setAdd( mapEnsure( mapEnsure(preStoreListenerIds, queryId, mapNew), preStore2, setNew, ), listenerId, ), ); const resetPreStores = (queryId) => { ifNotUndefined( mapGet(preStoreListenerIds, queryId), (queryPreStoreListenerIds) => { mapForEach(queryPreStoreListenerIds, (preStore2, listenerIds) => collForEach(listenerIds, (listenerId) => preStore2.delListener(listenerId), ), ); collClear(queryPreStoreListenerIds); }, ); arrayForEach([resultStore, preStore], (store2) => store2.delTable(queryId)); }; const synchronizeTransactions = (queryId, fromStore, toStore) => addPreStoreListener( fromStore, queryId, fromStore.addStartTransactionListener(toStore.startTransaction), fromStore.addDidFinishTransactionListener(() => toStore.finishTransaction(), ), ); const setQueryDefinition = (queryId, tableId, build) => { setDefinition(queryId, tableId); resetPreStores(queryId); const selectEntries = []; const joinEntries = [[null, [tableId, null, null, [], mapNew()]]]; const wheres = []; const groupEntries = []; const havings = []; const select = (arg1, arg2) => { const selectEntry = isFunction(arg1) ? [size(selectEntries) + EMPTY_STRING, arg1] : [ isUndefined(arg2) ? arg1 : arg2, (getTableCell) => getTableCell(arg1, arg2), ]; arrayPush(selectEntries, selectEntry); return {as: (selectedCellId) => (selectEntry[0] = selectedCellId)}; }; const join = (joinedTableId, arg1, arg2) => { const fromIntermediateJoinedTableId = isUndefined(arg2) || isFunction(arg1) ? null : arg1; const onArg = isUndefined(fromIntermediateJoinedTableId) ? arg1 : arg2; const joinEntry = [ joinedTableId, [ joinedTableId, fromIntermediateJoinedTableId, isFunction(onArg) ? onArg : (getCell) => getCell(onArg), [], mapNew(), ], ]; arrayPush(joinEntries, joinEntry); return {as: (joinedTableId2) => (joinEntry[0] = joinedTableId2)}; }; const where = (arg1, arg2, arg3) => arrayPush( wheres, isFunction(arg1) ? arg1 : isUndefined(arg3) ? (getTableCell) => getTableCell(arg1) === arg2 : (getTableCell) => getTableCell(arg1, arg2) === arg3, ); const group = ( selectedCellId, aggregate, aggregateAdd, aggregateRemove, aggregateReplace, ) => { const groupEntry = [ selectedCellId, [ selectedCellId, isFunction(aggregate) ? [aggregate, aggregateAdd, aggregateRemove, aggregateReplace] : (mapGet(numericAggregators, aggregate) ?? [ (_cells, length) => length, ]), ], ]; arrayPush(groupEntries, groupEntry); return {as: (groupedCellId) => (groupEntry[0] = groupedCellId)}; }; const having = (arg1, arg2) => arrayPush( havings, isFunction(arg1) ? arg1 : (getSelectedOrGroupedCell) => getSelectedOrGroupedCell(arg1) === arg2, ); build({select, join, where, group, having}); const selects = mapNew(selectEntries); if (collIsEmpty(selects)) { return queries; } const joins = mapNew(joinEntries); mapForEach(joins, (asTableId, [, fromAsTableId]) => ifNotUndefined(mapGet(joins, fromAsTableId), ({3: toAsTableIds}) => isUndefined(asTableId) ? 0 : arrayPush(toAsTableIds, asTableId), ), ); const groups = mapNew(groupEntries); let selectJoinWhereStore = preStore; if (collIsEmpty(groups) && arrayIsEmpty(havings)) { selectJoinWhereStore = resultStore; } else { synchronizeTransactions(queryId, selectJoinWhereStore, resultStore); const groupedSelectedCellIds = mapNew(); mapForEach(groups, (groupedCellId, [selectedCellId, aggregators]) => setAdd(mapEnsure(groupedSelectedCellIds, selectedCellId, setNew), [ groupedCellId, aggregators, ]), ); const groupBySelectedCellIds = setNew(); mapForEach(selects, (selectedCellId) => collHas(groupedSelectedCellIds, selectedCellId) ? 0 : setAdd(groupBySelectedCellIds, selectedCellId), ); const tree = mapNew(); const writeGroupRow = ( leaf, changedGroupedSelectedCells, selectedRowId, forceRemove, ) => ifNotUndefined( leaf, ([selectedCells, selectedRowIds, groupRowId, groupRow]) => { mapForEach( changedGroupedSelectedCells, (selectedCellId, [newCell]) => { const selectedCell = mapEnsure( selectedCells, selectedCellId, mapNew, ); const oldLeafCell = mapGet(selectedCell, selectedRowId); const newLeafCell = forceRemove ? void 0 : newCell; if (oldLeafCell !== newLeafCell) { const oldNewSet = setNew([[oldLeafCell, newLeafCell]]); const oldLength = collSize(selectedCell); mapSet(selectedCell, selectedRowId, newLeafCell); collForEach( mapGet(groupedSelectedCellIds, selectedCellId), ([groupedCellId, aggregators]) => { const aggregateValue = getAggregateValue( groupRow[groupedCellId], oldLength, selectedCell, oldNewSet, aggregators, ); groupRow[groupedCellId] = isUndefined( getCellOrValueType(aggregateValue), ) ? null : aggregateValue; }, ); } }, ); if ( collIsEmpty(selectedRowIds) || !arrayEvery(havings, (having2) => having2((cellId) => groupRow[cellId]), ) ) { resultStore.delRow(queryId, groupRowId); } else if (isUndefined(groupRowId)) { leaf[2] = resultStore.addRow(queryId, groupRow); } else { resultStore.setRow(queryId, groupRowId, groupRow); } }, ); addPreStoreListener( selectJoinWhereStore, queryId, selectJoinWhereStore.addRowListener( queryId, null, (_store, _tableId, selectedRowId, getCellChange) => { const oldPath = []; const newPath = []; const changedGroupedSelectedCells = mapNew(); const rowExists = selectJoinWhereStore.hasRow( queryId, selectedRowId, ); let changedLeaf = !rowExists; collForEach(groupBySelectedCellIds, (selectedCellId) => { const [changed, oldCell, newCell] = getCellChange( queryId, selectedRowId, selectedCellId, ); arrayPush(oldPath, oldCell); arrayPush(newPath, newCell); changedLeaf ||= changed; }); mapForEach(groupedSelectedCellIds, (selectedCellId) => { const [changed, , newCell] = getCellChange( queryId, selectedRowId, selectedCellId, ); if (changedLeaf || changed) { mapSet(changedGroupedSelectedCells, selectedCellId, [newCell]); } }); if (changedLeaf) { writeGroupRow( visitTree(tree, oldPath, void 0, ([, selectedRowIds]) => { collDel(selectedRowIds, selectedRowId); return collIsEmpty(selectedRowIds); }), changedGroupedSelectedCells, selectedRowId, 1, ); } if (rowExists) { writeGroupRow( visitTree( tree, newPath, () => { const groupRow = {}; collForEach( groupBySelectedCellIds, (selectedCellId) => (groupRow[selectedCellId] = selectJoinWhereStore.getCell( queryId, selectedRowId, selectedCellId, )), ); return [mapNew(), setNew(), void 0, groupRow]; }, ([, selectedRowIds]) => { setAdd(selectedRowIds, selectedRowId); }, ), changedGroupedSelectedCells, selectedRowId, ); } }, ), ); } synchronizeTransactions(queryId, store, selectJoinWhereStore); const writeSelectRow = (rootRowId) => { const getTableCell = (arg1, arg2) => store.getCell( ...(isUndefined(arg2) ? [tableId, rootRowId, arg1] : arg1 === tableId ? [tableId, rootRowId, arg2] : [ mapGet(joins, arg1)?.[0], mapGet(mapGet(joins, arg1)?.[4], rootRowId)?.[0], arg2, ]), ); selectJoinWhereStore.transaction(() => arrayEvery(wheres, (where2) => where2(getTableCell)) ? mapForEach(selects, (asCellId, tableCellGetter) => setOrDelCell( selectJoinWhereStore, queryId, rootRowId, asCellId, tableCellGetter(getTableCell, rootRowId), ), ) : selectJoinWhereStore.delRow(queryId, rootRowId), ); }; const listenToTable = (rootRowId, tableId2, rowId, joinedTableIds2) => { const getCell = (cellId) => store.getCell(tableId2, rowId, cellId); arrayForEach(joinedTableIds2, (remoteAsTableId) => { const [realJoinedTableId, , on, nextJoinedTableIds, remoteIdPair] = mapGet(joins, remoteAsTableId); const remoteRowId = on?.(getCell, rootRowId); const [previousRemoteRowId, previousRemoteListenerId] = mapGet(remoteIdPair, rootRowId) ?? []; if (remoteRowId != previousRemoteRowId) { if (!isUndefined(previousRemoteListenerId)) { delStoreListeners(queryId, previousRemoteListenerId); } mapSet( remoteIdPair, rootRowId, isUndefined(remoteRowId) ? null : [ remoteRowId, ...addStoreListeners( queryId, 1, store.addRowListener(realJoinedTableId, remoteRowId, () => listenToTable( rootRowId, realJoinedTableId, remoteRowId, nextJoinedTableIds, ), ), ), ], ); } }); writeSelectRow(rootRowId); }; const {3: joinedTableIds} = mapGet(joins, null); selectJoinWhereStore.transaction(() => addStoreListeners( queryId, 1, store.addRowListener(tableId, null, (_store, _tableId, rootRowId) => { if (store.hasRow(tableId, rootRowId)) { listenToTable(rootRowId, tableId, rootRowId, joinedTableIds); } else { selectJoinWhereStore.delRow(queryId, rootRowId); collForEach(joins, ({4: idsByRootRowId}) => ifNotUndefined( mapGet(idsByRootRowId, rootRowId), ([, listenerId]) => { delStoreListeners(queryId, listenerId); mapSet(idsByRootRowId, rootRowId); }, ), ); } }), ), ); return queries; }; const delQueryDefinition = (queryId) => { resetPreStores(queryId); delDefinition(queryId); return queries; }; const addQueryIdsListener = (listener) => addQueryIdsListenerImpl(() => listener(queries)); const delListener = (listenerId) => { delListenerImpl(listenerId); return queries; }; const getListenerStats = () => { const { tables: _1, tableIds: _2, transaction: _3, ...stats } = resultStore.getListenerStats(); return stats; }; const queries = { setQueryDefinition, delQueryDefinition, getStore, getQueryIds, forEachQuery, hasQuery, getTableId, addQueryIdsListener, delListener, destroy, getListenerStats, }; objMap( { [TABLE]: [1, 1], [TABLE + CELL_IDS]: [0, 1], [ROW_COUNT]: [0, 1], [ROW_IDS]: [0, 1], [SORTED_ROW_IDS]: [0, 5], [ROW]: [1, 2], [CELL_IDS]: [0, 2], [CELL]: [1, 3], }, ([hasAndForEach, argumentCount], gettable) => { arrayForEach( hasAndForEach ? [GET, 'has', 'forEach'] : [GET], (prefix) => (queries[prefix + RESULT + gettable] = (...args) => resultStore[prefix + gettable](...args)), ); queries[ADD + RESULT + gettable + LISTENER] = (...args) => resultStore[ADD + gettable + LISTENER]( ...slice(args, 0, argumentCount), (_store, ...listenerArgs) => args[argumentCount](queries, ...listenerArgs), true, ); }, ); return objFreeze(queries); }); export {createQueries};