tinybase 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -176,6 +176,32 @@ export type MapCell = (cell: CellOrUndefined) => Cell;
176
176
  */
177
177
  export type GetCell = (cellId: Id) => CellOrUndefined;
178
178
 
179
+ /**
180
+ * The TransactionListener type describes a function that is used to listen to
181
+ * the completion of a transaction for the Store.
182
+ *
183
+ * A TransactionListener is provided when using the
184
+ * addWillFinishTransactionListener and addDidFinishTransactionListener methods.
185
+ * See those methods for specific examples.
186
+ *
187
+ * When called, a TransactionListener is simply given a reference to the Store
188
+ * and a boolean to indicate whether Cell values have been touched during the
189
+ * transaction. The latter flag is intended as a hint about whether non-mutating
190
+ * listeners might be being called at the end of the transaction.
191
+ *
192
+ * Here, 'touched' means that Cell values have either been changed, or changed
193
+ * and then changed back to its original value during the transaction. The
194
+ * exception is a transaction that has been rolled back, for which the value of
195
+ * `cellsTouched` in the listener will be `false` because all changes have been
196
+ * reverted.
197
+ *
198
+ * @param store A reference to the Store that is completing a transaction.
199
+ * @param cellsTouched Whether Cell values have been touched during the
200
+ * transaction.
201
+ * @category Listener
202
+ */
203
+ export type TransactionListener = (store: Store, cellsTouched: boolean) => void;
204
+
179
205
  /**
180
206
  * The TablesListener type describes a function that is used to listen to
181
207
  * changes to the whole Store.
@@ -570,6 +596,10 @@ export type StoreListenerStats = {
570
596
  * The number of InvalidCellListeners registered with the Store.
571
597
  */
572
598
  invalidCell?: number;
599
+ /**
600
+ * The number of TransactionListeners registered with the Store.
601
+ */
602
+ transaction?: number;
573
603
  };
574
604
 
575
605
  /**
@@ -1652,7 +1682,7 @@ export interface Store {
1652
1682
 
1653
1683
  /**
1654
1684
  * The transaction method takes a function that makes multiple mutations to
1655
- * the store, buffering all calls to the relevant listeners until it
1685
+ * the Store, buffering all calls to the relevant listeners until it
1656
1686
  * completes.
1657
1687
  *
1658
1688
  * This method is useful for making bulk changes to the data in a Store, and
@@ -1771,6 +1801,136 @@ export interface Store {
1771
1801
  ) => boolean,
1772
1802
  ): Return;
1773
1803
 
1804
+ /**
1805
+ * The startTransaction method allows you to explicitly start a transaction
1806
+ * that will make multiple mutations to the Store, buffering all calls to the
1807
+ * relevant listeners until it completes when you call the finishTransaction
1808
+ * method.
1809
+ *
1810
+ * Transactions are useful for making bulk changes to the data in a Store, and
1811
+ * when you don't want listeners to be called as you make each change. Changes
1812
+ * are made silently during the transaction, and listeners relevant to the
1813
+ * changes you have made will instead only be called when the whole
1814
+ * transaction is complete.
1815
+ *
1816
+ * Generally it is preferable to use the transaction method to wrap a block of
1817
+ * code as a transaction. It simply calls both the startTransaction and
1818
+ * finishTransaction methods for you. See that method for several transaction
1819
+ * examples.
1820
+ *
1821
+ * Use this startTransaction method when you have a more 'open-ended'
1822
+ * transaction, such as one containing mutations triggered from other events
1823
+ * that are asynchronous or not occurring inline to your code. You must
1824
+ * remember to also call the finishTransaction method explicitly when it is
1825
+ * done, of course.
1826
+ *
1827
+ * @returns A reference to the Store.
1828
+ * @example
1829
+ * This example makes changes to two Cells, first outside, and secondly
1830
+ * within, a transaction that is explicitly started and finished. In the
1831
+ * second case, the Row listener is only called once.
1832
+ *
1833
+ * ```js
1834
+ * const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
1835
+ * store.addRowListener('pets', 'fido', () => console.log('Fido changed'));
1836
+ *
1837
+ * store.setCell('pets', 'fido', 'color', 'brown');
1838
+ * store.setCell('pets', 'fido', 'sold', false);
1839
+ * // -> 'Fido changed'
1840
+ * // -> 'Fido changed'
1841
+ *
1842
+ * store.startTransaction();
1843
+ * store.setCell('pets', 'fido', 'color', 'walnut');
1844
+ * store.setCell('pets', 'fido', 'sold', true);
1845
+ * store.finishTransaction();
1846
+ * // -> 'Fido changed'
1847
+ * ```
1848
+ * @category Transaction
1849
+ */
1850
+ startTransaction(): Store;
1851
+
1852
+ /**
1853
+ * The finishTransaction method allows you to explicitly finish a transaction
1854
+ * that has made multiple mutations to the Store, triggering all calls to the
1855
+ * relevant listeners.
1856
+ *
1857
+ * Transactions are useful for making bulk changes to the data in a Store, and
1858
+ * when you don't want listeners to be called as you make each change. Changes
1859
+ * are made silently during the transaction, and listeners relevant to the
1860
+ * changes you have made will instead only be called when the whole
1861
+ * transaction is complete.
1862
+ *
1863
+ * Generally it is preferable to use the transaction method to wrap a block of
1864
+ * code as a transaction. It simply calls both the startTransaction and
1865
+ * finishTransaction methods for you. See that method for several transaction
1866
+ * examples.
1867
+ *
1868
+ * Use this finishTransaction method when you have a more 'open-ended'
1869
+ * transaction, such as one containing mutations triggered from other events
1870
+ * that are asynchronous or not occurring inline to your code. There must have
1871
+ * been a corresponding startTransaction method that this completes, of
1872
+ * course, otherwise this function has no effect.
1873
+ *
1874
+ * @param doRollback An optional callback that should return `true` if you
1875
+ * want to rollback the transaction at the end.
1876
+ * @returns A reference to the Store.
1877
+ * @example
1878
+ * This example makes changes to two Cells, first outside, and secondly
1879
+ * within, a transaction that is explicitly started and finished. In the
1880
+ * second case, the Row listener is only called once.
1881
+ *
1882
+ * ```js
1883
+ * const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
1884
+ * store.addRowListener('pets', 'fido', () => console.log('Fido changed'));
1885
+ *
1886
+ * store.setCell('pets', 'fido', 'color', 'brown');
1887
+ * store.setCell('pets', 'fido', 'sold', false);
1888
+ * // -> 'Fido changed'
1889
+ * // -> 'Fido changed'
1890
+ *
1891
+ * store.startTransaction();
1892
+ * store.setCell('pets', 'fido', 'color', 'walnut');
1893
+ * store.setCell('pets', 'fido', 'sold', true);
1894
+ * store.finishTransaction();
1895
+ * // -> 'Fido changed'
1896
+ * ```
1897
+ * @example
1898
+ * This example makes multiple changes to the Store, including some attempts
1899
+ * to update a Cell with invalid values. The `doRollback` callback receives
1900
+ * information about the changes and invalid attempts, and then judges that
1901
+ * the transaction should be rolled back to its original state.
1902
+ *
1903
+ * ```js
1904
+ * const store = createStore().setTables({
1905
+ * pets: {fido: {species: 'dog', color: 'brown'}},
1906
+ * });
1907
+ *
1908
+ * store.startTransaction();
1909
+ * store.setCell('pets', 'fido', 'color', 'black');
1910
+ * store.setCell('pets', 'fido', 'eyes', ['left', 'right']);
1911
+ * store.setCell('pets', 'fido', 'info', {sold: null});
1912
+ * store.finishTransaction((changedCells, invalidCells) => {
1913
+ * console.log(store.getTables());
1914
+ * // -> {pets: {fido: {species: 'dog', color: 'black'}}}
1915
+ * console.log(changedCells);
1916
+ * // -> {pets: {fido: {color: ['brown', 'black']}}}
1917
+ * console.log(invalidCells);
1918
+ * // -> {pets: {fido: {eyes: [['left', 'right']], info: [{sold: null}]}}}
1919
+ * return invalidCells['pets'] != null;
1920
+ * });
1921
+ *
1922
+ * console.log(store.getTables());
1923
+ * // -> {pets: {fido: {species: 'dog', color: 'brown'}}}
1924
+ * ```
1925
+ * @category Transaction
1926
+ */
1927
+ finishTransaction(
1928
+ doRollback?: (
1929
+ changedCells: ChangedCells,
1930
+ invalidCells: InvalidCells,
1931
+ ) => boolean,
1932
+ ): Store;
1933
+
1774
1934
  /**
1775
1935
  * The forEachTable method takes a function that it will then call for each
1776
1936
  * Table in the Store.
@@ -2746,6 +2906,143 @@ export interface Store {
2746
2906
  mutator?: boolean,
2747
2907
  ): Id;
2748
2908
 
2909
+ /**
2910
+ * The addWillFinishTransactionListener method registers a listener function
2911
+ * with the Store that will be called just before other non-mutating listeners
2912
+ * are called at the end of the transaction.
2913
+ *
2914
+ * This is useful if you need to know that a set of listeners are about to be
2915
+ * called at the end of a transaction, perhaps to batch _their_ consequences
2916
+ * together.
2917
+ *
2918
+ * The provided TransactionListener will receive a reference to the Store and
2919
+ * a boolean to indicate whether Cell values have been touched during the
2920
+ * transaction. The latter flag is intended as a hint about whether
2921
+ * non-mutating listeners might be being called at the end of the transaction.
2922
+ *
2923
+ * Here, 'touched' means that Cell values have either been changed, or changed
2924
+ * and then changed back to its original value during the transaction. The
2925
+ * exception is a transaction that has been rolled back, for which the value
2926
+ * of `cellsTouched` in the listener will be `false` because all changes have
2927
+ * been reverted.
2928
+ *
2929
+ * @returns A unique Id for the listener that can later be used to remove it.
2930
+ * @example
2931
+ * This example registers a listener that is called at the end of the
2932
+ * transaction, just before its listeners will be called. The transactions
2933
+ * shown here variously change, touch, and rollback cells, demonstrating how
2934
+ * the `cellsTouched` parameter in the listener works.
2935
+ *
2936
+ * ```js
2937
+ * const store = createStore().setTables({
2938
+ * pets: {fido: {species: 'dog', color: 'brown'}},
2939
+ * });
2940
+ * const listenerId = store.addWillFinishTransactionListener(
2941
+ * (store, cellsTouched) => console.log(`Cells touched: ${cellsTouched}`),
2942
+ * );
2943
+ * const listenerId2 = store.addTablesListener(() =>
2944
+ * console.log('Tables changed'),
2945
+ * );
2946
+ *
2947
+ * store.transaction(() => store.setCell('pets', 'fido', 'color', 'brown'));
2948
+ * // -> 'Cells touched: false'
2949
+ *
2950
+ * store.transaction(() => store.setCell('pets', 'fido', 'color', 'walnut'));
2951
+ * // -> 'Cells touched: true'
2952
+ * // -> 'Tables changed'
2953
+ *
2954
+ * store.transaction(() => {
2955
+ * store.setRow('pets', 'felix', {species: 'cat'});
2956
+ * store.delRow('pets', 'felix');
2957
+ * });
2958
+ * // -> 'Cells touched: true'
2959
+ *
2960
+ * store.transaction(
2961
+ * () => store.setRow('pets', 'fido', {species: 'dog'}),
2962
+ * () => true,
2963
+ * );
2964
+ * // -> 'Cells touched: false'
2965
+ * // Transaction was rolled back.
2966
+ *
2967
+ * store.callListener(listenerId);
2968
+ * // -> 'Cells touched: undefined'
2969
+ * // It is meaningless to call this listener directly.
2970
+ *
2971
+ * store.delListener(listenerId).delListener(listenerId2);
2972
+ * ```
2973
+ * @category Listener
2974
+ */
2975
+ addWillFinishTransactionListener(listener: TransactionListener): Id;
2976
+
2977
+ /**
2978
+ * The addDidFinishTransactionListener method registers a listener function
2979
+ * with the Store that will be called just after other non-mutating listeners
2980
+ * are called at the end of the transaction.
2981
+ *
2982
+ * This is useful if you need to know that a set of listeners have just been
2983
+ * called at the end of a transaction, perhaps to batch _their_ consequences
2984
+ * together.
2985
+ *
2986
+ * The provided TransactionListener will receive a reference to the Store and
2987
+ * a boolean to indicate whether Cell values have been touched during the
2988
+ * transaction. The latter flag is intended as a hint about whether
2989
+ * non-mutating listeners might have been called at the end of the
2990
+ * transaction.
2991
+ *
2992
+ * Here, 'touched' means that Cell values have either been changed, or changed
2993
+ * and then changed back to its original value during the transaction. The
2994
+ * exception is a transaction that has been rolled back, for which the value
2995
+ * of `cellsTouched` in the listener will be `false` because all changes have
2996
+ * been reverted.
2997
+ *
2998
+ * @returns A unique Id for the listener that can later be used to remove it.
2999
+ * @example
3000
+ * This example registers a listener that is called at the end of the
3001
+ * transaction, just after its listeners have been called. The transactions
3002
+ * shown here variously change, touch, and rollback cells, demonstrating how
3003
+ * the `cellsTouched` parameter in the listener works.
3004
+ *
3005
+ * ```js
3006
+ * const store = createStore().setTables({
3007
+ * pets: {fido: {species: 'dog', color: 'brown'}},
3008
+ * });
3009
+ * const listenerId = store.addDidFinishTransactionListener(
3010
+ * (store, cellsTouched) => console.log(`Cells touched: ${cellsTouched}`),
3011
+ * );
3012
+ * const listenerId2 = store.addTablesListener(() =>
3013
+ * console.log('Tables changed'),
3014
+ * );
3015
+ *
3016
+ * store.transaction(() => store.setCell('pets', 'fido', 'color', 'brown'));
3017
+ * // -> 'Cells touched: false'
3018
+ *
3019
+ * store.transaction(() => store.setCell('pets', 'fido', 'color', 'walnut'));
3020
+ * // -> 'Tables changed'
3021
+ * // -> 'Cells touched: true'
3022
+ *
3023
+ * store.transaction(() => {
3024
+ * store.setRow('pets', 'felix', {species: 'cat'});
3025
+ * store.delRow('pets', 'felix');
3026
+ * });
3027
+ * // -> 'Cells touched: true'
3028
+ *
3029
+ * store.transaction(
3030
+ * () => store.setRow('pets', 'fido', {species: 'dog'}),
3031
+ * () => true,
3032
+ * );
3033
+ * // -> 'Cells touched: false'
3034
+ * // Transaction was rolled back.
3035
+ *
3036
+ * store.callListener(listenerId);
3037
+ * // -> 'Cells touched: undefined'
3038
+ * // It is meaningless to call this listener directly.
3039
+ *
3040
+ * store.delListener(listenerId).delListener(listenerId2);
3041
+ * ```
3042
+ * @category Listener
3043
+ */
3044
+ addDidFinishTransactionListener(listener: TransactionListener): Id;
3045
+
2749
3046
  /**
2750
3047
  * The callListener method provides a way for you to manually provoke a
2751
3048
  * listener to be called, even if the underlying data hasn't changed.
@@ -209,6 +209,7 @@ const idsChanged = (ids, id, added) =>
209
209
  mapSet(ids, id, mapGet(ids, id) == -added ? void 0 : added);
210
210
  const createStore = () => {
211
211
  let hasSchema;
212
+ let cellsTouched;
212
213
  let nextRowId = 0;
213
214
  let transactions = 0;
214
215
  const changedTableIds = mapNew();
@@ -227,6 +228,7 @@ const createStore = () => {
227
228
  const cellIdsListeners = mapNewPair();
228
229
  const cellListeners = mapNewPair();
229
230
  const invalidCellListeners = mapNewPair();
231
+ const finishTransactionListeners = mapNewPair(setNew);
230
232
  const [addListener, callListeners, delListenerImpl, callListenerImpl] =
231
233
  getListenerFunctions(() => store);
232
234
  const validateSchema = (schema) =>
@@ -459,75 +461,73 @@ const createStore = () => {
459
461
  )
460
462
  : 0;
461
463
  const callListenersForChanges = (mutator) => {
462
- if (!collIsEmpty(changedCells)) {
463
- const emptyIdListeners =
464
- collIsEmpty(cellIdsListeners[mutator]) &&
465
- collIsEmpty(rowIdsListeners[mutator]) &&
466
- collIsEmpty(tableIdsListeners[mutator]);
467
- const emptyOtherListeners =
468
- collIsEmpty(cellListeners[mutator]) &&
469
- collIsEmpty(rowListeners[mutator]) &&
470
- collIsEmpty(tableListeners[mutator]) &&
471
- collIsEmpty(tablesListeners[mutator]);
472
- if (!(emptyIdListeners && emptyOtherListeners)) {
473
- const changes = mutator
474
- ? [
475
- mapClone(changedTableIds),
476
- mapClone2(changedRowIds),
477
- mapClone(changedCellIds, mapClone2),
478
- mapClone(changedCells, mapClone2),
479
- ]
480
- : [changedTableIds, changedRowIds, changedCellIds, changedCells];
481
- if (!emptyIdListeners) {
482
- collForEach(changes[2], (rowCellIds, tableId) =>
483
- collForEach(rowCellIds, (changedIds, rowId) => {
484
- if (!collIsEmpty(changedIds)) {
485
- callListeners(cellIdsListeners[mutator], [tableId, rowId]);
486
- }
487
- }),
488
- );
489
- collForEach(changes[1], (changedIds, tableId) => {
464
+ const emptyIdListeners =
465
+ collIsEmpty(cellIdsListeners[mutator]) &&
466
+ collIsEmpty(rowIdsListeners[mutator]) &&
467
+ collIsEmpty(tableIdsListeners[mutator]);
468
+ const emptyOtherListeners =
469
+ collIsEmpty(cellListeners[mutator]) &&
470
+ collIsEmpty(rowListeners[mutator]) &&
471
+ collIsEmpty(tableListeners[mutator]) &&
472
+ collIsEmpty(tablesListeners[mutator]);
473
+ if (!(emptyIdListeners && emptyOtherListeners)) {
474
+ const changes = mutator
475
+ ? [
476
+ mapClone(changedTableIds),
477
+ mapClone2(changedRowIds),
478
+ mapClone(changedCellIds, mapClone2),
479
+ mapClone(changedCells, mapClone2),
480
+ ]
481
+ : [changedTableIds, changedRowIds, changedCellIds, changedCells];
482
+ if (!emptyIdListeners) {
483
+ collForEach(changes[2], (rowCellIds, tableId) =>
484
+ collForEach(rowCellIds, (changedIds, rowId) => {
490
485
  if (!collIsEmpty(changedIds)) {
491
- callListeners(rowIdsListeners[mutator], [tableId]);
486
+ callListeners(cellIdsListeners[mutator], [tableId, rowId]);
492
487
  }
493
- });
494
- if (!collIsEmpty(changes[0])) {
495
- callListeners(tableIdsListeners[mutator]);
488
+ }),
489
+ );
490
+ collForEach(changes[1], (changedIds, tableId) => {
491
+ if (!collIsEmpty(changedIds)) {
492
+ callListeners(rowIdsListeners[mutator], [tableId]);
496
493
  }
494
+ });
495
+ if (!collIsEmpty(changes[0])) {
496
+ callListeners(tableIdsListeners[mutator]);
497
497
  }
498
- if (!emptyOtherListeners) {
499
- let tablesChanged;
500
- collForEach(changes[3], (rows, tableId) => {
501
- let tableChanged;
502
- collForEach(rows, (cells, rowId) => {
503
- let rowChanged;
504
- collForEach(cells, ([oldCell, newCell], cellId) => {
505
- if (newCell !== oldCell) {
506
- callListeners(
507
- cellListeners[mutator],
508
- [tableId, rowId, cellId],
509
- newCell,
510
- oldCell,
511
- getCellChange,
512
- );
513
- tablesChanged = tableChanged = rowChanged = 1;
514
- }
515
- });
516
- if (rowChanged) {
498
+ }
499
+ if (!emptyOtherListeners) {
500
+ let tablesChanged;
501
+ collForEach(changes[3], (rows, tableId) => {
502
+ let tableChanged;
503
+ collForEach(rows, (cells, rowId) => {
504
+ let rowChanged;
505
+ collForEach(cells, ([oldCell, newCell], cellId) => {
506
+ if (newCell !== oldCell) {
517
507
  callListeners(
518
- rowListeners[mutator],
519
- [tableId, rowId],
508
+ cellListeners[mutator],
509
+ [tableId, rowId, cellId],
510
+ newCell,
511
+ oldCell,
520
512
  getCellChange,
521
513
  );
514
+ tablesChanged = tableChanged = rowChanged = 1;
522
515
  }
523
516
  });
524
- if (tableChanged) {
525
- callListeners(tableListeners[mutator], [tableId], getCellChange);
517
+ if (rowChanged) {
518
+ callListeners(
519
+ rowListeners[mutator],
520
+ [tableId, rowId],
521
+ getCellChange,
522
+ );
526
523
  }
527
524
  });
528
- if (tablesChanged) {
529
- callListeners(tablesListeners[mutator], [], getCellChange);
525
+ if (tableChanged) {
526
+ callListeners(tableListeners[mutator], [tableId], getCellChange);
530
527
  }
528
+ });
529
+ if (tablesChanged) {
530
+ callListeners(tablesListeners[mutator], [], getCellChange);
531
531
  }
532
532
  }
533
533
  }
@@ -656,61 +656,79 @@ const createStore = () => {
656
656
  if (transactions == -1) {
657
657
  return;
658
658
  }
659
- transactions++;
659
+ startTransaction();
660
660
  const result = actions?.();
661
- transactions--;
662
- if (transactions == 0) {
663
- transactions = 1;
664
- callInvalidCellListeners(1);
665
- callListenersForChanges(1);
666
- transactions = -1;
667
- if (
668
- doRollback?.(
669
- mapToObj(
670
- changedCells,
671
- (table) =>
672
- mapToObj(
673
- table,
674
- (row) =>
675
- mapToObj(
676
- row,
677
- (cells) => [...cells],
678
- ([oldCell, newCell]) => oldCell === newCell,
679
- ),
680
- objIsEmpty,
681
- ),
682
- objIsEmpty,
683
- ),
684
- mapToObj(invalidCells, (map) => mapToObj(map, mapToObj)),
685
- )
686
- ) {
661
+ finishTransaction(doRollback);
662
+ return result;
663
+ };
664
+ const startTransaction = () => {
665
+ transactions++;
666
+ return store;
667
+ };
668
+ const finishTransaction = (doRollback) => {
669
+ if (transactions > 0) {
670
+ transactions--;
671
+ if (transactions == 0) {
672
+ cellsTouched = !collIsEmpty(changedCells);
687
673
  transactions = 1;
688
- collForEach(changedCells, (table, tableId) =>
689
- collForEach(table, (row, rowId) =>
690
- collForEach(row, ([oldCell], cellId) =>
691
- isUndefined(oldCell)
692
- ? delCell(tableId, rowId, cellId, true)
693
- : setCell(tableId, rowId, cellId, oldCell),
674
+ callInvalidCellListeners(1);
675
+ if (cellsTouched) {
676
+ callListenersForChanges(1);
677
+ }
678
+ transactions = -1;
679
+ if (
680
+ doRollback?.(
681
+ mapToObj(
682
+ changedCells,
683
+ (table) =>
684
+ mapToObj(
685
+ table,
686
+ (row) =>
687
+ mapToObj(
688
+ row,
689
+ (cells) => [...cells],
690
+ ([oldCell, newCell]) => oldCell === newCell,
691
+ ),
692
+ objIsEmpty,
693
+ ),
694
+ objIsEmpty,
694
695
  ),
695
- ),
696
+ mapToObj(invalidCells, (map) => mapToObj(map, mapToObj)),
697
+ )
698
+ ) {
699
+ transactions = 1;
700
+ collForEach(changedCells, (table, tableId) =>
701
+ collForEach(table, (row, rowId) =>
702
+ collForEach(row, ([oldCell], cellId) =>
703
+ isUndefined(oldCell)
704
+ ? delCell(tableId, rowId, cellId, true)
705
+ : setCell(tableId, rowId, cellId, oldCell),
706
+ ),
707
+ ),
708
+ );
709
+ transactions = -1;
710
+ cellsTouched = false;
711
+ }
712
+ callListeners(finishTransactionListeners[0], [], cellsTouched);
713
+ callInvalidCellListeners(0);
714
+ if (cellsTouched) {
715
+ callListenersForChanges(0);
716
+ }
717
+ callListeners(finishTransactionListeners[1], [], cellsTouched);
718
+ transactions = 0;
719
+ arrayForEach(
720
+ [
721
+ changedCells,
722
+ invalidCells,
723
+ changedTableIds,
724
+ changedRowIds,
725
+ changedCellIds,
726
+ ],
727
+ collClear,
696
728
  );
697
- transactions = -1;
698
729
  }
699
- callInvalidCellListeners(0);
700
- callListenersForChanges(0);
701
- transactions = 0;
702
- arrayForEach(
703
- [
704
- changedCells,
705
- invalidCells,
706
- changedTableIds,
707
- changedRowIds,
708
- changedCellIds,
709
- ],
710
- collClear,
711
- );
712
730
  }
713
- return result;
731
+ return store;
714
732
  };
715
733
  const forEachTable = (tableCallback) =>
716
734
  collForEach(tablesMap, (tableMap, tableId) =>
@@ -752,6 +770,10 @@ const createStore = () => {
752
770
  rowId,
753
771
  cellId,
754
772
  ]);
773
+ const addWillFinishTransactionListener = (listener) =>
774
+ addListener(listener, finishTransactionListeners[0]);
775
+ const addDidFinishTransactionListener = (listener) =>
776
+ addListener(listener, finishTransactionListeners[1]);
755
777
  const callListener = (listenerId) => {
756
778
  callListenerImpl(listenerId, [getTableIds, getRowIds, getCellIds], (ids) =>
757
779
  isUndefined(ids[2]) ? [] : arrayPair(getCell(...ids)),
@@ -771,6 +793,7 @@ const createStore = () => {
771
793
  cellIds: collPairSize(cellIdsListeners, collSize3),
772
794
  cell: collPairSize(cellListeners, collSize4),
773
795
  invalidCell: collPairSize(invalidCellListeners, collSize4),
796
+ transaction: collPairSize(finishTransactionListeners),
774
797
  });
775
798
  const store = {
776
799
  getTables,
@@ -800,6 +823,8 @@ const createStore = () => {
800
823
  delCell,
801
824
  delSchema,
802
825
  transaction,
826
+ startTransaction,
827
+ finishTransaction,
803
828
  forEachTable,
804
829
  forEachRow,
805
830
  forEachCell,
@@ -811,6 +836,8 @@ const createStore = () => {
811
836
  addCellIdsListener,
812
837
  addCellListener,
813
838
  addInvalidCellListener,
839
+ addWillFinishTransactionListener,
840
+ addDidFinishTransactionListener,
814
841
  callListener,
815
842
  delListener,
816
843
  getListenerStats,