@keepkit/core 0.9.0 → 0.11.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
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import {
3
3
  KeepStore,
4
+ exportItems,
5
+ importItems,
4
6
  isKeepItemMetadataStale,
5
7
  queryKeepItems,
6
8
  revalidateKeepItems
7
- } from "./chunk-AWQWKK4V.js";
9
+ } from "./chunk-MDTL5L64.js";
8
10
  import {
9
11
  parseKeepMeta
10
12
  } from "./chunk-THZ3ACR2.js";
@@ -12,7 +14,7 @@ import "./chunk-5QSZP6MT.js";
12
14
  import {
13
15
  createBrowserStorageAdapter,
14
16
  normalizeKeepTags
15
- } from "./chunk-DBRHZ6XU.js";
17
+ } from "./chunk-36YIELZE.js";
16
18
 
17
19
  // src/hooks/useKeepItem.ts
18
20
  import { useCallback as useCallback3 } from "react";
@@ -67,6 +69,7 @@ function KeepProvider({
67
69
  onNoteUpdate,
68
70
  onTagsUpdate,
69
71
  onChange,
72
+ onUndo,
70
73
  onError,
71
74
  plugins = [],
72
75
  schemaVersion,
@@ -77,6 +80,10 @@ function KeepProvider({
77
80
  fallback,
78
81
  onBoundaryError,
79
82
  boundaryResetKey,
83
+ validateItem,
84
+ resolveItem,
85
+ autoRevalidation,
86
+ undoTimeoutMs,
80
87
  children
81
88
  }) {
82
89
  const content = /* @__PURE__ */ jsx(
@@ -89,6 +96,7 @@ function KeepProvider({
89
96
  onNoteUpdate,
90
97
  onTagsUpdate,
91
98
  onChange,
99
+ onUndo,
92
100
  onError,
93
101
  plugins,
94
102
  schemaVersion,
@@ -96,6 +104,10 @@ function KeepProvider({
96
104
  invalidItemPolicy,
97
105
  onInvalidItem,
98
106
  migrateMeta,
107
+ validateItem,
108
+ resolveItem,
109
+ autoRevalidation,
110
+ undoTimeoutMs,
99
111
  children
100
112
  }
101
113
  );
@@ -110,6 +122,7 @@ function KeepProviderContent({
110
122
  onNoteUpdate,
111
123
  onTagsUpdate,
112
124
  onChange,
125
+ onUndo,
113
126
  onError,
114
127
  plugins = [],
115
128
  schemaVersion,
@@ -117,6 +130,10 @@ function KeepProviderContent({
117
130
  invalidItemPolicy = "error",
118
131
  onInvalidItem,
119
132
  migrateMeta,
133
+ validateItem,
134
+ resolveItem,
135
+ autoRevalidation,
136
+ undoTimeoutMs = 5e3,
120
137
  children
121
138
  }) {
122
139
  const storeRef = useRef(null);
@@ -127,17 +144,19 @@ function KeepProviderContent({
127
144
  isHydrated: false,
128
145
  isMutating: false,
129
146
  error: null,
130
- lastChange: void 0
147
+ lastChange: void 0,
148
+ undo: { canUndo: false, ids: [] }
131
149
  });
132
150
  }
133
151
  const store = storeRef.current;
134
152
  const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
135
- const { items, isLoading, isHydrated, isMutating, error, lastChange } = state;
153
+ const { items, isLoading, isHydrated, isMutating, error, lastChange, undo: storedUndo } = state;
154
+ const undo = storedUndo ?? EMPTY_UNDO_STATE;
136
155
  const itemsRef = useRef(items);
137
156
  const pluginsRef = useRef(plugins);
138
157
  pluginsRef.current = plugins;
139
- const handlersRef = useRef({ onSave, onRemove, onNoteUpdate, onTagsUpdate, onChange, onError });
140
- handlersRef.current = { onSave, onRemove, onNoteUpdate, onTagsUpdate, onChange, onError };
158
+ const handlersRef = useRef({ onSave, onRemove, onNoteUpdate, onTagsUpdate, onChange, onUndo, onError });
159
+ handlersRef.current = { onSave, onRemove, onNoteUpdate, onTagsUpdate, onChange, onUndo, onError };
141
160
  const migrationRef = useRef({
142
161
  schemaVersion,
143
162
  migrateMeta,
@@ -149,6 +168,10 @@ function KeepProviderContent({
149
168
  const operationTailRef = useRef(Promise.resolve());
150
169
  const pendingRefreshesRef = useRef(0);
151
170
  const pendingMutationsRef = useRef(0);
171
+ const undoRef = useRef(void 0);
172
+ const autoRevalidationRef = useRef(autoRevalidation);
173
+ autoRevalidationRef.current = autoRevalidation;
174
+ const didMountRevalidateRef = useRef(false);
152
175
  const syncStorage = isSyncCapableStorage(storage) ? storage : void 0;
153
176
  const getSyncState = useCallback(() => syncStorage?.getSyncState() ?? IDLE_SYNC_STATE, [syncStorage]);
154
177
  const subscribeSync = useCallback(
@@ -466,6 +489,69 @@ function KeepProviderContent({
466
489
  },
467
490
  [runMutation, storage]
468
491
  );
492
+ const rememberUndo = useCallback(
493
+ (removedItems) => {
494
+ undoRef.current?.timer && clearTimeout(undoRef.current.timer);
495
+ const expiresAt = Date.now() + Math.max(0, undoTimeoutMs);
496
+ const timer = setTimeout(
497
+ () => {
498
+ undoRef.current = void 0;
499
+ store.setState({ undo: { canUndo: false, ids: [] } });
500
+ },
501
+ Math.max(0, undoTimeoutMs)
502
+ );
503
+ undoRef.current = { items: removedItems, expiresAt, timer };
504
+ store.setState({ undo: { canUndo: true, ids: removedItems.map((item) => item.id), expiresAt } });
505
+ },
506
+ [store, undoTimeoutMs]
507
+ );
508
+ const removeItemWithUndo = useCallback(
509
+ async (id) => {
510
+ const item = itemsRef.current.find((current) => current.id === id);
511
+ await removeItem(id);
512
+ if (item) rememberUndo([item]);
513
+ },
514
+ [rememberUndo, removeItem]
515
+ );
516
+ const removeItemsWithUndo = useCallback(
517
+ async (ids) => {
518
+ const idSet = new Set(ids);
519
+ const removedItems = itemsRef.current.filter((item) => idSet.has(item.id));
520
+ await removeItems(ids);
521
+ if (removedItems.length > 0) rememberUndo(removedItems);
522
+ },
523
+ [rememberUndo, removeItems]
524
+ );
525
+ const undoLastRemoval = useCallback(async () => {
526
+ const pending = undoRef.current;
527
+ if (!pending || pending.expiresAt < Date.now()) {
528
+ undoRef.current = void 0;
529
+ store.setState({ undo: { canUndo: false, ids: [] } });
530
+ return;
531
+ }
532
+ if (pending.timer) clearTimeout(pending.timer);
533
+ undoRef.current = void 0;
534
+ store.setState({ undo: { canUndo: false, ids: [] } });
535
+ await runMutation("undo", void 0, (previous) => {
536
+ const restored = new Map(pending.items.map((item) => [item.id, item]));
537
+ const next = [...previous.filter((item) => !restored.has(item.id)), ...pending.items].sort(
538
+ (a, b) => b.updatedAt - a.updatedAt
539
+ );
540
+ return {
541
+ next,
542
+ persist: async () => {
543
+ if (storage.setMany) await storage.setMany(pending.items);
544
+ else for (const item of pending.items) await storage.set(item);
545
+ },
546
+ onSuccess: () => handlersRef.current.onUndo?.(pending.items),
547
+ pluginContext: { action: "undo", items: pending.items }
548
+ };
549
+ });
550
+ }, [runMutation, storage, store]);
551
+ useEffect(() => {
552
+ if (syncState.status !== "error" || !undoRef.current) return;
553
+ void undoLastRemoval();
554
+ }, [syncState.status, undoLastRemoval]);
469
555
  const clear = useCallback(
470
556
  () => runMutation("clear", void 0, (_previous) => ({
471
557
  next: [],
@@ -482,7 +568,13 @@ function KeepProviderContent({
482
568
  const previous = itemsRef.current;
483
569
  let persistenceStarted = false;
484
570
  try {
485
- const summary = await revalidateKeepItems(previous, revalidator, options);
571
+ const activeRevalidator = revalidator ?? validateItem;
572
+ if (!activeRevalidator)
573
+ throw new Error("KeepProvider.revalidateItems requires a revalidator or validateItem.");
574
+ const summary = await revalidateKeepItems(previous, activeRevalidator, {
575
+ ...options,
576
+ resolveItem: options.resolveItem ?? resolveItem
577
+ });
486
578
  const pluginContext = { action: "revalidate", items: summary.updatedItems };
487
579
  await runBeforePlugins(pluginContext);
488
580
  if (summary.updatedItems.length > 0) {
@@ -518,8 +610,39 @@ function KeepProviderContent({
518
610
  if (pendingMutationsRef.current === 0) store.setState({ isMutating: false });
519
611
  });
520
612
  },
521
- [enqueueOperation, reportError, runAfterPlugins, runBeforePlugins, setItems, storage, store]
613
+ [
614
+ enqueueOperation,
615
+ reportError,
616
+ resolveItem,
617
+ runAfterPlugins,
618
+ runBeforePlugins,
619
+ setItems,
620
+ storage,
621
+ store,
622
+ validateItem
623
+ ]
522
624
  );
625
+ useEffect(() => {
626
+ const settings = autoRevalidationRef.current;
627
+ const activeRevalidator = settings?.revalidator ?? validateItem;
628
+ if (!settings || !activeRevalidator) return;
629
+ const run = () => void revalidateItems(activeRevalidator, { removeStatuses: settings.removeStatuses }).catch(() => void 0);
630
+ if (isHydrated && settings.onMount !== false && !didMountRevalidateRef.current) {
631
+ didMountRevalidateRef.current = true;
632
+ run();
633
+ }
634
+ const interval = settings.intervalMs && settings.intervalMs > 0 ? setInterval(run, settings.intervalMs) : void 0;
635
+ const onOnline = () => {
636
+ if (settings.onReconnect !== false) run();
637
+ };
638
+ if (typeof window !== "undefined" && settings.onReconnect !== false) {
639
+ window.addEventListener("online", onOnline);
640
+ }
641
+ return () => {
642
+ if (interval) clearInterval(interval);
643
+ if (typeof window !== "undefined") window.removeEventListener("online", onOnline);
644
+ };
645
+ }, [isHydrated, revalidateItems, validateItem]);
523
646
  const refreshItemMetadata = useCallback(
524
647
  async (id, refresh2) => {
525
648
  if (!itemsRef.current.some((item) => item.id === id)) {
@@ -533,6 +656,34 @@ function KeepProviderContent({
533
656
  [revalidateItems]
534
657
  );
535
658
  const flushSync = useCallback(() => syncStorage ? syncStorage.flushSync() : Promise.resolve(), [syncStorage]);
659
+ const exportBackup = useCallback(() => exportItems(storage), [storage]);
660
+ const importBackup = useCallback(
661
+ async (data, options = {}) => {
662
+ pendingMutationsRef.current += 1;
663
+ store.setState({ isMutating: true });
664
+ const run = enqueueOperation(async () => {
665
+ try {
666
+ const result = await importItems(storage, data, {
667
+ ...options,
668
+ schema: migrationRef.current.schema,
669
+ invalidItemPolicy: options.invalidItemPolicy ?? migrationRef.current.invalidItemPolicy,
670
+ onInvalidItem: options.onInvalidItem ?? migrationRef.current.onInvalidItem
671
+ });
672
+ setItems(result.items);
673
+ store.setState({ error: null });
674
+ return result;
675
+ } catch (cause) {
676
+ reportError(cause, { action: "import" });
677
+ throw cause;
678
+ }
679
+ });
680
+ return run.finally(() => {
681
+ pendingMutationsRef.current -= 1;
682
+ if (pendingMutationsRef.current === 0) store.setState({ isMutating: false });
683
+ });
684
+ },
685
+ [enqueueOperation, reportError, setItems, storage, store]
686
+ );
536
687
  const value = useMemo(
537
688
  () => ({
538
689
  items,
@@ -542,6 +693,7 @@ function KeepProviderContent({
542
693
  error,
543
694
  lastChange,
544
695
  syncState,
696
+ undo,
545
697
  saveItem,
546
698
  updateNote,
547
699
  updateTags,
@@ -550,11 +702,16 @@ function KeepProviderContent({
550
702
  removeTagsBatch,
551
703
  removeItem,
552
704
  removeItems,
705
+ removeItemWithUndo,
706
+ removeItemsWithUndo,
707
+ undoLastRemoval,
553
708
  clear,
554
709
  refresh,
555
710
  flushSync,
556
711
  refreshItemMetadata,
557
- revalidateItems
712
+ revalidateItems,
713
+ exportBackup,
714
+ importBackup
558
715
  }),
559
716
  [
560
717
  clear,
@@ -566,9 +723,13 @@ function KeepProviderContent({
566
723
  isMutating,
567
724
  items,
568
725
  syncState,
726
+ undo,
569
727
  refresh,
570
728
  removeItem,
571
729
  saveItem,
730
+ removeItemWithUndo,
731
+ removeItemsWithUndo,
732
+ undoLastRemoval,
572
733
  updateNote,
573
734
  updateTags,
574
735
  updateTagsBatch,
@@ -576,7 +737,9 @@ function KeepProviderContent({
576
737
  removeTagsBatch,
577
738
  removeItems,
578
739
  refreshItemMetadata,
579
- revalidateItems
740
+ revalidateItems,
741
+ exportBackup,
742
+ importBackup
580
743
  ]
581
744
  );
582
745
  const actions = useMemo(
@@ -589,6 +752,9 @@ function KeepProviderContent({
589
752
  removeTagsBatch,
590
753
  removeItem,
591
754
  removeItems,
755
+ removeItemWithUndo,
756
+ removeItemsWithUndo,
757
+ undoLastRemoval,
592
758
  clear,
593
759
  refresh,
594
760
  refreshItemMetadata,
@@ -606,7 +772,10 @@ function KeepProviderContent({
606
772
  updateTags,
607
773
  updateTagsBatch,
608
774
  refreshItemMetadata,
609
- revalidateItems
775
+ revalidateItems,
776
+ removeItemWithUndo,
777
+ removeItemsWithUndo,
778
+ undoLastRemoval
610
779
  ]
611
780
  );
612
781
  const storeAccess = useMemo(() => ({ store, actions }), [actions, store]);
@@ -617,6 +786,7 @@ var IDLE_SYNC_STATE = Object.freeze({
617
786
  pendingCount: 0,
618
787
  conflictIds: []
619
788
  });
789
+ var EMPTY_UNDO_STATE = Object.freeze({ canUndo: false, ids: [] });
620
790
  function isSyncCapableStorage(storage) {
621
791
  return "getSyncState" in storage && typeof storage.getSyncState === "function" && "subscribeSync" in storage && typeof storage.subscribeSync === "function" && "flushSync" in storage && typeof storage.flushSync === "function";
622
792
  }
@@ -689,6 +859,7 @@ function useKeepItem(input) {
689
859
  });
690
860
  }, [actions, input, item?.savedAt]);
691
861
  const remove = useCallback3(() => actions.removeItem(id), [actions, id]);
862
+ const removeWithUndo = useCallback3(() => actions.removeItemWithUndo(id), [actions, id]);
692
863
  const toggle = useCallback3(() => item ? remove() : save(), [item, remove, save]);
693
864
  const updateNote = useCallback3((note) => actions.updateNote(id, note), [actions, id]);
694
865
  const updateTags = useCallback3((tags) => actions.updateTags(id, tags), [actions, id]);
@@ -704,6 +875,8 @@ function useKeepItem(input) {
704
875
  error,
705
876
  save,
706
877
  remove,
878
+ removeWithUndo,
879
+ undo: actions.undoLastRemoval,
707
880
  toggle,
708
881
  updateNote,
709
882
  updateTags,
@@ -758,6 +931,8 @@ function useKeepList(query = {}) {
758
931
  const allTags = useKeepStoreSelector(store, tagsSelector);
759
932
  const remove = useCallback4((id) => actions.removeItem(id), [actions]);
760
933
  const removeBatch = useCallback4((ids) => actions.removeItems(ids), [actions]);
934
+ const removeWithUndo = useCallback4((id) => actions.removeItemWithUndo(id), [actions]);
935
+ const removeBatchWithUndo = useCallback4((ids) => actions.removeItemsWithUndo(ids), [actions]);
761
936
  const updateTagsBatch = useCallback4(
762
937
  (ids, nextTags) => actions.updateTagsBatch(ids, nextTags),
763
938
  [actions]
@@ -785,6 +960,8 @@ function useKeepList(query = {}) {
785
960
  error,
786
961
  remove,
787
962
  removeBatch,
963
+ removeWithUndo,
964
+ removeBatchWithUndo,
788
965
  updateTagsBatch,
789
966
  addTagsBatch,
790
967
  removeTagsBatch,