pbtsdb 0.0.1 → 0.1.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/dist/index.js CHANGED
@@ -1,9 +1,106 @@
1
- import { createCollection } from '@tanstack/db';
1
+ import { createCollection as createCollection$1 } from '@tanstack/react-db';
2
2
  import { queryCollectionOptions } from '@tanstack/query-db-collection';
3
+ import { parseWhereExpression, parseOrderByExpression } from '@tanstack/db';
3
4
  import { createContext, useContext } from 'react';
4
5
  import { jsx } from 'react/jsx-runtime';
5
6
 
6
7
  // src/collection.ts
8
+ function escapeValue(value) {
9
+ if (value === null) {
10
+ return "null";
11
+ }
12
+ if (typeof value === "boolean") {
13
+ return value ? "true" : "false";
14
+ }
15
+ if (typeof value === "number") {
16
+ return value.toString();
17
+ }
18
+ if (typeof value === "string") {
19
+ return `"${value.replace(/"/g, '\\"')}"`;
20
+ }
21
+ if (value instanceof Date) {
22
+ return `"${value.toISOString()}"`;
23
+ }
24
+ if (Array.isArray(value)) {
25
+ return `[${value.map(escapeValue).join(",")}]`;
26
+ }
27
+ return `"${String(value)}"`;
28
+ }
29
+ function fieldPathToString(path) {
30
+ return path.join(".");
31
+ }
32
+ function convertToPocketBaseFilter(where) {
33
+ if (!where) {
34
+ return void 0;
35
+ }
36
+ const result = parseWhereExpression(where, {
37
+ handlers: {
38
+ eq: (field, value) => {
39
+ return `${fieldPathToString(field)} = ${escapeValue(value)}`;
40
+ },
41
+ gt: (field, value) => {
42
+ return `${fieldPathToString(field)} > ${escapeValue(value)}`;
43
+ },
44
+ gte: (field, value) => {
45
+ return `${fieldPathToString(field)} >= ${escapeValue(value)}`;
46
+ },
47
+ lt: (field, value) => {
48
+ return `${fieldPathToString(field)} < ${escapeValue(value)}`;
49
+ },
50
+ lte: (field, value) => {
51
+ return `${fieldPathToString(field)} <= ${escapeValue(value)}`;
52
+ },
53
+ and: (...conditions) => {
54
+ if (conditions.length === 0) return "";
55
+ if (conditions.length === 1) return conditions[0];
56
+ return `(${conditions.join(" && ")})`;
57
+ },
58
+ or: (...conditions) => {
59
+ if (conditions.length === 0) return "";
60
+ if (conditions.length === 1) return conditions[0];
61
+ return `(${conditions.join(" || ")})`;
62
+ },
63
+ not: (condition) => {
64
+ return `!(${condition})`;
65
+ },
66
+ in: (field, values) => {
67
+ if (!Array.isArray(values)) {
68
+ values = [values];
69
+ }
70
+ const conditions = values.map((v) => `${fieldPathToString(field)} = ${escapeValue(v)}`);
71
+ return conditions.length > 1 ? `(${conditions.join(" || ")})` : conditions[0];
72
+ },
73
+ like: (field, value) => {
74
+ return `${fieldPathToString(field)} ~ ${escapeValue(value)}`;
75
+ },
76
+ isNull: (field) => {
77
+ return `${fieldPathToString(field)} = null`;
78
+ },
79
+ isUndefined: (field) => {
80
+ return `${fieldPathToString(field)} = null`;
81
+ }
82
+ },
83
+ onUnknownOperator: (operator, args) => {
84
+ throw new Error(
85
+ `Unsupported operator '${operator}' for PocketBase filter conversion. Supported operators: eq, gt, gte, lt, lte, in, like, and, or, not, isNull, isUndefined`
86
+ );
87
+ }
88
+ });
89
+ return result || void 0;
90
+ }
91
+ function convertToPocketBaseSort(orderBy) {
92
+ if (!orderBy) {
93
+ return void 0;
94
+ }
95
+ const sorts = parseOrderByExpression(orderBy);
96
+ if (sorts.length === 0) {
97
+ return void 0;
98
+ }
99
+ return sorts.map((sort) => {
100
+ const field = fieldPathToString(sort.field);
101
+ return sort.direction === "desc" ? `-${field}` : field;
102
+ }).join(",");
103
+ }
7
104
 
8
105
  // src/logger.ts
9
106
  var defaultLogger = {
@@ -12,6 +109,9 @@ var defaultLogger = {
12
109
  console.log(`[pbtsdb] ${msg}`, context || "");
13
110
  }
14
111
  },
112
+ info: (msg, context) => {
113
+ console.info(`[pbtsdb] ${msg}`, context || "");
114
+ },
15
115
  warn: (msg, context) => {
16
116
  console.warn(`[pbtsdb] ${msg}`, context || "");
17
117
  },
@@ -22,6 +122,7 @@ var defaultLogger = {
22
122
  var currentLogger = defaultLogger;
23
123
  var logger = {
24
124
  debug: (msg, context) => currentLogger.debug(msg, context),
125
+ info: (msg, context) => currentLogger.info(msg, context),
25
126
  warn: (msg, context) => currentLogger.warn(msg, context),
26
127
  error: (msg, context) => currentLogger.error(msg, context)
27
128
  };
@@ -32,42 +133,94 @@ function resetLogger() {
32
133
  currentLogger = defaultLogger;
33
134
  }
34
135
 
35
- // src/subscription-manager.ts
36
- var SUBSCRIPTION_CONFIG = {
37
- MAX_RECONNECT_ATTEMPTS: 5,
38
- BASE_RECONNECT_DELAY_MS: 1e3,
39
- DEFAULT_WAIT_TIMEOUT_MS: 5e3,
40
- CLEANUP_DELAY_MS: 5e3
41
- };
42
- function hasId(record) {
43
- return typeof record === "object" && record !== null && "id" in record && typeof record.id === "string";
44
- }
45
- function createPendingSubscriptionState(recordId) {
46
- return {
47
- unsubscribe: async () => {
48
- },
49
- recordId,
50
- reconnectAttempts: 0,
51
- isReconnecting: false
52
- };
53
- }
54
- function getSubscriptionKey(recordId) {
55
- return recordId || "*";
56
- }
57
- var SubscriptionManager = class {
58
- constructor(pocketbase) {
59
- this.pocketbase = pocketbase;
60
- this.subscriptions = /* @__PURE__ */ new Map();
61
- this.subscriptionPromises = /* @__PURE__ */ new Map();
62
- this.cleanupTimers = /* @__PURE__ */ new Map();
63
- this.subscriberCounts = /* @__PURE__ */ new Map();
64
- }
65
- // ============================================================================
66
- // Internal Helpers
67
- // ============================================================================
68
- async setupSubscription(collectionName, collection, recordId) {
69
- const subscriptionKey = getSubscriptionKey(recordId);
70
- const eventHandler = (event) => {
136
+ // src/collection.ts
137
+ function createCollection(pb, queryClient) {
138
+ return (collectionName, options) => {
139
+ const expandStores = options?.expand;
140
+ const expandString = expandStores ? Object.keys(expandStores).sort().join(",") : void 0;
141
+ async function fetchRecords(loadOptions) {
142
+ const filter = convertToPocketBaseFilter(loadOptions?.where);
143
+ const sort = convertToPocketBaseSort(loadOptions?.orderBy);
144
+ const limit = loadOptions?.limit;
145
+ let items;
146
+ if (!filter && !sort && !limit && !expandString) {
147
+ items = await pb.collection(collectionName).getFullList();
148
+ } else {
149
+ const result = await pb.collection(collectionName).getList(1, limit || 500, {
150
+ filter,
151
+ sort,
152
+ expand: expandString
153
+ });
154
+ items = result.items;
155
+ }
156
+ if (expandStores) {
157
+ for (const record of items) {
158
+ const expandData = record.expand;
159
+ if (!expandData) continue;
160
+ for (const [key, value] of Object.entries(expandData)) {
161
+ const targetStore = expandStores[key];
162
+ if (!targetStore.utils) continue;
163
+ if (!targetStore.isReady()) {
164
+ if (targetStore.config?.syncMode === "on-demand") {
165
+ await targetStore._sync.startSync();
166
+ } else {
167
+ logger.warn(`not syncing ${key} on ${collectionName} because store is not yet ready`);
168
+ continue;
169
+ }
170
+ }
171
+ const values = Array.isArray(value) ? value : [value];
172
+ targetStore.utils.writeUpsert(values);
173
+ }
174
+ }
175
+ }
176
+ return items;
177
+ }
178
+ const collectionOptions = queryCollectionOptions({
179
+ queryClient,
180
+ queryKey: [collectionName],
181
+ syncMode: options?.syncMode ?? "eager",
182
+ queryFn: async (ctx) => {
183
+ return fetchRecords(ctx.meta?.loadSubsetOptions);
184
+ },
185
+ getKey: (item) => {
186
+ const record = item;
187
+ if (!record || typeof record !== "object" || !("id" in record)) {
188
+ throw new Error(`Record in collection '${collectionName}' is missing required 'id' field. Received: ${JSON.stringify(item)}`);
189
+ }
190
+ return record.id;
191
+ },
192
+ onInsert: options?.onInsert === false ? void 0 : options?.onInsert ?? (async ({ transaction }) => {
193
+ await Promise.all(
194
+ transaction.mutations.map(async (mutation) => {
195
+ const { created, updated, collectionId, collectionName: _, ...data } = mutation.modified;
196
+ await pb.collection(collectionName).create(data);
197
+ })
198
+ );
199
+ }),
200
+ onUpdate: options?.onUpdate === false ? void 0 : options?.onUpdate ?? (async ({ transaction }) => {
201
+ await Promise.all(
202
+ transaction.mutations.map(async (mutation) => {
203
+ const recordWithId = mutation.original;
204
+ await pb.collection(collectionName).update(recordWithId.id, mutation.changes);
205
+ })
206
+ );
207
+ }),
208
+ onDelete: options?.onDelete === false ? void 0 : options?.onDelete ?? (async ({ transaction }) => {
209
+ await Promise.all(
210
+ transaction.mutations.map(async (mutation) => {
211
+ const recordWithId = mutation.original;
212
+ await pb.collection(collectionName).delete(recordWithId.id);
213
+ })
214
+ );
215
+ })
216
+ });
217
+ const collection = createCollection$1(collectionOptions);
218
+ let unsubscribeFn = null;
219
+ let isSubscribed = false;
220
+ let subscriptionPromise = null;
221
+ let subscriptionResolve = null;
222
+ const handleRealtimeEvent = (event) => {
223
+ if (!collection.utils) return;
71
224
  collection.utils.writeBatch(() => {
72
225
  switch (event.action) {
73
226
  case "create":
@@ -77,469 +230,119 @@ var SubscriptionManager = class {
77
230
  collection.utils.writeUpdate(event.record);
78
231
  break;
79
232
  case "delete":
80
- if (!hasId(event.record)) {
81
- logger.error("Delete event record missing id field", {
82
- collectionName,
83
- record: event.record
84
- });
85
- return;
233
+ if (event.record && "id" in event.record) {
234
+ collection.utils.writeDelete(event.record.id);
86
235
  }
87
- collection.utils.writeDelete(event.record.id);
88
236
  break;
89
237
  }
90
238
  });
91
239
  };
92
- const unsubscribe = await this.pocketbase.collection(collectionName).subscribe(subscriptionKey, eventHandler);
93
- logger.debug("Subscription established", { collectionName, subscriptionKey });
94
- return unsubscribe;
95
- }
96
- async handleReconnection(collectionName, collection, recordId) {
97
- const collectionSubs = this.subscriptions.get(collectionName);
98
- if (!collectionSubs) return;
99
- const subscriptionKey = getSubscriptionKey(recordId);
100
- const state = collectionSubs.get(subscriptionKey);
101
- if (!state || state.isReconnecting) return;
102
- state.isReconnecting = true;
103
- logger.warn("Starting reconnection attempts", { collectionName, subscriptionKey });
104
- while (state.reconnectAttempts < SUBSCRIPTION_CONFIG.MAX_RECONNECT_ATTEMPTS) {
105
- const delay = SUBSCRIPTION_CONFIG.BASE_RECONNECT_DELAY_MS * Math.pow(2, state.reconnectAttempts);
106
- logger.debug(`Reconnection attempt ${state.reconnectAttempts + 1}, waiting ${delay}ms`, {
107
- collectionName,
108
- subscriptionKey
109
- });
110
- await new Promise((resolve) => setTimeout(resolve, delay));
111
- try {
112
- const newUnsubscribe = await this.setupSubscription(
113
- collectionName,
114
- collection,
115
- recordId
116
- );
117
- state.unsubscribe = newUnsubscribe;
118
- state.reconnectAttempts = 0;
119
- state.isReconnecting = false;
120
- logger.debug("Reconnection successful", { collectionName, subscriptionKey });
121
- return;
122
- } catch (error) {
123
- state.reconnectAttempts++;
124
- logger.warn(`Reconnection attempt ${state.reconnectAttempts} failed`, {
125
- collectionName,
126
- subscriptionKey,
127
- error
240
+ const startSubscription = async () => {
241
+ if (isSubscribed) return;
242
+ if (!subscriptionPromise) {
243
+ subscriptionPromise = new Promise((resolve) => {
244
+ subscriptionResolve = resolve;
128
245
  });
129
246
  }
130
- }
131
- logger.error("Max reconnection attempts reached", {
132
- collectionName,
133
- subscriptionKey,
134
- attempts: state.reconnectAttempts
135
- });
136
- state.isReconnecting = false;
137
- collectionSubs.delete(subscriptionKey);
138
- }
139
- // ============================================================================
140
- // Core Subscription Methods
141
- // ============================================================================
142
- /**
143
- * Subscribe to real-time updates for a collection.
144
- * Returns a promise that resolves when the subscription is fully established.
145
- *
146
- * @param collectionName - The PocketBase collection name
147
- * @param collection - The TanStack DB collection to sync with
148
- * @param recordId - Optional: Subscribe to specific record, or omit for collection-wide updates
149
- */
150
- async subscribe(collectionName, collection, recordId) {
151
- if (!this.subscriptions.has(collectionName)) {
152
- this.subscriptions.set(collectionName, /* @__PURE__ */ new Map());
153
- }
154
- if (!this.subscriptionPromises.has(collectionName)) {
155
- this.subscriptionPromises.set(collectionName, /* @__PURE__ */ new Map());
156
- }
157
- const collectionSubs = this.subscriptions.get(collectionName);
158
- const collectionPromises = this.subscriptionPromises.get(collectionName);
159
- const subscriptionKey = getSubscriptionKey(recordId);
160
- if (collectionSubs.has(subscriptionKey)) {
161
- logger.debug("Already subscribed, skipping", { collectionName, subscriptionKey });
162
- return;
163
- }
164
- const existingPromise = collectionPromises.get(subscriptionKey);
165
- if (existingPromise) {
166
- logger.debug("Pending subscription found, waiting", { collectionName, subscriptionKey });
167
- return existingPromise;
168
- }
169
- collectionSubs.set(subscriptionKey, createPendingSubscriptionState(recordId));
170
- const subscriptionPromise = (async () => {
171
247
  try {
172
- const unsubscribe = await this.setupSubscription(collectionName, collection, recordId);
173
- const state = collectionSubs.get(subscriptionKey);
174
- if (state) {
175
- state.unsubscribe = unsubscribe;
248
+ unsubscribeFn = await pb.collection(collectionName).subscribe("*", handleRealtimeEvent);
249
+ isSubscribed = true;
250
+ logger.debug("Subscription started", { collectionName });
251
+ if (subscriptionResolve) {
252
+ subscriptionResolve();
176
253
  }
177
254
  } catch (error) {
178
- collectionSubs.delete(subscriptionKey);
179
- collectionPromises.delete(subscriptionKey);
180
- logger.error("Subscription failed", { collectionName, subscriptionKey, error });
181
- await this.handleReconnection(collectionName, collection, recordId);
182
- throw error;
255
+ logger.error("Failed to start subscription", { collectionName, error });
183
256
  }
184
- })();
185
- collectionPromises.set(subscriptionKey, subscriptionPromise);
186
- try {
187
- await subscriptionPromise;
188
- } catch (_error) {
189
- }
190
- }
191
- /**
192
- * Unsubscribe from real-time updates.
193
- *
194
- * @param collectionName - The PocketBase collection name
195
- * @param recordId - Optional: Unsubscribe from specific record, or omit for collection-wide
196
- */
197
- unsubscribe(collectionName, recordId) {
198
- const collectionSubs = this.subscriptions.get(collectionName);
199
- if (!collectionSubs) return;
200
- const subscriptionKey = getSubscriptionKey(recordId);
201
- const state = collectionSubs.get(subscriptionKey);
202
- if (state) {
203
- const unsubPromise = state.unsubscribe();
204
- if (unsubPromise && typeof unsubPromise.catch === "function") {
205
- unsubPromise.catch((error) => {
206
- logger.debug("Unsubscribe failed (expected if connection closed)", {
207
- collectionName,
208
- subscriptionKey,
209
- error
210
- });
211
- });
212
- }
213
- collectionSubs.delete(subscriptionKey);
214
- logger.debug("Unsubscribed", { collectionName, subscriptionKey });
215
- }
216
- const collectionPromises = this.subscriptionPromises.get(collectionName);
217
- if (collectionPromises) {
218
- collectionPromises.delete(subscriptionKey);
219
- if (collectionPromises.size === 0) {
220
- this.subscriptionPromises.delete(collectionName);
257
+ };
258
+ const stopSubscription = async () => {
259
+ if (!isSubscribed || !unsubscribeFn) return;
260
+ try {
261
+ await unsubscribeFn();
262
+ unsubscribeFn = null;
263
+ isSubscribed = false;
264
+ subscriptionPromise = null;
265
+ subscriptionResolve = null;
266
+ logger.debug("Subscription stopped", { collectionName });
267
+ } catch (error) {
268
+ logger.debug("Unsubscribe failed (expected if connection closed)", { collectionName, error });
221
269
  }
222
- }
223
- if (collectionSubs.size === 0) {
224
- this.subscriptions.delete(collectionName);
225
- }
226
- }
227
- /**
228
- * Unsubscribe from all subscriptions for a collection.
229
- *
230
- * @param collectionName - The PocketBase collection name
231
- */
232
- unsubscribeAll(collectionName) {
233
- const collectionSubs = this.subscriptions.get(collectionName);
234
- if (!collectionSubs) return;
235
- logger.debug("Unsubscribing from all subscriptions", { collectionName, count: collectionSubs.size });
236
- for (const state of collectionSubs.values()) {
237
- const unsubPromise = state.unsubscribe();
238
- if (unsubPromise && typeof unsubPromise.catch === "function") {
239
- unsubPromise.catch((error) => {
240
- logger.debug("Unsubscribe failed (expected if connection closed)", {
241
- collectionName,
242
- error
243
- });
270
+ };
271
+ const waitForSubscription = async (timeout = 5e3) => {
272
+ if (isSubscribed) return;
273
+ if (!subscriptionPromise) {
274
+ await new Promise((resolve) => {
275
+ const checkInterval = setInterval(() => {
276
+ if (subscriptionPromise) {
277
+ clearInterval(checkInterval);
278
+ resolve();
279
+ }
280
+ }, 10);
281
+ setTimeout(() => {
282
+ clearInterval(checkInterval);
283
+ resolve();
284
+ }, timeout);
244
285
  });
245
286
  }
246
- }
247
- this.subscriptions.delete(collectionName);
248
- this.subscriptionPromises.delete(collectionName);
249
- }
250
- // ============================================================================
251
- // State Queries
252
- // ============================================================================
253
- /**
254
- * Check if subscribed to a collection.
255
- *
256
- * @param collectionName - The PocketBase collection name
257
- * @param recordId - Optional: Check specific record subscription, or omit for collection-wide
258
- */
259
- isSubscribed(collectionName, recordId) {
260
- const collectionSubs = this.subscriptions.get(collectionName);
261
- if (!collectionSubs) return false;
262
- const subscriptionKey = getSubscriptionKey(recordId);
263
- return collectionSubs.has(subscriptionKey);
264
- }
265
- /**
266
- * Wait for a subscription to be fully established (useful for testing).
267
- *
268
- * @param collectionName - The collection name
269
- * @param recordId - Optional specific record ID
270
- * @param timeoutMs - Timeout in milliseconds
271
- */
272
- async waitForSubscription(collectionName, recordId, timeoutMs = SUBSCRIPTION_CONFIG.DEFAULT_WAIT_TIMEOUT_MS) {
273
- const subscriptionKey = getSubscriptionKey(recordId);
274
- const collectionSubs = this.subscriptions.get(collectionName);
275
- if (collectionSubs?.has(subscriptionKey)) {
276
- const collectionPromises2 = this.subscriptionPromises.get(collectionName);
277
- if (!collectionPromises2?.has(subscriptionKey)) {
278
- return;
287
+ if (subscriptionPromise) {
288
+ await Promise.race([
289
+ subscriptionPromise,
290
+ new Promise(
291
+ (_, reject) => setTimeout(() => reject(new Error("Subscription timeout")), timeout)
292
+ )
293
+ ]);
279
294
  }
280
- }
281
- const collectionPromises = this.subscriptionPromises.get(collectionName);
282
- const promise = collectionPromises?.get(subscriptionKey);
283
- if (!promise) {
284
- const isSubscribed = collectionSubs?.has(subscriptionKey);
285
- if (!isSubscribed) {
286
- throw new Error(`No subscription found for ${collectionName}:${subscriptionKey}`);
295
+ };
296
+ collection.on("subscribers:change", (event) => {
297
+ const newCount = event.subscriberCount;
298
+ const previousCount = event.previousSubscriberCount;
299
+ if (newCount > 0 && previousCount === 0) {
300
+ startSubscription().catch(() => {
301
+ });
302
+ } else if (newCount === 0 && previousCount > 0) {
303
+ stopSubscription().catch(() => {
304
+ });
287
305
  }
288
- return;
289
- }
290
- const timeoutPromise = new Promise((_, reject) => {
291
- setTimeout(() => reject(new Error(`Subscription timeout after ${timeoutMs}ms`)), timeoutMs);
292
306
  });
293
- await Promise.race([promise, timeoutPromise]);
294
- }
295
- // ============================================================================
296
- // Lifecycle Management
297
- // ============================================================================
298
- /**
299
- * Track subscriber addition for a collection.
300
- * Automatically subscribes when first subscriber is added.
301
- *
302
- * @param collectionName - The PocketBase collection name
303
- * @param collection - The TanStack DB collection to sync with
304
- */
305
- async addSubscriber(collectionName, collection) {
306
- const currentCount = this.subscriberCounts.get(collectionName) || 0;
307
- const newCount = currentCount + 1;
308
- this.subscriberCounts.set(collectionName, newCount);
309
- logger.debug("Subscriber added", { collectionName, count: newCount });
310
- const cleanupTimer = this.cleanupTimers.get(collectionName);
311
- if (cleanupTimer) {
312
- clearTimeout(cleanupTimer);
313
- this.cleanupTimers.delete(collectionName);
314
- logger.debug("Cleanup timer cancelled", { collectionName });
315
- }
316
- if (newCount === 1 && !this.isSubscribed(collectionName)) {
317
- logger.debug("First subscriber - starting subscription", { collectionName });
318
- await this.subscribe(collectionName, collection);
319
- }
320
- }
321
- /**
322
- * Track subscriber removal for a collection.
323
- * Automatically unsubscribes (with delay) when last subscriber is removed.
324
- *
325
- * @param collectionName - The PocketBase collection name
326
- */
327
- removeSubscriber(collectionName) {
328
- const currentCount = this.subscriberCounts.get(collectionName) || 0;
329
- const newCount = Math.max(0, currentCount - 1);
330
- this.subscriberCounts.set(collectionName, newCount);
331
- logger.debug("Subscriber removed", { collectionName, count: newCount });
332
- if (newCount === 0) {
333
- const existingTimer = this.cleanupTimers.get(collectionName);
334
- if (existingTimer) {
335
- clearTimeout(existingTimer);
336
- }
337
- const cleanupTimer = setTimeout(() => {
338
- const finalCount = this.subscriberCounts.get(collectionName) || 0;
339
- if (finalCount === 0) {
340
- logger.debug("Cleanup timer fired - unsubscribing", { collectionName });
341
- this.unsubscribeAll(collectionName);
342
- this.subscriberCounts.delete(collectionName);
343
- }
344
- this.cleanupTimers.delete(collectionName);
345
- }, SUBSCRIPTION_CONFIG.CLEANUP_DELAY_MS);
346
- this.cleanupTimers.set(collectionName, cleanupTimer);
347
- logger.debug("Cleanup timer scheduled", {
348
- collectionName,
349
- delayMs: SUBSCRIPTION_CONFIG.CLEANUP_DELAY_MS
350
- });
351
- }
352
- }
353
- /**
354
- * Get the current subscriber count for a collection.
355
- * Useful for debugging and testing.
356
- *
357
- * @param collectionName - The PocketBase collection name
358
- * @returns Current subscriber count
359
- */
360
- getSubscriberCount(collectionName) {
361
- return this.subscriberCounts.get(collectionName) || 0;
362
- }
363
- };
364
- var CollectionsContext = createContext(null);
365
- function CollectionsProvider({ collections, children }) {
366
- return /* @__PURE__ */ jsx(CollectionsContext.Provider, { value: collections, children });
367
- }
368
- function useStore(key) {
369
- const context = useContext(CollectionsContext);
370
- if (!context) {
371
- throw new Error("useStore must be used within a CollectionsProvider");
372
- }
373
- if (!(key in context)) {
374
- throw new Error(`Collection "${key}" not found in CollectionsProvider`);
375
- }
376
- return context[key];
307
+ Object.assign(collection, {
308
+ collectionName,
309
+ waitForSubscription,
310
+ isSubscribed: () => isSubscribed
311
+ });
312
+ return collection;
313
+ };
377
314
  }
378
- function useStores(keys) {
379
- const context = useContext(CollectionsContext);
380
- if (!context) {
381
- throw new Error("useStores must be used within a CollectionsProvider");
382
- }
383
- const collections = keys.map((key) => {
384
- if (!(key in context)) {
385
- throw new Error(`Collection "${key}" not found in CollectionsProvider`);
315
+ function createReactProvider(collections) {
316
+ const Context = createContext(null);
317
+ const Provider = ({ children }) => /* @__PURE__ */ jsx(Context.Provider, { value: collections, children });
318
+ function useStore(...keys) {
319
+ const context = useContext(Context);
320
+ if (!context) {
321
+ throw new Error("useStore must be used within the Provider returned by createReactProvider");
386
322
  }
387
- return context[key];
388
- });
389
- return collections;
390
- }
391
-
392
- // src/collection.ts
393
- var CollectionFactory = class {
394
- constructor(pocketbase, queryClient) {
395
- this.pocketbase = pocketbase;
396
- this.queryClient = queryClient;
397
- this.subscriptionManager = new SubscriptionManager(pocketbase);
398
- }
399
- /**
400
- * Setup automatic subscription lifecycle management.
401
- * Hooks into TanStack DB's subscriber events to manage real-time subscriptions.
402
- */
403
- setupSubscriptionLifecycle(collectionName, baseCollection) {
404
- baseCollection.on("subscribers:change", (event) => {
405
- const newCount = event.subscriberCount;
406
- const previousCount = event.previousSubscriberCount;
407
- if (newCount > previousCount) {
408
- this.subscriptionManager.addSubscriber(collectionName, baseCollection).catch(() => {
409
- });
410
- } else if (newCount < previousCount) {
411
- this.subscriptionManager.removeSubscriber(collectionName);
323
+ return keys.map((key) => {
324
+ if (!(key in context)) {
325
+ throw new Error(`Collection "${String(key)}" not found in collections`);
412
326
  }
327
+ return context[key];
413
328
  });
414
329
  }
415
- /**
416
- * Create a TanStack DB collection from a PocketBase collection.
417
- *
418
- * Collections are lazy by default - they don't fetch data or subscribe until queried.
419
- * Real-time subscriptions automatically start when the first query becomes active
420
- * and stop when the last query unmounts (with a cleanup delay to prevent thrashing).
421
- *
422
- * @param collection - The name of the collection
423
- * @param options - Optional configuration including relations and expand
424
- *
425
- * @example
426
- * Basic usage with automatic lifecycle management:
427
- * ```ts
428
- * const jobsCollection = factory.create('jobs');
429
- *
430
- * // In your component - subscription starts automatically
431
- * const { data } = useLiveQuery((q) =>
432
- * q.from({ jobs: jobsCollection })
433
- * );
434
- * // Subscription stops automatically when component unmounts
435
- * ```
436
- *
437
- * @example
438
- * With query operators (filters, sorting):
439
- * ```ts
440
- * const jobsCollection = factory.create('jobs');
441
- *
442
- * // In your component:
443
- * const { data } = useLiveQuery((q) =>
444
- * q.from({ jobs: jobsCollection })
445
- * .where(({ jobs }) => and(
446
- * eq(jobs.status, 'ACTIVE'),
447
- * gt(jobs.created, new Date('2025-01-01'))
448
- * ))
449
- * .orderBy(({ jobs }) => jobs.created, 'desc')
450
- * );
451
- * ```
452
- *
453
- * @example
454
- * With relation expansion:
455
- * ```ts
456
- * const jobsCollection = factory.create('jobs', {
457
- * expand: 'customer,location'
458
- * });
459
- *
460
- * // Expanded relations available in record.expand
461
- * ```
462
- *
463
- * @example
464
- * With relations (for manual joins):
465
- * ```ts
466
- * const customersCollection = factory.create('customers');
467
- * const jobsCollection = factory.create('jobs', {
468
- * relations: { customer: customersCollection }
469
- * });
470
- *
471
- * // In your component, manually build joins:
472
- * const { data } = useLiveQuery((q) =>
473
- * q.from({ job: jobsCollection })
474
- * .join(
475
- * { customer: customersCollection },
476
- * ({ job, customer }) => eq(job.customer, customer.id),
477
- * "left"
478
- * )
479
- * .select(({ job, customer }) => ({
480
- * ...job,
481
- * expand: {
482
- * customer: customer ? { ...customer } : undefined
483
- * }
484
- * }))
485
- * );
486
- * ```
487
- *
488
- * @example
489
- * Manual subscription control (advanced):
490
- * ```ts
491
- * const jobsCollection = factory.create('jobs');
492
- *
493
- * // Manually subscribe to specific record (bypasses automatic lifecycle)
494
- * await jobsCollection.subscribe('record_id_123');
495
- *
496
- * // Check subscription status
497
- * const isSubbed = jobsCollection.isSubscribed('record_id_123');
498
- *
499
- * // Manually unsubscribe
500
- * jobsCollection.unsubscribe('record_id_123');
501
- * ```
502
- */
503
- create(collection, options) {
504
- const baseCollection = createCollection(
505
- queryCollectionOptions({
506
- queryKey: [collection],
507
- queryFn: async () => {
508
- const queryOptions = {};
509
- if (options?.expand) {
510
- queryOptions.expand = options.expand;
511
- }
512
- const result = await this.pocketbase.collection(collection).getFullList(queryOptions);
513
- return result;
514
- },
515
- queryClient: this.queryClient,
516
- getKey: (item) => item.id,
517
- startSync: options?.startSync ?? false
518
- })
519
- );
520
- const subscribableCollection = Object.assign(baseCollection, {
521
- subscribe: async (recordId) => {
522
- await this.subscriptionManager.subscribe(collection, baseCollection, recordId);
523
- },
524
- unsubscribe: (recordId) => {
525
- this.subscriptionManager.unsubscribe(collection, recordId);
526
- },
527
- unsubscribeAll: () => {
528
- this.subscriptionManager.unsubscribeAll(collection);
529
- },
530
- isSubscribed: (recordId) => {
531
- return this.subscriptionManager.isSubscribed(collection, recordId);
532
- },
533
- waitForSubscription: async (recordId, timeoutMs) => {
534
- await this.subscriptionManager.waitForSubscription(collection, recordId, timeoutMs);
535
- },
536
- relations: options?.relations || {}
537
- });
538
- this.setupSubscriptionLifecycle(collection, baseCollection);
539
- return subscribableCollection;
330
+ return {
331
+ Provider,
332
+ useStore
333
+ };
334
+ }
335
+
336
+ // src/util.ts
337
+ function newRecordId() {
338
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
339
+ let result = "";
340
+ for (let i = 0; i < 15; i++) {
341
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
540
342
  }
541
- };
343
+ return result;
344
+ }
542
345
 
543
- export { CollectionFactory, CollectionsProvider, SUBSCRIPTION_CONFIG, SubscriptionManager, resetLogger, setLogger, useStore, useStores };
346
+ export { createCollection, createReactProvider, newRecordId, resetLogger, setLogger };
544
347
  //# sourceMappingURL=index.js.map
545
348
  //# sourceMappingURL=index.js.map