UNPKG

tinybase

Version:

A reactive data store and sync engine.

1,126 lines (1,106 loc) 32.1 kB
const getTypeOf = (thing) => typeof thing; const TINYBASE = 'tinybase'; const EMPTY_STRING = ''; const COMMA = ','; const STRING = getTypeOf(EMPTY_STRING); const UNDEFINED = '\uFFFC'; const promise = Promise; const THOUSAND = 1e3; const startInterval = (callback, sec, immediate) => { return setInterval(callback, sec * THOUSAND); }; const stopInterval = clearInterval; 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 slice = (arrayOrString, start, end) => arrayOrString.slice(start, end); const size = (arrayOrString) => arrayOrString.length; const test = (regex, subject) => regex.test(subject); const promiseAll = async (promises) => promise.all(promises); const errorNew = (message) => { throw new Error(message); }; const arrayForEach = (array, cb) => array.forEach(cb); const arrayJoin = (array, sep = EMPTY_STRING) => array.join(sep); const arrayMap = (array, cb) => array.map(cb); const arrayIsEmpty = (array) => size(array) == 0; const arrayFilter = (array, cb) => array.filter(cb); const arrayClear = (array, to) => array.splice(0, to); const arrayPush = (array, ...values) => array.push(...values); const arrayShift = (array) => array.shift(); 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 SINGLE_ROW_ID = '_'; const DEFAULT_ROW_ID_COLUMN_NAME = '_id'; const SELECT = 'SELECT'; const WHERE = 'WHERE'; const TABLE = 'TABLE'; const ALTER_TABLE = 'ALTER ' + TABLE; const DELETE_FROM = 'DELETE FROM'; const SELECT_STAR_FROM = SELECT + '*FROM'; const PRAGMA = 'pragma_'; const DATA_VERSION = 'data_version'; const SCHEMA_VERSION = 'schema_version'; const FROM = 'FROM '; const PRAGMA_TABLE = 'pragma_table_'; const getWrappedCommand = (executeCommand, onSqlCommand) => onSqlCommand ? async (sql, params) => { onSqlCommand(sql, params); return await executeCommand(sql, params); } : executeCommand; const escapeId = (str) => `"${str.replace(/"/g, '""')}"`; const escapeColumnNames = (...columnNames) => arrayJoin(arrayMap(columnNames, escapeId), COMMA); const getPlaceholders = (array, offset = [1]) => arrayJoin( arrayMap(array, () => '$' + offset[0]++), COMMA, ); const object = Object; const getPrototypeOf = (obj) => object.getPrototypeOf(obj); const objEntries = object.entries; const isObject = (obj) => !isUndefined(obj) && ifNotUndefined( getPrototypeOf(obj), (objPrototype) => objPrototype == object.prototype || isUndefined(getPrototypeOf(objPrototype)), /* istanbul ignore next */ () => true, ); const objIds = object.keys; const objFreeze = object.freeze; const objNew = (entries = []) => object.fromEntries(entries); const objMerge = (...objs) => object.assign({}, ...objs); const objHas = (obj, id) => id in obj; const objDel = (obj, id) => { delete obj[id]; return obj; }; 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 objValues = (obj) => object.values(obj); const objSize = (obj) => size(objIds(obj)); const objIsEmpty = (obj) => isObject(obj) && objSize(obj) == 0; const mapNew = (entries) => new Map(entries); const mapGet = (map, key) => map?.get(key); const mapMap = (coll, cb) => arrayMap([...(coll?.entries() ?? [])], ([key, value]) => cb(value, key)); 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 COLUMN_NAME = 'ColumnName'; const STORE = 'store'; const JSON$1 = 'json'; const STORE_TABLE_NAME = STORE + 'TableName'; const STORE_ID_COLUMN_NAME = STORE + 'Id' + COLUMN_NAME; const STORE_COLUMN_NAME = STORE + COLUMN_NAME; const AUTO_LOAD_INTERVAL_SECONDS = 'autoLoadIntervalSeconds'; const ROW_ID_COLUMN_NAME = 'rowId' + COLUMN_NAME; const TABLE_ID = 'tableId'; const TABLE_NAME = 'tableName'; const DELETE_EMPTY_COLUMNS = 'deleteEmptyColumns'; const DELETE_EMPTY_TABLE = 'deleteEmptyTable'; const DEFAULT_CONFIG = { mode: JSON$1, [AUTO_LOAD_INTERVAL_SECONDS]: 1, }; const DEFAULT_TABULAR_VALUES_CONFIG = { load: 0, save: 0, [TABLE_NAME]: TINYBASE + '_values', }; const getDefaultedConfig = (configOrStoreTableName) => objMerge( DEFAULT_CONFIG, isString(configOrStoreTableName) ? {[STORE_TABLE_NAME]: configOrStoreTableName} : (configOrStoreTableName ?? {}), ); const getDefaultedTabularConfigMap = ( configsObj, defaultObj, tableField, exclude, then, ) => { const configMap = mapNew(); objMap(configsObj, (configObj, id) => { const defaultedConfig = slice( objValues( objMerge( defaultObj, isString(configObj) ? {[tableField]: configObj} : configObj, ), ), 0, objSize(defaultObj), ); if (!isUndefined(defaultedConfig[0]) && !exclude(id, defaultedConfig[0])) { then(id, defaultedConfig[0]); mapSet(configMap, id, defaultedConfig); } }); return configMap; }; const getConfigStructures = (configOrStoreTableName) => { const config = getDefaultedConfig(configOrStoreTableName); const autoLoadIntervalSeconds = config[AUTO_LOAD_INTERVAL_SECONDS]; if (config.mode == JSON$1) { const storeTableName = config[STORE_TABLE_NAME] ?? TINYBASE; return [ 1, autoLoadIntervalSeconds, [ storeTableName, config[STORE_ID_COLUMN_NAME] ?? DEFAULT_ROW_ID_COLUMN_NAME, config[STORE_COLUMN_NAME] ?? STORE, ], setNew(storeTableName), ]; } const {tables: {load = {}, save = {}} = {}, values = {}} = config; const valuesConfig = slice( objValues(objMerge(DEFAULT_TABULAR_VALUES_CONFIG, values)), 0, objSize(DEFAULT_TABULAR_VALUES_CONFIG), ); const valuesTable = valuesConfig[2]; const managedTableNames = setNew(valuesTable); const excludedTableNames = setNew(valuesTable); const tabularConfig = [ getDefaultedTabularConfigMap( load, {[TABLE_ID]: null, [ROW_ID_COLUMN_NAME]: DEFAULT_ROW_ID_COLUMN_NAME}, TABLE_ID, (tableName) => collHas(excludedTableNames, tableName), (tableName) => setAdd(managedTableNames, tableName), ), getDefaultedTabularConfigMap( save, { [TABLE_NAME]: null, [ROW_ID_COLUMN_NAME]: DEFAULT_ROW_ID_COLUMN_NAME, [DELETE_EMPTY_COLUMNS]: 0, [DELETE_EMPTY_TABLE]: 0, }, TABLE_NAME, (_, tableName) => collHas(excludedTableNames, tableName), (_, tableName) => setAdd(managedTableNames, tableName), ), valuesConfig, ]; return [0, autoLoadIntervalSeconds, tabularConfig, managedTableNames]; }; const jsonString = JSON.stringify; const jsonParse = JSON.parse; const jsonStringWithUndefined = (obj) => jsonString(obj, (_key, value) => (value === void 0 ? UNDEFINED : value)); const jsonParseWithUndefined = (str) => jsonParse(str, (_key, value) => (value === UNDEFINED ? void 0 : value)); 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 scheduleRunning = mapNew(); const scheduleActions = mapNew(); const getStoreFunctions = ( persist = 1 /* StoreOnly */, store, isSynchronizer, ) => persist != 1 /* StoreOnly */ && store.isMergeable() ? [ 1, store.getMergeableContent, () => store.getTransactionMergeableChanges(!isSynchronizer), ([[changedTables], [changedValues]]) => !objIsEmpty(changedTables) || !objIsEmpty(changedValues), store.setDefaultContent, ] : persist != 2 /* MergeableStoreOnly */ ? [ 0, store.getContent, store.getTransactionChanges, ([changedTables, changedValues]) => !objIsEmpty(changedTables) || !objIsEmpty(changedValues), store.setContent, ] : errorNew('Store type not supported by this Persister'); const createCustomPersister = ( store, getPersisted, setPersisted, addPersisterListener, delPersisterListener, onIgnoredError, persist, extra = {}, isSynchronizer = 0, scheduleId = [], ) => { let status = 0; /* Idle */ let loads = 0; let saves = 0; let action; let autoLoadHandle; let autoSaveListenerId; mapEnsure(scheduleRunning, scheduleId, () => 0); mapEnsure(scheduleActions, scheduleId, () => []); const statusListeners = mapNew(); const [ isMergeableStore, getContent, getChanges, hasChanges, setDefaultContent, ] = getStoreFunctions(persist, store, isSynchronizer); const [addListener, callListeners, delListenerImpl] = getListenerFunctions( () => persister, ); const setStatus = (newStatus) => { if (newStatus != status) { status = newStatus; callListeners(statusListeners, void 0, status); } }; const run = async () => { /* istanbul ignore else */ if (!mapGet(scheduleRunning, scheduleId)) { mapSet(scheduleRunning, scheduleId, 1); while ( !isUndefined((action = arrayShift(mapGet(scheduleActions, scheduleId)))) ) { try { await action(); } catch (error) { /* istanbul ignore next */ onIgnoredError?.(error); } } mapSet(scheduleRunning, scheduleId, 0); } }; const setContentOrChanges = (contentOrChanges) => { (isMergeableStore && isArray(contentOrChanges?.[0]) ? contentOrChanges?.[2] === 1 ? store.applyMergeableChanges : store.setMergeableContent : contentOrChanges?.[2] === 1 ? store.applyChanges : store.setContent)(contentOrChanges); }; const load = async (initialContent) => { /* istanbul ignore else */ if (status != 2 /* Saving */) { setStatus(1 /* Loading */); loads++; await schedule(async () => { try { const content = await getPersisted(); if (isArray(content)) { setContentOrChanges(content); } else if (initialContent) { setDefaultContent(initialContent); } else { errorNew(`Content is not an array: ${content}`); } } catch (error) { onIgnoredError?.(error); if (initialContent) { setDefaultContent(initialContent); } } setStatus(0 /* Idle */); }); } return persister; }; const startAutoLoad = async (initialContent) => { stopAutoLoad(); await load(initialContent); try { autoLoadHandle = await addPersisterListener(async (content, changes) => { if (changes || content) { /* istanbul ignore else */ if (status != 2 /* Saving */) { setStatus(1 /* Loading */); loads++; setContentOrChanges(changes ?? content); setStatus(0 /* Idle */); } } else { await load(); } }); } catch (error) { /* istanbul ignore next */ onIgnoredError?.(error); } return persister; }; const stopAutoLoad = () => { if (autoLoadHandle) { delPersisterListener(autoLoadHandle); autoLoadHandle = void 0; } return persister; }; const isAutoLoading = () => !isUndefined(autoLoadHandle); const save = async (changes) => { /* istanbul ignore else */ if (status != 1 /* Loading */) { setStatus(2 /* Saving */); saves++; await schedule(async () => { try { await setPersisted(getContent, changes); } catch (error) { /* istanbul ignore next */ onIgnoredError?.(error); } setStatus(0 /* Idle */); }); } return persister; }; const startAutoSave = async () => { stopAutoSave(); await save(); autoSaveListenerId = store.addDidFinishTransactionListener(() => { const changes = getChanges(); if (hasChanges(changes)) { save(changes); } }); return persister; }; const stopAutoSave = () => { if (autoSaveListenerId) { store.delListener(autoSaveListenerId); autoSaveListenerId = void 0; } return persister; }; const isAutoSaving = () => !isUndefined(autoSaveListenerId); const getStatus = () => status; const addStatusListener = (listener) => addListener(listener, statusListeners); const delListener = (listenerId) => { delListenerImpl(listenerId); return store; }; const schedule = async (...actions) => { arrayPush(mapGet(scheduleActions, scheduleId), ...actions); await run(); return persister; }; const getStore = () => store; const destroy = () => { arrayClear(mapGet(scheduleActions, scheduleId)); return stopAutoLoad().stopAutoSave(); }; const getStats = () => ({loads, saves}); const persister = { load, startAutoLoad, stopAutoLoad, isAutoLoading, save, startAutoSave, stopAutoSave, isAutoSaving, getStatus, addStatusListener, delListener, schedule, getStore, destroy, getStats, ...extra, }; return objFreeze(persister); }; const getCommandFunctions = ( databaseExecuteCommand, managedTableNames, querySchema, onIgnoredError, columnType, upsert = defaultUpsert, encode, decode, ) => { const schemaMap = mapNew(); const canSelect = (tableName, rowIdColumnName) => collHas(mapGet(schemaMap, tableName), rowIdColumnName); const refreshSchema = async () => { collClear(schemaMap); arrayMap( await querySchema(databaseExecuteCommand, managedTableNames), ({tn, cn}) => setAdd(mapEnsure(schemaMap, tn, setNew), cn), ); }; const loadTable = async (tableName, rowIdColumnName) => canSelect(tableName, rowIdColumnName) ? objNew( arrayFilter( arrayMap( await databaseExecuteCommand( SELECT_STAR_FROM + escapeId(tableName), ), (row) => [ row[rowIdColumnName], decode ? objMap(objDel(row, rowIdColumnName), decode) : objDel(row, rowIdColumnName), ], ), ([rowId, row]) => !isUndefined(rowId) && !objIsEmpty(row), ), ) : {}; const saveTable = async ( tableName, rowIdColumnName, content, deleteEmptyColumns, deleteEmptyTable, partial = false, ) => { const settingColumnNameSet = setNew(); objMap(content ?? {}, (contentRow) => arrayMap(objIds(contentRow ?? {}), (cellOrValueId) => setAdd(settingColumnNameSet, cellOrValueId), ), ); const settingColumnNames = collValues(settingColumnNameSet); if ( !partial && deleteEmptyTable && arrayIsEmpty(settingColumnNames) && collHas(schemaMap, tableName) ) { await databaseExecuteCommand('DROP ' + TABLE + escapeId(tableName)); mapSet(schemaMap, tableName); return; } const currentColumnNames = mapGet(schemaMap, tableName); const unaccountedColumnNames = setNew(collValues(currentColumnNames)); if (!arrayIsEmpty(settingColumnNames)) { if (!collHas(schemaMap, tableName)) { await databaseExecuteCommand( 'CREATE ' + TABLE + escapeId(tableName) + `(${escapeId(rowIdColumnName)}${columnType} PRIMARY KEY${arrayJoin( arrayMap( settingColumnNames, (settingColumnName) => COMMA + escapeId(settingColumnName) + columnType, ), )});`, ); mapSet( schemaMap, tableName, setNew([rowIdColumnName, ...settingColumnNames]), ); } else { await promiseAll( arrayMap( [rowIdColumnName, ...settingColumnNames], async (settingColumnName, index) => { if (!collDel(unaccountedColumnNames, settingColumnName)) { await databaseExecuteCommand( ALTER_TABLE + escapeId(tableName) + 'ADD' + escapeId(settingColumnName) + columnType, ); if (index == 0) { await databaseExecuteCommand( 'CREATE UNIQUE INDEX pk ON ' + escapeId(tableName) + `(${escapeId(rowIdColumnName)})`, ); } setAdd(currentColumnNames, settingColumnName); } }, ), ); } } await promiseAll([ ...(!partial && deleteEmptyColumns ? arrayMap( collValues(unaccountedColumnNames), async (unaccountedColumnName) => { if (unaccountedColumnName != rowIdColumnName) { await databaseExecuteCommand( ALTER_TABLE + escapeId(tableName) + 'DROP' + escapeId(unaccountedColumnName), ); collDel(currentColumnNames, unaccountedColumnName); } }, ) : []), ]); if (partial) { if (isUndefined(content)) { await databaseExecuteCommand( DELETE_FROM + escapeId(tableName) + WHERE + ' true', ); } else { await promiseAll( objToArray(content, async (row, rowId) => { if (isUndefined(row)) { await databaseExecuteCommand( DELETE_FROM + escapeId(tableName) + WHERE + escapeId(rowIdColumnName) + '=$1', [rowId], ); } else if (!arrayIsEmpty(settingColumnNames)) { await upsert( databaseExecuteCommand, tableName, rowIdColumnName, objIds(row), { [rowId]: encode ? arrayMap(objValues(row), encode) : objValues(row), }, currentColumnNames, ); } }), ); } } else { if (!arrayIsEmpty(settingColumnNames)) { const changingColumnNames = arrayFilter( collValues(mapGet(schemaMap, tableName)), (changingColumnName) => changingColumnName != rowIdColumnName, ); const rows = {}; const deleteRowIds = []; objMap(content ?? {}, (row, rowId) => { rows[rowId] = arrayMap(changingColumnNames, (cellId) => encode ? encode(row?.[cellId]) : row?.[cellId], ); arrayPush(deleteRowIds, rowId); }); await upsert( databaseExecuteCommand, tableName, rowIdColumnName, changingColumnNames, rows, ); await databaseExecuteCommand( DELETE_FROM + escapeId(tableName) + WHERE + escapeId(rowIdColumnName) + `NOT IN(${getPlaceholders(deleteRowIds)})`, deleteRowIds, ); } else if (collHas(schemaMap, tableName)) { await databaseExecuteCommand( DELETE_FROM + escapeId(tableName) + WHERE + ' true', ); } } }; const transaction = async (actions) => { let result; await databaseExecuteCommand('BEGIN'); try { result = await actions(); } catch (error) { onIgnoredError?.(error); } await databaseExecuteCommand('END'); return result; }; return [refreshSchema, loadTable, saveTable, transaction]; }; const defaultUpsert = async ( executeCommand, tableName, rowIdColumnName, changingColumnNames, rows, ) => { const offset = [1]; await executeCommand( 'INSERT INTO' + escapeId(tableName) + '(' + escapeColumnNames(rowIdColumnName, ...changingColumnNames) + ')VALUES' + arrayJoin( objToArray( rows, (row) => '($' + offset[0]++ + ',' + getPlaceholders(row, offset) + ')', ), COMMA, ) + 'ON CONFLICT(' + escapeId(rowIdColumnName) + ')DO UPDATE SET' + arrayJoin( arrayMap( changingColumnNames, (columnName) => escapeId(columnName) + '=excluded.' + escapeId(columnName), ), COMMA, ), objToArray(rows, (row, id) => [ id, ...arrayMap(row, (value) => value ?? null), ]).flat(), ); }; const createJsonPersister = ( store, executeCommand, addPersisterListener, delPersisterListener, onIgnoredError, destroyImpl, persist, [storeTableName, storeIdColumnName, storeColumnName], managedTableNames, querySchema, thing, getThing, columnType, upsert, ) => { const [refreshSchema, loadTable, saveTable, transaction] = getCommandFunctions( executeCommand, managedTableNames, querySchema, onIgnoredError, columnType, upsert, ); const getPersisted = async () => await transaction(async () => { await refreshSchema(); return jsonParseWithUndefined( (await loadTable(storeTableName, storeIdColumnName))[SINGLE_ROW_ID]?.[ storeColumnName ] ?? 'null', ); }); const setPersisted = async (getContent) => await transaction(async () => { await refreshSchema(); await saveTable( storeTableName, storeIdColumnName, { [SINGLE_ROW_ID]: { [storeColumnName]: jsonStringWithUndefined(getContent() ?? null), }, }, true, true, ); }); const destroy = () => { persister.stopAutoLoad().stopAutoSave(); destroyImpl(); return persister; }; const persister = createCustomPersister( store, getPersisted, setPersisted, addPersisterListener, delPersisterListener, onIgnoredError, persist, {[getThing]: () => thing, destroy}, 0, thing, ); return persister; }; const createTabularPersister = ( store, executeCommand, addPersisterListener, delPersisterListener, onIgnoredError, destroyImpl, persist, [ tablesLoadConfig, tablesSaveConfig, [valuesLoad, valuesSave, valuesTableName], ], managedTableNames, querySchema, thing, getThing, columnType, upsert, encode, decode, ) => { const [refreshSchema, loadTable, saveTable, transaction] = getCommandFunctions( executeCommand, managedTableNames, querySchema, onIgnoredError, columnType, upsert, encode, decode, ); const saveTables = async (tables, partial) => await promiseAll( mapMap( tablesSaveConfig, async ( [tableName, rowIdColumnName, deleteEmptyColumns, deleteEmptyTable], tableId, ) => { if (!partial || objHas(tables, tableId)) { await saveTable( tableName, rowIdColumnName, tables[tableId], deleteEmptyColumns, deleteEmptyTable, partial, ); } }, ), ); const saveValues = async (values, partial) => valuesSave ? await saveTable( valuesTableName, DEFAULT_ROW_ID_COLUMN_NAME, {[SINGLE_ROW_ID]: values}, true, true, partial, ) : null; const loadTables = async () => objNew( arrayFilter( await promiseAll( mapMap( tablesLoadConfig, async ([tableId, rowIdColumnName], tableName) => [ tableId, await loadTable(tableName, rowIdColumnName), ], ), ), (pair) => !objIsEmpty(pair[1]), ), ); const loadValues = async () => valuesLoad ? (await loadTable(valuesTableName, DEFAULT_ROW_ID_COLUMN_NAME))[ SINGLE_ROW_ID ] : {}; const getPersisted = async () => await transaction(async () => { await refreshSchema(); const tables = await loadTables(); const values = await loadValues(); return !objIsEmpty(tables) || !isUndefined(values) ? [tables, values] : void 0; }); const setPersisted = async (getContent, changes) => await transaction(async () => { await refreshSchema(); if (!isUndefined(changes)) { await saveTables(changes[0], true); await saveValues(changes[1], true); } else { const [tables, values] = getContent(); await saveTables(tables); await saveValues(values); } }); const destroy = () => { persister.stopAutoLoad().stopAutoSave(); destroyImpl(); return persister; }; const persister = createCustomPersister( store, getPersisted, setPersisted, addPersisterListener, delPersisterListener, onIgnoredError, persist, {[getThing]: () => thing, destroy}, 0, thing, ); return persister; }; const createCustomSqlitePersister = ( store, configOrStoreTableName, rawExecuteCommand, addChangeListener, delChangeListener, onSqlCommand, onIgnoredError, destroy, persist, thing, getThing = 'getDb', upsert, ) => { let dataVersion; let schemaVersion; let totalChanges; const executeCommand = getWrappedCommand(rawExecuteCommand, onSqlCommand); const [ isJson, autoLoadIntervalSeconds, defaultedConfig, managedTableNamesSet, ] = getConfigStructures(configOrStoreTableName); const addPersisterListener = (listener) => { let interval; const startPolling = () => (interval = startInterval(async () => { try { const [{d, s, c}] = await executeCommand( // eslint-disable-next-line max-len `${SELECT} ${DATA_VERSION} d,${SCHEMA_VERSION} s,TOTAL_CHANGES() c FROM ${PRAGMA}${DATA_VERSION} JOIN ${PRAGMA}${SCHEMA_VERSION}`, ); if (d != dataVersion || s != schemaVersion || c != totalChanges) { if (dataVersion != null) { listener(); } dataVersion = d; schemaVersion = s; totalChanges = c; } } catch {} }, autoLoadIntervalSeconds)); const stopPolling = () => { dataVersion = schemaVersion = totalChanges = null; stopInterval(interval); }; const listeningHandle = addChangeListener((tableName) => { if (managedTableNamesSet.has(tableName)) { stopPolling(); listener(); startPolling(); } }); startPolling(); return () => { stopPolling(); delChangeListener(listeningHandle); }; }; const delPersisterListener = (stopPollingAndDelUpdateListener) => stopPollingAndDelUpdateListener(); return (isJson ? createJsonPersister : createTabularPersister)( store, executeCommand, addPersisterListener, delPersisterListener, onIgnoredError, destroy, persist, defaultedConfig, collValues(managedTableNamesSet), async (executeCommand2, managedTableNames) => await executeCommand2( // eslint-disable-next-line max-len `${SELECT} t.name tn,c.name cn ${FROM}${PRAGMA_TABLE}list()t,${PRAGMA_TABLE}info(t.name)c ${WHERE} t.schema='main'AND t.type IN('table','view')AND t.name IN(${getPlaceholders(managedTableNames)})ORDER BY t.name,c.name`, managedTableNames, ), thing, getThing, EMPTY_STRING, upsert, (cellOrValue) => cellOrValue === true ? 1 : cellOrValue === false ? 0 : cellOrValue, void 0, ); }; const createLibSqlPersister = ( store, client, configOrStoreTableName, onSqlCommand, onIgnoredError, ) => createCustomSqlitePersister( store, configOrStoreTableName, async (sql, args = []) => (await client.execute({sql, args})).rows, () => () => 0, (unsubscribeFunction) => unsubscribeFunction(), onSqlCommand, onIgnoredError, () => 0, 1, // StoreOnly, client, 'getClient', ); export {createLibSqlPersister};