diff --git a/src/@types/store/index.d.ts b/src/@types/store/index.d.ts index 6b100f64051..db87c02ca71 100644 --- a/src/@types/store/index.d.ts +++ b/src/@types/store/index.d.ts @@ -605,6 +605,11 @@ export interface Store { /// Store.transaction transaction(actions: () => Return, doRollback?: DoRollback): Return; + internalTransaction( + actions: () => Return, + doRollback?: DoRollback, + ): Return; + /// Store.startTransaction startTransaction(): this; diff --git a/src/checkpoints/index.ts b/src/checkpoints/index.ts index 59257ce611c..61e1609e9c9 100644 --- a/src/checkpoints/index.ts +++ b/src/checkpoints/index.ts @@ -68,7 +68,7 @@ export const createCheckpoints = getCreateFunction( const updateStore = (oldOrNew: 0 | 1, checkpointId: Id) => { listening = 0; - store.transaction(() => { + store.internalTransaction(() => { const [cellsDelta, valuesDelta] = mapGet(deltas, checkpointId) as [ CellsDelta, ValuesDelta, @@ -96,7 +96,7 @@ export const createCheckpoints = getCreateFunction( const clearCheckpointId = (checkpointId: Id): void => { mapSet(deltas, checkpointId); mapSet(labels, checkpointId); - callListeners(checkpointListeners, [checkpointId]); + callListeners(false, checkpointListeners, [checkpointId]); }; const clearCheckpointIds = (checkpointIds: Ids, to?: number): void => @@ -157,7 +157,7 @@ export const createCheckpoints = getCreateFunction( const callListenersIfChanged = (): void => { if (checkpointsChanged) { - callListeners(checkpointIdsListeners); + callListeners(false, checkpointIdsListeners); checkpointsChanged = 0; } }; @@ -180,7 +180,7 @@ export const createCheckpoints = getCreateFunction( mapGet(labels, checkpointId) !== label ) { mapSet(labels, checkpointId, label); - callListeners(checkpointListeners, [checkpointId]); + callListeners(false, checkpointListeners, [checkpointId]); } return checkpoints; }; @@ -227,12 +227,12 @@ export const createCheckpoints = getCreateFunction( }; const addCheckpointIdsListener = (listener: CheckpointIdsListener): Id => - addListener(listener, checkpointIdsListeners); + addListener(false, listener, checkpointIdsListeners); const addCheckpointListener = ( checkpointId: IdOrNull, listener: CheckpointListener, - ): Id => addListener(listener, checkpointListeners, [checkpointId]); + ): Id => addListener(false, listener, checkpointListeners, [checkpointId]); const delListener = (listenerId: Id): Checkpoints => { delListenerImpl(listenerId); @@ -254,7 +254,7 @@ export const createCheckpoints = getCreateFunction( const clearForward = (): Checkpoints => { if (!arrayIsEmpty(forwardIds)) { clearCheckpointIds(forwardIds); - callListeners(checkpointIdsListeners); + callListeners(false, checkpointIdsListeners); } return checkpoints; }; diff --git a/src/common/definable.ts b/src/common/definable.ts index 44d15ca0554..6d986379979 100644 --- a/src/common/definable.ts +++ b/src/common/definable.ts @@ -124,7 +124,7 @@ export const getDefinableFunctions = ( mapSet(things, id, getDefaultThing()); mapSet(allRowValues, id, mapNew()); mapSet(allSortKeys, id, mapNew()); - callListeners(thingIdListeners); + callListeners(true, thingIdListeners); } }; @@ -206,10 +206,15 @@ export const getDefinableFunctions = ( addStoreListeners( id, 0, - store.addRowListener(tableId, null, (_store, _tableId, rowId) => - processRow(rowId), + store.addRowListener.call( + {isInternal: true}, + tableId, + null, + (_store, _tableId, rowId) => processRow(rowId), + ), + store.addTableListener.call({isInternal: true}, tableId, () => + processTable(), ), - store.addTableListener(tableId, () => processTable()), ); }; @@ -219,11 +224,11 @@ export const getDefinableFunctions = ( mapSet(allRowValues, id); mapSet(allSortKeys, id); delStoreListeners(id); - callListeners(thingIdListeners); + callListeners(false, thingIdListeners); }; const addThingIdsListener = (listener: () => void) => - addListener(listener, thingIdListeners); + addListener(false, listener, thingIdListeners); const destroy = (): void => mapForEach(storeListenerIds, delDefinition); diff --git a/src/common/listeners.ts b/src/common/listeners.ts index f247c66d9b0..e776aed6c15 100644 --- a/src/common/listeners.ts +++ b/src/common/listeners.ts @@ -68,6 +68,7 @@ export type ListenerArgument = IdOrNull | boolean | number | undefined; export type PathGetters = ((...ids: Ids) => Ids)[]; export type ExtraArgsGetter = (ids: Ids) => any[]; export type AddListener = ( + isInternal: boolean, listener: Listener, idSetNode: IdSetNode, path?: ListenerArgument[], @@ -75,6 +76,7 @@ export type AddListener = ( extraArgsGetter?: ExtraArgsGetter, ) => Id; export type CallListeners = ( + isInternal: boolean, idSetNode: IdSetNode, ids?: Ids, ...extra: any[] @@ -159,8 +161,10 @@ export const getListenerFunctions = ( const allListeners: IdMap< [Listener, IdSetNode, ListenerArgument[], PathGetters, ExtraArgsGetter] > = mapNew(); + const internalListenerIds: IdSet = setNew(); const addListener = ( + isInternal: boolean, listener: Listener, idSetNode: IdSetNode, path?: ListenerArgument[], @@ -184,22 +188,29 @@ export const getListenerFunctions = ( ), id, ) as IdSet; + if (isInternal) { + internalListenerIds.add(id); + } return id; }; const callListeners = ( + isInternal: boolean, idSetNode: IdSetNode, ids?: Ids, ...extraArgs: any[] ): void => arrayForEach(getWildcardedLeaves(idSetNode, ids), (set) => - collForEach(set, (id: Id) => + collForEach(set, (id: Id) => { + if (internalListenerIds.has(id) !== isInternal) { + return; + } (mapGet(allListeners, id) as any)[0]( thing, ...(ids ?? []), ...extraArgs, - ), - ), + ); + }), ); const delListener = (id: Id): Ids => @@ -215,6 +226,7 @@ export const getListenerFunctions = ( ); mapSet(allListeners, id); releaseId(id); + internalListenerIds.delete(id); return idOrNulls; }) as Ids; diff --git a/src/indexes/index.ts b/src/indexes/index.ts index bcb83c915f0..ea32a1baa52 100644 --- a/src/indexes/index.ts +++ b/src/indexes/index.ts @@ -204,10 +204,10 @@ export const createIndexes = getCreateFunction((store: Store): Indexes => { } if (sliceIdsChanged) { - callListeners(sliceIdsListeners, [indexId]); + callListeners(false, sliceIdsListeners, [indexId]); } collForEach(changedSlices, (sliceId) => - callListeners(sliceRowIdsListeners, [indexId, sliceId]), + callListeners(false, sliceRowIdsListeners, [indexId, sliceId]), ); }, getRowCellFunction(getSliceIdOrIds), @@ -256,13 +256,14 @@ export const createIndexes = getCreateFunction((store: Store): Indexes => { const addSliceIdsListener = ( indexId: IdOrNull, listener: SliceIdsListener, - ): Id => addListener(listener, sliceIdsListeners, [indexId]); + ): Id => addListener(false, listener, sliceIdsListeners, [indexId]); const addSliceRowIdsListener = ( indexId: IdOrNull, sliceId: IdOrNull, listener: SliceRowIdsListener, - ): Id => addListener(listener, sliceRowIdsListeners, [indexId, sliceId]); + ): Id => + addListener(false, listener, sliceRowIdsListeners, [indexId, sliceId]); const delListener = (listenerId: Id): Indexes => { delListenerImpl(listenerId); diff --git a/src/metrics/index.ts b/src/metrics/index.ts index aa45f61181f..57b5ad97206 100644 --- a/src/metrics/index.ts +++ b/src/metrics/index.ts @@ -116,7 +116,13 @@ export const createMetrics = getCreateFunction((store: Store): Metrics => { if (newMetric != oldMetric) { setMetric(metricId, newMetric); - callListeners(metricListeners, [metricId], newMetric, oldMetric); + callListeners( + false, + metricListeners, + [metricId], + newMetric, + oldMetric, + ); } }, getRowCellFunction(getNumber, 1), @@ -132,7 +138,7 @@ export const createMetrics = getCreateFunction((store: Store): Metrics => { const addMetricListener = ( metricId: IdOrNull, listener: MetricListener, - ): Id => addListener(listener, metricListeners, [metricId]); + ): Id => addListener(false, listener, metricListeners, [metricId]); const delListener = (listenerId: Id): Metrics => { delListenerImpl(listenerId); diff --git a/src/persisters/common/create.ts b/src/persisters/common/create.ts index d3dbc914b60..78037e8c3ff 100644 --- a/src/persisters/common/create.ts +++ b/src/persisters/common/create.ts @@ -149,7 +149,7 @@ export const createCustomPersister = < const setStatus = (newStatus: StatusValues): void => { if (newStatus != status) { status = newStatus; - callListeners(statusListeners, undefined, status); + callListeners(false, statusListeners, undefined, status); } }; @@ -332,7 +332,7 @@ export const createCustomPersister = < const getStatus = (): StatusValues => status; const addStatusListener = (listener: StatusListener): Id => - addListener(listener, statusListeners); + addListener(false, listener, statusListeners); const delListener = (listenerId: Id): Store => { delListenerImpl(listenerId); diff --git a/src/queries/index.ts b/src/queries/index.ts index a2d6b6eb767..9a36c263fb9 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -484,7 +484,7 @@ export const createQueries = getCreateFunction((store: Store): Queries => { arg2, ]) as [Id, Id, Id]), ); - selectJoinWhereStore.transaction(() => + selectJoinWhereStore.internalTransaction(() => arrayEvery(wheres, (where) => where(getTableCell)) ? mapForEach(selects, (asCellId, tableCellGetter) => setOrDelCell( @@ -543,7 +543,7 @@ export const createQueries = getCreateFunction((store: Store): Queries => { }; const {3: joinedTableIds} = mapGet(joins, undefined) as JoinClause; - selectJoinWhereStore.transaction(() => + selectJoinWhereStore.internalTransaction(() => addStoreListeners( queryId, 1, diff --git a/src/relationships/index.ts b/src/relationships/index.ts index 58f5dff7ddd..8a5309e4b6b 100644 --- a/src/relationships/index.ts +++ b/src/relationships/index.ts @@ -164,14 +164,23 @@ export const createRelationships = getCreateFunction( change(); collForEach(changedLocalRows, (localRowId) => - callListeners(remoteRowIdListeners, [relationshipId, localRowId]), + callListeners(false, remoteRowIdListeners, [ + relationshipId, + localRowId, + ]), ); collForEach(changedRemoteRows, (remoteRowId) => - callListeners(localRowIdsListeners, [relationshipId, remoteRowId]), + callListeners(false, localRowIdsListeners, [ + relationshipId, + remoteRowId, + ]), ); collForEach(changedLinkedRows, (firstRowId) => { delLinkedRowIdsCache(relationshipId, firstRowId); - callListeners(linkedRowIdsListeners, [relationshipId, firstRowId]); + callListeners(false, linkedRowIdsListeners, [ + relationshipId, + firstRowId, + ]); }); }, getRowCellFunction(getRemoteRowId), @@ -219,14 +228,17 @@ export const createRelationships = getCreateFunction( localRowId: IdOrNull, listener: RemoteRowIdListener, ): Id => - addListener(listener, remoteRowIdListeners, [relationshipId, localRowId]); + addListener(false, listener, remoteRowIdListeners, [ + relationshipId, + localRowId, + ]); const addLocalRowIdsListener = ( relationshipId: IdOrNull, remoteRowId: IdOrNull, listener: LocalRowIdsListener, ): Id => - addListener(listener, localRowIdsListeners, [ + addListener(false, listener, localRowIdsListeners, [ relationshipId, remoteRowId, ]); @@ -237,7 +249,7 @@ export const createRelationships = getCreateFunction( listener: LinkedRowIdsListener, ): Id => { getLinkedRowIdsCache(relationshipId, firstRowId); - return addListener(listener, linkedRowIdsListeners, [ + return addListener(false, listener, linkedRowIdsListeners, [ relationshipId, firstRowId, ]); diff --git a/src/store/index.ts b/src/store/index.ts index 9016f19a81b..8ff745bf951 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -165,6 +165,8 @@ export const createStore: typeof createStoreDecl = (): Store => { let hadTables = false; let hadValues = false; let transactions = 0; + let localTransactions = 0; + let isRollingBack = false; let internalListeners: [ preStartTransaction?: () => void, preFinishTransaction?: () => void, @@ -177,14 +179,23 @@ export const createStore: typeof createStoreDecl = (): Store => { ) => void, valueChanged?: (valueId: Id, newValue: ValueOrUndefined) => void, ] = []; - const changedTableIds: ChangedIdsMap = mapNew(); - const changedTableCellIds: ChangedIdsMap2 = mapNew(); - const changedRowCount: IdMap = mapNew(); - const changedRowIds: ChangedIdsMap2 = mapNew(); - const changedCellIds: ChangedIdsMap3 = mapNew(); - const changedCells: IdMap3 = mapNew(); - const changedValueIds: ChangedIdsMap = mapNew(); - const changedValues: IdMap = mapNew(); + const changedTableIds: [ChangedIdsMap, ChangedIdsMap] = [mapNew(), mapNew()]; + const changedTableCellIds: [ChangedIdsMap2, ChangedIdsMap2] = [ + mapNew(), + mapNew(), + ]; + const changedRowCount: [IdMap, IdMap] = [mapNew(), mapNew()]; + const changedRowIds: [ChangedIdsMap2, ChangedIdsMap2] = [mapNew(), mapNew()]; + const changedCellIds: [ChangedIdsMap3, ChangedIdsMap3] = [mapNew(), mapNew()]; + const changedCells: [IdMap3, IdMap3] = [ + mapNew(), + mapNew(), + ]; + const changedValueIds: [ChangedIdsMap, ChangedIdsMap] = [mapNew(), mapNew()]; + const changedValues: [IdMap, IdMap] = [ + mapNew(), + mapNew(), + ]; const invalidCells: IdMap3 = mapNew(); const invalidValues: IdMap = mapNew(); const tablesSchemaMap: TablesSchemaMap = mapNew(); @@ -611,23 +622,43 @@ export const createStore: typeof createStoreDecl = (): Store => { const tableIdsChanged = ( tableId: Id, addedOrRemoved: IdAddedOrRemoved, - ): ChangedIdsMap => idsChanged(changedTableIds, tableId, addedOrRemoved); + ): ChangedIdsMap => { + idsChanged(changedTableIds[1], tableId, addedOrRemoved); + return idsChanged(changedTableIds[0], tableId, addedOrRemoved); + }; const rowIdsChanged = ( tableId: Id, rowId: Id, addedOrRemoved: IdAddedOrRemoved, - ): IdMap | undefined => - idsChanged( - mapEnsure(changedRowIds, tableId, mapNew) as ChangedIdsMap, - rowId, - addedOrRemoved, - ) && - mapSet( - changedRowCount, - tableId, - mapEnsure(changedRowCount, tableId, () => 0) + addedOrRemoved, + ): IdMap | undefined => { + if ( + idsChanged( + mapEnsure(changedRowIds[1], tableId, mapNew) as ChangedIdsMap, + rowId, + addedOrRemoved, + ) + ) { + mapSet( + changedRowCount[1], + tableId, + mapEnsure(changedRowCount[1], tableId, () => 0) + addedOrRemoved, + ); + } + + return ( + idsChanged( + mapEnsure(changedRowIds[0], tableId, mapNew) as ChangedIdsMap, + rowId, + addedOrRemoved, + ) && + mapSet( + changedRowCount[0], + tableId, + mapEnsure(changedRowCount[0], tableId, () => 0) + addedOrRemoved, + ) ); + }; const cellIdsChanged = ( tableId: Id, @@ -642,7 +673,12 @@ export const createStore: typeof createStoreDecl = (): Store => { (count == 1 && addedOrRemoved == -1) ) { idsChanged( - mapEnsure(changedTableCellIds, tableId, mapNew) as ChangedIdsMap, + mapEnsure(changedTableCellIds[0], tableId, mapNew) as ChangedIdsMap, + cellId, + addedOrRemoved, + ); + idsChanged( + mapEnsure(changedTableCellIds[1], tableId, mapNew) as ChangedIdsMap, cellId, addedOrRemoved, ); @@ -655,7 +691,16 @@ export const createStore: typeof createStoreDecl = (): Store => { idsChanged( mapEnsure( - mapEnsure(changedCellIds, tableId, mapNew) as ChangedIdsMap2, + mapEnsure(changedCellIds[0], tableId, mapNew) as ChangedIdsMap2, + rowId, + mapNew, + ) as ChangedIdsMap, + cellId, + addedOrRemoved, + ); + idsChanged( + mapEnsure( + mapEnsure(changedCellIds[1], tableId, mapNew) as ChangedIdsMap2, rowId, mapNew, ) as ChangedIdsMap, @@ -673,7 +718,16 @@ export const createStore: typeof createStoreDecl = (): Store => { ): void => { mapEnsure( mapEnsure>( - mapEnsure>(changedCells, tableId, mapNew), + mapEnsure>(changedCells[0], tableId, mapNew), + rowId, + mapNew, + ), + cellId, + () => [oldCell, 0], + )[1] = newCell; + mapEnsure( + mapEnsure>( + mapEnsure>(changedCells[1], tableId, mapNew), rowId, mapNew, ), @@ -686,14 +740,21 @@ export const createStore: typeof createStoreDecl = (): Store => { const valueIdsChanged = ( valueId: Id, addedOrRemoved: IdAddedOrRemoved, - ): ChangedIdsMap => idsChanged(changedValueIds, valueId, addedOrRemoved); + ): ChangedIdsMap => { + idsChanged(changedValueIds[1], valueId, addedOrRemoved); + return idsChanged(changedValueIds[0], valueId, addedOrRemoved); + }; const valueChanged = ( valueId: Id, oldValue?: ValueOrUndefined, newValue?: ValueOrUndefined, ): void => { - mapEnsure(changedValues, valueId, () => [ + mapEnsure(changedValues[0], valueId, () => [ + oldValue, + 0, + ])[1] = newValue; + mapEnsure(changedValues[1], valueId, () => [ oldValue, 0, ])[1] = newValue; @@ -738,20 +799,25 @@ export const createStore: typeof createStoreDecl = (): Store => { return defaultedValue; }; - const getCellChange: GetCellChange = (tableId: Id, rowId: Id, cellId: Id) => - ifNotUndefined( - mapGet(mapGet(mapGet(changedCells, tableId), rowId), cellId), - ([oldCell, newCell]) => [true, oldCell, newCell], - () => [false, ...pairNew(getCell(tableId, rowId, cellId))] as CellChange, - ) as CellChange; - - const getValueChange: GetValueChange = (valueId: Id) => - ifNotUndefined( - mapGet(changedValues, valueId), - ([oldValue, newValue]) => [true, oldValue, newValue], - () => [false, ...pairNew(getValue(valueId))] as ValueChange, - ) as ValueChange; - + const getCellChange: (isInternal: 0 | 1) => GetCellChange = + (isInternal: 0 | 1) => (tableId: Id, rowId: Id, cellId: Id) => + ifNotUndefined( + mapGet( + mapGet(mapGet(changedCells[isInternal], tableId), rowId), + cellId, + ), + ([oldCell, newCell]) => [true, oldCell, newCell], + () => + [false, ...pairNew(getCell(tableId, rowId, cellId))] as CellChange, + ) as CellChange; + + const getValueChange: (isInternal: 0 | 1) => GetValueChange = + (isInternal: 0 | 1) => (valueId: Id) => + ifNotUndefined( + mapGet(changedValues[isInternal], valueId), + ([oldValue, newValue]) => [true, oldValue, newValue], + () => [false, ...pairNew(getValue(valueId))] as ValueChange, + ) as ValueChange; const callInvalidCellListeners = (mutator: 0 | 1) => !collIsEmpty(invalidCells) && !collIsEmpty(invalidCellListeners[mutator]) ? collForEach( @@ -760,6 +826,7 @@ export const createStore: typeof createStoreDecl = (): Store => { collForEach(rows, (cells, rowId) => collForEach(cells, (invalidCell, cellId) => callListeners( + false, invalidCellListeners[mutator], [tableId, rowId, cellId], invalidCell, @@ -775,6 +842,7 @@ export const createStore: typeof createStoreDecl = (): Store => { mutator ? mapClone(invalidValues) : invalidValues, (invalidValue, valueId) => callListeners( + false, invalidValueListeners[mutator], [valueId], invalidValue, @@ -783,24 +851,40 @@ export const createStore: typeof createStoreDecl = (): Store => { : 0; const callIdsAndHasListenersIfChanged = ( + isInternal: 0 | 1, changedIds: ChangedIdsMap, idListeners: IdSetNode, hasListeners: IdSetNode, ids?: Ids, ): 1 | void => { if (!collIsEmpty(changedIds)) { - callListeners(idListeners, ids, () => mapToObj(changedIds)); + callListeners(isInternal === 1, idListeners, ids, () => + mapToObj(changedIds), + ); mapForEach(changedIds, (changedId, changed) => - callListeners(hasListeners, [...(ids ?? []), changedId], changed == 1), + callListeners( + isInternal === 1, + hasListeners, + [...(ids ?? []), changedId], + changed == 1, + ), ); return 1; } }; - const callTabularListenersForChanges = (mutator: 0 | 1) => { + const callTabularListenersForChanges = ( + isInternal: 0 | 1, + mutator: 0 | 1, + ) => { const hasTablesNow = hasTables(); if (hasTablesNow != hadTables) { - callListeners(hasTablesListeners[mutator], undefined, hasTablesNow); + callListeners( + isInternal === 1, + hasTablesListeners[mutator], + undefined, + hasTablesNow, + ); } const emptySortedRowIdListeners = collIsEmpty( @@ -832,24 +916,25 @@ export const createStore: typeof createStoreDecl = (): Store => { IdMap3, ] = mutator ? [ - mapClone(changedTableIds), - mapClone2(changedTableCellIds), - mapClone(changedRowCount), - mapClone2(changedRowIds), - mapClone3(changedCellIds), - mapClone3(changedCells), + mapClone(changedTableIds[isInternal]), + mapClone2(changedTableCellIds[isInternal]), + mapClone(changedRowCount[isInternal]), + mapClone2(changedRowIds[isInternal]), + mapClone3(changedCellIds[isInternal]), + mapClone3(changedCells[isInternal]), ] : [ - changedTableIds, - changedTableCellIds, - changedRowCount, - changedRowIds, - changedCellIds, - changedCells, + changedTableIds[isInternal], + changedTableCellIds[isInternal], + changedRowCount[isInternal], + changedRowIds[isInternal], + changedCellIds[isInternal], + changedCells[isInternal], ]; if (!emptyIdAndHasListeners) { callIdsAndHasListenersIfChanged( + isInternal, changes[0], tableIdsListeners[mutator], hasTableListeners[mutator], @@ -857,6 +942,7 @@ export const createStore: typeof createStoreDecl = (): Store => { collForEach(changes[1], (changedIds, tableId) => callIdsAndHasListenersIfChanged( + isInternal, changedIds, tableCellIdsListeners[mutator], hasTableCellListeners[mutator], @@ -867,6 +953,7 @@ export const createStore: typeof createStoreDecl = (): Store => { collForEach(changes[2], (changedCount, tableId) => { if (changedCount != 0) { callListeners( + isInternal === 1, rowCountListeners[mutator], [tableId], getRowCount(tableId), @@ -878,6 +965,7 @@ export const createStore: typeof createStoreDecl = (): Store => { collForEach(changes[3], (changedIds, tableId) => { if ( callIdsAndHasListenersIfChanged( + isInternal, changedIds, rowIdsListeners[mutator], hasRowListeners[mutator], @@ -885,7 +973,10 @@ export const createStore: typeof createStoreDecl = (): Store => { ) && !emptySortedRowIdListeners ) { - callListeners(sortedRowIdsListeners[mutator], [tableId, null]); + callListeners(isInternal === 1, sortedRowIdsListeners[mutator], [ + tableId, + null, + ]); setAdd(calledSortableTableIds, tableId); } }); @@ -902,10 +993,11 @@ export const createStore: typeof createStoreDecl = (): Store => { ), ); collForEach(sortableCellIds, (cellId) => - callListeners(sortedRowIdsListeners[mutator], [ - tableId, - cellId, - ]), + callListeners( + isInternal === 1, + sortedRowIdsListeners[mutator], + [tableId, cellId], + ), ); } }); @@ -914,6 +1006,7 @@ export const createStore: typeof createStoreDecl = (): Store => { collForEach(changes[4], (rowCellIds, tableId) => collForEach(rowCellIds, (changedIds, rowId) => callIdsAndHasListenersIfChanged( + isInternal, changedIds, cellIdsListeners[mutator], hasCellListeners[mutator], @@ -932,38 +1025,55 @@ export const createStore: typeof createStoreDecl = (): Store => { collForEach(cells, ([oldCell, newCell], cellId) => { if (newCell !== oldCell) { callListeners( + isInternal === 1, cellListeners[mutator], [tableId, rowId, cellId], newCell, oldCell, - getCellChange, + getCellChange(isInternal), ); tablesChanged = tableChanged = rowChanged = 1; } }); if (rowChanged) { callListeners( + isInternal === 1, rowListeners[mutator], [tableId, rowId], - getCellChange, + getCellChange(isInternal), ); } }); if (tableChanged) { - callListeners(tableListeners[mutator], [tableId], getCellChange); + callListeners( + isInternal === 1, + tableListeners[mutator], + [tableId], + getCellChange(isInternal), + ); } }); if (tablesChanged) { - callListeners(tablesListeners[mutator], undefined, getCellChange); + callListeners( + isInternal === 1, + tablesListeners[mutator], + undefined, + getCellChange(isInternal), + ); } } } }; - const callValuesListenersForChanges = (mutator: 0 | 1) => { + const callValuesListenersForChanges = (isInternal: 0 | 1, mutator: 0 | 1) => { const hasValuesNow = hasValues(); if (hasValuesNow != hadValues) { - callListeners(hasValuesListeners[mutator], undefined, hasValuesNow); + callListeners( + isInternal === 1, + hasValuesListeners[mutator], + undefined, + hasValuesNow, + ); } const emptyIdAndHasListeners = @@ -974,11 +1084,15 @@ export const createStore: typeof createStoreDecl = (): Store => { collIsEmpty(valuesListeners[mutator]); if (!emptyIdAndHasListeners || !emptyOtherListeners) { const changes: [ChangedIdsMap, IdMap] = mutator - ? [mapClone(changedValueIds), mapClone(changedValues)] - : [changedValueIds, changedValues]; + ? [ + mapClone(changedValueIds[isInternal]), + mapClone(changedValues[isInternal]), + ] + : [changedValueIds[isInternal], changedValues[isInternal]]; if (!emptyIdAndHasListeners) { callIdsAndHasListenersIfChanged( + isInternal, changes[0], valueIdsListeners[mutator], hasValueListeners[mutator], @@ -990,17 +1104,23 @@ export const createStore: typeof createStoreDecl = (): Store => { collForEach(changes[1], ([oldValue, newValue], valueId) => { if (newValue !== oldValue) { callListeners( + isInternal === 1, valueListeners[mutator], [valueId], newValue, oldValue, - getValueChange, + getValueChange(isInternal), ); valuesChanged = 1; } }); if (valuesChanged) { - callListeners(valuesListeners[mutator], undefined, getValueChange); + callListeners( + isInternal === 1, + valuesListeners[mutator], + undefined, + getValueChange(isInternal), + ); } } } @@ -1010,7 +1130,7 @@ export const createStore: typeof createStoreDecl = (): Store => { actions: (...idArgs: Id[]) => unknown, ...args: unknown[] ): Store => { - transaction(() => actions(...arrayMap(args, id))); + internalTransaction(() => actions(...arrayMap(args, id))); return store; }; @@ -1023,6 +1143,7 @@ export const createStore: typeof createStoreDecl = (): Store => { ): Id => { let sortedRowIds = getSortedRowIds(tableId, cellId, ...otherArgs); return addListener( + false, () => { const newSortedRowIds = getSortedRowIds(tableId, cellId, ...otherArgs); if (!arrayIsEqual(newSortedRowIds, sortedRowIds)) { @@ -1169,7 +1290,7 @@ export const createStore: typeof createStoreDecl = (): Store => { ); const addRow = (tableId: Id, row: Row, reuseRowIds = true): Id | undefined => - transaction(() => { + internalTransaction(() => { let rowId: Id | undefined = undefined; if (validateRow(tableId, rowId, row)) { tableId = id(tableId); @@ -1410,27 +1531,39 @@ export const createStore: typeof createStoreDecl = (): Store => { } }; + const internalTransaction = ( + actions: () => Return, + doRollback?: DoRollback, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore void return only occurs internally + ): Return => { + localTransactions++; + const result = transaction(actions, doRollback); + localTransactions--; + return result; + }; + const startTransaction = (): Store => { if (transactions != -1) { transactions++; } if (transactions == 1) { internalListeners[0]?.(); - callListeners(startTransactionListeners); + callListeners(false, startTransactionListeners); } return store; }; const getTransactionChanges = (): Changes => [ mapToObj( - changedCells, + changedCells[0], (table, tableId) => - mapGet(changedTableIds, tableId) === -1 + mapGet(changedTableIds[0], tableId) === -1 ? undefined : mapToObj( table, (row, rowId) => - mapGet(mapGet(changedRowIds, tableId), rowId) === -1 + mapGet(mapGet(changedRowIds[0], tableId), rowId) === -1 ? undefined : mapToObj( row, @@ -1444,7 +1577,7 @@ export const createStore: typeof createStoreDecl = (): Store => { objIsEmpty, ), mapToObj( - changedValues, + changedValues[0], ([, newValue]) => newValue, (changedValue) => pairIsEqual(changedValue), ), @@ -1452,61 +1585,108 @@ export const createStore: typeof createStoreDecl = (): Store => { ]; const getTransactionLog = (): TransactionLog => [ - !collIsEmpty(changedCells), - !collIsEmpty(changedValues), - mapToObj3(changedCells, pairClone, pairIsEqual), + !collIsEmpty(changedCells[0]), + !collIsEmpty(changedValues[0]), + mapToObj3(changedCells[0], pairClone, pairIsEqual), mapToObj3(invalidCells), - mapToObj(changedValues, pairClone, pairIsEqual), + mapToObj(changedValues[0], pairClone, pairIsEqual), mapToObj(invalidValues), - mapToObj(changedTableIds), - mapToObj2(changedRowIds), - mapToObj3(changedCellIds), - mapToObj(changedValueIds), + mapToObj(changedTableIds[0]), + mapToObj2(changedRowIds[0]), + mapToObj3(changedCellIds[0]), + mapToObj(changedValueIds[0]), ]; + const callInternalListeners = () => { + if (isRollingBack || localTransactions > 1) { + return; + } + + const oldTransactions = transactions; + transactions = 1; + if (!collIsEmpty(changedCells[1])) { + callTabularListenersForChanges(1, 1); + } + if (!collIsEmpty(changedValues[1])) { + callValuesListenersForChanges(1, 1); + } + + transactions = -1; + + if (!collIsEmpty(changedCells[1])) { + callTabularListenersForChanges(1, 0); + } + callInvalidValueListeners(1); + if (!collIsEmpty(changedValues[1])) { + callValuesListenersForChanges(1, 0); + } + + arrayForEach( + [ + changedTableIds[1], + changedTableCellIds[1], + changedRowCount[1], + changedRowIds[1], + changedCellIds[1], + changedCells[1], + changedValueIds[1], + changedValues[1], + ], + collClear, + ); + + transactions = oldTransactions; + }; + const finishTransaction = (doRollback?: DoRollback): Store => { if (transactions > 0) { transactions--; + // Process internal listeners except for deep internal transactions + callInternalListeners(); + if (transactions == 0) { transactions = 1; callInvalidCellListeners(1); - if (!collIsEmpty(changedCells)) { - callTabularListenersForChanges(1); + if (!collIsEmpty(changedCells[0])) { + callTabularListenersForChanges(0, 1); } callInvalidValueListeners(1); - if (!collIsEmpty(changedValues)) { - callValuesListenersForChanges(1); + if (!collIsEmpty(changedValues[0])) { + callValuesListenersForChanges(0, 1); } if (doRollback?.(store)) { - collForEach(changedCells, (table, tableId) => + isRollingBack = true; + collForEach(changedCells[0], (table, tableId) => collForEach(table, (row, rowId) => collForEach(row, ([oldCell], cellId) => setOrDelCell(store, tableId, rowId, cellId, oldCell), ), ), ); - collClear(changedCells); - collForEach(changedValues, ([oldValue], valueId) => + collClear(changedCells[0]); + collForEach(changedValues[0], ([oldValue], valueId) => setOrDelValue(store, valueId, oldValue), ); - collClear(changedValues); + collClear(changedValues[0]); + isRollingBack = false; + callInternalListeners(); } - callListeners(finishTransactionListeners[0], undefined); + callListeners(false, finishTransactionListeners[0], undefined); transactions = -1; callInvalidCellListeners(0); - if (!collIsEmpty(changedCells)) { - callTabularListenersForChanges(0); + if (!collIsEmpty(changedCells[0])) { + callTabularListenersForChanges(0, 0); } callInvalidValueListeners(0); - if (!collIsEmpty(changedValues)) { - callValuesListenersForChanges(0); + if (!collIsEmpty(changedValues[0])) { + callValuesListenersForChanges(0, 0); } internalListeners[1]?.(); - callListeners(finishTransactionListeners[1], undefined); + callListeners(false, finishTransactionListeners[1], undefined); internalListeners[2]?.(); transactions = 0; @@ -1514,15 +1694,15 @@ export const createStore: typeof createStoreDecl = (): Store => { hadValues = hasValues(); arrayForEach( [ - changedTableIds, - changedTableCellIds, - changedRowCount, - changedRowIds, - changedCellIds, - changedCells, + changedTableIds[0], + changedTableCellIds[0], + changedRowCount[0], + changedRowIds[0], + changedCellIds[0], + changedCells[0], invalidCells, - changedValueIds, - changedValues, + changedValueIds[0], + changedValues[0], invalidValues, ], collClear, @@ -1593,14 +1773,14 @@ export const createStore: typeof createStoreDecl = (): Store => { ); const addStartTransactionListener = (listener: TransactionListener): Id => - addListener(listener, startTransactionListeners); + addListener(false, listener, startTransactionListeners); const addWillFinishTransactionListener = ( listener: TransactionListener, - ): Id => addListener(listener, finishTransactionListeners[0]); + ): Id => addListener(false, listener, finishTransactionListeners[0]); const addDidFinishTransactionListener = (listener: TransactionListener): Id => - addListener(listener, finishTransactionListeners[1]); + addListener(false, listener, finishTransactionListeners[1]); const callListener = (listenerId: Id) => { callListenerImpl(listenerId); @@ -1728,6 +1908,7 @@ export const createStore: typeof createStoreDecl = (): Store => { getTransactionChanges, getTransactionLog, finishTransaction, + internalTransaction, forEachTable, forEachTableCell, @@ -1823,14 +2004,16 @@ export const createStore: typeof createStoreDecl = (): Store => { ], listenable, ) => { - store[ADD + listenable + LISTENER] = (...args: any[]): Id => - addListener( + store[ADD + listenable + LISTENER] = function (...args: any[]): Id { + return addListener( + typeof this.isInternal === 'boolean' ? this.isInternal : false, args[argumentCount] as any, idSetNode[args[argumentCount + 1] ? 1 : 0], argumentCount > 0 ? slice(args, 0, argumentCount) : undefined, pathGetters, extraArgsGetter, ); + }; }, ); diff --git a/src/synchronizers/synchronizer-ws-server/index.ts b/src/synchronizers/synchronizer-ws-server/index.ts index b98a20a0439..94f11423dd3 100644 --- a/src/synchronizers/synchronizer-ws-server/index.ts +++ b/src/synchronizers/synchronizer-ws-server/index.ts @@ -192,11 +192,11 @@ export const createWsServer = (< ); if (collIsEmpty(clients)) { - callListeners(pathIdListeners, undefined, pathId, 1); + callListeners(false, pathIdListeners, undefined, pathId, 1); await configureServerClient(serverClient, pathId, clients); } mapSet(clients, clientId, client); - callListeners(clientIdListeners, [pathId], clientId, 1); + callListeners(false, clientIdListeners, [pathId], clientId, 1); client.on(MESSAGE, (data) => { const payload = data.toString(UTF8); @@ -215,12 +215,12 @@ export const createWsServer = (< client.on('close', async () => { collDel(clients, clientId); - callListeners(clientIdListeners, [pathId], clientId, -1); + callListeners(false, clientIdListeners, [pathId], clientId, -1); if (collIsEmpty(clients)) { await stopServerClient(serverClient); collDel(serverClientsByPath, pathId); collDel(clientsByPath, pathId); - callListeners(pathIdListeners, undefined, pathId, -1); + callListeners(false, pathIdListeners, undefined, pathId, -1); } }); @@ -243,12 +243,12 @@ export const createWsServer = (< mapKeys(mapGet(clientsByPath, pathId)); const addPathIdsListener = (listener: PathIdsListener) => - addListener(listener, pathIdListeners); + addListener(false, listener, pathIdListeners); const addClientIdsListener = ( pathId: IdOrNull, listener: ClientIdsListener, - ) => addListener(listener, clientIdListeners, [pathId]); + ) => addListener(false, listener, clientIdListeners, [pathId]); const delListener = (listenerId: Id): WsServer => { delListenerImpl(listenerId);