@sync-subscribe/client-react 0.3.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 fromkeith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # @sync-subscribe/client-react
2
+
3
+ React bindings for `@sync-subscribe/client`. Provides a context provider and hooks for subscribing to synced data, making mutations, and automatically replaying changes when the device comes back online.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @sync-subscribe/client-react @sync-subscribe/client @sync-subscribe/core
9
+ ```
10
+
11
+ React ≥ 18 is a peer dependency.
12
+
13
+ ## Quick start
14
+
15
+ ```tsx
16
+ import { SyncClient, IdbLocalStore, createFetchTransport } from "@sync-subscribe/client";
17
+ import { SyncProvider, useRecords, useMutate } from "@sync-subscribe/client-react";
18
+
19
+ // Create the client once at module level (outside components)
20
+ const client = new SyncClient(
21
+ createFetchTransport({ baseUrl: "/api" }),
22
+ new IdbLocalStore("my-app-db"), // persists across reloads; omit for in-memory
23
+ );
24
+
25
+ function App() {
26
+ return (
27
+ <SyncProvider client={client}>
28
+ <NotesList />
29
+ </SyncProvider>
30
+ );
31
+ }
32
+
33
+ function NotesList() {
34
+ const notes = useRecords<NoteRecord>({ filter: { isDeleted: false } });
35
+ const mutate = useMutate<NoteRecord>();
36
+
37
+ async function handleDelete(note: NoteRecord) {
38
+ await mutate({
39
+ ...note,
40
+ isDeleted: true,
41
+ updatedAt: Date.now(),
42
+ revisionCount: note.revisionCount + 1,
43
+ });
44
+ }
45
+
46
+ return (
47
+ <ul>
48
+ {notes.map((n) => (
49
+ <li key={n.recordId}>
50
+ {n.title}
51
+ <button onClick={() => handleDelete(n)}>Delete</button>
52
+ </li>
53
+ ))}
54
+ </ul>
55
+ );
56
+ }
57
+ ```
58
+
59
+ ## API
60
+
61
+ ### `<SyncProvider client={client}>`
62
+
63
+ Provides the `SyncClient` to all child hooks. Place it once near the root of your app.
64
+
65
+ The provider also manages the **offline mutation queue**. Mutations made while the device is offline are written to the local store immediately (read-your-own-writes) and pushed to the server automatically when connectivity is restored.
66
+
67
+ ```tsx
68
+ <SyncProvider client={client}>
69
+ <App />
70
+ </SyncProvider>
71
+ ```
72
+
73
+ ---
74
+
75
+ ### `useRecords<T>(options)`
76
+
77
+ Subscribes to a filtered view of records and returns them as a live array.
78
+
79
+ ```ts
80
+ const notes = useRecords<NoteRecord>({
81
+ filter: { isDeleted: false },
82
+ name: "active-notes", // optional — persists subscription state across sessions
83
+ });
84
+ ```
85
+
86
+ **What it does:**
87
+ - On mount: registers a subscription locally for `filter`, pulls the initial batch, and opens an SSE stream
88
+ - Re-renders on every incoming patch (from pull, stream, or conflict resolution)
89
+ - When `filter` changes: registers a new subscription, replacing the old one. Gap analysis runs locally to determine whether any records matching the new filter are not yet cached — if so they are fetched before joining the stream. Records that only the old filter needed are evicted.
90
+ - Filters the local store client-side with `matchesFilter`, so multiple overlapping `useRecords` calls with different filters work correctly
91
+
92
+ **Filter operators:**
93
+
94
+ ```ts
95
+ // Equality
96
+ useRecords({ filter: { color: "blue" } });
97
+
98
+ // Comparison operators
99
+ useRecords({ filter: { createdAt: { $gte: Date.now() - 30 * 24 * 60 * 60 * 1000 } } });
100
+
101
+ // Multiple conditions (all must match)
102
+ useRecords({ filter: { isDeleted: false, category: "work" } });
103
+
104
+ // $or
105
+ useRecords({ filter: { $or: [{ color: "blue" }, { color: "red" }] } });
106
+ ```
107
+
108
+ **Stable filter references:**
109
+
110
+ `useRecords` serialises the filter to detect changes, so passing an inline object literal is safe — it won't trigger unnecessary re-subscriptions:
111
+
112
+ ```tsx
113
+ // Fine — does NOT re-subscribe on every render
114
+ const notes = useRecords({ filter: { isDeleted: false } });
115
+ ```
116
+
117
+ ---
118
+
119
+ ### `useMutate<T>()`
120
+
121
+ Returns a `mutate` function scoped to the nearest `SyncProvider`.
122
+
123
+ ```ts
124
+ const mutate = useMutate<NoteRecord>();
125
+ ```
126
+
127
+ **`mutate(record): Promise<boolean>`**
128
+ - Writes the record to the local store immediately (read-your-own-writes)
129
+ - If **online**: pushes to the server; returns `true` on success, `false` on conflict (server record wins and is applied locally)
130
+ - If **offline**: queues the push and returns `true` optimistically; the push is retried when the device comes back online
131
+
132
+ Always increment `revisionCount` on every change:
133
+
134
+ ```ts
135
+ // Create
136
+ await mutate({
137
+ recordId: crypto.randomUUID(),
138
+ createdAt: Date.now(),
139
+ updatedAt: Date.now(),
140
+ revisionCount: 1,
141
+ title: "New note",
142
+ isDeleted: false,
143
+ });
144
+
145
+ // Update
146
+ await mutate({
147
+ ...existing,
148
+ title: "Edited title",
149
+ updatedAt: Date.now(),
150
+ revisionCount: existing.revisionCount + 1,
151
+ });
152
+
153
+ // Soft-delete
154
+ await mutate({
155
+ ...existing,
156
+ isDeleted: true,
157
+ updatedAt: Date.now(),
158
+ revisionCount: existing.revisionCount + 1,
159
+ });
160
+ ```
161
+
162
+ ---
163
+
164
+ ### `useSyncClient<T>()`
165
+
166
+ Escape hatch to access the raw `SyncClient` from context.
167
+
168
+ ```ts
169
+ const client = useSyncClient<NoteRecord>();
170
+ await client.reset(); // e.g. on logout
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Offline behaviour
176
+
177
+ `SyncProvider` listens to the browser's `online` event. When the device reconnects, it drains the pending queue in the order mutations were made, deduplicating by `recordId` (only the latest mutation per record is sent).
178
+
179
+ ```
180
+ offline → mutate(noteA v1) → mutate(noteA v2) → online
181
+
182
+ push(noteA v2) ← only latest sent
183
+ ```
184
+
185
+ If a push fails after reconnecting (e.g. the server is still unreachable), the record is re-queued and retried on the next `online` event.
186
+
187
+ ---
188
+
189
+ ## Multiple subscriptions
190
+
191
+ Multiple `useRecords` calls create independent subscriptions. The local store deduplicates records by `recordId`, so records matching more than one filter are stored only once. Each hook filters client-side to return its own view.
192
+
193
+ ```tsx
194
+ // Two subscriptions, no duplicates in the local store
195
+ const recentNotes = useRecords({ filter: { createdAt: { $gte: thirtyDaysAgo } } });
196
+ const blueNotes = useRecords({ filter: { color: "blue" } });
197
+ ```
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SyncRecord } from "@sync-subscribe/core";
3
+ import type { SyncClient } from "@sync-subscribe/client";
4
+ export interface SyncContextValue {
5
+ client: SyncClient<any>;
6
+ /**
7
+ * Enqueue a mutation. If the device is online the record is pushed to the
8
+ * server immediately via client.mutate(). If offline, the record is written
9
+ * to the local store for read-your-own-writes and queued; it will be pushed
10
+ * automatically when connectivity is restored.
11
+ */
12
+ enqueue: (record: SyncRecord) => Promise<boolean>;
13
+ }
14
+ export declare const SyncContext: import("react").Context<SyncContextValue | null>;
15
+ export interface SyncProviderProps {
16
+ client: SyncClient<any>;
17
+ children: ReactNode;
18
+ }
19
+ /**
20
+ * Provides a SyncClient to all child hooks.
21
+ * Place this once near the root of your app.
22
+ *
23
+ * The provider also manages the offline mutation queue:
24
+ * mutations made while the device is offline are held in memory and
25
+ * automatically replayed (in order, deduplicated by recordId) when
26
+ * connectivity resumes.
27
+ */
28
+ export declare function SyncProvider({ client, children }: SyncProviderProps): import("react/jsx-runtime").JSX.Element;
29
+ /**
30
+ * Returns the SyncClient from the nearest SyncProvider.
31
+ * The generic parameter lets you recover the typed client at the call site.
32
+ */
33
+ export declare function useSyncClient<T extends SyncRecord = SyncRecord>(): SyncClient<T>;
34
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAE/B,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IACxB;;;;;OAKG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACnD;AAED,eAAO,MAAM,WAAW,kDAA+C,CAAC;AAExE,MAAM,WAAW,iBAAiB;IAEhC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,iBAAiB,2CA4CnE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,CAAC,SAAS,UAAU,GAAG,UAAU,KAC9B,UAAU,CAAC,CAAC,CAAC,CAIjB"}
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from "react";
3
+ export const SyncContext = createContext(null);
4
+ /**
5
+ * Provides a SyncClient to all child hooks.
6
+ * Place this once near the root of your app.
7
+ *
8
+ * The provider also manages the offline mutation queue:
9
+ * mutations made while the device is offline are held in memory and
10
+ * automatically replayed (in order, deduplicated by recordId) when
11
+ * connectivity resumes.
12
+ */
13
+ export function SyncProvider({ client, children }) {
14
+ // Map<recordId, record> — only the latest mutation per record is kept.
15
+ const queue = useRef(new Map());
16
+ // Drain pending mutations when connectivity is restored.
17
+ useEffect(() => {
18
+ async function drain() {
19
+ if (queue.current.size === 0)
20
+ return;
21
+ const pending = [...queue.current.values()];
22
+ queue.current.clear();
23
+ for (const record of pending) {
24
+ await client.mutate(record).catch(() => {
25
+ // Push still failing — re-queue so we retry on the next online event.
26
+ queue.current.set(record.recordId, record);
27
+ });
28
+ }
29
+ }
30
+ const isWindow = typeof window !== "undefined";
31
+ if (isWindow)
32
+ window.addEventListener("online", drain);
33
+ return () => {
34
+ if (isWindow)
35
+ window.removeEventListener("online", drain);
36
+ };
37
+ }, [client]);
38
+ const enqueue = useCallback(async (record) => {
39
+ const online = typeof navigator === "undefined" ? true : navigator.onLine;
40
+ if (online) {
41
+ return client.mutate(record);
42
+ }
43
+ // Offline path: queue the raw record. mutate() will stamp on drain.
44
+ queue.current.set(record.recordId, record);
45
+ return true; // optimistic — push will happen when back online
46
+ }, [client]);
47
+ const value = useMemo(() => ({ client, enqueue }), [client, enqueue]);
48
+ return _jsx(SyncContext.Provider, { value: value, children: children });
49
+ }
50
+ /**
51
+ * Returns the SyncClient from the nearest SyncProvider.
52
+ * The generic parameter lets you recover the typed client at the call site.
53
+ */
54
+ export function useSyncClient() {
55
+ const ctx = useContext(SyncContext);
56
+ if (!ctx)
57
+ throw new Error("useSyncClient must be used inside <SyncProvider>");
58
+ return ctx.client;
59
+ }
60
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,OAAO,EACP,MAAM,GACP,MAAM,OAAO,CAAC;AAiBf,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAA0B,IAAI,CAAC,CAAC;AAQxE;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAqB;IAClE,uEAAuE;IACvE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,GAAG,EAAsB,CAAC,CAAC;IAEpD,yDAAyD;IACzD,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,UAAU,KAAK;YAClB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;gBAAE,OAAO;YACrC,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YAC5C,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACtB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;oBACrC,sEAAsE;oBACtE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;gBAC7C,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC;QAC/C,IAAI,QAAQ;YAAE,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QACvD,OAAO,GAAG,EAAE;YACV,IAAI,QAAQ;gBAAE,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAC5D,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,MAAM,OAAO,GAAG,WAAW,CACzB,KAAK,EAAE,MAAkB,EAAoB,EAAE;QAC7C,MAAM,MAAM,GACV,OAAO,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC;QAE7D,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC;QAED,oEAAoE;QACpE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,CAAC,iDAAiD;IAChE,CAAC,EACD,CAAC,MAAM,CAAC,CACT,CAAC;IAEF,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAEtE,OAAO,KAAC,WAAW,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAwB,CAAC;AAC/E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAG3B,MAAM,GAAG,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IAC9E,OAAO,GAAG,CAAC,MAAuB,CAAC;AACrC,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { SyncProvider, useSyncClient } from "./context.js";
2
+ export type { SyncProviderProps, SyncContextValue } from "./context.js";
3
+ export { useRecords } from "./useRecords.js";
4
+ export type { UseRecordsOptions } from "./useRecords.js";
5
+ export { useQuery } from "./useQuery.js";
6
+ export { useMutate } from "./useMutate.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC3D,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAExE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { SyncProvider, useSyncClient } from "./context.js";
2
+ export { useRecords } from "./useRecords.js";
3
+ export { useQuery } from "./useQuery.js";
4
+ export { useMutate } from "./useMutate.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { SyncRecord } from "@sync-subscribe/core";
2
+ /**
3
+ * Returns a `mutate` function that writes a record to the local store and
4
+ * pushes it to the server.
5
+ *
6
+ * `mutate` automatically stamps `updatedAt` and increments `revisionCount` —
7
+ * callers should not set those fields.
8
+ *
9
+ * When the device is **offline** the mutation is queued and replayed
10
+ * automatically by `SyncProvider` when connectivity is restored.
11
+ *
12
+ * Returns `true` on success (or when queued offline), `false` when the
13
+ * server rejected the push due to a conflict (server record wins).
14
+ *
15
+ * @example
16
+ * const mutate = useMutate<NoteRecord>();
17
+ *
18
+ * await mutate({ ...note, title: "Updated title" });
19
+ */
20
+ export declare function useMutate<T extends SyncRecord>(): (record: T) => Promise<boolean>;
21
+ //# sourceMappingURL=useMutate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMutate.d.ts","sourceRoot":"","sources":["../src/useMutate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,UAAU,KAAK,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CASjF"}
@@ -0,0 +1,28 @@
1
+ import { useCallback, useContext } from "react";
2
+ import { SyncContext } from "./context.js";
3
+ /**
4
+ * Returns a `mutate` function that writes a record to the local store and
5
+ * pushes it to the server.
6
+ *
7
+ * `mutate` automatically stamps `updatedAt` and increments `revisionCount` —
8
+ * callers should not set those fields.
9
+ *
10
+ * When the device is **offline** the mutation is queued and replayed
11
+ * automatically by `SyncProvider` when connectivity is restored.
12
+ *
13
+ * Returns `true` on success (or when queued offline), `false` when the
14
+ * server rejected the push due to a conflict (server record wins).
15
+ *
16
+ * @example
17
+ * const mutate = useMutate<NoteRecord>();
18
+ *
19
+ * await mutate({ ...note, title: "Updated title" });
20
+ */
21
+ export function useMutate() {
22
+ const ctx = useContext(SyncContext);
23
+ if (!ctx)
24
+ throw new Error("useMutate must be used inside <SyncProvider>");
25
+ const { enqueue } = ctx;
26
+ return useCallback((record) => enqueue(record), [enqueue]);
27
+ }
28
+ //# sourceMappingURL=useMutate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMutate.js","sourceRoot":"","sources":["../src/useMutate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAEhD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,SAAS;IACvB,MAAM,GAAG,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC1E,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC;IAExB,OAAO,WAAW,CAChB,CAAC,MAAS,EAAE,EAAE,CAAC,OAAO,CAAC,MAAoB,CAAC,EAC5C,CAAC,OAAO,CAAC,CACV,CAAC;AACJ,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { SyncRecord } from "@sync-subscribe/core";
2
+ import type { SyncQuery } from "@sync-subscribe/client";
3
+ /**
4
+ * Subscribes to a `SyncQuery` and returns its current `{ data, loading }` state.
5
+ *
6
+ * The `syncQuery` reference must be stable across renders — create it with
7
+ * `useMemo` or at module level, otherwise it will re-subscribe on every render.
8
+ *
9
+ * Works with both `client.query()` (local-store only) and `client.liveQuery()`
10
+ * (sync + query combined).
11
+ *
12
+ * @example
13
+ * // Local-only query (data already synced elsewhere)
14
+ * const q = useMemo(() => client.query({ filter: { isDeleted: false } }), [client]);
15
+ * const { data, loading } = useQuery(q);
16
+ *
17
+ * @example
18
+ * // Live query — manages its own sync subscription
19
+ * const q = useMemo(() => client.liveQuery({ filter: { isDeleted: false } }), [client]);
20
+ * const { data, loading } = useQuery(q);
21
+ */
22
+ export declare function useQuery<T extends SyncRecord>(syncQuery: SyncQuery<T>): {
23
+ data: T[];
24
+ loading: boolean;
25
+ };
26
+ //# sourceMappingURL=useQuery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useQuery.d.ts","sourceRoot":"","sources":["../src/useQuery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,UAAU,EAC3C,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GACtB;IAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAWjC"}
@@ -0,0 +1,31 @@
1
+ import { useEffect, useState } from "react";
2
+ /**
3
+ * Subscribes to a `SyncQuery` and returns its current `{ data, loading }` state.
4
+ *
5
+ * The `syncQuery` reference must be stable across renders — create it with
6
+ * `useMemo` or at module level, otherwise it will re-subscribe on every render.
7
+ *
8
+ * Works with both `client.query()` (local-store only) and `client.liveQuery()`
9
+ * (sync + query combined).
10
+ *
11
+ * @example
12
+ * // Local-only query (data already synced elsewhere)
13
+ * const q = useMemo(() => client.query({ filter: { isDeleted: false } }), [client]);
14
+ * const { data, loading } = useQuery(q);
15
+ *
16
+ * @example
17
+ * // Live query — manages its own sync subscription
18
+ * const q = useMemo(() => client.liveQuery({ filter: { isDeleted: false } }), [client]);
19
+ * const { data, loading } = useQuery(q);
20
+ */
21
+ export function useQuery(syncQuery) {
22
+ const [state, setState] = useState({
23
+ data: [],
24
+ loading: true,
25
+ });
26
+ useEffect(() => {
27
+ return syncQuery.subscribe((value) => setState(value));
28
+ }, [syncQuery]);
29
+ return state;
30
+ }
31
+ //# sourceMappingURL=useQuery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useQuery.js","sourceRoot":"","sources":["../src/useQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAI5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,QAAQ,CACtB,SAAuB;IAEvB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAkC;QAClE,IAAI,EAAE,EAAE;QACR,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,SAAS,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACzD,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { SyncRecord, SubscriptionFilter } from "@sync-subscribe/core";
2
+ export interface UseRecordsOptions<T extends SyncRecord = SyncRecord> {
3
+ filter: SubscriptionFilter<T>;
4
+ /**
5
+ * Stable name for this subscription. When provided, the subscription state
6
+ * is persisted to the local store and automatically restored on next startup,
7
+ * enabling incremental sync instead of a full re-fetch.
8
+ */
9
+ name?: string;
10
+ }
11
+ /**
12
+ * Sync-and-query shorthand. Registers a live sync subscription for `filter`
13
+ * and returns its records as `{ data, loading }`.
14
+ *
15
+ * `loading` is `true` until the first pull completes.
16
+ * The sync subscription is automatically removed when the component unmounts.
17
+ *
18
+ * For a narrower in-memory view over a broader background sync (e.g. showing
19
+ * 1 day of data while syncing 30 days), use `useQuery` with `client.query()`
20
+ * instead — it reads from the local store without registering a new subscription.
21
+ *
22
+ * @example
23
+ * const { data: notes, loading } = useRecords<NoteRecord>({
24
+ * filter: { isDeleted: false },
25
+ * name: "active-notes",
26
+ * });
27
+ */
28
+ export declare function useRecords<T extends SyncRecord>(options: UseRecordsOptions<T>): {
29
+ data: T[];
30
+ loading: boolean;
31
+ };
32
+ //# sourceMappingURL=useRecords.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRecords.d.ts","sourceRoot":"","sources":["../src/useRecords.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAI3E,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAClE,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAC9B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,UAAU,EAC7C,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC5B;IAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAejC"}
@@ -0,0 +1,33 @@
1
+ import { useMemo } from "react";
2
+ import { useSyncClient } from "./context.js";
3
+ import { useQuery } from "./useQuery.js";
4
+ /**
5
+ * Sync-and-query shorthand. Registers a live sync subscription for `filter`
6
+ * and returns its records as `{ data, loading }`.
7
+ *
8
+ * `loading` is `true` until the first pull completes.
9
+ * The sync subscription is automatically removed when the component unmounts.
10
+ *
11
+ * For a narrower in-memory view over a broader background sync (e.g. showing
12
+ * 1 day of data while syncing 30 days), use `useQuery` with `client.query()`
13
+ * instead — it reads from the local store without registering a new subscription.
14
+ *
15
+ * @example
16
+ * const { data: notes, loading } = useRecords<NoteRecord>({
17
+ * filter: { isDeleted: false },
18
+ * name: "active-notes",
19
+ * });
20
+ */
21
+ export function useRecords(options) {
22
+ const client = useSyncClient();
23
+ // Stringify the filter so object literals don't cause a new liveQuery on every render.
24
+ const filterKey = JSON.stringify(options.filter);
25
+ const liveQuery = useMemo(() => client.liveQuery({
26
+ filter: options.filter,
27
+ ...(options.name !== undefined && { name: options.name }),
28
+ }),
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ [client, filterKey, options.name]);
31
+ return useQuery(liveQuery);
32
+ }
33
+ //# sourceMappingURL=useRecords.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRecords.js","sourceRoot":"","sources":["../src/useRecords.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAEhC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAYzC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,UAAU,CACxB,OAA6B;IAE7B,MAAM,MAAM,GAAG,aAAa,EAAK,CAAC;IAClC,uFAAuF;IACvF,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAEjD,MAAM,SAAS,GAAG,OAAO,CACvB,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC;QACrB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;KAC1D,CAAC;IACF,uDAAuD;IACvD,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAClC,CAAC;IAEF,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC7B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@sync-subscribe/client-react",
3
+ "version": "0.3.2",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "license": "MIT",
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "import": "./dist/index.js",
19
+ "types": "./dist/index.d.ts"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@sync-subscribe/client": "0.3.2",
24
+ "@sync-subscribe/core": "0.3.2"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "@testing-library/react": "^16.0.0",
31
+ "@types/react": "^19.1.0",
32
+ "jsdom": "^26.0.0",
33
+ "typescript": "*",
34
+ "vitest": "*"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc --build",
38
+ "dev": "tsc --build --watch",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "vitest run",
41
+ "clean": "rm -rf dist *.tsbuildinfo"
42
+ }
43
+ }