pbtsdb 0.7.2 → 0.8.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 +74 -28
- package/dist/chunk-KMR6I4ZC.js +950 -0
- package/dist/chunk-KMR6I4ZC.js.map +1 -0
- package/dist/core.d.ts +138 -60
- package/dist/core.js +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/llms.txt +11 -7
- package/package.json +4 -4
- package/dist/chunk-7DHFWXB3.js +0 -510
- package/dist/chunk-7DHFWXB3.js.map +0 -1
package/llms.txt
CHANGED
|
@@ -202,16 +202,12 @@ function BooksList() {
|
|
|
202
202
|
const c = createCollection<Schema>(pb, queryClient);
|
|
203
203
|
const authors = c('authors', {});
|
|
204
204
|
const books = c('books', {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
205
|
+
relations: { author: authors }, // where expanded authors are upserted
|
|
206
|
+
alwaysExpand: ['author'], // expanded on every fetch
|
|
208
207
|
});
|
|
209
208
|
|
|
210
|
-
// Expand is automatic - no .expand() call needed
|
|
211
209
|
const { data } = useLiveQuery((q) => q.from({ books }));
|
|
212
|
-
|
|
213
|
-
// Expanded records automatically inserted into authors collection
|
|
214
|
-
// data[0].expand?.author is typed!
|
|
210
|
+
// data[0].expand?.author is typed and populated
|
|
215
211
|
```
|
|
216
212
|
|
|
217
213
|
**When to use Auto-Expand:**
|
|
@@ -219,6 +215,14 @@ const { data } = useLiveQuery((q) => q.from({ books }));
|
|
|
219
215
|
- Performance: Single server query with server-side expand
|
|
220
216
|
- Related data always available when fetching parent records
|
|
221
217
|
|
|
218
|
+
**Per-query expand**
|
|
219
|
+
```typescript
|
|
220
|
+
const books = c('books', { relations: { author: authors, tags } });
|
|
221
|
+
const { data } = useLiveQuery((q) => q.from({ books: books.expand('tags') }));
|
|
222
|
+
// data[0].expand?.tags typed; nested paths like 'book.author' resolve through
|
|
223
|
+
// the target collection's own relations. Views share the base store.
|
|
224
|
+
```
|
|
225
|
+
|
|
222
226
|
**Approach 2: TanStack Joins**
|
|
223
227
|
```typescript
|
|
224
228
|
const c = createCollection<Schema>(pb, queryClient);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbtsdb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Type-safe PocketBase integration with TanStack Query and TanStack DB",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pocketbase",
|
|
@@ -78,9 +78,9 @@
|
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@biomejs/biome": "^2.5.0",
|
|
81
|
-
"@tanstack/db": ">=0.
|
|
82
|
-
"@tanstack/query-db-collection": ">=1.
|
|
83
|
-
"@tanstack/react-db": ">=0.
|
|
81
|
+
"@tanstack/db": ">=0.9.0",
|
|
82
|
+
"@tanstack/query-db-collection": ">=1.2.13",
|
|
83
|
+
"@tanstack/react-db": ">=0.3.8",
|
|
84
84
|
"@tanstack/react-query": ">=5.101.0",
|
|
85
85
|
"@testing-library/react": ">=16.3.2",
|
|
86
86
|
"@types/node": ">=25.0.10",
|
package/dist/chunk-7DHFWXB3.js
DELETED
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
import { createCollection as createCollection$1, parseWhereExpression, parseOrderByExpression } from '@tanstack/db';
|
|
2
|
-
export { BTreeIndex, BasicIndex, ReverseIndex, createEffect, toArray } from '@tanstack/db';
|
|
3
|
-
import { queryCollectionOptions, DeleteOperationItemNotFoundError } from '@tanstack/query-db-collection';
|
|
4
|
-
|
|
5
|
-
// src/core.ts
|
|
6
|
-
|
|
7
|
-
// src/logger.ts
|
|
8
|
-
var defaultLogger = {
|
|
9
|
-
debug: (msg, context) => {
|
|
10
|
-
if (process.env.NODE_ENV === "development") {
|
|
11
|
-
console.debug(`[pbtsdb] ${msg}`, context || "");
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
info: (msg, context) => {
|
|
15
|
-
console.info(`[pbtsdb] ${msg}`, context || "");
|
|
16
|
-
},
|
|
17
|
-
warn: (msg, context) => {
|
|
18
|
-
console.warn(`[pbtsdb] ${msg}`, context || "");
|
|
19
|
-
},
|
|
20
|
-
error: (msg, context) => {
|
|
21
|
-
console.error(`[pbtsdb] ${msg}`, context || "");
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
var currentLogger = defaultLogger;
|
|
25
|
-
var logger = {
|
|
26
|
-
debug: (msg, context) => currentLogger.debug(msg, context),
|
|
27
|
-
info: (msg, context) => currentLogger.info(msg, context),
|
|
28
|
-
warn: (msg, context) => currentLogger.warn(msg, context),
|
|
29
|
-
error: (msg, context) => currentLogger.error(msg, context)
|
|
30
|
-
};
|
|
31
|
-
function setLogger(customLogger) {
|
|
32
|
-
currentLogger = customLogger;
|
|
33
|
-
}
|
|
34
|
-
function resetLogger() {
|
|
35
|
-
currentLogger = defaultLogger;
|
|
36
|
-
}
|
|
37
|
-
function escapeValue(value) {
|
|
38
|
-
if (value === null) {
|
|
39
|
-
return "null";
|
|
40
|
-
}
|
|
41
|
-
if (typeof value === "boolean") {
|
|
42
|
-
return value ? "true" : "false";
|
|
43
|
-
}
|
|
44
|
-
if (typeof value === "number") {
|
|
45
|
-
return value.toString();
|
|
46
|
-
}
|
|
47
|
-
if (typeof value === "string") {
|
|
48
|
-
return `"${value.replace(/"/g, '\\"')}"`;
|
|
49
|
-
}
|
|
50
|
-
if (value instanceof Date) {
|
|
51
|
-
return `"${value.toISOString()}"`;
|
|
52
|
-
}
|
|
53
|
-
if (Array.isArray(value)) {
|
|
54
|
-
return `[${value.map(escapeValue).join(",")}]`;
|
|
55
|
-
}
|
|
56
|
-
return `"${String(value)}"`;
|
|
57
|
-
}
|
|
58
|
-
function fieldPathToString(path) {
|
|
59
|
-
return path.join(".");
|
|
60
|
-
}
|
|
61
|
-
function convertToPocketBaseFilter(where) {
|
|
62
|
-
if (!where) {
|
|
63
|
-
return void 0;
|
|
64
|
-
}
|
|
65
|
-
const result = parseWhereExpression(where, {
|
|
66
|
-
handlers: {
|
|
67
|
-
eq: (field, value) => {
|
|
68
|
-
return `${fieldPathToString(field)} = ${escapeValue(value)}`;
|
|
69
|
-
},
|
|
70
|
-
gt: (field, value) => {
|
|
71
|
-
return `${fieldPathToString(field)} > ${escapeValue(value)}`;
|
|
72
|
-
},
|
|
73
|
-
gte: (field, value) => {
|
|
74
|
-
return `${fieldPathToString(field)} >= ${escapeValue(value)}`;
|
|
75
|
-
},
|
|
76
|
-
lt: (field, value) => {
|
|
77
|
-
return `${fieldPathToString(field)} < ${escapeValue(value)}`;
|
|
78
|
-
},
|
|
79
|
-
lte: (field, value) => {
|
|
80
|
-
return `${fieldPathToString(field)} <= ${escapeValue(value)}`;
|
|
81
|
-
},
|
|
82
|
-
and: (...conditions) => {
|
|
83
|
-
if (conditions.length === 0) return "";
|
|
84
|
-
if (conditions.length === 1) return conditions[0];
|
|
85
|
-
return `(${conditions.join(" && ")})`;
|
|
86
|
-
},
|
|
87
|
-
or: (...conditions) => {
|
|
88
|
-
if (conditions.length === 0) return "";
|
|
89
|
-
if (conditions.length === 1) return conditions[0];
|
|
90
|
-
return `(${conditions.join(" || ")})`;
|
|
91
|
-
},
|
|
92
|
-
not: (condition) => {
|
|
93
|
-
return `!(${condition})`;
|
|
94
|
-
},
|
|
95
|
-
in: (field, values) => {
|
|
96
|
-
const valueArray = Array.isArray(values) ? values : [values];
|
|
97
|
-
const uniqueValues = [...new Set(valueArray)];
|
|
98
|
-
const fieldStr = fieldPathToString(field);
|
|
99
|
-
const conditions = uniqueValues.map((v) => `${fieldStr} = ${escapeValue(v)}`);
|
|
100
|
-
return conditions.length > 1 ? `(${conditions.join(" || ")})` : conditions[0];
|
|
101
|
-
},
|
|
102
|
-
like: (field, value) => {
|
|
103
|
-
return `${fieldPathToString(field)} ~ ${escapeValue(value)}`;
|
|
104
|
-
},
|
|
105
|
-
isNull: (field) => {
|
|
106
|
-
return `${fieldPathToString(field)} = null`;
|
|
107
|
-
},
|
|
108
|
-
isUndefined: (field) => {
|
|
109
|
-
return `${fieldPathToString(field)} = null`;
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
onUnknownOperator: (operator, _args) => {
|
|
113
|
-
throw new Error(
|
|
114
|
-
`Unsupported operator '${operator}' for PocketBase filter conversion. Supported operators: eq, gt, gte, lt, lte, in, like, and, or, not, isNull, isUndefined`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
return result || void 0;
|
|
119
|
-
}
|
|
120
|
-
function convertToPocketBaseSort(orderBy) {
|
|
121
|
-
if (!orderBy) {
|
|
122
|
-
return void 0;
|
|
123
|
-
}
|
|
124
|
-
const sorts = parseOrderByExpression(orderBy);
|
|
125
|
-
if (sorts.length === 0) {
|
|
126
|
-
return void 0;
|
|
127
|
-
}
|
|
128
|
-
return sorts.map((sort) => {
|
|
129
|
-
const field = fieldPathToString(sort.field);
|
|
130
|
-
return sort.direction === "desc" ? `-${field}` : field;
|
|
131
|
-
}).join(",");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// src/collection.ts
|
|
135
|
-
function createCollection(pb, queryClient, factoryOptions) {
|
|
136
|
-
return (collectionName, options) => {
|
|
137
|
-
const expandStores = options?.expand;
|
|
138
|
-
const expandString = expandStores ? Object.keys(expandStores).sort().join(",") : void 0;
|
|
139
|
-
const ignoreAutoCancellation = options?.ignoreAutoCancellation ?? true;
|
|
140
|
-
const refetchOnMutation = options?.refetchOnMutation ?? false;
|
|
141
|
-
async function upsertExpandedRelation(key, value, stores) {
|
|
142
|
-
const targetStore = stores[key];
|
|
143
|
-
if (!targetStore.utils) return;
|
|
144
|
-
if (!targetStore.isReady()) {
|
|
145
|
-
if (targetStore.config?.syncMode === "on-demand") {
|
|
146
|
-
await targetStore._sync.startSync();
|
|
147
|
-
} else {
|
|
148
|
-
logger.warn(
|
|
149
|
-
`not syncing ${key} on ${collectionName} because store is not yet ready`
|
|
150
|
-
);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const values = Array.isArray(value) ? value : [value];
|
|
155
|
-
targetStore.utils.writeUpsert(values);
|
|
156
|
-
}
|
|
157
|
-
async function upsertExpandedRelations(items) {
|
|
158
|
-
if (!expandStores) return;
|
|
159
|
-
for (const record of items) {
|
|
160
|
-
const expandData = record.expand;
|
|
161
|
-
if (!expandData) continue;
|
|
162
|
-
for (const [key, value] of Object.entries(expandData)) {
|
|
163
|
-
await upsertExpandedRelation(key, value, expandStores);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
async function fetchItems(loadOptions) {
|
|
168
|
-
const filter = convertToPocketBaseFilter(loadOptions?.where);
|
|
169
|
-
const sort = convertToPocketBaseSort(loadOptions?.orderBy);
|
|
170
|
-
const limit = loadOptions?.limit;
|
|
171
|
-
if (limit) {
|
|
172
|
-
const result = await pb.collection(collectionName).getList(1, limit, {
|
|
173
|
-
filter,
|
|
174
|
-
sort,
|
|
175
|
-
skipTotal: true,
|
|
176
|
-
// Optimize by skipping total count
|
|
177
|
-
expand: expandString
|
|
178
|
-
});
|
|
179
|
-
return result.items;
|
|
180
|
-
}
|
|
181
|
-
return await pb.collection(collectionName).getFullList({
|
|
182
|
-
filter,
|
|
183
|
-
sort,
|
|
184
|
-
expand: expandString
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
const inFlightConfirmedIds = /* @__PURE__ */ new Set();
|
|
188
|
-
function markConfirmedPresent(records) {
|
|
189
|
-
if (inFlightConfirmedIds.size === 0) return;
|
|
190
|
-
for (const record of records) {
|
|
191
|
-
const id = record?.id;
|
|
192
|
-
if (typeof id !== "string") continue;
|
|
193
|
-
for (const confirmed of inFlightConfirmedIds) confirmed.add(id);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
function withRowsConfirmedMidFlight(items, confirmedMidFlight) {
|
|
197
|
-
if (confirmedMidFlight.size === 0) return items;
|
|
198
|
-
const resultIds = new Set(
|
|
199
|
-
items.map((item) => item?.id)
|
|
200
|
-
);
|
|
201
|
-
const mergedIds = [...confirmedMidFlight].filter(
|
|
202
|
-
(id) => !resultIds.has(id) && collection._state.syncedData.has(id)
|
|
203
|
-
);
|
|
204
|
-
if (mergedIds.length === 0) return items;
|
|
205
|
-
logger.debug("Merging rows confirmed while fetch was in flight", {
|
|
206
|
-
collectionName,
|
|
207
|
-
ids: mergedIds
|
|
208
|
-
});
|
|
209
|
-
return [
|
|
210
|
-
...items,
|
|
211
|
-
...mergedIds.map((id) => collection._state.syncedData.get(id))
|
|
212
|
-
];
|
|
213
|
-
}
|
|
214
|
-
async function fetchRecords(loadOptions, queryKey) {
|
|
215
|
-
const confirmedMidFlight = /* @__PURE__ */ new Set();
|
|
216
|
-
inFlightConfirmedIds.add(confirmedMidFlight);
|
|
217
|
-
try {
|
|
218
|
-
let items;
|
|
219
|
-
try {
|
|
220
|
-
items = await fetchItems(loadOptions);
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (ignoreAutoCancellation && error instanceof Error && error.message.includes("autocancelled")) {
|
|
223
|
-
return withRowsConfirmedMidFlight(
|
|
224
|
-
queryClient.getQueryData(queryKey ?? [collectionName]) ?? [],
|
|
225
|
-
confirmedMidFlight
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
throw error;
|
|
229
|
-
}
|
|
230
|
-
await upsertExpandedRelations(items);
|
|
231
|
-
return withRowsConfirmedMidFlight(items, confirmedMidFlight);
|
|
232
|
-
} finally {
|
|
233
|
-
inFlightConfirmedIds.delete(confirmedMidFlight);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
const collectionOptions = queryCollectionOptions({
|
|
237
|
-
...options?.collectionOptions,
|
|
238
|
-
queryClient,
|
|
239
|
-
queryKey: [collectionName],
|
|
240
|
-
syncMode: options?.syncMode ?? "eager",
|
|
241
|
-
queryFn: async (ctx) => {
|
|
242
|
-
return fetchRecords(
|
|
243
|
-
ctx.meta?.loadSubsetOptions,
|
|
244
|
-
ctx.queryKey
|
|
245
|
-
);
|
|
246
|
-
},
|
|
247
|
-
getKey: (item) => {
|
|
248
|
-
const record = item;
|
|
249
|
-
if (!record || typeof record !== "object" || !("id" in record)) {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`Record in collection '${collectionName}' is missing required 'id' field. Received: ${JSON.stringify(item)}`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
return record.id;
|
|
255
|
-
},
|
|
256
|
-
onInsert: options?.onInsert === false ? void 0 : options?.onInsert ?? (async ({ transaction }) => {
|
|
257
|
-
const created = await Promise.all(
|
|
258
|
-
transaction.mutations.map(async (mutation) => {
|
|
259
|
-
const {
|
|
260
|
-
created: _created,
|
|
261
|
-
updated: _updated,
|
|
262
|
-
collectionId: _collectionId,
|
|
263
|
-
collectionName: _collectionName,
|
|
264
|
-
...data
|
|
265
|
-
} = mutation.modified;
|
|
266
|
-
return pb.collection(collectionName).create(data);
|
|
267
|
-
})
|
|
268
|
-
);
|
|
269
|
-
writeServerRecords(created);
|
|
270
|
-
return { refetch: refetchOnMutation };
|
|
271
|
-
}),
|
|
272
|
-
onUpdate: options?.onUpdate === false ? void 0 : options?.onUpdate ?? (async ({ transaction }) => {
|
|
273
|
-
const updated = await Promise.all(
|
|
274
|
-
transaction.mutations.map(async (mutation) => {
|
|
275
|
-
const recordWithId = mutation.original;
|
|
276
|
-
return pb.collection(collectionName).update(recordWithId.id, mutation.changes);
|
|
277
|
-
})
|
|
278
|
-
);
|
|
279
|
-
writeServerRecords(updated);
|
|
280
|
-
return { refetch: refetchOnMutation };
|
|
281
|
-
}),
|
|
282
|
-
onDelete: options?.onDelete === false ? void 0 : options?.onDelete ?? (async ({ transaction }) => {
|
|
283
|
-
await Promise.all(
|
|
284
|
-
transaction.mutations.map(async (mutation) => {
|
|
285
|
-
const recordWithId = mutation.original;
|
|
286
|
-
await pb.collection(collectionName).delete(recordWithId.id);
|
|
287
|
-
})
|
|
288
|
-
);
|
|
289
|
-
return { refetch: refetchOnMutation };
|
|
290
|
-
})
|
|
291
|
-
});
|
|
292
|
-
let applyingOwnWrite = false;
|
|
293
|
-
function writeOwn(fn) {
|
|
294
|
-
applyingOwnWrite = true;
|
|
295
|
-
try {
|
|
296
|
-
fn();
|
|
297
|
-
} finally {
|
|
298
|
-
applyingOwnWrite = false;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function syncedWriteKey(op) {
|
|
302
|
-
if (op.type !== "insert" && op.type !== "update") return null;
|
|
303
|
-
if (typeof op.key === "string") return op.key;
|
|
304
|
-
const id = op.value?.id;
|
|
305
|
-
return typeof id === "string" ? id : null;
|
|
306
|
-
}
|
|
307
|
-
function shouldDropSyncedWrite(op) {
|
|
308
|
-
const key = syncedWriteKey(op);
|
|
309
|
-
if (key === null) return false;
|
|
310
|
-
if (!applyingOwnWrite && hasPendingOptimisticMutation(key) && collection._state.syncedData.has(key)) {
|
|
311
|
-
logger.debug("Dropping synced write for optimistically-pending row", {
|
|
312
|
-
collectionName,
|
|
313
|
-
id: key
|
|
314
|
-
});
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
if (isStaleServerRecord(op.value, !applyingOwnWrite)) {
|
|
318
|
-
logger.debug("Dropping stale synced write", { collectionName, id: key });
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
const innerSync = collectionOptions.sync.sync;
|
|
324
|
-
collectionOptions.sync = {
|
|
325
|
-
...collectionOptions.sync,
|
|
326
|
-
sync: (params) => {
|
|
327
|
-
const guardedWrite = (message) => {
|
|
328
|
-
if (shouldDropSyncedWrite(
|
|
329
|
-
message
|
|
330
|
-
)) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
return params.write(message);
|
|
334
|
-
};
|
|
335
|
-
return innerSync({ ...params, write: guardedWrite });
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
const collection = createCollection$1(collectionOptions);
|
|
339
|
-
function hasPendingOptimisticMutation(key) {
|
|
340
|
-
const state = collection._state;
|
|
341
|
-
return state.optimisticUpserts.has(key) || state.optimisticDeletes.has(key);
|
|
342
|
-
}
|
|
343
|
-
function recordUpdatedAt(record) {
|
|
344
|
-
const updated = record?.updated;
|
|
345
|
-
return typeof updated === "string" && updated !== "" ? updated : void 0;
|
|
346
|
-
}
|
|
347
|
-
function isStaleServerRecord(record, treatEqualAsStale = false) {
|
|
348
|
-
const id = record?.id;
|
|
349
|
-
if (typeof id !== "string") return false;
|
|
350
|
-
const incoming = recordUpdatedAt(record);
|
|
351
|
-
if (!incoming) return false;
|
|
352
|
-
const current = recordUpdatedAt(
|
|
353
|
-
collection._state.syncedData.get(id)
|
|
354
|
-
);
|
|
355
|
-
if (current === void 0) return false;
|
|
356
|
-
return treatEqualAsStale ? incoming <= current : incoming < current;
|
|
357
|
-
}
|
|
358
|
-
function writeServerRecords(records) {
|
|
359
|
-
markConfirmedPresent(records);
|
|
360
|
-
if (!collection.utils || !collection.isReady()) return;
|
|
361
|
-
const fresh = records.filter((record) => !isStaleServerRecord(record));
|
|
362
|
-
if (fresh.length === 0) return;
|
|
363
|
-
writeOwn(() => collection.utils.writeUpsert(fresh));
|
|
364
|
-
}
|
|
365
|
-
function isStaleEcho(event) {
|
|
366
|
-
if (event.action !== "create" && event.action !== "update") return false;
|
|
367
|
-
if (!isStaleServerRecord(event.record)) return false;
|
|
368
|
-
logger.debug("Ignoring stale realtime echo", {
|
|
369
|
-
collectionName,
|
|
370
|
-
id: event.record?.id
|
|
371
|
-
});
|
|
372
|
-
return true;
|
|
373
|
-
}
|
|
374
|
-
let unsubscribeFn = null;
|
|
375
|
-
let isSubscribed = false;
|
|
376
|
-
let subscriptionPromise = null;
|
|
377
|
-
let subscriptionResolve = null;
|
|
378
|
-
function markEchoPresence(event) {
|
|
379
|
-
if (event.action !== "delete") markConfirmedPresent([event.record]);
|
|
380
|
-
}
|
|
381
|
-
const handleRealtimeEvent = (event) => {
|
|
382
|
-
if (!collection.utils) return;
|
|
383
|
-
markEchoPresence(event);
|
|
384
|
-
if (isStaleEcho(event)) return;
|
|
385
|
-
try {
|
|
386
|
-
writeOwn(
|
|
387
|
-
() => collection.utils.writeBatch(() => {
|
|
388
|
-
switch (event.action) {
|
|
389
|
-
case "create":
|
|
390
|
-
collection.utils.writeInsert(event.record);
|
|
391
|
-
break;
|
|
392
|
-
case "update":
|
|
393
|
-
collection.utils.writeUpsert(event.record);
|
|
394
|
-
break;
|
|
395
|
-
case "delete":
|
|
396
|
-
if (event.record && "id" in event.record) {
|
|
397
|
-
collection.utils.writeDelete(
|
|
398
|
-
event.record.id
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
})
|
|
404
|
-
);
|
|
405
|
-
} catch (error) {
|
|
406
|
-
if (error instanceof DeleteOperationItemNotFoundError) {
|
|
407
|
-
logger.debug("Ignoring delete echo for already-removed record", {
|
|
408
|
-
collectionName,
|
|
409
|
-
id: event.record?.id
|
|
410
|
-
});
|
|
411
|
-
} else {
|
|
412
|
-
throw error;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
const startSubscription = async () => {
|
|
417
|
-
if (isSubscribed) return;
|
|
418
|
-
if (!subscriptionPromise) {
|
|
419
|
-
subscriptionPromise = new Promise((resolve) => {
|
|
420
|
-
subscriptionResolve = resolve;
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
unsubscribeFn = await pb.collection(collectionName).subscribe("*", handleRealtimeEvent, factoryOptions?.subscribeOptions?.());
|
|
425
|
-
isSubscribed = true;
|
|
426
|
-
logger.debug("Subscription started", { collectionName });
|
|
427
|
-
if (subscriptionResolve) {
|
|
428
|
-
subscriptionResolve();
|
|
429
|
-
}
|
|
430
|
-
} catch (error) {
|
|
431
|
-
logger.error("Failed to start subscription", { collectionName, error });
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
const stopSubscription = async () => {
|
|
435
|
-
if (!isSubscribed || !unsubscribeFn) return;
|
|
436
|
-
try {
|
|
437
|
-
await unsubscribeFn();
|
|
438
|
-
unsubscribeFn = null;
|
|
439
|
-
isSubscribed = false;
|
|
440
|
-
subscriptionPromise = null;
|
|
441
|
-
subscriptionResolve = null;
|
|
442
|
-
logger.debug("Subscription stopped", { collectionName });
|
|
443
|
-
} catch (error) {
|
|
444
|
-
logger.debug("Unsubscribe failed (expected if connection closed)", {
|
|
445
|
-
collectionName,
|
|
446
|
-
error
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
const waitForSubscription = async (timeout = 5e3) => {
|
|
451
|
-
if (isSubscribed) return;
|
|
452
|
-
if (!subscriptionPromise) {
|
|
453
|
-
await new Promise((resolve) => {
|
|
454
|
-
const checkInterval = setInterval(() => {
|
|
455
|
-
if (subscriptionPromise) {
|
|
456
|
-
clearInterval(checkInterval);
|
|
457
|
-
resolve();
|
|
458
|
-
}
|
|
459
|
-
}, 10);
|
|
460
|
-
setTimeout(() => {
|
|
461
|
-
clearInterval(checkInterval);
|
|
462
|
-
resolve();
|
|
463
|
-
}, timeout);
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
if (subscriptionPromise) {
|
|
467
|
-
await Promise.race([
|
|
468
|
-
subscriptionPromise,
|
|
469
|
-
new Promise(
|
|
470
|
-
(_, reject) => setTimeout(() => reject(new Error("Subscription timeout")), timeout)
|
|
471
|
-
)
|
|
472
|
-
]);
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
collection.on(
|
|
476
|
-
"subscribers:change",
|
|
477
|
-
(event) => {
|
|
478
|
-
const newCount = event.subscriberCount;
|
|
479
|
-
const previousCount = event.previousSubscriberCount;
|
|
480
|
-
if (newCount > 0 && previousCount === 0) {
|
|
481
|
-
startSubscription().catch(() => {
|
|
482
|
-
});
|
|
483
|
-
} else if (newCount === 0 && previousCount > 0) {
|
|
484
|
-
stopSubscription().catch(() => {
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
);
|
|
489
|
-
Object.assign(collection, {
|
|
490
|
-
collectionName,
|
|
491
|
-
waitForSubscription,
|
|
492
|
-
isSubscribed: () => isSubscribed
|
|
493
|
-
});
|
|
494
|
-
return collection;
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// src/util.ts
|
|
499
|
-
function newRecordId() {
|
|
500
|
-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
501
|
-
let result = "";
|
|
502
|
-
for (let i = 0; i < 15; i++) {
|
|
503
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
504
|
-
}
|
|
505
|
-
return result;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
export { createCollection, newRecordId, resetLogger, setLogger };
|
|
509
|
-
//# sourceMappingURL=chunk-7DHFWXB3.js.map
|
|
510
|
-
//# sourceMappingURL=chunk-7DHFWXB3.js.map
|