@taladb/react 0.9.0 → 0.9.1
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 +230 -1
- package/dist/index.d.ts +230 -1
- package/dist/index.js +398 -0
- package/dist/index.mjs +399 -0
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.9.1",
|
|
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.
|
|
44
|
+
"taladb": "^0.9.1"
|
|
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.
|
|
56
|
+
"taladb": "0.9.1"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "tsup",
|