@taladb/react 0.9.0 → 0.9.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/dist/index.d.mts CHANGED
@@ -118,4 +118,233 @@ interface FindOneResult<T> {
118
118
  */
119
119
  declare function useFindOne<T extends Document>(collection: Collection<T>, filter: Filter<T>): FindOneResult<T>;
120
120
 
121
- export { type FindOneResult, type FindResult, TalaDBProvider, type TalaDBProviderProps, useCollection, useFind, useFindOne, useTalaDB };
121
+ /** Resolved network configuration for one replicated slice. */
122
+ interface ResolvedReplicationConfig {
123
+ /** Base URL; `/push` and `/pull` are appended by {@link HttpSyncAdapter}. */
124
+ endpoint: string;
125
+ /**
126
+ * Async (or sync) resolver for per-request headers — typically the
127
+ * `Authorization` bearer. Called **once per pass, at send time**, so a token
128
+ * that refreshed while a write sat in the local database is picked up when the
129
+ * write finally flushes.
130
+ */
131
+ getAuth?: () => Promise<Record<string, string>> | Record<string, string>;
132
+ /** `fetch` implementation. Defaults to the global `fetch`. */
133
+ fetch?: typeof fetch;
134
+ /** Override the `/push` and `/pull` sub-paths to match an existing API. */
135
+ paths?: {
136
+ push?: string;
137
+ pull?: string;
138
+ };
139
+ }
140
+
141
+ /** A slice to warm on first run — a collection, optionally on a specific endpoint. */
142
+ type PrefetchSlice = {
143
+ collection: string;
144
+ endpoint?: string;
145
+ };
146
+ /** A prefetch entry: a collection name (shorthand) or a {@link PrefetchSlice}. */
147
+ type PrefetchEntry = string | PrefetchSlice;
148
+ /** `'once'` warms a slice only if it has never synced; `'always'` on every mount. */
149
+ type PrefetchMode = 'once' | 'always';
150
+ /**
151
+ * Replication settings shared by `useQuery` / `useMutation`, supplied once by
152
+ * `<ReplicationProvider>` and overridable per hook.
153
+ *
154
+ * The origin is *your* API — never a database credential. It authorizes the
155
+ * session token from {@link ReplicationConfig.getAuth} and returns only that
156
+ * user's slice, so the auth header doubles as the per-user scope.
157
+ */
158
+ interface ReplicationConfig {
159
+ /** Base sync URL, e.g. `/api/sync`. `/push` and `/pull` are appended. */
160
+ endpoint: string;
161
+ /**
162
+ * Per-request header resolver — typically `{ Authorization: 'Bearer …' }`.
163
+ * Async so it can await a token refresh. Resolved at **send time**, once per
164
+ * pass, so an offline write flushed later carries a current token.
165
+ */
166
+ getAuth?: () => Promise<Record<string, string>> | Record<string, string>;
167
+ /** `fetch` implementation. Defaults to the global `fetch`. */
168
+ fetch?: typeof fetch;
169
+ /** Override the `/push` and `/pull` sub-paths to match an existing API. */
170
+ paths?: {
171
+ push?: string;
172
+ pull?: string;
173
+ };
174
+ /**
175
+ * Default background refresh interval (ms) for `useQuery`. A replication
176
+ * *interval*, not a cache TTL — the local data is never evicted, only
177
+ * refreshed. Omit or set `0` to disable polling by default; a hook can still
178
+ * opt in per query. `30_000` matches the guide's own example cadence.
179
+ */
180
+ pollMs?: number;
181
+ /**
182
+ * Slices to warm into the local replica in the background on first run, so a
183
+ * later `useQuery` for that collection reads local instead of waiting on the
184
+ * network. Best-effort and non-blocking: deferred to browser idle, run in the
185
+ * sync Worker on web, and silently skipped on failure. Each entry is a
186
+ * collection name or a {@link PrefetchSlice}.
187
+ */
188
+ prefetch?: PrefetchEntry[];
189
+ /** How prefetch decides to warm a slice. Default `'once'`. */
190
+ prefetchMode?: PrefetchMode;
191
+ /** Max concurrent prefetch pulls — keeps the active page from starving. Default `2`. */
192
+ prefetchConcurrency?: number;
193
+ }
194
+ interface ReplicationProviderProps extends ReplicationConfig {
195
+ children: ReactNode;
196
+ }
197
+ /**
198
+ * Supplies replication defaults (endpoint, auth, poll interval, prefetch) to the
199
+ * `useQuery` / `useMutation` hooks below it. Compose it inside a
200
+ * `<TalaDBProvider>`:
201
+ *
202
+ * ```tsx
203
+ * <TalaDBProvider name="app.db" fallback={<Splash />}>
204
+ * <ReplicationProvider
205
+ * endpoint="/api/sync"
206
+ * getAuth={async () => ({ Authorization: `Bearer ${await session.token()}` })}
207
+ * pollMs={30_000}
208
+ * prefetch={['products', 'categories']}
209
+ * >
210
+ * <App />
211
+ * </ReplicationProvider>
212
+ * </TalaDBProvider>
213
+ * ```
214
+ */
215
+ declare function ReplicationProvider({ children, ...config }: ReplicationProviderProps): react_jsx_runtime.JSX.Element;
216
+ /**
217
+ * Read the nearest replication config, merged with per-hook overrides.
218
+ * Non-throwing: `config` is `null` when no endpoint is resolvable (valid for
219
+ * `source: 'local-only'`); the caller decides whether that's an error.
220
+ */
221
+ declare function useReplicationConfig(overrides?: Partial<ReplicationConfig>): {
222
+ config: ResolvedReplicationConfig | null;
223
+ pollMs: number;
224
+ };
225
+
226
+ /**
227
+ * How a `useQuery` combines the local replica with the remote origin.
228
+ * *(`remote-only` is a planned addition; the durable-replica model makes it a
229
+ * rare escape hatch.)*
230
+ */
231
+ type ReadSource = 'local-first' | 'remote-first' | 'local-only';
232
+ interface UseQueryOptions<T extends Document> extends Partial<Pick<ReplicationConfig, 'endpoint' | 'getAuth' | 'fetch' | 'paths' | 'pollMs'>> {
233
+ /** Collection name — replicated as a whole; the `filter` narrows locally. */
234
+ collection: string;
235
+ /** Live filter over the local collection. Inline objects are safe. */
236
+ filter?: Filter<T>;
237
+ /**
238
+ * - `local-first` *(default)* — serve local immediately; refresh in the
239
+ * background, and the live query re-renders when the pull lands.
240
+ * - `remote-first` — stay `loading` until the first pull completes, then serve.
241
+ * - `local-only` — never touch the network (no endpoint required).
242
+ */
243
+ source?: ReadSource;
244
+ }
245
+ interface QueryResult<T> {
246
+ /** Current matching documents from the local replica. Reactive. */
247
+ data: T[];
248
+ /** First local snapshot pending — plus the first pull, for `remote-first`. */
249
+ loading: boolean;
250
+ /** Most recent local-read error. */
251
+ error: unknown | null;
252
+ /** A background replication pass is in flight. */
253
+ syncing: boolean;
254
+ /** Most recent replication error (the local data is still served). */
255
+ syncError: unknown | null;
256
+ /** Trigger a pull now. No-op for `local-only`. */
257
+ refetch: () => Promise<void>;
258
+ }
259
+ /**
260
+ * Bind a component to a slice of a remote origin, backed by the local replica.
261
+ *
262
+ * The read is a live query over the local collection (`useFind`); the network
263
+ * pull writes into that same collection, so the live query re-renders on its
264
+ * own — one-way data flow, no `queryKey`, no `invalidateQueries`. See
265
+ * `docs/scoped-replication.md`.
266
+ *
267
+ * @example
268
+ * const { data, loading, syncing } = useQuery<Product>({
269
+ * collection: 'products',
270
+ * filter: { category: 'kitchen' },
271
+ * pollMs: 30_000,
272
+ * })
273
+ */
274
+ declare function useQuery<T extends Document>(options: UseQueryOptions<T>): QueryResult<T>;
275
+
276
+ /**
277
+ * Run several scoped queries at once — one replication + live query per entry,
278
+ * in parallel. The result array is index-aligned with `queries`.
279
+ *
280
+ * Each entry is an independent `useQuery`: its own collection, filter, source,
281
+ * and (optionally) endpoint. This is the multi-slice page case — e.g. a
282
+ * dashboard that needs `orders` and `products` from the same origin. There are
283
+ * no cross-collection joins (TalaDB is a document store); compose in the
284
+ * component.
285
+ *
286
+ * v1 note: entries are typed as `Document`. For a strictly-typed single slice
287
+ * use `useQuery<T>`; per-entry generics (tuple typing) are a follow-up.
288
+ *
289
+ * @example
290
+ * const [orders, products] = useQueries([
291
+ * { collection: 'orders', filter: { open: true } },
292
+ * { collection: 'products' },
293
+ * ])
294
+ */
295
+ declare function useQueries(queries: UseQueryOptions<Document>[]): QueryResult<Document>[];
296
+
297
+ /** A single write intent against the local replica. Discriminated on `type`. */
298
+ type WriteOp<T extends Document> = {
299
+ type: 'insert';
300
+ doc: Omit<T, '_id'>;
301
+ } | {
302
+ type: 'update';
303
+ where: Filter<T>;
304
+ set: Partial<Omit<T, '_id'>>;
305
+ } | {
306
+ type: 'delete';
307
+ where: Filter<T>;
308
+ };
309
+ interface UseMutationOptions extends Partial<Pick<ReplicationConfig, 'endpoint' | 'getAuth' | 'fetch' | 'paths'>> {
310
+ /** Collection the write targets. */
311
+ collection: string;
312
+ /**
313
+ * Replication direction for the drain. `push` *(default)* sends the write;
314
+ * read hooks reconcile the authoritative value on their next pull. `both`
315
+ * also pulls the authoritative echo inline (heavier — replays the collection).
316
+ */
317
+ direction?: 'push' | 'both';
318
+ /**
319
+ * On mount, attempt to flush any local writes left unsent from a previous
320
+ * (offline) session for this collection. Default `true`.
321
+ */
322
+ drainOnMount?: boolean;
323
+ }
324
+ interface MutationResult<T extends Document> {
325
+ /** Fire-and-forget write. Errors surface on `error`, never thrown to render. */
326
+ mutate: (op: WriteOp<T>) => void;
327
+ /** Awaitable write. Resolves once the local write and drain settle; rejects on error. */
328
+ mutateAsync: (op: WriteOp<T>) => Promise<void>;
329
+ /** A write (local + drain) is in flight. */
330
+ pending: boolean;
331
+ /** Most recent write/drain error. The local write is durable regardless. */
332
+ error: unknown | null;
333
+ }
334
+ /**
335
+ * Local-first write hook. A mutation writes the local replica **first**
336
+ * (immediate, durable, reactive — every `useQuery`/`useFind` on the collection
337
+ * re-renders) and then replicates the change outward over the sync-contract with
338
+ * bounded retry. The network step never rolls the local write back: it is
339
+ * already committed, and a later drain still delivers it (write-behind).
340
+ *
341
+ * Write-authority is origin-authoritative by default — the push sends the
342
+ * change and the server is the arbiter; read hooks pull the authoritative value.
343
+ *
344
+ * @example
345
+ * const { mutate, pending } = useMutation<Order>({ collection: 'orders' })
346
+ * mutate({ type: 'update', where: { _id }, set: { status: 'shipped' } })
347
+ */
348
+ declare function useMutation<T extends Document>(options: UseMutationOptions): MutationResult<T>;
349
+
350
+ export { type FindOneResult, type FindResult, type MutationResult, type PrefetchEntry, type PrefetchMode, type PrefetchSlice, type QueryResult, type ReadSource, type ReplicationConfig, ReplicationProvider, type ReplicationProviderProps, TalaDBProvider, type TalaDBProviderProps, type UseMutationOptions, type UseQueryOptions, type WriteOp, useCollection, useFind, useFindOne, useMutation, useQueries, useQuery, useReplicationConfig, useTalaDB };
package/dist/index.d.ts CHANGED
@@ -118,4 +118,233 @@ interface FindOneResult<T> {
118
118
  */
119
119
  declare function useFindOne<T extends Document>(collection: Collection<T>, filter: Filter<T>): FindOneResult<T>;
120
120
 
121
- export { type FindOneResult, type FindResult, TalaDBProvider, type TalaDBProviderProps, useCollection, useFind, useFindOne, useTalaDB };
121
+ /** Resolved network configuration for one replicated slice. */
122
+ interface ResolvedReplicationConfig {
123
+ /** Base URL; `/push` and `/pull` are appended by {@link HttpSyncAdapter}. */
124
+ endpoint: string;
125
+ /**
126
+ * Async (or sync) resolver for per-request headers — typically the
127
+ * `Authorization` bearer. Called **once per pass, at send time**, so a token
128
+ * that refreshed while a write sat in the local database is picked up when the
129
+ * write finally flushes.
130
+ */
131
+ getAuth?: () => Promise<Record<string, string>> | Record<string, string>;
132
+ /** `fetch` implementation. Defaults to the global `fetch`. */
133
+ fetch?: typeof fetch;
134
+ /** Override the `/push` and `/pull` sub-paths to match an existing API. */
135
+ paths?: {
136
+ push?: string;
137
+ pull?: string;
138
+ };
139
+ }
140
+
141
+ /** A slice to warm on first run — a collection, optionally on a specific endpoint. */
142
+ type PrefetchSlice = {
143
+ collection: string;
144
+ endpoint?: string;
145
+ };
146
+ /** A prefetch entry: a collection name (shorthand) or a {@link PrefetchSlice}. */
147
+ type PrefetchEntry = string | PrefetchSlice;
148
+ /** `'once'` warms a slice only if it has never synced; `'always'` on every mount. */
149
+ type PrefetchMode = 'once' | 'always';
150
+ /**
151
+ * Replication settings shared by `useQuery` / `useMutation`, supplied once by
152
+ * `<ReplicationProvider>` and overridable per hook.
153
+ *
154
+ * The origin is *your* API — never a database credential. It authorizes the
155
+ * session token from {@link ReplicationConfig.getAuth} and returns only that
156
+ * user's slice, so the auth header doubles as the per-user scope.
157
+ */
158
+ interface ReplicationConfig {
159
+ /** Base sync URL, e.g. `/api/sync`. `/push` and `/pull` are appended. */
160
+ endpoint: string;
161
+ /**
162
+ * Per-request header resolver — typically `{ Authorization: 'Bearer …' }`.
163
+ * Async so it can await a token refresh. Resolved at **send time**, once per
164
+ * pass, so an offline write flushed later carries a current token.
165
+ */
166
+ getAuth?: () => Promise<Record<string, string>> | Record<string, string>;
167
+ /** `fetch` implementation. Defaults to the global `fetch`. */
168
+ fetch?: typeof fetch;
169
+ /** Override the `/push` and `/pull` sub-paths to match an existing API. */
170
+ paths?: {
171
+ push?: string;
172
+ pull?: string;
173
+ };
174
+ /**
175
+ * Default background refresh interval (ms) for `useQuery`. A replication
176
+ * *interval*, not a cache TTL — the local data is never evicted, only
177
+ * refreshed. Omit or set `0` to disable polling by default; a hook can still
178
+ * opt in per query. `30_000` matches the guide's own example cadence.
179
+ */
180
+ pollMs?: number;
181
+ /**
182
+ * Slices to warm into the local replica in the background on first run, so a
183
+ * later `useQuery` for that collection reads local instead of waiting on the
184
+ * network. Best-effort and non-blocking: deferred to browser idle, run in the
185
+ * sync Worker on web, and silently skipped on failure. Each entry is a
186
+ * collection name or a {@link PrefetchSlice}.
187
+ */
188
+ prefetch?: PrefetchEntry[];
189
+ /** How prefetch decides to warm a slice. Default `'once'`. */
190
+ prefetchMode?: PrefetchMode;
191
+ /** Max concurrent prefetch pulls — keeps the active page from starving. Default `2`. */
192
+ prefetchConcurrency?: number;
193
+ }
194
+ interface ReplicationProviderProps extends ReplicationConfig {
195
+ children: ReactNode;
196
+ }
197
+ /**
198
+ * Supplies replication defaults (endpoint, auth, poll interval, prefetch) to the
199
+ * `useQuery` / `useMutation` hooks below it. Compose it inside a
200
+ * `<TalaDBProvider>`:
201
+ *
202
+ * ```tsx
203
+ * <TalaDBProvider name="app.db" fallback={<Splash />}>
204
+ * <ReplicationProvider
205
+ * endpoint="/api/sync"
206
+ * getAuth={async () => ({ Authorization: `Bearer ${await session.token()}` })}
207
+ * pollMs={30_000}
208
+ * prefetch={['products', 'categories']}
209
+ * >
210
+ * <App />
211
+ * </ReplicationProvider>
212
+ * </TalaDBProvider>
213
+ * ```
214
+ */
215
+ declare function ReplicationProvider({ children, ...config }: ReplicationProviderProps): react_jsx_runtime.JSX.Element;
216
+ /**
217
+ * Read the nearest replication config, merged with per-hook overrides.
218
+ * Non-throwing: `config` is `null` when no endpoint is resolvable (valid for
219
+ * `source: 'local-only'`); the caller decides whether that's an error.
220
+ */
221
+ declare function useReplicationConfig(overrides?: Partial<ReplicationConfig>): {
222
+ config: ResolvedReplicationConfig | null;
223
+ pollMs: number;
224
+ };
225
+
226
+ /**
227
+ * How a `useQuery` combines the local replica with the remote origin.
228
+ * *(`remote-only` is a planned addition; the durable-replica model makes it a
229
+ * rare escape hatch.)*
230
+ */
231
+ type ReadSource = 'local-first' | 'remote-first' | 'local-only';
232
+ interface UseQueryOptions<T extends Document> extends Partial<Pick<ReplicationConfig, 'endpoint' | 'getAuth' | 'fetch' | 'paths' | 'pollMs'>> {
233
+ /** Collection name — replicated as a whole; the `filter` narrows locally. */
234
+ collection: string;
235
+ /** Live filter over the local collection. Inline objects are safe. */
236
+ filter?: Filter<T>;
237
+ /**
238
+ * - `local-first` *(default)* — serve local immediately; refresh in the
239
+ * background, and the live query re-renders when the pull lands.
240
+ * - `remote-first` — stay `loading` until the first pull completes, then serve.
241
+ * - `local-only` — never touch the network (no endpoint required).
242
+ */
243
+ source?: ReadSource;
244
+ }
245
+ interface QueryResult<T> {
246
+ /** Current matching documents from the local replica. Reactive. */
247
+ data: T[];
248
+ /** First local snapshot pending — plus the first pull, for `remote-first`. */
249
+ loading: boolean;
250
+ /** Most recent local-read error. */
251
+ error: unknown | null;
252
+ /** A background replication pass is in flight. */
253
+ syncing: boolean;
254
+ /** Most recent replication error (the local data is still served). */
255
+ syncError: unknown | null;
256
+ /** Trigger a pull now. No-op for `local-only`. */
257
+ refetch: () => Promise<void>;
258
+ }
259
+ /**
260
+ * Bind a component to a slice of a remote origin, backed by the local replica.
261
+ *
262
+ * The read is a live query over the local collection (`useFind`); the network
263
+ * pull writes into that same collection, so the live query re-renders on its
264
+ * own — one-way data flow, no `queryKey`, no `invalidateQueries`. See
265
+ * `docs/scoped-replication.md`.
266
+ *
267
+ * @example
268
+ * const { data, loading, syncing } = useQuery<Product>({
269
+ * collection: 'products',
270
+ * filter: { category: 'kitchen' },
271
+ * pollMs: 30_000,
272
+ * })
273
+ */
274
+ declare function useQuery<T extends Document>(options: UseQueryOptions<T>): QueryResult<T>;
275
+
276
+ /**
277
+ * Run several scoped queries at once — one replication + live query per entry,
278
+ * in parallel. The result array is index-aligned with `queries`.
279
+ *
280
+ * Each entry is an independent `useQuery`: its own collection, filter, source,
281
+ * and (optionally) endpoint. This is the multi-slice page case — e.g. a
282
+ * dashboard that needs `orders` and `products` from the same origin. There are
283
+ * no cross-collection joins (TalaDB is a document store); compose in the
284
+ * component.
285
+ *
286
+ * v1 note: entries are typed as `Document`. For a strictly-typed single slice
287
+ * use `useQuery<T>`; per-entry generics (tuple typing) are a follow-up.
288
+ *
289
+ * @example
290
+ * const [orders, products] = useQueries([
291
+ * { collection: 'orders', filter: { open: true } },
292
+ * { collection: 'products' },
293
+ * ])
294
+ */
295
+ declare function useQueries(queries: UseQueryOptions<Document>[]): QueryResult<Document>[];
296
+
297
+ /** A single write intent against the local replica. Discriminated on `type`. */
298
+ type WriteOp<T extends Document> = {
299
+ type: 'insert';
300
+ doc: Omit<T, '_id'>;
301
+ } | {
302
+ type: 'update';
303
+ where: Filter<T>;
304
+ set: Partial<Omit<T, '_id'>>;
305
+ } | {
306
+ type: 'delete';
307
+ where: Filter<T>;
308
+ };
309
+ interface UseMutationOptions extends Partial<Pick<ReplicationConfig, 'endpoint' | 'getAuth' | 'fetch' | 'paths'>> {
310
+ /** Collection the write targets. */
311
+ collection: string;
312
+ /**
313
+ * Replication direction for the drain. `push` *(default)* sends the write;
314
+ * read hooks reconcile the authoritative value on their next pull. `both`
315
+ * also pulls the authoritative echo inline (heavier — replays the collection).
316
+ */
317
+ direction?: 'push' | 'both';
318
+ /**
319
+ * On mount, attempt to flush any local writes left unsent from a previous
320
+ * (offline) session for this collection. Default `true`.
321
+ */
322
+ drainOnMount?: boolean;
323
+ }
324
+ interface MutationResult<T extends Document> {
325
+ /** Fire-and-forget write. Errors surface on `error`, never thrown to render. */
326
+ mutate: (op: WriteOp<T>) => void;
327
+ /** Awaitable write. Resolves once the local write and drain settle; rejects on error. */
328
+ mutateAsync: (op: WriteOp<T>) => Promise<void>;
329
+ /** A write (local + drain) is in flight. */
330
+ pending: boolean;
331
+ /** Most recent write/drain error. The local write is durable regardless. */
332
+ error: unknown | null;
333
+ }
334
+ /**
335
+ * Local-first write hook. A mutation writes the local replica **first**
336
+ * (immediate, durable, reactive — every `useQuery`/`useFind` on the collection
337
+ * re-renders) and then replicates the change outward over the sync-contract with
338
+ * bounded retry. The network step never rolls the local write back: it is
339
+ * already committed, and a later drain still delivers it (write-behind).
340
+ *
341
+ * Write-authority is origin-authoritative by default — the push sends the
342
+ * change and the server is the arbiter; read hooks pull the authoritative value.
343
+ *
344
+ * @example
345
+ * const { mutate, pending } = useMutation<Order>({ collection: 'orders' })
346
+ * mutate({ type: 'update', where: { _id }, set: { status: 'shipped' } })
347
+ */
348
+ declare function useMutation<T extends Document>(options: UseMutationOptions): MutationResult<T>;
349
+
350
+ export { type FindOneResult, type FindResult, type MutationResult, type PrefetchEntry, type PrefetchMode, type PrefetchSlice, type QueryResult, type ReadSource, type ReplicationConfig, ReplicationProvider, type ReplicationProviderProps, TalaDBProvider, type TalaDBProviderProps, type UseMutationOptions, type UseQueryOptions, type WriteOp, useCollection, useFind, useFindOne, useMutation, useQueries, useQuery, useReplicationConfig, useTalaDB };
package/dist/index.js CHANGED
@@ -31,10 +31,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/index.ts
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
+ ReplicationProvider: () => ReplicationProvider,
34
35
  TalaDBProvider: () => TalaDBProvider,
35
36
  useCollection: () => useCollection,
36
37
  useFind: () => useFind,
37
38
  useFindOne: () => useFindOne,
39
+ useMutation: () => useMutation,
40
+ useQueries: () => useQueries,
41
+ useQuery: () => useQuery,
42
+ useReplicationConfig: () => useReplicationConfig,
38
43
  useTalaDB: () => useTalaDB
39
44
  });
40
45
  module.exports = __toCommonJS(index_exports);
@@ -143,11 +148,404 @@ function useFindOne(collection, filter) {
143
148
  const getSnapshot = (0, import_react4.useCallback)(() => snapshotRef.current, []);
144
149
  return (0, import_react4.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
145
150
  }
151
+
152
+ // src/replication/config.tsx
153
+ var import_react5 = require("react");
154
+
155
+ // src/replication/engine.ts
156
+ var import_taladb = require("taladb");
157
+ function replicationTarget(endpoint, collection) {
158
+ return `${endpoint}::${collection}`;
159
+ }
160
+ async function buildAdapter(config) {
161
+ const headers = config.getAuth ? await config.getAuth() : void 0;
162
+ return new import_taladb.HttpSyncAdapter({
163
+ endpoint: config.endpoint,
164
+ headers,
165
+ fetch: config.fetch,
166
+ paths: config.paths
167
+ });
168
+ }
169
+ var inflight = /* @__PURE__ */ new Map();
170
+ function inflightKey(endpoint, collection, direction) {
171
+ return `${endpoint}::${collection}::${direction}`;
172
+ }
173
+ function replicate(db, config, collection, direction) {
174
+ const key = inflightKey(config.endpoint, collection, direction);
175
+ const existing = inflight.get(key);
176
+ if (existing) return existing;
177
+ const pass = (async () => {
178
+ const adapter = await buildAdapter(config);
179
+ await db.sync(adapter, {
180
+ collections: [collection],
181
+ direction,
182
+ target: replicationTarget(config.endpoint, collection)
183
+ });
184
+ })().finally(() => {
185
+ inflight.delete(key);
186
+ });
187
+ inflight.set(key, pass);
188
+ return pass;
189
+ }
190
+ var BACKOFFS_MS = [200, 400, 800];
191
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
192
+ async function replicateWithRetry(db, config, collection, direction) {
193
+ let lastError;
194
+ for (let attempt = 0; attempt <= BACKOFFS_MS.length; attempt++) {
195
+ try {
196
+ await replicate(db, config, collection, direction);
197
+ return;
198
+ } catch (error) {
199
+ lastError = error;
200
+ if (attempt < BACKOFFS_MS.length) await sleep(BACKOFFS_MS[attempt]);
201
+ }
202
+ }
203
+ throw lastError;
204
+ }
205
+
206
+ // src/replication/config.tsx
207
+ var import_jsx_runtime2 = require("react/jsx-runtime");
208
+ var ReplicationContext = (0, import_react5.createContext)(null);
209
+ function ReplicationProvider({ children, ...config }) {
210
+ const key = `${config.endpoint}|${config.pollMs ?? ""}|${JSON.stringify(config.paths ?? null)}|${JSON.stringify(config.prefetch ?? null)}|${config.prefetchMode ?? ""}|${config.prefetchConcurrency ?? ""}`;
211
+ const value = (0, import_react5.useMemo)(
212
+ () => config,
213
+ // eslint-disable-next-line react-hooks/exhaustive-deps
214
+ [key]
215
+ );
216
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(ReplicationContext.Provider, { value, children: [
217
+ value.prefetch && value.prefetch.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrefetchRunner, {}) : null,
218
+ children
219
+ ] });
220
+ }
221
+ function resolveReplicationConfig(base, overrides) {
222
+ const endpoint = overrides?.endpoint ?? base?.endpoint;
223
+ const pollMs = overrides?.pollMs ?? base?.pollMs ?? 0;
224
+ if (!endpoint) return { config: null, pollMs };
225
+ return {
226
+ config: {
227
+ endpoint,
228
+ getAuth: overrides?.getAuth ?? base?.getAuth,
229
+ fetch: overrides?.fetch ?? base?.fetch,
230
+ paths: overrides?.paths ?? base?.paths
231
+ },
232
+ pollMs
233
+ };
234
+ }
235
+ function useReplicationBase() {
236
+ return (0, import_react5.useContext)(ReplicationContext);
237
+ }
238
+ function useReplicationConfig(overrides) {
239
+ return resolveReplicationConfig((0, import_react5.useContext)(ReplicationContext), overrides);
240
+ }
241
+ var CURSOR_COLLECTION = "__taladb_sync";
242
+ function normalizePrefetch(entries) {
243
+ return (entries ?? []).map((e) => typeof e === "string" ? { collection: e } : e);
244
+ }
245
+ var idleScheduler = (fn) => {
246
+ const g = globalThis;
247
+ if (typeof g.requestIdleCallback === "function") {
248
+ const id2 = g.requestIdleCallback(fn, { timeout: 2e3 });
249
+ return () => g.cancelIdleCallback?.(id2);
250
+ }
251
+ const id = setTimeout(fn, 0);
252
+ return () => clearTimeout(id);
253
+ };
254
+ var schedule = idleScheduler;
255
+ async function hasSynced(db, target) {
256
+ try {
257
+ const doc = await db.collection(CURSOR_COLLECTION).findOne({ target });
258
+ return doc != null;
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+ function PrefetchRunner() {
264
+ const db = useTalaDB();
265
+ const base = useReplicationBase();
266
+ const slices = normalizePrefetch(base?.prefetch);
267
+ const mode = base?.prefetchMode ?? "once";
268
+ const concurrency = Math.max(1, base?.prefetchConcurrency ?? 2);
269
+ const baseRef = (0, import_react5.useRef)(base);
270
+ baseRef.current = base;
271
+ const sig = JSON.stringify({ slices, mode, concurrency, endpoint: base?.endpoint ?? null });
272
+ (0, import_react5.useEffect)(() => {
273
+ if (slices.length === 0) return void 0;
274
+ let cancelled = false;
275
+ const cancelSchedule = schedule(() => {
276
+ void run();
277
+ });
278
+ async function run() {
279
+ const b = baseRef.current;
280
+ const queue = normalizePrefetch(b?.prefetch);
281
+ const worker = async () => {
282
+ while (!cancelled) {
283
+ const slice = queue.shift();
284
+ if (!slice) return;
285
+ const { config } = resolveReplicationConfig(b, { endpoint: slice.endpoint });
286
+ if (!config) continue;
287
+ const target = replicationTarget(config.endpoint, slice.collection);
288
+ if (mode === "once" && await hasSynced(db, target)) continue;
289
+ if (cancelled) return;
290
+ try {
291
+ await replicate(db, config, slice.collection, "pull");
292
+ } catch {
293
+ }
294
+ }
295
+ };
296
+ const lanes = Math.min(concurrency, queue.length);
297
+ await Promise.all(Array.from({ length: lanes }, () => worker()));
298
+ }
299
+ return () => {
300
+ cancelled = true;
301
+ cancelSchedule();
302
+ };
303
+ }, [db, sig]);
304
+ return null;
305
+ }
306
+
307
+ // src/useQuery.ts
308
+ var import_react6 = require("react");
309
+ function useQuery(options) {
310
+ const { collection, filter, source = "local-first" } = options;
311
+ const networked = source !== "local-only";
312
+ const db = useTalaDB();
313
+ const col = useCollection(collection);
314
+ const read = useFind(col, filter);
315
+ const { config, pollMs } = useReplicationConfig({
316
+ endpoint: options.endpoint,
317
+ getAuth: options.getAuth,
318
+ fetch: options.fetch,
319
+ paths: options.paths,
320
+ pollMs: options.pollMs
321
+ });
322
+ const configRef = (0, import_react6.useRef)(config);
323
+ configRef.current = config;
324
+ const [syncing, setSyncing] = (0, import_react6.useState)(false);
325
+ const [syncError, setSyncError] = (0, import_react6.useState)(null);
326
+ const [firstSyncDone, setFirstSyncDone] = (0, import_react6.useState)(false);
327
+ const endpoint = config?.endpoint;
328
+ const refetch = (0, import_react6.useCallback)(async () => {
329
+ const cfg = configRef.current;
330
+ if (!networked || !cfg) return;
331
+ setSyncing(true);
332
+ setSyncError(null);
333
+ try {
334
+ await replicate(db, cfg, collection, "pull");
335
+ } catch (e) {
336
+ setSyncError(e);
337
+ } finally {
338
+ setSyncing(false);
339
+ setFirstSyncDone(true);
340
+ }
341
+ }, [db, collection, networked, endpoint]);
342
+ (0, import_react6.useEffect)(() => {
343
+ if (!networked) return;
344
+ void refetch();
345
+ if (pollMs > 0) {
346
+ const id = setInterval(() => void refetch(), pollMs);
347
+ return () => clearInterval(id);
348
+ }
349
+ return void 0;
350
+ }, [refetch, networked, pollMs]);
351
+ if (networked && !config) {
352
+ throw new Error(
353
+ `useQuery({ collection: '${collection}' }) needs an endpoint for source '${source}'. Wrap the tree in <ReplicationProvider endpoint="\u2026">, pass { endpoint }, or use source: "local-only".`
354
+ );
355
+ }
356
+ const loading = source === "remote-first" ? read.loading || !firstSyncDone : read.loading;
357
+ return { data: read.data, loading, error: read.error, syncing, syncError, refetch };
358
+ }
359
+
360
+ // src/useQueries.ts
361
+ var import_react7 = require("react");
362
+ var NOOP_REFETCH = async () => {
363
+ };
364
+ function emptyResult() {
365
+ return { data: [], loading: true, error: null, syncing: false, syncError: null, refetch: NOOP_REFETCH };
366
+ }
367
+ function useQueries(queries) {
368
+ const db = useTalaDB();
369
+ const base = useReplicationBase();
370
+ for (const q of queries) {
371
+ const networked = (q.source ?? "local-first") !== "local-only";
372
+ if (networked && !(q.endpoint ?? base?.endpoint)) {
373
+ throw new Error(
374
+ `useQueries: the query for '${q.collection}' needs an endpoint for source '${q.source ?? "local-first"}'. Provide <ReplicationProvider endpoint="\u2026">, pass { endpoint }, or use source: "local-only".`
375
+ );
376
+ }
377
+ }
378
+ const sig = JSON.stringify(
379
+ queries.map((q) => ({
380
+ collection: q.collection,
381
+ filter: q.filter ?? null,
382
+ source: q.source ?? "local-first",
383
+ endpoint: q.endpoint ?? null,
384
+ pollMs: q.pollMs ?? null
385
+ }))
386
+ );
387
+ const queriesRef = (0, import_react7.useRef)(queries);
388
+ queriesRef.current = queries;
389
+ const baseRef = (0, import_react7.useRef)(base);
390
+ baseRef.current = base;
391
+ const [results, setResults] = (0, import_react7.useState)(
392
+ () => queries.map(() => emptyResult())
393
+ );
394
+ (0, import_react7.useEffect)(() => {
395
+ const qs = queriesRef.current;
396
+ const b = baseRef.current;
397
+ let cancelled = false;
398
+ const setAt = (i, fn) => {
399
+ setResults((prev) => {
400
+ if (i >= prev.length) return prev;
401
+ const copy = prev.slice();
402
+ copy[i] = fn(copy[i]);
403
+ return copy;
404
+ });
405
+ };
406
+ const resolved = qs.map((q, i) => {
407
+ const { config } = resolveReplicationConfig(b, {
408
+ endpoint: q.endpoint,
409
+ getAuth: q.getAuth,
410
+ fetch: q.fetch,
411
+ paths: q.paths,
412
+ pollMs: q.pollMs
413
+ });
414
+ const networked = (q.source ?? "local-first") !== "local-only";
415
+ const pollMs = q.pollMs ?? b?.pollMs ?? 0;
416
+ const refetch = async () => {
417
+ if (!networked || !config) return;
418
+ setAt(i, (r) => ({ ...r, syncing: true, syncError: null }));
419
+ try {
420
+ await replicate(db, config, q.collection, "pull");
421
+ } catch (e) {
422
+ if (!cancelled) setAt(i, (r) => ({ ...r, syncError: e }));
423
+ } finally {
424
+ if (!cancelled) setAt(i, (r) => ({ ...r, syncing: false }));
425
+ }
426
+ };
427
+ return { config, networked, pollMs, refetch };
428
+ });
429
+ setResults(
430
+ qs.map((_q, i) => ({
431
+ data: [],
432
+ loading: true,
433
+ error: null,
434
+ syncing: false,
435
+ syncError: null,
436
+ refetch: resolved[i].refetch
437
+ }))
438
+ );
439
+ const unsubs = qs.map((q, i) => {
440
+ const col = db.collection(q.collection);
441
+ return col.subscribe(
442
+ q.filter ?? {},
443
+ (docs) => {
444
+ if (!cancelled) setAt(i, (r) => ({ ...r, data: docs, loading: false, error: null }));
445
+ },
446
+ (error) => {
447
+ if (!cancelled) setAt(i, (r) => ({ ...r, loading: false, error }));
448
+ }
449
+ );
450
+ });
451
+ const intervals = [];
452
+ resolved.forEach((res) => {
453
+ if (!res.networked || !res.config) return;
454
+ void res.refetch();
455
+ if (res.pollMs > 0) intervals.push(setInterval(() => void res.refetch(), res.pollMs));
456
+ });
457
+ return () => {
458
+ cancelled = true;
459
+ unsubs.forEach((u) => u());
460
+ intervals.forEach((id) => clearInterval(id));
461
+ };
462
+ }, [db, sig]);
463
+ return queries.map((_q, i) => results[i] ?? emptyResult());
464
+ }
465
+
466
+ // src/useMutation.ts
467
+ var import_react8 = require("react");
468
+ function useMutation(options) {
469
+ const { collection, direction = "push", drainOnMount = true } = options;
470
+ const db = useTalaDB();
471
+ const col = useCollection(collection);
472
+ const { config } = useReplicationConfig({
473
+ endpoint: options.endpoint,
474
+ getAuth: options.getAuth,
475
+ fetch: options.fetch,
476
+ paths: options.paths
477
+ });
478
+ const configRef = (0, import_react8.useRef)(config);
479
+ configRef.current = config;
480
+ const [pending, setPending] = (0, import_react8.useState)(false);
481
+ const [error, setError] = (0, import_react8.useState)(null);
482
+ const endpoint = config?.endpoint;
483
+ const applyLocal = (0, import_react8.useCallback)(
484
+ async (op) => {
485
+ switch (op.type) {
486
+ case "insert":
487
+ await col.insert(op.doc);
488
+ return;
489
+ case "update":
490
+ await col.updateOne(op.where, { $set: op.set });
491
+ return;
492
+ case "delete":
493
+ await col.deleteOne(op.where);
494
+ return;
495
+ }
496
+ },
497
+ [col]
498
+ );
499
+ const drain = (0, import_react8.useCallback)(async () => {
500
+ const cfg = configRef.current;
501
+ if (!cfg) return;
502
+ await replicateWithRetry(db, cfg, collection, direction);
503
+ }, [db, collection, direction, endpoint]);
504
+ const mutateAsync = (0, import_react8.useCallback)(
505
+ async (op) => {
506
+ setPending(true);
507
+ setError(null);
508
+ try {
509
+ await applyLocal(op);
510
+ await drain();
511
+ } catch (e) {
512
+ setError(e);
513
+ throw e;
514
+ } finally {
515
+ setPending(false);
516
+ }
517
+ },
518
+ [applyLocal, drain]
519
+ );
520
+ const mutate = (0, import_react8.useCallback)(
521
+ (op) => {
522
+ void mutateAsync(op).catch(() => {
523
+ });
524
+ },
525
+ [mutateAsync]
526
+ );
527
+ (0, import_react8.useEffect)(() => {
528
+ if (!drainOnMount || !configRef.current) return;
529
+ void drain().catch(() => {
530
+ });
531
+ }, [drain, drainOnMount]);
532
+ if (!config) {
533
+ throw new Error(
534
+ `useMutation({ collection: '${collection}' }) needs an endpoint. Wrap the tree in <ReplicationProvider endpoint="\u2026"> or pass { endpoint }.`
535
+ );
536
+ }
537
+ return { mutate, mutateAsync, pending, error };
538
+ }
146
539
  // Annotate the CommonJS export names for ESM import in node:
147
540
  0 && (module.exports = {
541
+ ReplicationProvider,
148
542
  TalaDBProvider,
149
543
  useCollection,
150
544
  useFind,
151
545
  useFindOne,
546
+ useMutation,
547
+ useQueries,
548
+ useQuery,
549
+ useReplicationConfig,
152
550
  useTalaDB
153
551
  });
package/dist/index.mjs CHANGED
@@ -104,10 +104,409 @@ function useFindOne(collection, filter) {
104
104
  const getSnapshot = useCallback2(() => snapshotRef.current, []);
105
105
  return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
106
106
  }
107
+
108
+ // src/replication/config.tsx
109
+ import {
110
+ createContext as createContext2,
111
+ useContext as useContext2,
112
+ useEffect as useEffect2,
113
+ useMemo as useMemo2,
114
+ useRef as useRef3
115
+ } from "react";
116
+
117
+ // src/replication/engine.ts
118
+ import { HttpSyncAdapter } from "taladb";
119
+ function replicationTarget(endpoint, collection) {
120
+ return `${endpoint}::${collection}`;
121
+ }
122
+ async function buildAdapter(config) {
123
+ const headers = config.getAuth ? await config.getAuth() : void 0;
124
+ return new HttpSyncAdapter({
125
+ endpoint: config.endpoint,
126
+ headers,
127
+ fetch: config.fetch,
128
+ paths: config.paths
129
+ });
130
+ }
131
+ var inflight = /* @__PURE__ */ new Map();
132
+ function inflightKey(endpoint, collection, direction) {
133
+ return `${endpoint}::${collection}::${direction}`;
134
+ }
135
+ function replicate(db, config, collection, direction) {
136
+ const key = inflightKey(config.endpoint, collection, direction);
137
+ const existing = inflight.get(key);
138
+ if (existing) return existing;
139
+ const pass = (async () => {
140
+ const adapter = await buildAdapter(config);
141
+ await db.sync(adapter, {
142
+ collections: [collection],
143
+ direction,
144
+ target: replicationTarget(config.endpoint, collection)
145
+ });
146
+ })().finally(() => {
147
+ inflight.delete(key);
148
+ });
149
+ inflight.set(key, pass);
150
+ return pass;
151
+ }
152
+ var BACKOFFS_MS = [200, 400, 800];
153
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
154
+ async function replicateWithRetry(db, config, collection, direction) {
155
+ let lastError;
156
+ for (let attempt = 0; attempt <= BACKOFFS_MS.length; attempt++) {
157
+ try {
158
+ await replicate(db, config, collection, direction);
159
+ return;
160
+ } catch (error) {
161
+ lastError = error;
162
+ if (attempt < BACKOFFS_MS.length) await sleep(BACKOFFS_MS[attempt]);
163
+ }
164
+ }
165
+ throw lastError;
166
+ }
167
+
168
+ // src/replication/config.tsx
169
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
170
+ var ReplicationContext = createContext2(null);
171
+ function ReplicationProvider({ children, ...config }) {
172
+ const key = `${config.endpoint}|${config.pollMs ?? ""}|${JSON.stringify(config.paths ?? null)}|${JSON.stringify(config.prefetch ?? null)}|${config.prefetchMode ?? ""}|${config.prefetchConcurrency ?? ""}`;
173
+ const value = useMemo2(
174
+ () => config,
175
+ // eslint-disable-next-line react-hooks/exhaustive-deps
176
+ [key]
177
+ );
178
+ return /* @__PURE__ */ jsxs(ReplicationContext.Provider, { value, children: [
179
+ value.prefetch && value.prefetch.length > 0 ? /* @__PURE__ */ jsx2(PrefetchRunner, {}) : null,
180
+ children
181
+ ] });
182
+ }
183
+ function resolveReplicationConfig(base, overrides) {
184
+ const endpoint = overrides?.endpoint ?? base?.endpoint;
185
+ const pollMs = overrides?.pollMs ?? base?.pollMs ?? 0;
186
+ if (!endpoint) return { config: null, pollMs };
187
+ return {
188
+ config: {
189
+ endpoint,
190
+ getAuth: overrides?.getAuth ?? base?.getAuth,
191
+ fetch: overrides?.fetch ?? base?.fetch,
192
+ paths: overrides?.paths ?? base?.paths
193
+ },
194
+ pollMs
195
+ };
196
+ }
197
+ function useReplicationBase() {
198
+ return useContext2(ReplicationContext);
199
+ }
200
+ function useReplicationConfig(overrides) {
201
+ return resolveReplicationConfig(useContext2(ReplicationContext), overrides);
202
+ }
203
+ var CURSOR_COLLECTION = "__taladb_sync";
204
+ function normalizePrefetch(entries) {
205
+ return (entries ?? []).map((e) => typeof e === "string" ? { collection: e } : e);
206
+ }
207
+ var idleScheduler = (fn) => {
208
+ const g = globalThis;
209
+ if (typeof g.requestIdleCallback === "function") {
210
+ const id2 = g.requestIdleCallback(fn, { timeout: 2e3 });
211
+ return () => g.cancelIdleCallback?.(id2);
212
+ }
213
+ const id = setTimeout(fn, 0);
214
+ return () => clearTimeout(id);
215
+ };
216
+ var schedule = idleScheduler;
217
+ async function hasSynced(db, target) {
218
+ try {
219
+ const doc = await db.collection(CURSOR_COLLECTION).findOne({ target });
220
+ return doc != null;
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ function PrefetchRunner() {
226
+ const db = useTalaDB();
227
+ const base = useReplicationBase();
228
+ const slices = normalizePrefetch(base?.prefetch);
229
+ const mode = base?.prefetchMode ?? "once";
230
+ const concurrency = Math.max(1, base?.prefetchConcurrency ?? 2);
231
+ const baseRef = useRef3(base);
232
+ baseRef.current = base;
233
+ const sig = JSON.stringify({ slices, mode, concurrency, endpoint: base?.endpoint ?? null });
234
+ useEffect2(() => {
235
+ if (slices.length === 0) return void 0;
236
+ let cancelled = false;
237
+ const cancelSchedule = schedule(() => {
238
+ void run();
239
+ });
240
+ async function run() {
241
+ const b = baseRef.current;
242
+ const queue = normalizePrefetch(b?.prefetch);
243
+ const worker = async () => {
244
+ while (!cancelled) {
245
+ const slice = queue.shift();
246
+ if (!slice) return;
247
+ const { config } = resolveReplicationConfig(b, { endpoint: slice.endpoint });
248
+ if (!config) continue;
249
+ const target = replicationTarget(config.endpoint, slice.collection);
250
+ if (mode === "once" && await hasSynced(db, target)) continue;
251
+ if (cancelled) return;
252
+ try {
253
+ await replicate(db, config, slice.collection, "pull");
254
+ } catch {
255
+ }
256
+ }
257
+ };
258
+ const lanes = Math.min(concurrency, queue.length);
259
+ await Promise.all(Array.from({ length: lanes }, () => worker()));
260
+ }
261
+ return () => {
262
+ cancelled = true;
263
+ cancelSchedule();
264
+ };
265
+ }, [db, sig]);
266
+ return null;
267
+ }
268
+
269
+ // src/useQuery.ts
270
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef4, useState as useState2 } from "react";
271
+ function useQuery(options) {
272
+ const { collection, filter, source = "local-first" } = options;
273
+ const networked = source !== "local-only";
274
+ const db = useTalaDB();
275
+ const col = useCollection(collection);
276
+ const read = useFind(col, filter);
277
+ const { config, pollMs } = useReplicationConfig({
278
+ endpoint: options.endpoint,
279
+ getAuth: options.getAuth,
280
+ fetch: options.fetch,
281
+ paths: options.paths,
282
+ pollMs: options.pollMs
283
+ });
284
+ const configRef = useRef4(config);
285
+ configRef.current = config;
286
+ const [syncing, setSyncing] = useState2(false);
287
+ const [syncError, setSyncError] = useState2(null);
288
+ const [firstSyncDone, setFirstSyncDone] = useState2(false);
289
+ const endpoint = config?.endpoint;
290
+ const refetch = useCallback3(async () => {
291
+ const cfg = configRef.current;
292
+ if (!networked || !cfg) return;
293
+ setSyncing(true);
294
+ setSyncError(null);
295
+ try {
296
+ await replicate(db, cfg, collection, "pull");
297
+ } catch (e) {
298
+ setSyncError(e);
299
+ } finally {
300
+ setSyncing(false);
301
+ setFirstSyncDone(true);
302
+ }
303
+ }, [db, collection, networked, endpoint]);
304
+ useEffect3(() => {
305
+ if (!networked) return;
306
+ void refetch();
307
+ if (pollMs > 0) {
308
+ const id = setInterval(() => void refetch(), pollMs);
309
+ return () => clearInterval(id);
310
+ }
311
+ return void 0;
312
+ }, [refetch, networked, pollMs]);
313
+ if (networked && !config) {
314
+ throw new Error(
315
+ `useQuery({ collection: '${collection}' }) needs an endpoint for source '${source}'. Wrap the tree in <ReplicationProvider endpoint="\u2026">, pass { endpoint }, or use source: "local-only".`
316
+ );
317
+ }
318
+ const loading = source === "remote-first" ? read.loading || !firstSyncDone : read.loading;
319
+ return { data: read.data, loading, error: read.error, syncing, syncError, refetch };
320
+ }
321
+
322
+ // src/useQueries.ts
323
+ import { useEffect as useEffect4, useRef as useRef5, useState as useState3 } from "react";
324
+ var NOOP_REFETCH = async () => {
325
+ };
326
+ function emptyResult() {
327
+ return { data: [], loading: true, error: null, syncing: false, syncError: null, refetch: NOOP_REFETCH };
328
+ }
329
+ function useQueries(queries) {
330
+ const db = useTalaDB();
331
+ const base = useReplicationBase();
332
+ for (const q of queries) {
333
+ const networked = (q.source ?? "local-first") !== "local-only";
334
+ if (networked && !(q.endpoint ?? base?.endpoint)) {
335
+ throw new Error(
336
+ `useQueries: the query for '${q.collection}' needs an endpoint for source '${q.source ?? "local-first"}'. Provide <ReplicationProvider endpoint="\u2026">, pass { endpoint }, or use source: "local-only".`
337
+ );
338
+ }
339
+ }
340
+ const sig = JSON.stringify(
341
+ queries.map((q) => ({
342
+ collection: q.collection,
343
+ filter: q.filter ?? null,
344
+ source: q.source ?? "local-first",
345
+ endpoint: q.endpoint ?? null,
346
+ pollMs: q.pollMs ?? null
347
+ }))
348
+ );
349
+ const queriesRef = useRef5(queries);
350
+ queriesRef.current = queries;
351
+ const baseRef = useRef5(base);
352
+ baseRef.current = base;
353
+ const [results, setResults] = useState3(
354
+ () => queries.map(() => emptyResult())
355
+ );
356
+ useEffect4(() => {
357
+ const qs = queriesRef.current;
358
+ const b = baseRef.current;
359
+ let cancelled = false;
360
+ const setAt = (i, fn) => {
361
+ setResults((prev) => {
362
+ if (i >= prev.length) return prev;
363
+ const copy = prev.slice();
364
+ copy[i] = fn(copy[i]);
365
+ return copy;
366
+ });
367
+ };
368
+ const resolved = qs.map((q, i) => {
369
+ const { config } = resolveReplicationConfig(b, {
370
+ endpoint: q.endpoint,
371
+ getAuth: q.getAuth,
372
+ fetch: q.fetch,
373
+ paths: q.paths,
374
+ pollMs: q.pollMs
375
+ });
376
+ const networked = (q.source ?? "local-first") !== "local-only";
377
+ const pollMs = q.pollMs ?? b?.pollMs ?? 0;
378
+ const refetch = async () => {
379
+ if (!networked || !config) return;
380
+ setAt(i, (r) => ({ ...r, syncing: true, syncError: null }));
381
+ try {
382
+ await replicate(db, config, q.collection, "pull");
383
+ } catch (e) {
384
+ if (!cancelled) setAt(i, (r) => ({ ...r, syncError: e }));
385
+ } finally {
386
+ if (!cancelled) setAt(i, (r) => ({ ...r, syncing: false }));
387
+ }
388
+ };
389
+ return { config, networked, pollMs, refetch };
390
+ });
391
+ setResults(
392
+ qs.map((_q, i) => ({
393
+ data: [],
394
+ loading: true,
395
+ error: null,
396
+ syncing: false,
397
+ syncError: null,
398
+ refetch: resolved[i].refetch
399
+ }))
400
+ );
401
+ const unsubs = qs.map((q, i) => {
402
+ const col = db.collection(q.collection);
403
+ return col.subscribe(
404
+ q.filter ?? {},
405
+ (docs) => {
406
+ if (!cancelled) setAt(i, (r) => ({ ...r, data: docs, loading: false, error: null }));
407
+ },
408
+ (error) => {
409
+ if (!cancelled) setAt(i, (r) => ({ ...r, loading: false, error }));
410
+ }
411
+ );
412
+ });
413
+ const intervals = [];
414
+ resolved.forEach((res) => {
415
+ if (!res.networked || !res.config) return;
416
+ void res.refetch();
417
+ if (res.pollMs > 0) intervals.push(setInterval(() => void res.refetch(), res.pollMs));
418
+ });
419
+ return () => {
420
+ cancelled = true;
421
+ unsubs.forEach((u) => u());
422
+ intervals.forEach((id) => clearInterval(id));
423
+ };
424
+ }, [db, sig]);
425
+ return queries.map((_q, i) => results[i] ?? emptyResult());
426
+ }
427
+
428
+ // src/useMutation.ts
429
+ import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef6, useState as useState4 } from "react";
430
+ function useMutation(options) {
431
+ const { collection, direction = "push", drainOnMount = true } = options;
432
+ const db = useTalaDB();
433
+ const col = useCollection(collection);
434
+ const { config } = useReplicationConfig({
435
+ endpoint: options.endpoint,
436
+ getAuth: options.getAuth,
437
+ fetch: options.fetch,
438
+ paths: options.paths
439
+ });
440
+ const configRef = useRef6(config);
441
+ configRef.current = config;
442
+ const [pending, setPending] = useState4(false);
443
+ const [error, setError] = useState4(null);
444
+ const endpoint = config?.endpoint;
445
+ const applyLocal = useCallback4(
446
+ async (op) => {
447
+ switch (op.type) {
448
+ case "insert":
449
+ await col.insert(op.doc);
450
+ return;
451
+ case "update":
452
+ await col.updateOne(op.where, { $set: op.set });
453
+ return;
454
+ case "delete":
455
+ await col.deleteOne(op.where);
456
+ return;
457
+ }
458
+ },
459
+ [col]
460
+ );
461
+ const drain = useCallback4(async () => {
462
+ const cfg = configRef.current;
463
+ if (!cfg) return;
464
+ await replicateWithRetry(db, cfg, collection, direction);
465
+ }, [db, collection, direction, endpoint]);
466
+ const mutateAsync = useCallback4(
467
+ async (op) => {
468
+ setPending(true);
469
+ setError(null);
470
+ try {
471
+ await applyLocal(op);
472
+ await drain();
473
+ } catch (e) {
474
+ setError(e);
475
+ throw e;
476
+ } finally {
477
+ setPending(false);
478
+ }
479
+ },
480
+ [applyLocal, drain]
481
+ );
482
+ const mutate = useCallback4(
483
+ (op) => {
484
+ void mutateAsync(op).catch(() => {
485
+ });
486
+ },
487
+ [mutateAsync]
488
+ );
489
+ useEffect5(() => {
490
+ if (!drainOnMount || !configRef.current) return;
491
+ void drain().catch(() => {
492
+ });
493
+ }, [drain, drainOnMount]);
494
+ if (!config) {
495
+ throw new Error(
496
+ `useMutation({ collection: '${collection}' }) needs an endpoint. Wrap the tree in <ReplicationProvider endpoint="\u2026"> or pass { endpoint }.`
497
+ );
498
+ }
499
+ return { mutate, mutateAsync, pending, error };
500
+ }
107
501
  export {
502
+ ReplicationProvider,
108
503
  TalaDBProvider,
109
504
  useCollection,
110
505
  useFind,
111
506
  useFindOne,
507
+ useMutation,
508
+ useQueries,
509
+ useQuery,
510
+ useReplicationConfig,
112
511
  useTalaDB
113
512
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taladb/react",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "React hooks for TalaDB — useFind, useFindOne, and useCollection for React and React Native",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "peerDependencies": {
43
43
  "react": ">=18.0.0",
44
- "taladb": "^0.9.0"
44
+ "taladb": "^0.9.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@testing-library/react": "^16.0.0",
@@ -53,7 +53,7 @@
53
53
  "tsup": "^8.0.0",
54
54
  "typescript": "^5.9.3",
55
55
  "vitest": "^3.2.0",
56
- "taladb": "0.9.0"
56
+ "taladb": "0.9.2"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "tsup",