pbtsdb 0.10.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,12 @@
1
1
  # pbtsdb: PocketBase TanStack Database Integration
2
2
 
3
- > Type-safe PocketBase integration with TanStack Query and TanStack DB
3
+ > Type-safe PocketBase integration with TanStack DB
4
4
 
5
- A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.io) with [TanStack Query](https://tanstack.com/query) and [TanStack DB](https://tanstack.com/db), providing:
5
+ A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.io) with [TanStack DB](https://tanstack.com/db), providing:
6
6
 
7
7
  - 🔥 **Real-time subscriptions** with automatic synchronization
8
8
  - 🎯 **Full TypeScript type safety** for queries and relations
9
9
  - ⚡ **Reactive collections** with TanStack DB
10
- - 🔄 **Automatic caching** via TanStack Query
11
10
  - ✨ **Optimistic mutations** with insert/update/delete support
12
11
  - 🎨 **React hooks** for easy component integration
13
12
  - 🔗 **Type-safe joins** and relation fetching into their own collections
@@ -32,16 +31,14 @@ A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.
32
31
  ## Installation
33
32
 
34
33
  ```bash
35
- npm install pbtsdb pocketbase @tanstack/db @tanstack/query-db-collection @tanstack/react-query @tanstack/react-db
34
+ npm install pbtsdb pocketbase @tanstack/db @tanstack/react-db
36
35
  ```
37
36
 
38
37
  ### Peer Dependencies
39
38
 
40
39
  - `pocketbase` >= 0.22.0
41
- - `@tanstack/db` >= 0.6.0
42
- - `@tanstack/query-db-collection` >= 1.0.40
43
- - `@tanstack/react-query` >= 5.0.0
44
- - `@tanstack/react-db` >= 0.1.86 (optional; only for `createReactProvider`)
40
+ - `@tanstack/db` >= 0.12.1
41
+ - `@tanstack/react-db` >= 0.5.5 (optional; only for `createReactProvider`)
45
42
  - `react` and `react-dom` >= 18.0.0 (optional)
46
43
 
47
44
  All peer dependencies use minimum version constraints; newer versions should work. The
@@ -110,18 +107,12 @@ type BlogSchema = {
110
107
  ```typescript
111
108
  // app.tsx
112
109
  import PocketBase from 'pocketbase';
113
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
114
110
  import { createCollection, createReactProvider } from 'pbtsdb';
115
111
 
116
112
  const pb = new PocketBase('http://localhost:8090');
117
- const queryClient = new QueryClient({
118
- defaultOptions: {
119
- queries: { staleTime: 60_000 } // Cache for 1 minute
120
- }
121
- });
122
113
 
123
114
  // Create collections with automatic type inference
124
- const c = createCollection<BlogSchema>(pb, queryClient);
115
+ const c = createCollection<BlogSchema>(pb);
125
116
  const users = c('users', {});
126
117
  export const { Provider, useStore } = createReactProvider({
127
118
  users,
@@ -139,11 +130,9 @@ export const { Provider, useStore } = createReactProvider({
139
130
 
140
131
  export function App() {
141
132
  return (
142
- <QueryClientProvider client={queryClient}>
143
- <Provider>
144
- <BlogDashboard />
145
- </Provider>
146
- </QueryClientProvider>
133
+ <Provider>
134
+ <BlogDashboard />
135
+ </Provider>
147
136
  );
148
137
  }
149
138
  ```
@@ -253,14 +242,13 @@ Collections are reactive data stores that automatically sync with PocketBase:
253
242
 
254
243
  ```typescript
255
244
  // Create a collection using the curried API
256
- const c = createCollection<MySchema>(pb, queryClient);
245
+ const c = createCollection<MySchema>(pb);
257
246
  const booksCollection = c('books', {});
258
247
 
259
248
  // Collections automatically:
260
249
  // - Fetch data from PocketBase
261
250
  // - Subscribe to real-time updates
262
251
  // - Update React components when data changes
263
- // - Cache data via TanStack Query
264
252
  ```
265
253
 
266
254
  ### Real-time Subscriptions
@@ -269,7 +257,7 @@ Collections manage subscriptions **automatically** based on query lifecycle:
269
257
 
270
258
  ```typescript
271
259
  // Collections are lazy - no subscription until queried
272
- const c = createCollection<MySchema>(pb, queryClient);
260
+ const c = createCollection<MySchema>(pb);
273
261
  const booksCollection = c('books', {});
274
262
 
275
263
  // Subscription starts automatically when query becomes active
@@ -286,6 +274,31 @@ const { data } = useLiveQuery((q) =>
286
274
  - **Shared:** Multiple components using the same collection share one subscription
287
275
  - **No manual control needed:** The collection handles all subscription management internally
288
276
 
277
+ ### Reconnects
278
+
279
+ pbtsdb runs its own realtime connection. When the connection drops, it reconnects with backoff and refetches every live query, because PocketBase does not replay events missed during the gap. A server that supports pbtsdb's resume extension (`?resume=<clientId>&after=<seq>` on the SSE URL, `resumed: true` in `PB_CONNECT`, `seq` on each event) replays the gap instead, and no refetch runs.
280
+
281
+ pbtsdb's connection is independent of `pb.realtime`. When `pb.authStore` changes to another auth record (login, logout, switching users), pbtsdb forgets the connection's server-side session and reconnects at once, re-sending every subscribed topic under the new auth; every ready collection reloads once that POST succeeds, the same as a non-resumed reconnect. A token refresh for the same record changes nothing. PocketBase would otherwise answer the next subscriptions POST with a 403, or keep serving the previous user's data.
282
+
283
+ Call `resetRealtime(pb)` yourself for a change the auth store cannot see, such as pointing `pb` at another server:
284
+
285
+ ```typescript
286
+ import { disconnectRealtime, resetRealtime } from 'pbtsdb';
287
+
288
+ resetRealtime(pb); // forget the session and re-subscribe every open topic now
289
+
290
+ disconnectRealtime(pb); // close the connection and keep it closed
291
+ resetRealtime(pb); // open it again; every ready collection reloads
292
+ ```
293
+
294
+ `disconnectRealtime(pb)` closes the connection and keeps it closed until `resetRealtime(pb)`. Collections keep working over REST and keep their subscriptions registered. Use it at logout, or at startup where realtime is not wanted, such as an embedded view.
295
+
296
+ ### Subset Lifetime
297
+
298
+ In on-demand mode a live query's subset is released when the last subscriber unmounts, after a grace window of `subsetGcTime` milliseconds (default 5000). A query with an equal request that mounts within the window reuses the rows with no request. Realtime keeps the parked rows fresh, and rows a parent filed through a relation stay for the same window, so a panel that mounts and unmounts quickly costs no refetch. `collection.reload()` releases every parked subset. An auth change releases every waiting subset and every accepted row at once and reloads every live subset under the new auth, whether or not realtime is connected, so a load right after a logout never reuses the previous user's rows and the order of `authStore.clear()` and `disconnectRealtime(pb)` does not matter. Set `subsetGcTime: 0` to release a subset as soon as it unloads; `0` also keeps accepted rows until a reload or idle instead of expiring them.
299
+
300
+ A subset load that fails is retried with backoff (`loadRetryDelays`, default 1, 2, 4, 8, 15 and then every 30 seconds) until it succeeds, so a live query stays loading through an outage instead of entering an error state it cannot leave. A `collection.reload()` or a realtime reconnect retries at once, also while another reload is running. A response the server gave on purpose, a 4xx other than 401, 408 or 429, is not retried and reports its error. A 401 waits for the next auth change and then retries. Delays carry a little jitter so many queries do not retry in lockstep.
301
+
289
302
  ### Sync Modes
290
303
 
291
304
  Every collection is either **eager** (the default) or **on-demand**:
@@ -302,11 +315,38 @@ for enter the store, and different filters are cached under different keys.
302
315
  Use on-demand for large collections; realtime keeps both modes current once rows
303
316
  are loaded.
304
317
 
318
+ #### Paging
319
+
320
+ An on-demand query with `orderBy` and `limit` fetches one page of that size.
321
+ When TanStack DB needs more rows (`setWindow`, or a window past what is loaded)
322
+ it hands the sync layer a cursor on the sort field and the count of rows it
323
+ already has. pbtsdb conjoins the cursor to the fetch filter and fetches only the
324
+ delta; an offset without a cursor becomes a PocketBase page.
325
+
326
+ ```typescript
327
+ const { data, collection } = useLiveQuery((q) =>
328
+ q.from({ b: books }).where(({ b }) => eq(b.author, id)).orderBy(({ b }) => b.page_count).limit(20)
329
+ );
330
+ // fetches only the rows past what is loaded: one cursor request plus a boundary tie-check
331
+ collection.utils.setWindow({ offset: 20, limit: 20 });
332
+ ```
333
+
334
+ Realtime stays on the base `where`: every page of one query shares one
335
+ subscription, and a new row that sorts into the window arrives on its own. A
336
+ query whose `where` holds its own boundary (`lt(b.page_count, cursor)`)
337
+ subscribes to that slice only, which is the shape for numbered pages: page one
338
+ has no boundary and receives every new row. A join's or include's key batch
339
+ keeps its own filter, so lazily loaded rows stay covered.
340
+
341
+ TanStack applies `offset` in memory after loading, so `.offset(n)` on the
342
+ builder still loads the first `n + limit` rows. Put a page boundary in the
343
+ `where` when a deep page must not load what comes before it.
344
+
305
345
  ### Mutations and Refetch
306
346
 
307
- By default, pbtsdb does **not** refetch the entire collection after a successful insert, update, or delete. The realtime subscription delivers server-confirmed rows — including server-assigned fields like `id`, `created`, `updated`, and any values rewritten by PocketBase hooks — and TanStack DB's optimistic write keeps the UI consistent in the meantime. The post-mutation refetch is therefore redundant.
347
+ The built-in handlers write PocketBase's response into the collection before they return: server-assigned fields like `created` and `updated`, and any values rewritten by PocketBase hooks. A built-in delete removes the row the same way. TanStack DB drops a mutation's optimistic state when its handler settles, and rows written while the handler runs publish together with that drop, so the settled row is the server's row with no gap. The realtime echo that follows changes nothing.
308
348
 
309
- Set `refetchOnMutation: true` to opt back into the previous behavior — for example, when realtime is unreliable in your environment, or when a server-side hook produces a field you must read synchronously after the mutation resolves:
349
+ By default, pbtsdb does **not** refetch after a successful insert, update, or delete. Set `refetchOnMutation: true` to refetch the collection's active queries before the handler settles — for example, when a server-side hook changes other rows you must read right after the mutation:
310
350
 
311
351
  ```typescript
312
352
  const collection = c('books', {
@@ -314,7 +354,23 @@ const collection = c('books', {
314
354
  });
315
355
  ```
316
356
 
317
- The option only affects the built-in default handlers. If you supply your own `onInsert`, `onUpdate`, or `onDelete`, you control its return value yourself — return `{ refetch: false }` (or omit a return) to skip the refetch, return `{ refetch: true }` to force one.
357
+ The option only affects the built-in default handlers. A custom `onInsert`, `onUpdate`, or `onDelete` controls write-back itself:
358
+
359
+ - TanStack DB drops the optimistic state when your handler returns. Until the realtime echo arrives, the row shows its previous server value: an updated row reverts, an inserted row disappears, and a deleted row comes back.
360
+ - For an insert or an update, `await collection.accept(serverRows)` before you return. This closes the gap.
361
+ - For a delete, `await collection.evict(ids)` before you return. Without it, the deleted row shows again until the delete echo arrives.
362
+ - In on-demand mode, when realtime goes idle (no live query on the collection), rows held only by an accepted write-back are released. A row written back while nothing is mounted is fetched again on the next mount.
363
+ - The handler returns `void`. Core no longer reads a `{ refetch }` result; call `collection.reload()` yourself if the handler needs a refetch.
364
+
365
+ ### Writing server rows yourself
366
+
367
+ `collection.accept(rows)` lands rows the server returned, for example the response of a custom endpoint, as confirmed state. A row older than the stored one is ignored. Use it when the screen must update before the realtime echo arrives. It resolves when the rows are accepted, so a custom mutation handler can await it.
368
+
369
+ `collection.evict(ids)` removes rows the server deleted, for example after a custom endpoint deleted them. The rows leave every holder, and a fetch in flight does not put them back. It resolves when the removal is accepted, so a custom delete handler can await it.
370
+
371
+ `collection.reload()` refetches every live query's subset. Then it releases the realtime-topic and accepted holders of the rows the results do not confirm. A row that a parent filed through a relation stays. Use it when the server state changed with no realtime event, such as after a user loses access to rows.
372
+
373
+ All three are also on `collection.utils`.
318
374
 
319
375
  ### Type Safety
320
376
 
@@ -340,7 +396,6 @@ The main function for creating type-safe collections. Uses a curried API for bet
340
396
  ```typescript
341
397
  const c = createCollection<Schema>(
342
398
  pb: PocketBase,
343
- queryClient: QueryClient,
344
399
  factoryOptions?: CreateCollectionFactoryOptions
345
400
  );
346
401
  const collection = c(collectionName: string, options?: CreateCollectionOptions);
@@ -348,7 +403,6 @@ const collection = c(collectionName: string, options?: CreateCollectionOptions);
348
403
 
349
404
  **Parameters:**
350
405
  - `pb` - PocketBase instance
351
- - `queryClient` - TanStack Query QueryClient instance
352
406
  - `factoryOptions` - Optional configuration applied to every collection this factory builds (see [Subscription Options](#subscription-options))
353
407
  - `collectionName` - Name of the PocketBase collection
354
408
  - `options` - Optional configuration
@@ -363,7 +417,6 @@ const collection = c(collectionName: string, options?: CreateCollectionOptions);
363
417
  - `onUpdate?: UpdateMutationFn | false` - Custom update handler or `false` to disable
364
418
  - `onDelete?: DeleteMutationFn | false` - Custom delete handler or `false` to disable
365
419
  - `refetchOnMutation?: boolean` - Refetch the collection after a built-in insert/update/delete succeeds (default: `false`; see [Mutations and Refetch](#mutations-and-refetch))
366
- - `ignoreAutoCancellation?: boolean` - Ignore PocketBase auto-cancellation errors (default: `true`)
367
420
  - `collectionOptions?: object` - Additional TanStack DB collection options passed through directly (see [Collection Options Passthrough](#collection-options-passthrough))
368
421
 
369
422
  **Returns:** Fully-typed Collection instance with subscription capabilities
@@ -372,7 +425,7 @@ const collection = c(collectionName: string, options?: CreateCollectionOptions);
372
425
 
373
426
  Basic collection (lazy, subscribes automatically on first query):
374
427
  ```typescript
375
- const c = createCollection<MySchema>(pb, queryClient);
428
+ const c = createCollection<MySchema>(pb);
376
429
  const booksCollection = c('books', {});
377
430
  ```
378
431
 
@@ -383,7 +436,7 @@ collections. Rows never carry `expand`; read related records from the target
383
436
  collection.
384
437
 
385
438
  ```typescript
386
- const c = createCollection<MySchema>(pb, queryClient);
439
+ const c = createCollection<MySchema>(pb);
387
440
  const authors = c('authors', { syncMode: 'on-demand' });
388
441
  const tags = c('tags', { syncMode: 'on-demand' });
389
442
  const books = c('books', {
@@ -394,8 +447,8 @@ const books = c('books', {
394
447
 
395
448
  Every books request expands `author`; the expanded authors are filed into
396
449
  `authors` (an on-demand target has its sync started) and removed from the
397
- book rows. Read them through `materialize()` in a query, a join, or
398
- `authors.get(book.author)`:
450
+ book rows. Read them through `materialize()` in a query or through a join,
451
+ inside the `useLiveQuery` that needs them:
399
452
 
400
453
  ```typescript
401
454
  import { eq } from '@tanstack/db';
@@ -419,6 +472,14 @@ reads through a `fetchRelations()` view, whose fetch goes to PocketBase so its
419
472
  paths get filed; anything else is fetched in one batched request. An empty
420
473
  `inArray(id, [])` yields no rows and no request.
421
474
 
475
+ Do not read a related row with `authors.get(book.author)` in a component. A
476
+ `get()` is a one-time read: the component does not re-render when the author
477
+ changes, and the row can leave the store while the component still shows it,
478
+ because a filed row stays only as long as a live query holds the parent row
479
+ that filed it. A `useLiveQuery` with `materialize()` or a join holds the rows
480
+ it reads and re-renders when they change. `get()` is for code outside React
481
+ that needs a value right now, such as a mutation handler or a test.
482
+
422
483
  Fetch a relation for one query only with `fetchRelations()`; the view shares the
423
484
  collection's store, realtime subscription, and mutations, and only its fetches
424
485
  add the `expand` parameter:
@@ -463,7 +524,7 @@ never treated as complete.
463
524
  Pass any [TanStack DB `BaseCollectionConfig`](https://tanstack.com/db/latest/docs/overview) option directly via `collectionOptions`. This is useful for configuring indexing, garbage collection, and other collection-level settings:
464
525
 
465
526
  ```typescript
466
- const c = createCollection<MySchema>(pb, queryClient);
527
+ const c = createCollection<MySchema>(pb);
467
528
  const booksCollection = c('books', {
468
529
  collectionOptions: {
469
530
  gcTime: 60000, // 1 minute GC
@@ -477,7 +538,7 @@ pbtsdb defaults `autoIndex` to `'eager'` with `defaultIndexType: BTreeIndex`, so
477
538
  TanStack DB does not warn about a missing index). Pass `autoIndex: 'off'` or a
478
539
  different `defaultIndexType` in `collectionOptions` to change that per collection.
479
540
 
480
- The following fields are managed by pbtsdb and excluded from `collectionOptions`: `getKey`, `syncMode`, `onInsert`, `onUpdate`, `onDelete`, `schema`.
541
+ The following fields are managed by pbtsdb and excluded from `collectionOptions`: `getKey`, `syncMode`, `onInsert`, `onUpdate`, `onDelete`, `schema`, `utils`.
481
542
 
482
543
  ### React Integration
483
544
 
@@ -500,7 +561,7 @@ const { Provider, useStore } = createReactProvider(collections: CollectionsMap);
500
561
  ```typescript
501
562
  import { createCollection, createReactProvider } from 'pbtsdb';
502
563
 
503
- const c = createCollection<MySchema>(pb, queryClient);
564
+ const c = createCollection<MySchema>(pb);
504
565
  const collections = {
505
566
  authors: c('authors', {}),
506
567
  books: c('books', {
@@ -658,7 +719,7 @@ options — `headers`, `filter`, `expand`, `fields` — to every real-time
658
719
  subscription the factory creates.
659
720
 
660
721
  ```typescript
661
- const c = createCollection<Schema>(pb, queryClient, {
722
+ const c = createCollection<Schema>(pb, {
662
723
  subscribeOptions: () => {
663
724
  const token = getShareToken();
664
725
  return token ? { headers: { 'X-Share-Token': token } } : undefined;
@@ -941,7 +1002,7 @@ export function CreateBookForm() {
941
1002
  try {
942
1003
  // Optimistic insert - appears instantly
943
1004
  const tx = books.insert({ id: newRecordId(), title, author: 'author_id' });
944
- await tx.isPersisted.promise;
1005
+ await tx.when('settled');
945
1006
 
946
1007
  if (tx.state === 'completed') setTitle('');
947
1008
  else setError('Failed to create book');
@@ -1116,7 +1177,7 @@ Always create collections with proper type parameters:
1116
1177
 
1117
1178
  ```typescript
1118
1179
  // ✅ Good - full type safety
1119
- const c = createCollection<MySchema>(pb, queryClient);
1180
+ const c = createCollection<MySchema>(pb);
1120
1181
  const books = c('books', {
1121
1182
  omitOnInsert: ['created', 'updated'] as const
1122
1183
  });
@@ -1137,7 +1198,7 @@ Define all collections once at app initialization:
1137
1198
 
1138
1199
  ```typescript
1139
1200
  // ✅ Do this - centralized, type-safe
1140
- const c = createCollection<MySchema>(pb, queryClient);
1201
+ const c = createCollection<MySchema>(pb);
1141
1202
 
1142
1203
  export const { Provider, useStore } = createReactProvider({
1143
1204
  posts: c('posts', { omitOnInsert: ['created', 'updated'] as const }),
@@ -1152,7 +1213,7 @@ When declaring `relations`, create the target collection first:
1152
1213
 
1153
1214
  ```typescript
1154
1215
  // ✅ Good - authors exists before books references it
1155
- const c = createCollection<MySchema>(pb, queryClient);
1216
+ const c = createCollection<MySchema>(pb);
1156
1217
  const authors = c('authors', {});
1157
1218
  const books = c('books', {
1158
1219
  relations: { author: authors }, // authors is already created
@@ -1203,7 +1264,7 @@ return <PostsList posts={data} />;
1203
1264
  per parent row, on every fetch of the parent:
1204
1265
 
1205
1266
  ```typescript
1206
- const c = createCollection<MySchema>(pb, queryClient);
1267
+ const c = createCollection<MySchema>(pb);
1207
1268
  const authors = c('authors', {});
1208
1269
  const posts = c('posts', {
1209
1270
  relations: { author: authors },
@@ -1219,20 +1280,6 @@ it) and, once the rows are filed, subsequent queries make no request at all.
1219
1280
  Prefer `alwaysFetchRelations` when the parent is the only path by which those
1220
1281
  rows enter an on-demand collection; otherwise let the query load them.
1221
1282
 
1222
- ### 6. Configure QueryClient Defaults
1223
-
1224
- ```typescript
1225
- const queryClient = new QueryClient({
1226
- defaultOptions: {
1227
- queries: {
1228
- staleTime: 60_000, // 1 minute
1229
- gcTime: 300_000, // 5 minutes
1230
- refetchOnWindowFocus: false
1231
- }
1232
- }
1233
- });
1234
- ```
1235
-
1236
1283
  ## Configuration
1237
1284
 
1238
1285
  ### Custom Logger Integration
@@ -1354,6 +1401,5 @@ npm run typecheck # TypeScript only
1354
1401
 
1355
1402
  **Built with:**
1356
1403
  - [PocketBase](https://pocketbase.io) - Backend-as-a-Service
1357
- - [TanStack Query](https://tanstack.com/query) - Powerful data fetching
1358
1404
  - [TanStack DB](https://tanstack.com/db) - Reactive database
1359
1405
  - [TypeScript](https://www.typescriptlang.org) - Type safety