@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/README.md +229 -115
- package/dist/index.d.mts +219 -46
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +247 -69
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1501
|
-
* compared by identity, the `data` slice
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
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
|
|
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
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1667
|
-
const
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|