@tanstack/react-db 0.1.36 → 0.1.38

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.
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const useLiveQuery = require("./useLiveQuery.cjs");
4
+ const usePacedMutations = require("./usePacedMutations.cjs");
4
5
  const useLiveInfiniteQuery = require("./useLiveInfiniteQuery.cjs");
5
6
  const db = require("@tanstack/db");
6
7
  exports.useLiveQuery = useLiveQuery.useLiveQuery;
8
+ exports.usePacedMutations = usePacedMutations.usePacedMutations;
7
9
  exports.useLiveInfiniteQuery = useLiveInfiniteQuery.useLiveInfiniteQuery;
8
10
  Object.defineProperty(exports, "createTransaction", {
9
11
  enumerable: true,
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;"}
@@ -1,4 +1,5 @@
1
1
  export * from './useLiveQuery.cjs';
2
+ export * from './usePacedMutations.cjs';
2
3
  export * from './useLiveInfiniteQuery.cjs';
3
4
  export * from '@tanstack/db';
4
5
  export type { Collection } from '@tanstack/db';
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const react = require("react");
4
+ const db = require("@tanstack/db");
5
+ function usePacedMutations(config) {
6
+ const onMutateRef = react.useRef(config.onMutate);
7
+ onMutateRef.current = config.onMutate;
8
+ const mutationFnRef = react.useRef(config.mutationFn);
9
+ mutationFnRef.current = config.mutationFn;
10
+ const stableOnMutate = react.useCallback((variables) => {
11
+ return onMutateRef.current(variables);
12
+ }, []);
13
+ const stableMutationFn = react.useCallback((params) => {
14
+ return mutationFnRef.current(params);
15
+ }, []);
16
+ const mutate = react.useMemo(() => {
17
+ return db.createPacedMutations({
18
+ ...config,
19
+ onMutate: stableOnMutate,
20
+ mutationFn: stableMutationFn
21
+ });
22
+ }, [
23
+ stableOnMutate,
24
+ stableMutationFn,
25
+ config.metadata,
26
+ // Serialize strategy to avoid recreating when object reference changes but values are same
27
+ JSON.stringify({
28
+ type: config.strategy._type,
29
+ options: config.strategy.options
30
+ })
31
+ ]);
32
+ const stableMutate = react.useCallback(mutate, [mutate]);
33
+ return stableMutate;
34
+ }
35
+ exports.usePacedMutations = usePacedMutations;
36
+ //# sourceMappingURL=usePacedMutations.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePacedMutations.cjs","sources":["../../src/usePacedMutations.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\"\nimport { createPacedMutations } from \"@tanstack/db\"\nimport type { PacedMutationsConfig, Transaction } from \"@tanstack/db\"\n\n/**\n * React hook for managing paced mutations with timing strategies.\n *\n * Provides optimistic mutations with pluggable strategies like debouncing,\n * queuing, or throttling. The optimistic updates are applied immediately via\n * `onMutate`, and the actual persistence is controlled by the strategy.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A mutate function that accepts variables and returns Transaction objects\n *\n * @example\n * ```tsx\n * // Debounced auto-save\n * function AutoSaveForm({ formId }: { formId: string }) {\n * const mutate = usePacedMutations<string>({\n * onMutate: (value) => {\n * // Apply optimistic update immediately\n * formCollection.update(formId, draft => {\n * draft.content = value\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * const handleChange = async (value: string) => {\n * const tx = mutate(value)\n *\n * // Optional: await persistence or handle errors\n * try {\n * await tx.isPersisted.promise\n * console.log('Saved!')\n * } catch (error) {\n * console.error('Save failed:', error)\n * }\n * }\n *\n * return <textarea onChange={e => handleChange(e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Throttled slider updates\n * function VolumeSlider() {\n * const mutate = usePacedMutations<number>({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => {\n * draft.value = volume\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n *\n * return <input type=\"range\" onChange={e => mutate(+e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Debounce with leading/trailing for color picker (persist first + final only)\n * function ColorPicker() {\n * const mutate = usePacedMutations<string>({\n * onMutate: (color) => {\n * themeCollection.update('primary', draft => {\n * draft.color = color\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateTheme(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })\n * })\n *\n * return (\n * <input\n * type=\"color\"\n * onChange={e => mutate(e.target.value)}\n * />\n * )\n * }\n * ```\n */\nexport function usePacedMutations<\n TVariables = unknown,\n T extends object = Record<string, unknown>,\n>(\n config: PacedMutationsConfig<TVariables, T>\n): (variables: TVariables) => Transaction<T> {\n // Keep refs to the latest callbacks so we can call them without recreating the instance\n const onMutateRef = useRef(config.onMutate)\n onMutateRef.current = config.onMutate\n\n const mutationFnRef = useRef(config.mutationFn)\n mutationFnRef.current = config.mutationFn\n\n // Create stable wrappers that always call the latest version\n const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {\n return onMutateRef.current(variables)\n }, [])\n\n const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {\n return mutationFnRef.current(params)\n }, [])\n\n // Create paced mutations instance with proper dependency tracking\n // Serialize strategy for stable comparison since strategy objects are recreated on each render\n const mutate = useMemo(() => {\n return createPacedMutations<TVariables, T>({\n ...config,\n onMutate: stableOnMutate,\n mutationFn: stableMutationFn,\n })\n }, [\n stableOnMutate,\n stableMutationFn,\n config.metadata,\n // Serialize strategy to avoid recreating when object reference changes but values are same\n JSON.stringify({\n type: config.strategy._type,\n options: config.strategy.options,\n }),\n ])\n\n // Return stable mutate callback\n const stableMutate = useCallback(mutate, [mutate])\n\n return stableMutate\n}\n"],"names":["useRef","useCallback","useMemo","createPacedMutations"],"mappings":";;;;AA4FO,SAAS,kBAId,QAC2C;AAE3C,QAAM,cAAcA,MAAAA,OAAO,OAAO,QAAQ;AAC1C,cAAY,UAAU,OAAO;AAE7B,QAAM,gBAAgBA,MAAAA,OAAO,OAAO,UAAU;AAC9C,gBAAc,UAAU,OAAO;AAG/B,QAAM,iBAAiBC,kBAAoC,CAAC,cAAc;AACxE,WAAO,YAAY,QAAQ,SAAS;AAAA,EACtC,GAAG,CAAA,CAAE;AAEL,QAAM,mBAAmBA,kBAAsC,CAAC,WAAW;AACzE,WAAO,cAAc,QAAQ,MAAM;AAAA,EACrC,GAAG,CAAA,CAAE;AAIL,QAAM,SAASC,MAAAA,QAAQ,MAAM;AAC3B,WAAOC,wBAAoC;AAAA,MACzC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,YAAY;AAAA,IAAA,CACb;AAAA,EACH,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,KAAK,UAAU;AAAA,MACb,MAAM,OAAO,SAAS;AAAA,MACtB,SAAS,OAAO,SAAS;AAAA,IAAA,CAC1B;AAAA,EAAA,CACF;AAGD,QAAM,eAAeF,MAAAA,YAAY,QAAQ,CAAC,MAAM,CAAC;AAEjD,SAAO;AACT;;"}
@@ -0,0 +1,90 @@
1
+ import { PacedMutationsConfig, Transaction } from '@tanstack/db';
2
+ /**
3
+ * React hook for managing paced mutations with timing strategies.
4
+ *
5
+ * Provides optimistic mutations with pluggable strategies like debouncing,
6
+ * queuing, or throttling. The optimistic updates are applied immediately via
7
+ * `onMutate`, and the actual persistence is controlled by the strategy.
8
+ *
9
+ * @param config - Configuration including onMutate, mutationFn and strategy
10
+ * @returns A mutate function that accepts variables and returns Transaction objects
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * // Debounced auto-save
15
+ * function AutoSaveForm({ formId }: { formId: string }) {
16
+ * const mutate = usePacedMutations<string>({
17
+ * onMutate: (value) => {
18
+ * // Apply optimistic update immediately
19
+ * formCollection.update(formId, draft => {
20
+ * draft.content = value
21
+ * })
22
+ * },
23
+ * mutationFn: async ({ transaction }) => {
24
+ * await api.save(transaction.mutations)
25
+ * },
26
+ * strategy: debounceStrategy({ wait: 500 })
27
+ * })
28
+ *
29
+ * const handleChange = async (value: string) => {
30
+ * const tx = mutate(value)
31
+ *
32
+ * // Optional: await persistence or handle errors
33
+ * try {
34
+ * await tx.isPersisted.promise
35
+ * console.log('Saved!')
36
+ * } catch (error) {
37
+ * console.error('Save failed:', error)
38
+ * }
39
+ * }
40
+ *
41
+ * return <textarea onChange={e => handleChange(e.target.value)} />
42
+ * }
43
+ * ```
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * // Throttled slider updates
48
+ * function VolumeSlider() {
49
+ * const mutate = usePacedMutations<number>({
50
+ * onMutate: (volume) => {
51
+ * settingsCollection.update('volume', draft => {
52
+ * draft.value = volume
53
+ * })
54
+ * },
55
+ * mutationFn: async ({ transaction }) => {
56
+ * await api.updateVolume(transaction.mutations)
57
+ * },
58
+ * strategy: throttleStrategy({ wait: 200 })
59
+ * })
60
+ *
61
+ * return <input type="range" onChange={e => mutate(+e.target.value)} />
62
+ * }
63
+ * ```
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * // Debounce with leading/trailing for color picker (persist first + final only)
68
+ * function ColorPicker() {
69
+ * const mutate = usePacedMutations<string>({
70
+ * onMutate: (color) => {
71
+ * themeCollection.update('primary', draft => {
72
+ * draft.color = color
73
+ * })
74
+ * },
75
+ * mutationFn: async ({ transaction }) => {
76
+ * await api.updateTheme(transaction.mutations)
77
+ * },
78
+ * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })
79
+ * })
80
+ *
81
+ * return (
82
+ * <input
83
+ * type="color"
84
+ * onChange={e => mutate(e.target.value)}
85
+ * />
86
+ * )
87
+ * }
88
+ * ```
89
+ */
90
+ export declare function usePacedMutations<TVariables = unknown, T extends object = Record<string, unknown>>(config: PacedMutationsConfig<TVariables, T>): (variables: TVariables) => Transaction<T>;
@@ -1,4 +1,5 @@
1
1
  export * from './useLiveQuery.js';
2
+ export * from './usePacedMutations.js';
2
3
  export * from './useLiveInfiniteQuery.js';
3
4
  export * from '@tanstack/db';
4
5
  export type { Collection } from '@tanstack/db';
package/dist/esm/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { useLiveQuery } from "./useLiveQuery.js";
2
+ import { usePacedMutations } from "./usePacedMutations.js";
2
3
  import { useLiveInfiniteQuery } from "./useLiveInfiniteQuery.js";
3
4
  export * from "@tanstack/db";
4
5
  import { createTransaction } from "@tanstack/db";
5
6
  export {
6
7
  createTransaction,
7
8
  useLiveInfiniteQuery,
8
- useLiveQuery
9
+ useLiveQuery,
10
+ usePacedMutations
9
11
  };
10
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}
@@ -0,0 +1,90 @@
1
+ import { PacedMutationsConfig, Transaction } from '@tanstack/db';
2
+ /**
3
+ * React hook for managing paced mutations with timing strategies.
4
+ *
5
+ * Provides optimistic mutations with pluggable strategies like debouncing,
6
+ * queuing, or throttling. The optimistic updates are applied immediately via
7
+ * `onMutate`, and the actual persistence is controlled by the strategy.
8
+ *
9
+ * @param config - Configuration including onMutate, mutationFn and strategy
10
+ * @returns A mutate function that accepts variables and returns Transaction objects
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * // Debounced auto-save
15
+ * function AutoSaveForm({ formId }: { formId: string }) {
16
+ * const mutate = usePacedMutations<string>({
17
+ * onMutate: (value) => {
18
+ * // Apply optimistic update immediately
19
+ * formCollection.update(formId, draft => {
20
+ * draft.content = value
21
+ * })
22
+ * },
23
+ * mutationFn: async ({ transaction }) => {
24
+ * await api.save(transaction.mutations)
25
+ * },
26
+ * strategy: debounceStrategy({ wait: 500 })
27
+ * })
28
+ *
29
+ * const handleChange = async (value: string) => {
30
+ * const tx = mutate(value)
31
+ *
32
+ * // Optional: await persistence or handle errors
33
+ * try {
34
+ * await tx.isPersisted.promise
35
+ * console.log('Saved!')
36
+ * } catch (error) {
37
+ * console.error('Save failed:', error)
38
+ * }
39
+ * }
40
+ *
41
+ * return <textarea onChange={e => handleChange(e.target.value)} />
42
+ * }
43
+ * ```
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * // Throttled slider updates
48
+ * function VolumeSlider() {
49
+ * const mutate = usePacedMutations<number>({
50
+ * onMutate: (volume) => {
51
+ * settingsCollection.update('volume', draft => {
52
+ * draft.value = volume
53
+ * })
54
+ * },
55
+ * mutationFn: async ({ transaction }) => {
56
+ * await api.updateVolume(transaction.mutations)
57
+ * },
58
+ * strategy: throttleStrategy({ wait: 200 })
59
+ * })
60
+ *
61
+ * return <input type="range" onChange={e => mutate(+e.target.value)} />
62
+ * }
63
+ * ```
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * // Debounce with leading/trailing for color picker (persist first + final only)
68
+ * function ColorPicker() {
69
+ * const mutate = usePacedMutations<string>({
70
+ * onMutate: (color) => {
71
+ * themeCollection.update('primary', draft => {
72
+ * draft.color = color
73
+ * })
74
+ * },
75
+ * mutationFn: async ({ transaction }) => {
76
+ * await api.updateTheme(transaction.mutations)
77
+ * },
78
+ * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })
79
+ * })
80
+ *
81
+ * return (
82
+ * <input
83
+ * type="color"
84
+ * onChange={e => mutate(e.target.value)}
85
+ * />
86
+ * )
87
+ * }
88
+ * ```
89
+ */
90
+ export declare function usePacedMutations<TVariables = unknown, T extends object = Record<string, unknown>>(config: PacedMutationsConfig<TVariables, T>): (variables: TVariables) => Transaction<T>;
@@ -0,0 +1,36 @@
1
+ import { useRef, useCallback, useMemo } from "react";
2
+ import { createPacedMutations } from "@tanstack/db";
3
+ function usePacedMutations(config) {
4
+ const onMutateRef = useRef(config.onMutate);
5
+ onMutateRef.current = config.onMutate;
6
+ const mutationFnRef = useRef(config.mutationFn);
7
+ mutationFnRef.current = config.mutationFn;
8
+ const stableOnMutate = useCallback((variables) => {
9
+ return onMutateRef.current(variables);
10
+ }, []);
11
+ const stableMutationFn = useCallback((params) => {
12
+ return mutationFnRef.current(params);
13
+ }, []);
14
+ const mutate = useMemo(() => {
15
+ return createPacedMutations({
16
+ ...config,
17
+ onMutate: stableOnMutate,
18
+ mutationFn: stableMutationFn
19
+ });
20
+ }, [
21
+ stableOnMutate,
22
+ stableMutationFn,
23
+ config.metadata,
24
+ // Serialize strategy to avoid recreating when object reference changes but values are same
25
+ JSON.stringify({
26
+ type: config.strategy._type,
27
+ options: config.strategy.options
28
+ })
29
+ ]);
30
+ const stableMutate = useCallback(mutate, [mutate]);
31
+ return stableMutate;
32
+ }
33
+ export {
34
+ usePacedMutations
35
+ };
36
+ //# sourceMappingURL=usePacedMutations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePacedMutations.js","sources":["../../src/usePacedMutations.ts"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\"\nimport { createPacedMutations } from \"@tanstack/db\"\nimport type { PacedMutationsConfig, Transaction } from \"@tanstack/db\"\n\n/**\n * React hook for managing paced mutations with timing strategies.\n *\n * Provides optimistic mutations with pluggable strategies like debouncing,\n * queuing, or throttling. The optimistic updates are applied immediately via\n * `onMutate`, and the actual persistence is controlled by the strategy.\n *\n * @param config - Configuration including onMutate, mutationFn and strategy\n * @returns A mutate function that accepts variables and returns Transaction objects\n *\n * @example\n * ```tsx\n * // Debounced auto-save\n * function AutoSaveForm({ formId }: { formId: string }) {\n * const mutate = usePacedMutations<string>({\n * onMutate: (value) => {\n * // Apply optimistic update immediately\n * formCollection.update(formId, draft => {\n * draft.content = value\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.save(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 500 })\n * })\n *\n * const handleChange = async (value: string) => {\n * const tx = mutate(value)\n *\n * // Optional: await persistence or handle errors\n * try {\n * await tx.isPersisted.promise\n * console.log('Saved!')\n * } catch (error) {\n * console.error('Save failed:', error)\n * }\n * }\n *\n * return <textarea onChange={e => handleChange(e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Throttled slider updates\n * function VolumeSlider() {\n * const mutate = usePacedMutations<number>({\n * onMutate: (volume) => {\n * settingsCollection.update('volume', draft => {\n * draft.value = volume\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateVolume(transaction.mutations)\n * },\n * strategy: throttleStrategy({ wait: 200 })\n * })\n *\n * return <input type=\"range\" onChange={e => mutate(+e.target.value)} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Debounce with leading/trailing for color picker (persist first + final only)\n * function ColorPicker() {\n * const mutate = usePacedMutations<string>({\n * onMutate: (color) => {\n * themeCollection.update('primary', draft => {\n * draft.color = color\n * })\n * },\n * mutationFn: async ({ transaction }) => {\n * await api.updateTheme(transaction.mutations)\n * },\n * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })\n * })\n *\n * return (\n * <input\n * type=\"color\"\n * onChange={e => mutate(e.target.value)}\n * />\n * )\n * }\n * ```\n */\nexport function usePacedMutations<\n TVariables = unknown,\n T extends object = Record<string, unknown>,\n>(\n config: PacedMutationsConfig<TVariables, T>\n): (variables: TVariables) => Transaction<T> {\n // Keep refs to the latest callbacks so we can call them without recreating the instance\n const onMutateRef = useRef(config.onMutate)\n onMutateRef.current = config.onMutate\n\n const mutationFnRef = useRef(config.mutationFn)\n mutationFnRef.current = config.mutationFn\n\n // Create stable wrappers that always call the latest version\n const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {\n return onMutateRef.current(variables)\n }, [])\n\n const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {\n return mutationFnRef.current(params)\n }, [])\n\n // Create paced mutations instance with proper dependency tracking\n // Serialize strategy for stable comparison since strategy objects are recreated on each render\n const mutate = useMemo(() => {\n return createPacedMutations<TVariables, T>({\n ...config,\n onMutate: stableOnMutate,\n mutationFn: stableMutationFn,\n })\n }, [\n stableOnMutate,\n stableMutationFn,\n config.metadata,\n // Serialize strategy to avoid recreating when object reference changes but values are same\n JSON.stringify({\n type: config.strategy._type,\n options: config.strategy.options,\n }),\n ])\n\n // Return stable mutate callback\n const stableMutate = useCallback(mutate, [mutate])\n\n return stableMutate\n}\n"],"names":[],"mappings":";;AA4FO,SAAS,kBAId,QAC2C;AAE3C,QAAM,cAAc,OAAO,OAAO,QAAQ;AAC1C,cAAY,UAAU,OAAO;AAE7B,QAAM,gBAAgB,OAAO,OAAO,UAAU;AAC9C,gBAAc,UAAU,OAAO;AAG/B,QAAM,iBAAiB,YAAoC,CAAC,cAAc;AACxE,WAAO,YAAY,QAAQ,SAAS;AAAA,EACtC,GAAG,CAAA,CAAE;AAEL,QAAM,mBAAmB,YAAsC,CAAC,WAAW;AACzE,WAAO,cAAc,QAAQ,MAAM;AAAA,EACrC,GAAG,CAAA,CAAE;AAIL,QAAM,SAAS,QAAQ,MAAM;AAC3B,WAAO,qBAAoC;AAAA,MACzC,GAAG;AAAA,MACH,UAAU;AAAA,MACV,YAAY;AAAA,IAAA,CACb;AAAA,EACH,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,IAEP,KAAK,UAAU;AAAA,MACb,MAAM,OAAO,SAAS;AAAA,MACtB,SAAS,OAAO,SAAS;AAAA,IAAA,CAC1B;AAAA,EAAA,CACF;AAGD,QAAM,eAAe,YAAY,QAAQ,CAAC,MAAM,CAAC;AAEjD,SAAO;AACT;"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/react-db",
3
3
  "description": "React integration for @tanstack/db",
4
- "version": "0.1.36",
4
+ "version": "0.1.38",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -17,10 +17,10 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "use-sync-external-store": "^1.6.0",
20
- "@tanstack/db": "0.4.14"
20
+ "@tanstack/db": "0.4.16"
21
21
  },
22
22
  "devDependencies": {
23
- "@electric-sql/client": "1.0.14",
23
+ "@electric-sql/client": "1.1.0",
24
24
  "@testing-library/react": "^16.3.0",
25
25
  "@types/react": "^19.2.2",
26
26
  "@types/react-dom": "^19.2.2",
@@ -56,6 +56,7 @@
56
56
  "types": "dist/esm/index.d.ts",
57
57
  "scripts": {
58
58
  "build": "vite build",
59
+ "build:minified": "vite build --minify",
59
60
  "dev": "vite build --watch",
60
61
  "test": "npx vitest --run",
61
62
  "lint": "eslint . --fix"
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Re-export all public APIs
2
2
  export * from "./useLiveQuery"
3
+ export * from "./usePacedMutations"
3
4
  export * from "./useLiveInfiniteQuery"
4
5
 
5
6
  // Re-export everything from @tanstack/db
@@ -0,0 +1,138 @@
1
+ import { useCallback, useMemo, useRef } from "react"
2
+ import { createPacedMutations } from "@tanstack/db"
3
+ import type { PacedMutationsConfig, Transaction } from "@tanstack/db"
4
+
5
+ /**
6
+ * React hook for managing paced mutations with timing strategies.
7
+ *
8
+ * Provides optimistic mutations with pluggable strategies like debouncing,
9
+ * queuing, or throttling. The optimistic updates are applied immediately via
10
+ * `onMutate`, and the actual persistence is controlled by the strategy.
11
+ *
12
+ * @param config - Configuration including onMutate, mutationFn and strategy
13
+ * @returns A mutate function that accepts variables and returns Transaction objects
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * // Debounced auto-save
18
+ * function AutoSaveForm({ formId }: { formId: string }) {
19
+ * const mutate = usePacedMutations<string>({
20
+ * onMutate: (value) => {
21
+ * // Apply optimistic update immediately
22
+ * formCollection.update(formId, draft => {
23
+ * draft.content = value
24
+ * })
25
+ * },
26
+ * mutationFn: async ({ transaction }) => {
27
+ * await api.save(transaction.mutations)
28
+ * },
29
+ * strategy: debounceStrategy({ wait: 500 })
30
+ * })
31
+ *
32
+ * const handleChange = async (value: string) => {
33
+ * const tx = mutate(value)
34
+ *
35
+ * // Optional: await persistence or handle errors
36
+ * try {
37
+ * await tx.isPersisted.promise
38
+ * console.log('Saved!')
39
+ * } catch (error) {
40
+ * console.error('Save failed:', error)
41
+ * }
42
+ * }
43
+ *
44
+ * return <textarea onChange={e => handleChange(e.target.value)} />
45
+ * }
46
+ * ```
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * // Throttled slider updates
51
+ * function VolumeSlider() {
52
+ * const mutate = usePacedMutations<number>({
53
+ * onMutate: (volume) => {
54
+ * settingsCollection.update('volume', draft => {
55
+ * draft.value = volume
56
+ * })
57
+ * },
58
+ * mutationFn: async ({ transaction }) => {
59
+ * await api.updateVolume(transaction.mutations)
60
+ * },
61
+ * strategy: throttleStrategy({ wait: 200 })
62
+ * })
63
+ *
64
+ * return <input type="range" onChange={e => mutate(+e.target.value)} />
65
+ * }
66
+ * ```
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * // Debounce with leading/trailing for color picker (persist first + final only)
71
+ * function ColorPicker() {
72
+ * const mutate = usePacedMutations<string>({
73
+ * onMutate: (color) => {
74
+ * themeCollection.update('primary', draft => {
75
+ * draft.color = color
76
+ * })
77
+ * },
78
+ * mutationFn: async ({ transaction }) => {
79
+ * await api.updateTheme(transaction.mutations)
80
+ * },
81
+ * strategy: debounceStrategy({ wait: 0, leading: true, trailing: true })
82
+ * })
83
+ *
84
+ * return (
85
+ * <input
86
+ * type="color"
87
+ * onChange={e => mutate(e.target.value)}
88
+ * />
89
+ * )
90
+ * }
91
+ * ```
92
+ */
93
+ export function usePacedMutations<
94
+ TVariables = unknown,
95
+ T extends object = Record<string, unknown>,
96
+ >(
97
+ config: PacedMutationsConfig<TVariables, T>
98
+ ): (variables: TVariables) => Transaction<T> {
99
+ // Keep refs to the latest callbacks so we can call them without recreating the instance
100
+ const onMutateRef = useRef(config.onMutate)
101
+ onMutateRef.current = config.onMutate
102
+
103
+ const mutationFnRef = useRef(config.mutationFn)
104
+ mutationFnRef.current = config.mutationFn
105
+
106
+ // Create stable wrappers that always call the latest version
107
+ const stableOnMutate = useCallback<typeof config.onMutate>((variables) => {
108
+ return onMutateRef.current(variables)
109
+ }, [])
110
+
111
+ const stableMutationFn = useCallback<typeof config.mutationFn>((params) => {
112
+ return mutationFnRef.current(params)
113
+ }, [])
114
+
115
+ // Create paced mutations instance with proper dependency tracking
116
+ // Serialize strategy for stable comparison since strategy objects are recreated on each render
117
+ const mutate = useMemo(() => {
118
+ return createPacedMutations<TVariables, T>({
119
+ ...config,
120
+ onMutate: stableOnMutate,
121
+ mutationFn: stableMutationFn,
122
+ })
123
+ }, [
124
+ stableOnMutate,
125
+ stableMutationFn,
126
+ config.metadata,
127
+ // Serialize strategy to avoid recreating when object reference changes but values are same
128
+ JSON.stringify({
129
+ type: config.strategy._type,
130
+ options: config.strategy.options,
131
+ }),
132
+ ])
133
+
134
+ // Return stable mutate callback
135
+ const stableMutate = useCallback(mutate, [mutate])
136
+
137
+ return stableMutate
138
+ }