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