tinybase
Version:
A reactive data store and sync engine.
817 lines (805 loc) • 26 kB
JavaScript
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};