@keepkit/core 0.10.0 → 0.12.0

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/react.js CHANGED
@@ -6,15 +6,14 @@ import {
6
6
  isKeepItemMetadataStale,
7
7
  queryKeepItems,
8
8
  revalidateKeepItems
9
- } from "./chunk-XF5VI6O5.js";
9
+ } from "./chunk-MDTL5L64.js";
10
10
  import {
11
11
  parseKeepMeta
12
12
  } from "./chunk-THZ3ACR2.js";
13
- import "./chunk-5QSZP6MT.js";
14
13
  import {
15
14
  createBrowserStorageAdapter,
16
15
  normalizeKeepTags
17
- } from "./chunk-EXE4E3GA.js";
16
+ } from "./chunk-VJDO3GFH.js";
18
17
 
19
18
  // src/hooks/useKeepItem.ts
20
19
  import { useCallback as useCallback3 } from "react";
@@ -69,6 +68,7 @@ function KeepProvider({
69
68
  onNoteUpdate,
70
69
  onTagsUpdate,
71
70
  onChange,
71
+ onUndo,
72
72
  onError,
73
73
  plugins = [],
74
74
  schemaVersion,
@@ -81,6 +81,8 @@ function KeepProvider({
81
81
  boundaryResetKey,
82
82
  validateItem,
83
83
  resolveItem,
84
+ autoRevalidation,
85
+ undoTimeoutMs,
84
86
  children
85
87
  }) {
86
88
  const content = /* @__PURE__ */ jsx(
@@ -93,6 +95,7 @@ function KeepProvider({
93
95
  onNoteUpdate,
94
96
  onTagsUpdate,
95
97
  onChange,
98
+ onUndo,
96
99
  onError,
97
100
  plugins,
98
101
  schemaVersion,
@@ -102,6 +105,8 @@ function KeepProvider({
102
105
  migrateMeta,
103
106
  validateItem,
104
107
  resolveItem,
108
+ autoRevalidation,
109
+ undoTimeoutMs,
105
110
  children
106
111
  }
107
112
  );
@@ -116,6 +121,7 @@ function KeepProviderContent({
116
121
  onNoteUpdate,
117
122
  onTagsUpdate,
118
123
  onChange,
124
+ onUndo,
119
125
  onError,
120
126
  plugins = [],
121
127
  schemaVersion,
@@ -125,6 +131,8 @@ function KeepProviderContent({
125
131
  migrateMeta,
126
132
  validateItem,
127
133
  resolveItem,
134
+ autoRevalidation,
135
+ undoTimeoutMs = 5e3,
128
136
  children
129
137
  }) {
130
138
  const storeRef = useRef(null);
@@ -135,17 +143,19 @@ function KeepProviderContent({
135
143
  isHydrated: false,
136
144
  isMutating: false,
137
145
  error: null,
138
- lastChange: void 0
146
+ lastChange: void 0,
147
+ undo: { canUndo: false, ids: [] }
139
148
  });
140
149
  }
141
150
  const store = storeRef.current;
142
151
  const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
143
- const { items, isLoading, isHydrated, isMutating, error, lastChange } = state;
152
+ const { items, isLoading, isHydrated, isMutating, error, lastChange, undo: storedUndo } = state;
153
+ const undo = storedUndo ?? EMPTY_UNDO_STATE;
144
154
  const itemsRef = useRef(items);
145
155
  const pluginsRef = useRef(plugins);
146
156
  pluginsRef.current = plugins;
147
- const handlersRef = useRef({ onSave, onRemove, onNoteUpdate, onTagsUpdate, onChange, onError });
148
- handlersRef.current = { onSave, onRemove, onNoteUpdate, onTagsUpdate, onChange, onError };
157
+ const handlersRef = useRef({ onSave, onRemove, onNoteUpdate, onTagsUpdate, onChange, onUndo, onError });
158
+ handlersRef.current = { onSave, onRemove, onNoteUpdate, onTagsUpdate, onChange, onUndo, onError };
149
159
  const migrationRef = useRef({
150
160
  schemaVersion,
151
161
  migrateMeta,
@@ -157,6 +167,10 @@ function KeepProviderContent({
157
167
  const operationTailRef = useRef(Promise.resolve());
158
168
  const pendingRefreshesRef = useRef(0);
159
169
  const pendingMutationsRef = useRef(0);
170
+ const undoRef = useRef(void 0);
171
+ const autoRevalidationRef = useRef(autoRevalidation);
172
+ autoRevalidationRef.current = autoRevalidation;
173
+ const didMountRevalidateRef = useRef(false);
160
174
  const syncStorage = isSyncCapableStorage(storage) ? storage : void 0;
161
175
  const getSyncState = useCallback(() => syncStorage?.getSyncState() ?? IDLE_SYNC_STATE, [syncStorage]);
162
176
  const subscribeSync = useCallback(
@@ -257,6 +271,16 @@ function KeepProviderContent({
257
271
  if (!storage.subscribe) return;
258
272
  return storage.subscribe(() => void refresh());
259
273
  }, [refresh, storage]);
274
+ useEffect(() => {
275
+ if (!isScopeAwareStorage(storage)) return;
276
+ return storage.subscribeScope(() => {
277
+ undoRef.current?.timer && clearTimeout(undoRef.current.timer);
278
+ undoRef.current = void 0;
279
+ itemsRef.current = [];
280
+ store.setState({ items: [], isHydrated: false, isLoading: true, error: null, undo: EMPTY_UNDO_STATE });
281
+ void refresh();
282
+ });
283
+ }, [refresh, storage, store]);
260
284
  const runMutation = useCallback(
261
285
  (action, id, createPlan) => {
262
286
  pendingMutationsRef.current += 1;
@@ -474,6 +498,69 @@ function KeepProviderContent({
474
498
  },
475
499
  [runMutation, storage]
476
500
  );
501
+ const rememberUndo = useCallback(
502
+ (removedItems) => {
503
+ undoRef.current?.timer && clearTimeout(undoRef.current.timer);
504
+ const expiresAt = Date.now() + Math.max(0, undoTimeoutMs);
505
+ const timer = setTimeout(
506
+ () => {
507
+ undoRef.current = void 0;
508
+ store.setState({ undo: { canUndo: false, ids: [] } });
509
+ },
510
+ Math.max(0, undoTimeoutMs)
511
+ );
512
+ undoRef.current = { items: removedItems, expiresAt, timer };
513
+ store.setState({ undo: { canUndo: true, ids: removedItems.map((item) => item.id), expiresAt } });
514
+ },
515
+ [store, undoTimeoutMs]
516
+ );
517
+ const removeItemWithUndo = useCallback(
518
+ async (id) => {
519
+ const item = itemsRef.current.find((current) => current.id === id);
520
+ await removeItem(id);
521
+ if (item) rememberUndo([item]);
522
+ },
523
+ [rememberUndo, removeItem]
524
+ );
525
+ const removeItemsWithUndo = useCallback(
526
+ async (ids) => {
527
+ const idSet = new Set(ids);
528
+ const removedItems = itemsRef.current.filter((item) => idSet.has(item.id));
529
+ await removeItems(ids);
530
+ if (removedItems.length > 0) rememberUndo(removedItems);
531
+ },
532
+ [rememberUndo, removeItems]
533
+ );
534
+ const undoLastRemoval = useCallback(async () => {
535
+ const pending = undoRef.current;
536
+ if (!pending || pending.expiresAt < Date.now()) {
537
+ undoRef.current = void 0;
538
+ store.setState({ undo: { canUndo: false, ids: [] } });
539
+ return;
540
+ }
541
+ if (pending.timer) clearTimeout(pending.timer);
542
+ undoRef.current = void 0;
543
+ store.setState({ undo: { canUndo: false, ids: [] } });
544
+ await runMutation("undo", void 0, (previous) => {
545
+ const restored = new Map(pending.items.map((item) => [item.id, item]));
546
+ const next = [...previous.filter((item) => !restored.has(item.id)), ...pending.items].sort(
547
+ (a, b) => b.updatedAt - a.updatedAt
548
+ );
549
+ return {
550
+ next,
551
+ persist: async () => {
552
+ if (storage.setMany) await storage.setMany(pending.items);
553
+ else for (const item of pending.items) await storage.set(item);
554
+ },
555
+ onSuccess: () => handlersRef.current.onUndo?.(pending.items),
556
+ pluginContext: { action: "undo", items: pending.items }
557
+ };
558
+ });
559
+ }, [runMutation, storage, store]);
560
+ useEffect(() => {
561
+ if (syncState.status !== "error" || !undoRef.current) return;
562
+ void undoLastRemoval();
563
+ }, [syncState.status, undoLastRemoval]);
477
564
  const clear = useCallback(
478
565
  () => runMutation("clear", void 0, (_previous) => ({
479
566
  next: [],
@@ -544,6 +631,27 @@ function KeepProviderContent({
544
631
  validateItem
545
632
  ]
546
633
  );
634
+ useEffect(() => {
635
+ const settings = autoRevalidationRef.current;
636
+ const activeRevalidator = settings?.revalidator ?? validateItem;
637
+ if (!settings || !activeRevalidator) return;
638
+ const run = () => void revalidateItems(activeRevalidator, { removeStatuses: settings.removeStatuses }).catch(() => void 0);
639
+ if (isHydrated && settings.onMount !== false && !didMountRevalidateRef.current) {
640
+ didMountRevalidateRef.current = true;
641
+ run();
642
+ }
643
+ const interval = settings.intervalMs && settings.intervalMs > 0 ? setInterval(run, settings.intervalMs) : void 0;
644
+ const onOnline = () => {
645
+ if (settings.onReconnect !== false) run();
646
+ };
647
+ if (typeof window !== "undefined" && settings.onReconnect !== false) {
648
+ window.addEventListener("online", onOnline);
649
+ }
650
+ return () => {
651
+ if (interval) clearInterval(interval);
652
+ if (typeof window !== "undefined") window.removeEventListener("online", onOnline);
653
+ };
654
+ }, [isHydrated, revalidateItems, validateItem]);
547
655
  const refreshItemMetadata = useCallback(
548
656
  async (id, refresh2) => {
549
657
  if (!itemsRef.current.some((item) => item.id === id)) {
@@ -557,6 +665,15 @@ function KeepProviderContent({
557
665
  [revalidateItems]
558
666
  );
559
667
  const flushSync = useCallback(() => syncStorage ? syncStorage.flushSync() : Promise.resolve(), [syncStorage]);
668
+ const resolveSyncConflict = useCallback(
669
+ (id, resolution, item) => {
670
+ if (!syncStorage?.resolveSyncConflict) {
671
+ return Promise.reject(new Error("The configured storage does not support sync conflict resolution."));
672
+ }
673
+ return syncStorage.resolveSyncConflict(id, resolution, item);
674
+ },
675
+ [syncStorage]
676
+ );
560
677
  const exportBackup = useCallback(() => exportItems(storage), [storage]);
561
678
  const importBackup = useCallback(
562
679
  async (data, options = {}) => {
@@ -594,6 +711,7 @@ function KeepProviderContent({
594
711
  error,
595
712
  lastChange,
596
713
  syncState,
714
+ undo,
597
715
  saveItem,
598
716
  updateNote,
599
717
  updateTags,
@@ -602,9 +720,13 @@ function KeepProviderContent({
602
720
  removeTagsBatch,
603
721
  removeItem,
604
722
  removeItems,
723
+ removeItemWithUndo,
724
+ removeItemsWithUndo,
725
+ undoLastRemoval,
605
726
  clear,
606
727
  refresh,
607
728
  flushSync,
729
+ resolveSyncConflict,
608
730
  refreshItemMetadata,
609
731
  revalidateItems,
610
732
  exportBackup,
@@ -615,14 +737,19 @@ function KeepProviderContent({
615
737
  error,
616
738
  lastChange,
617
739
  flushSync,
740
+ resolveSyncConflict,
618
741
  isHydrated,
619
742
  isLoading,
620
743
  isMutating,
621
744
  items,
622
745
  syncState,
746
+ undo,
623
747
  refresh,
624
748
  removeItem,
625
749
  saveItem,
750
+ removeItemWithUndo,
751
+ removeItemsWithUndo,
752
+ undoLastRemoval,
626
753
  updateNote,
627
754
  updateTags,
628
755
  updateTagsBatch,
@@ -645,6 +772,9 @@ function KeepProviderContent({
645
772
  removeTagsBatch,
646
773
  removeItem,
647
774
  removeItems,
775
+ removeItemWithUndo,
776
+ removeItemsWithUndo,
777
+ undoLastRemoval,
648
778
  clear,
649
779
  refresh,
650
780
  refreshItemMetadata,
@@ -662,7 +792,10 @@ function KeepProviderContent({
662
792
  updateTags,
663
793
  updateTagsBatch,
664
794
  refreshItemMetadata,
665
- revalidateItems
795
+ revalidateItems,
796
+ removeItemWithUndo,
797
+ removeItemsWithUndo,
798
+ undoLastRemoval
666
799
  ]
667
800
  );
668
801
  const storeAccess = useMemo(() => ({ store, actions }), [actions, store]);
@@ -671,11 +804,16 @@ function KeepProviderContent({
671
804
  var IDLE_SYNC_STATE = Object.freeze({
672
805
  status: "idle",
673
806
  pendingCount: 0,
674
- conflictIds: []
807
+ conflictIds: [],
808
+ conflicts: []
675
809
  });
810
+ var EMPTY_UNDO_STATE = Object.freeze({ canUndo: false, ids: [] });
676
811
  function isSyncCapableStorage(storage) {
677
812
  return "getSyncState" in storage && typeof storage.getSyncState === "function" && "subscribeSync" in storage && typeof storage.subscribeSync === "function" && "flushSync" in storage && typeof storage.flushSync === "function";
678
813
  }
814
+ function isScopeAwareStorage(storage) {
815
+ return "subscribeScope" in storage && typeof storage.subscribeScope === "function";
816
+ }
679
817
  async function parseKeepMetaItem(item, schema) {
680
818
  return { ...item, meta: await parseKeepMeta(schema, item.meta) };
681
819
  }
@@ -745,6 +883,7 @@ function useKeepItem(input) {
745
883
  });
746
884
  }, [actions, input, item?.savedAt]);
747
885
  const remove = useCallback3(() => actions.removeItem(id), [actions, id]);
886
+ const removeWithUndo = useCallback3(() => actions.removeItemWithUndo(id), [actions, id]);
748
887
  const toggle = useCallback3(() => item ? remove() : save(), [item, remove, save]);
749
888
  const updateNote = useCallback3((note) => actions.updateNote(id, note), [actions, id]);
750
889
  const updateTags = useCallback3((tags) => actions.updateTags(id, tags), [actions, id]);
@@ -760,6 +899,8 @@ function useKeepItem(input) {
760
899
  error,
761
900
  save,
762
901
  remove,
902
+ removeWithUndo,
903
+ undo: actions.undoLastRemoval,
763
904
  toggle,
764
905
  updateNote,
765
906
  updateTags,
@@ -814,6 +955,8 @@ function useKeepList(query = {}) {
814
955
  const allTags = useKeepStoreSelector(store, tagsSelector);
815
956
  const remove = useCallback4((id) => actions.removeItem(id), [actions]);
816
957
  const removeBatch = useCallback4((ids) => actions.removeItems(ids), [actions]);
958
+ const removeWithUndo = useCallback4((id) => actions.removeItemWithUndo(id), [actions]);
959
+ const removeBatchWithUndo = useCallback4((ids) => actions.removeItemsWithUndo(ids), [actions]);
817
960
  const updateTagsBatch = useCallback4(
818
961
  (ids, nextTags) => actions.updateTagsBatch(ids, nextTags),
819
962
  [actions]
@@ -841,6 +984,8 @@ function useKeepList(query = {}) {
841
984
  error,
842
985
  remove,
843
986
  removeBatch,
987
+ removeWithUndo,
988
+ removeBatchWithUndo,
844
989
  updateTagsBatch,
845
990
  addTagsBatch,
846
991
  removeTagsBatch,