@hvakr/firestate 0.1.3 → 0.1.5

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.
package/dist/index.mjs CHANGED
@@ -628,11 +628,13 @@ const createCollectionSubscription = (options) => {
628
628
  const getPublicState = () => ({
629
629
  data: getMergedData(),
630
630
  isLoading: state.isLoading,
631
+ isLoaded: state.isActive && !state.isLoading,
631
632
  isSynced: state.localState === void 0,
632
633
  isActive: state.isActive,
633
634
  error: state.error
634
635
  });
635
636
  let lastPublished = null;
637
+ let cachedState = null;
636
638
  const publicStateChanged = (prev, next) => prev.isActive !== next.isActive || observableStateChanged(prev, next);
637
639
  const notify = () => {
638
640
  reconcileDisplayOverrides(state.localState, state.displayOverrides);
@@ -640,6 +642,7 @@ const createCollectionSubscription = (options) => {
640
642
  if (lastPublished !== null && !publicStateChanged(lastPublished, publicState)) return;
641
643
  lastPublished = publicState;
642
644
  cachedHandle = null;
645
+ cachedState = publicState;
643
646
  subscribers.forEach((fn) => fn(publicState));
644
647
  store.reportSyncState(syncKey, publicState.isSynced);
645
648
  };
@@ -856,8 +859,7 @@ const createCollectionSubscription = (options) => {
856
859
  update: updateState,
857
860
  add: addDocument,
858
861
  remove: removeDocument,
859
- isLoading: state.isLoading,
860
- isSynced: state.localState === void 0,
862
+ isLoaded: state.isActive && !state.isLoading,
861
863
  isActive: state.isActive,
862
864
  load,
863
865
  sync,
@@ -868,11 +870,15 @@ const createCollectionSubscription = (options) => {
868
870
  if (cachedHandle === null) cachedHandle = buildHandle();
869
871
  return cachedHandle;
870
872
  };
873
+ const getState = () => {
874
+ if (cachedState === null) cachedState = getPublicState();
875
+ return cachedState;
876
+ };
871
877
  return {
872
878
  load,
873
879
  stop,
874
880
  subscribe,
875
- getState: getPublicState,
881
+ getState,
876
882
  getHandle,
877
883
  sync
878
884
  };
@@ -939,10 +945,12 @@ const createDocumentSubscription = (options) => {
939
945
  const getPublicState = () => ({
940
946
  data: getMergedData(),
941
947
  isLoading: state.isLoading,
948
+ isLoaded: !state.isLoading,
942
949
  isSynced: state.localState === void 0,
943
950
  error: state.error
944
951
  });
945
952
  let lastPublished = null;
953
+ let cachedState = null;
946
954
  const publicStateChanged = observableStateChanged;
947
955
  const notify = () => {
948
956
  reconcileDisplayOverrides(state.localState && typeof state.localState === "object" ? state.localState : void 0, state.displayOverrides);
@@ -950,6 +958,7 @@ const createDocumentSubscription = (options) => {
950
958
  if (lastPublished !== null && !publicStateChanged(lastPublished, publicState)) return;
951
959
  lastPublished = publicState;
952
960
  cachedHandle = null;
961
+ cachedState = publicState;
953
962
  subscribers.forEach((fn) => fn(publicState));
954
963
  store.reportSyncState(syncKey, publicState.isSynced);
955
964
  };
@@ -1168,8 +1177,7 @@ const createDocumentSubscription = (options) => {
1168
1177
  update: updateState,
1169
1178
  set: setData,
1170
1179
  delete: deleteDocument,
1171
- isLoading: state.isLoading,
1172
- isSynced: state.localState === void 0,
1180
+ isLoaded: !state.isLoading,
1173
1181
  sync,
1174
1182
  error: state.error,
1175
1183
  ref: docRef
@@ -1178,11 +1186,15 @@ const createDocumentSubscription = (options) => {
1178
1186
  if (cachedHandle === null) cachedHandle = buildHandle();
1179
1187
  return cachedHandle;
1180
1188
  };
1189
+ const getState = () => {
1190
+ if (cachedState === null) cachedState = getPublicState();
1191
+ return cachedState;
1192
+ };
1181
1193
  return {
1182
1194
  load,
1183
1195
  stop,
1184
1196
  subscribe,
1185
- getState: getPublicState,
1197
+ getState,
1186
1198
  getHandle,
1187
1199
  sync
1188
1200
  };
@@ -1286,6 +1298,7 @@ const getDocumentShared = ({ store, definition, collectionPath, docId, readOnly
1286
1298
  }
1287
1299
  return readOnlyHandle;
1288
1300
  },
1301
+ getState: () => ent.sub.getState(),
1289
1302
  load: () => ent.sub.load(),
1290
1303
  setUndoable: (enabled) => {
1291
1304
  if (facadeReadOnly) return;
@@ -1368,6 +1381,7 @@ const getCollectionShared = ({ store, definition, collectionPath, readOnly, quer
1368
1381
  }
1369
1382
  return readOnlyHandle;
1370
1383
  },
1384
+ getState: () => ent.sub.getState(),
1371
1385
  load: () => ent.sub.load(),
1372
1386
  setUndoable: (enabled) => {
1373
1387
  if (facadeReadOnly) return;
@@ -1476,8 +1490,7 @@ const DISABLED_DOCUMENT_HANDLE = {
1476
1490
  update: NOOP,
1477
1491
  set: NOOP,
1478
1492
  delete: NOOP,
1479
- isLoading: false,
1480
- isSynced: true,
1493
+ isLoaded: false,
1481
1494
  sync: ASYNC_NOOP,
1482
1495
  error: void 0,
1483
1496
  ref: void 0
@@ -1488,21 +1501,36 @@ const DISABLED_COLLECTION_HANDLE = {
1488
1501
  update: NOOP,
1489
1502
  add: DISABLED_ADD,
1490
1503
  remove: NOOP,
1491
- isLoading: false,
1492
- isSynced: true,
1504
+ isLoaded: false,
1493
1505
  isActive: false,
1494
1506
  load: NOOP,
1495
1507
  sync: ASYNC_NOOP,
1496
1508
  error: void 0,
1497
1509
  ref: void 0
1498
1510
  };
1511
+ const DISABLED_DOCUMENT_STATE = {
1512
+ data: void 0,
1513
+ isLoading: false,
1514
+ isLoaded: false,
1515
+ isSynced: true,
1516
+ error: void 0
1517
+ };
1518
+ const DISABLED_COLLECTION_STATE = {
1519
+ data: EMPTY_RECORD,
1520
+ isLoading: false,
1521
+ isLoaded: false,
1522
+ isSynced: true,
1523
+ isActive: false,
1524
+ error: void 0
1525
+ };
1499
1526
  /**
1500
- * Equality over an {@link ObservableSelection} (no-selector path): status fields
1501
- * compared by identity, the `data` slice by `dataEqual` (the default value
1502
- * comparison). This is what lets a change to a value-equal `data` be collapsed
1503
- * while any status change still re-renders the plain handle.
1527
+ * Equality over a {@link DefaultSelection} (no-selector path): `isLoaded`,
1528
+ * `error`, and a collection's `isActive` compared by identity, the `data` slice
1529
+ * by `dataEqual` (the default value comparison). `isSynced` is absent by design,
1530
+ * so a sync flip cannot re-render the plain handle; a change to value-equal
1531
+ * `data` is still collapsed, while a load transition re-renders.
1504
1532
  */
1505
- const selectionEqual = (a, b, dataEqual) => a.isLoading === b.isLoading && a.isSynced === b.isSynced && a.error === b.error && a.isActive === b.isActive && dataEqual(a.data, b.data);
1533
+ const defaultSelectionEqual = (a, b, dataEqual) => a.isLoaded === b.isLoaded && a.error === b.error && a.isActive === b.isActive && dataEqual(a.data, b.data);
1506
1534
  const defaultDataEqual = valuesEqualForNoOp;
1507
1535
  /**
1508
1536
  * Context for providing the Firestate store
@@ -1575,19 +1603,16 @@ function useDocument(options) {
1575
1603
  }
1576
1604
  return release;
1577
1605
  }, [shared]);
1578
- const getSnapshot = useCallback(() => shared ? shared.getHandle() : DISABLED_DOCUMENT_HANDLE, [shared]);
1579
- const selection = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, useCallback((handle) => {
1580
- const state = {
1581
- data: handle.data,
1582
- isLoading: handle.isLoading,
1583
- isSynced: handle.isSynced,
1584
- error: handle.error
1585
- };
1586
- return selector ? selector(state) : state;
1587
- }, [selector]), useCallback((a, b) => selector ? (isEqual ?? defaultDataEqual)(a, b) : selectionEqual(a, b, defaultDataEqual), [selector, isEqual]));
1606
+ const getStateSnapshot = useCallback(() => shared ? shared.getState() : DISABLED_DOCUMENT_STATE, [shared]);
1607
+ const getHandle = useCallback(() => shared ? shared.getHandle() : DISABLED_DOCUMENT_HANDLE, [shared]);
1608
+ const selection = useSyncExternalStoreWithSelector(subscribe, getStateSnapshot, getStateSnapshot, useCallback((state) => selector ? selector(state) : {
1609
+ data: state.data,
1610
+ isLoaded: state.isLoaded,
1611
+ error: state.error
1612
+ }, [selector]), useCallback((a, b) => selector ? (isEqual ?? defaultDataEqual)(a, b) : defaultSelectionEqual(a, b, defaultDataEqual), [selector, isEqual]));
1588
1613
  const hasSelector = selector != null;
1589
1614
  return useMemo(() => {
1590
- const handle = getSnapshot();
1615
+ const handle = getHandle();
1591
1616
  if (hasSelector) return {
1592
1617
  data: selection,
1593
1618
  update: handle.update,
@@ -1602,15 +1627,14 @@ function useDocument(options) {
1602
1627
  update: handle.update,
1603
1628
  set: handle.set,
1604
1629
  delete: handle.delete,
1605
- isLoading: s.isLoading,
1606
- isSynced: s.isSynced,
1630
+ isLoaded: s.isLoaded,
1607
1631
  sync: handle.sync,
1608
1632
  error: s.error,
1609
1633
  ref: handle.ref
1610
1634
  };
1611
1635
  }, [
1612
1636
  selection,
1613
- getSnapshot,
1637
+ getHandle,
1614
1638
  hasSelector
1615
1639
  ]);
1616
1640
  }
@@ -1663,20 +1687,17 @@ function useCollection(options) {
1663
1687
  }
1664
1688
  return release;
1665
1689
  }, [shared, isLazy]);
1666
- const getSnapshot = useCallback(() => shared ? shared.getHandle() : DISABLED_COLLECTION_HANDLE, [shared]);
1667
- const selection = useSyncExternalStoreWithSelector(subscribe, getSnapshot, getSnapshot, useCallback((handle) => {
1668
- const state = {
1669
- data: handle.data,
1670
- isLoading: handle.isLoading,
1671
- isSynced: handle.isSynced,
1672
- isActive: handle.isActive,
1673
- error: handle.error
1674
- };
1675
- return selector ? selector(state) : state;
1676
- }, [selector]), useCallback((a, b) => selector ? (isEqual ?? defaultDataEqual)(a, b) : selectionEqual(a, b, defaultDataEqual), [selector, isEqual]));
1690
+ const getStateSnapshot = useCallback(() => shared ? shared.getState() : DISABLED_COLLECTION_STATE, [shared]);
1691
+ const getHandle = useCallback(() => shared ? shared.getHandle() : DISABLED_COLLECTION_HANDLE, [shared]);
1692
+ const selection = useSyncExternalStoreWithSelector(subscribe, getStateSnapshot, getStateSnapshot, useCallback((state) => selector ? selector(state) : {
1693
+ data: state.data,
1694
+ isLoaded: state.isLoaded,
1695
+ isActive: state.isActive,
1696
+ error: state.error
1697
+ }, [selector]), useCallback((a, b) => selector ? (isEqual ?? defaultDataEqual)(a, b) : defaultSelectionEqual(a, b, defaultDataEqual), [selector, isEqual]));
1677
1698
  const hasSelector = selector != null;
1678
1699
  return useMemo(() => {
1679
- const handle = getSnapshot();
1700
+ const handle = getHandle();
1680
1701
  if (hasSelector) return {
1681
1702
  data: selection,
1682
1703
  update: handle.update,
@@ -1692,8 +1713,7 @@ function useCollection(options) {
1692
1713
  update: handle.update,
1693
1714
  add: handle.add,
1694
1715
  remove: handle.remove,
1695
- isLoading: s.isLoading,
1696
- isSynced: s.isSynced,
1716
+ isLoaded: s.isLoaded,
1697
1717
  isActive: s.isActive ?? false,
1698
1718
  load: handle.load,
1699
1719
  sync: handle.sync,
@@ -1702,10 +1722,96 @@ function useCollection(options) {
1702
1722
  };
1703
1723
  }, [
1704
1724
  selection,
1705
- getSnapshot,
1725
+ getHandle,
1706
1726
  hasSelector
1707
1727
  ]);
1708
1728
  }
1729
+ const syncStatusSelector = (state) => ({
1730
+ isSynced: state.isSynced,
1731
+ isSaving: !state.isSynced
1732
+ });
1733
+ const loadingStatusSelector = (state) => ({
1734
+ isLoading: state.isLoading,
1735
+ isLoaded: state.isLoaded
1736
+ });
1737
+ /**
1738
+ * Subscribe to a document's **sync status only** — `{ isSynced, isSaving }`.
1739
+ *
1740
+ * The opt-in counterpart to the sync-agnostic default handle (see
1741
+ * {@link DocumentHandle}): it re-renders when sync state flips but never on data
1742
+ * changes, and shares the resource's one `onSnapshot` listener with
1743
+ * `useDocument` and any slice hooks, so opting in adds no listener. While
1744
+ * disabled it reports `{ isSynced: true, isSaving: false }`.
1745
+ */
1746
+ function useDocumentSyncStatus(options) {
1747
+ return useDocument({
1748
+ definition: options.definition,
1749
+ params: options.params,
1750
+ enabled: options.enabled,
1751
+ readOnly: true,
1752
+ selector: syncStatusSelector
1753
+ }).data;
1754
+ }
1755
+ /**
1756
+ * Subscribe to a document's **loading status only** — `{ isLoading, isLoaded }`.
1757
+ *
1758
+ * A spinner channel that shares the resource's listener and does NOT re-render
1759
+ * on data changes — for a progress indicator rendered apart from the data. The
1760
+ * data handle keeps `isLoaded` for the common render path; this is an extra
1761
+ * channel, not a replacement.
1762
+ */
1763
+ function useDocumentLoadingStatus(options) {
1764
+ return useDocument({
1765
+ definition: options.definition,
1766
+ params: options.params,
1767
+ enabled: options.enabled,
1768
+ readOnly: true,
1769
+ selector: loadingStatusSelector
1770
+ }).data;
1771
+ }
1772
+ /**
1773
+ * Collection counterpart of {@link useDocumentSyncStatus} — `{ isSynced,
1774
+ * isSaving }` over a collection query, sharing its one listener.
1775
+ *
1776
+ * **Lazy caveat.** On a `lazy` collection this hook never calls `load()` itself:
1777
+ * activating a lazy listener is the data hook's job, and a passive status reader
1778
+ * must not silently start the listener (and bill the reads) the laziness exists
1779
+ * to defer. As the *lone* subscriber it therefore attaches no listener and stays
1780
+ * at the idle `{ isSynced: true, isSaving: false }`. Mount it alongside a
1781
+ * {@link useCollection} on the same query whose `load()` has run — the status
1782
+ * hook rides that one shared listener and reports real sync state. Non-lazy
1783
+ * collections activate on mount, so this hook works standalone there.
1784
+ */
1785
+ function useCollectionSyncStatus(options) {
1786
+ return useCollection({
1787
+ definition: options.definition,
1788
+ params: options.params,
1789
+ queryConstraints: options.queryConstraints,
1790
+ enabled: options.enabled,
1791
+ readOnly: true,
1792
+ selector: syncStatusSelector
1793
+ }).data;
1794
+ }
1795
+ /**
1796
+ * Collection counterpart of {@link useDocumentLoadingStatus} — `{ isLoading,
1797
+ * isLoaded }` over a collection query, sharing its one listener.
1798
+ *
1799
+ * Same lazy caveat as {@link useCollectionSyncStatus}: on a `lazy` collection it
1800
+ * never calls `load()`, so as the lone subscriber it attaches no listener and
1801
+ * stays at the idle `{ isLoading: false, isLoaded: false }` until a co-mounted
1802
+ * {@link useCollection} (or any active hook on the same query) activates the
1803
+ * shared listener via `load()`. Non-lazy collections activate on mount.
1804
+ */
1805
+ function useCollectionLoadingStatus(options) {
1806
+ return useCollection({
1807
+ definition: options.definition,
1808
+ params: options.params,
1809
+ queryConstraints: options.queryConstraints,
1810
+ enabled: options.enabled,
1811
+ readOnly: true,
1812
+ selector: loadingStatusSelector
1813
+ }).data;
1814
+ }
1709
1815
  /**
1710
1816
  * Keyboard shortcut hook for undo/redo
1711
1817
  *
@@ -1865,6 +1971,16 @@ function createFirestate(registry) {
1865
1971
  definition,
1866
1972
  params
1867
1973
  });
1974
+ api[`${hookName}SyncStatus`] = (params = {}, options = {}) => useDocumentSyncStatus({
1975
+ definition,
1976
+ params,
1977
+ enabled: options.enabled
1978
+ });
1979
+ api[`${hookName}LoadingStatus`] = (params = {}, options = {}) => useDocumentLoadingStatus({
1980
+ definition,
1981
+ params,
1982
+ enabled: options.enabled
1983
+ });
1868
1984
  } else if (entry.__kind === "collection") {
1869
1985
  const definition = colDefFor(entry);
1870
1986
  api[hookName] = (params = {}, options = {}) => useCollection({
@@ -1872,6 +1988,18 @@ function createFirestate(registry) {
1872
1988
  definition,
1873
1989
  params
1874
1990
  });
1991
+ api[`${hookName}SyncStatus`] = (params = {}, options = {}) => useCollectionSyncStatus({
1992
+ definition,
1993
+ params,
1994
+ enabled: options.enabled,
1995
+ queryConstraints: options.queryConstraints
1996
+ });
1997
+ api[`${hookName}LoadingStatus`] = (params = {}, options = {}) => useCollectionLoadingStatus({
1998
+ definition,
1999
+ params,
2000
+ enabled: options.enabled,
2001
+ queryConstraints: options.queryConstraints
2002
+ });
1875
2003
  } else if (entry.__kind === "document-selected") {
1876
2004
  const definition = docDefFor(entry.base);
1877
2005
  const { selector, isEqual } = entry;
@@ -2032,6 +2160,37 @@ const shallow = (a, b) => {
2032
2160
 
2033
2161
  //#endregion
2034
2162
  //#region src/utils/undo.ts
2163
+ const getGroupedActions = (action) => action.actions ?? [action];
2164
+ /**
2165
+ * Apply a group in the required direction. If a member fails, compensate the
2166
+ * members already applied so a failed undo/redo does not leave the group
2167
+ * partially applied (provided the compensating actions themselves succeed).
2168
+ */
2169
+ const applyGroup = async (actions, operation) => {
2170
+ const ordered = operation === "undo" ? [...actions].reverse() : actions;
2171
+ const applied = [];
2172
+ try {
2173
+ for (const action of ordered) {
2174
+ await action[operation]();
2175
+ applied.push(action);
2176
+ }
2177
+ } catch (error) {
2178
+ const compensate = operation === "undo" ? "redo" : "undo";
2179
+ for (const action of applied.reverse()) await action[compensate]();
2180
+ throw error;
2181
+ }
2182
+ };
2183
+ const mergeGroupedActions = (older, newer) => {
2184
+ const actions = [...getGroupedActions(older), newer];
2185
+ return {
2186
+ undo: () => applyGroup(actions, "undo"),
2187
+ redo: () => applyGroup(actions, "redo"),
2188
+ groupId: newer.groupId,
2189
+ path: newer.path ?? older.path,
2190
+ description: newer.description ?? older.description,
2191
+ actions
2192
+ };
2193
+ };
2035
2194
  /**
2036
2195
  * Create an undo manager instance.
2037
2196
  * This is a standalone, framework-agnostic implementation.
@@ -2051,7 +2210,7 @@ const shallow = (a, b) => {
2051
2210
  * ```
2052
2211
  */
2053
2212
  const createUndoManager = (config = {}) => {
2054
- const { maxLength = 20, onNavigate } = config;
2213
+ const { maxLength = 20, onNavigate, onUndo, onRedo, onError } = config;
2055
2214
  let undoStack = [];
2056
2215
  let redoStack = [];
2057
2216
  const subscribers = /* @__PURE__ */ new Set();
@@ -2075,19 +2234,7 @@ const createUndoManager = (config = {}) => {
2075
2234
  const last = undoStack[undoStack.length - 1];
2076
2235
  if (last?.groupId === action.groupId) {
2077
2236
  undoStack.pop();
2078
- undoStack.push({
2079
- undo: async () => {
2080
- await action.undo();
2081
- await last.undo();
2082
- },
2083
- redo: async () => {
2084
- await last.redo();
2085
- await action.redo();
2086
- },
2087
- groupId: action.groupId,
2088
- path: action.path ?? last.path,
2089
- description: action.description ?? last.description
2090
- });
2237
+ undoStack.push(mergeGroupedActions(last, action));
2091
2238
  redoStack = [];
2092
2239
  notify();
2093
2240
  return;
@@ -2101,31 +2248,35 @@ const createUndoManager = (config = {}) => {
2101
2248
  const undo = async () => {
2102
2249
  const action = undoStack.pop();
2103
2250
  if (!action) return;
2104
- if (action.path && onNavigate) onNavigate(action.path);
2105
2251
  try {
2252
+ if (action.path && onNavigate) onNavigate(action.path);
2106
2253
  await action.undo();
2107
- redoStack.push(action);
2108
2254
  } catch (error) {
2109
2255
  undoStack.push(action);
2110
- console.error("Undo failed:", error);
2256
+ if (onError) onError(error, action, "undo");
2257
+ else console.error("Undo failed:", error);
2111
2258
  throw error;
2112
2259
  }
2260
+ redoStack.push(action);
2113
2261
  notify();
2262
+ onUndo?.(action);
2114
2263
  };
2115
2264
  const redo = async () => {
2116
2265
  const action = redoStack.pop();
2117
2266
  if (!action) return;
2118
- if (action.path && onNavigate) onNavigate(action.path);
2119
2267
  try {
2268
+ if (action.path && onNavigate) onNavigate(action.path);
2120
2269
  await action.redo();
2121
- undoStack.push(action);
2122
- if (undoStack.length > maxLength) undoStack.shift();
2123
2270
  } catch (error) {
2124
2271
  redoStack.push(action);
2125
- console.error("Redo failed:", error);
2272
+ if (onError) onError(error, action, "redo");
2273
+ else console.error("Redo failed:", error);
2126
2274
  throw error;
2127
2275
  }
2276
+ undoStack.push(action);
2277
+ if (undoStack.length > maxLength) undoStack.shift();
2128
2278
  notify();
2279
+ onRedo?.(action);
2129
2280
  };
2130
2281
  const clear = () => {
2131
2282
  undoStack = [];
@@ -2183,9 +2334,22 @@ const createStore = (config) => {
2183
2334
  const { firestore, autosave = 1e3, minLoadTime = 0, maxUndoLength = 20 } = config;
2184
2335
  let onError = config.onError;
2185
2336
  let onNavigate = config.onNavigate;
2337
+ let onUndo = config.onUndo;
2338
+ let onRedo = config.onRedo;
2186
2339
  const undoManager = createUndoManager({
2187
2340
  maxLength: maxUndoLength,
2188
- onNavigate: (path) => onNavigate?.(path)
2341
+ onNavigate: (path) => onNavigate?.(path),
2342
+ onUndo: (action) => onUndo?.(action),
2343
+ onRedo: (action) => onRedo?.(action),
2344
+ onError: (error, action, operation) => {
2345
+ const context = {
2346
+ type: "undo",
2347
+ path: action.path ?? "undo",
2348
+ operation
2349
+ };
2350
+ if (onError) onError(error, context);
2351
+ else console.error(`Firestate error in ${context.type} ${context.path} during ${context.operation}:`, error);
2352
+ }
2189
2353
  });
2190
2354
  const syncStates = /* @__PURE__ */ new Map();
2191
2355
  const syncSubscribers = /* @__PURE__ */ new Set();
@@ -2212,6 +2376,12 @@ const createStore = (config) => {
2212
2376
  setOnNavigate: (handler) => {
2213
2377
  onNavigate = handler;
2214
2378
  },
2379
+ setOnUndo: (handler) => {
2380
+ onUndo = handler;
2381
+ },
2382
+ setOnRedo: (handler) => {
2383
+ onRedo = handler;
2384
+ },
2215
2385
  subscribeToSyncState: (fn) => {
2216
2386
  syncSubscribers.add(fn);
2217
2387
  return () => syncSubscribers.delete(fn);
@@ -2258,14 +2428,16 @@ const createStore = (config) => {
2258
2428
  * }
2259
2429
  * ```
2260
2430
  */
2261
- const FirestateProvider = ({ firestore, autosave = 1e3, minLoadTime = 0, maxUndoLength = 20, onError, onNavigate, children }) => {
2431
+ const FirestateProvider = ({ firestore, autosave = 1e3, minLoadTime = 0, maxUndoLength = 20, onError, onNavigate, onUndo, onRedo, children }) => {
2262
2432
  const store = useMemo(() => createStore({
2263
2433
  firestore,
2264
2434
  autosave,
2265
2435
  minLoadTime,
2266
2436
  maxUndoLength,
2267
2437
  onError,
2268
- onNavigate
2438
+ onNavigate,
2439
+ onUndo,
2440
+ onRedo
2269
2441
  }), [
2270
2442
  firestore,
2271
2443
  autosave,
@@ -2278,6 +2450,12 @@ const FirestateProvider = ({ firestore, autosave = 1e3, minLoadTime = 0, maxUndo
2278
2450
  useEffect(() => {
2279
2451
  store.setOnNavigate(onNavigate);
2280
2452
  }, [store, onNavigate]);
2453
+ useEffect(() => {
2454
+ store.setOnUndo(onUndo);
2455
+ }, [store, onUndo]);
2456
+ useEffect(() => {
2457
+ store.setOnRedo(onRedo);
2458
+ }, [store, onRedo]);
2281
2459
  return /* @__PURE__ */ jsx(FirestateContext.Provider, {
2282
2460
  value: store,
2283
2461
  children
@@ -2338,5 +2516,5 @@ const useUnsavedChangesBlocker = () => {
2338
2516
  };
2339
2517
 
2340
2518
  //#endregion
2341
- export { FirestateContext, FirestateProvider, FirestateStoreProvider, applyDiff, applyDiffMutable, col, computeDiff, computeUndoDiff, createCollectionSubscription, createDiffAtPath, createDocumentSubscription, createFirestate, createStore, createUndoManager, deepClone, defineCollection, defineDocument, diffContainsPath, doc, extractDiffValue, flattenDiff, flattenDiffToFieldPaths, isDeepEqual, isDiffEmpty, mergeDiffs, shallow, unflattenDiff, useCollection, useDocument, useIsSynced, useStore, useUndoKeyboardShortcuts, useUndoManager, useUnsavedChangesBlocker };
2519
+ export { FirestateContext, FirestateProvider, FirestateStoreProvider, applyDiff, applyDiffMutable, col, computeDiff, computeUndoDiff, createCollectionSubscription, createDiffAtPath, createDocumentSubscription, createFirestate, createStore, createUndoManager, deepClone, defineCollection, defineDocument, diffContainsPath, doc, extractDiffValue, flattenDiff, flattenDiffToFieldPaths, isDeepEqual, isDiffEmpty, mergeDiffs, shallow, unflattenDiff, useCollection, useCollectionLoadingStatus, useCollectionSyncStatus, useDocument, useDocumentLoadingStatus, useDocumentSyncStatus, useIsSynced, useStore, useUndoKeyboardShortcuts, useUndoManager, useUnsavedChangesBlocker };
2342
2520
  //# sourceMappingURL=index.mjs.map