pbtsdb 0.7.3 → 0.9.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 +184 -80
- package/dist/chunk-YWTU6QXM.js +857 -0
- package/dist/chunk-YWTU6QXM.js.map +1 -0
- package/dist/core.d.ts +105 -90
- package/dist/core.js +1 -1
- package/dist/index.d.ts +19 -13
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/llms.txt +37 -13
- package/package.json +4 -4
- package/dist/chunk-P3KUBRZK.js +0 -517
- package/dist/chunk-P3KUBRZK.js.map +0 -1
package/llms.txt
CHANGED
|
@@ -197,28 +197,52 @@ function BooksList() {
|
|
|
197
197
|
|
|
198
198
|
## Common Patterns
|
|
199
199
|
|
|
200
|
-
**Approach 1:
|
|
200
|
+
**Approach 1: Related data (Recommended)**
|
|
201
|
+
|
|
202
|
+
PocketBase `expand` only files related records into their own collections;
|
|
203
|
+
rows never carry `expand`. Read related records from the target collection.
|
|
204
|
+
|
|
201
205
|
```typescript
|
|
206
|
+
import { eq } from '@tanstack/db';
|
|
207
|
+
import { materialize } from 'pbtsdb';
|
|
208
|
+
|
|
202
209
|
const c = createCollection<Schema>(pb, queryClient);
|
|
203
|
-
const authors = c('authors', {});
|
|
210
|
+
const authors = c('authors', { syncMode: 'on-demand' });
|
|
204
211
|
const books = c('books', {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
212
|
+
relations: { author: authors }, // where expanded records are filed
|
|
213
|
+
alwaysFetchRelations: ['author'], // fetched with every books request
|
|
208
214
|
});
|
|
209
215
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
const { data } = useLiveQuery((q) =>
|
|
217
|
+
q.from({ b: books }).select(({ b }) => ({
|
|
218
|
+
...b,
|
|
219
|
+
author: materialize(
|
|
220
|
+
q.from({ a: authors }).where(({ a }) => eq(a.id, b.author)).findOne()
|
|
221
|
+
),
|
|
222
|
+
}))
|
|
223
|
+
);
|
|
224
|
+
// data[0].author?.name is Authors | undefined and updates when the author changes
|
|
215
225
|
```
|
|
216
226
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
227
|
+
An id-only load (`eq(id, x)`, `inArray(id, [...])`, or an `or` of those) is
|
|
228
|
+
served from the synced store when every id is present, except through a
|
|
229
|
+
`fetchRelations()` view, whose fetch always goes to PocketBase; anything else
|
|
230
|
+
is fetched in one batched request.
|
|
231
|
+
|
|
232
|
+
**When to use alwaysFetchRelations:**
|
|
233
|
+
- The parent is the only path by which those rows enter an on-demand collection
|
|
234
|
+
- Performance: single server request per fetch of the parent
|
|
220
235
|
- Related data always available when fetching parent records
|
|
221
236
|
|
|
237
|
+
**Per-query fetchRelations**
|
|
238
|
+
```typescript
|
|
239
|
+
const books = c('books', { relations: { author: authors, tags } });
|
|
240
|
+
const { data } = useLiveQuery((q) => q.from({ books: books.fetchRelations('tags') }));
|
|
241
|
+
// tags referenced by these books are filed into the tags collection; nested
|
|
242
|
+
// paths like 'book.author' resolve through the target's own relations.
|
|
243
|
+
// Views share the base store, subscription, and mutations.
|
|
244
|
+
```
|
|
245
|
+
|
|
222
246
|
**Approach 2: TanStack Joins**
|
|
223
247
|
```typescript
|
|
224
248
|
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.9.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-P3KUBRZK.js
DELETED
|
@@ -1,517 +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
|
-
writeBackAfterPersisted(transaction, 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
|
-
writeBackAfterPersisted(transaction, 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
|
-
function writeBackAfterPersisted(transaction, records) {
|
|
293
|
-
markConfirmedPresent(records);
|
|
294
|
-
void transaction.isPersisted.promise.then(
|
|
295
|
-
() => {
|
|
296
|
-
if (!collection.utils || !collection.isReady()) return;
|
|
297
|
-
const fresh = records.filter((record) => !isStaleServerRecord(record));
|
|
298
|
-
if (fresh.length === 0) return;
|
|
299
|
-
writeOwn(() => collection.utils.writeUpsert(fresh));
|
|
300
|
-
},
|
|
301
|
-
// A rejected transaction rolled its draft back; there is
|
|
302
|
-
// nothing to write and nothing to report here.
|
|
303
|
-
() => void 0
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
let applyingOwnWrite = false;
|
|
307
|
-
function writeOwn(fn) {
|
|
308
|
-
applyingOwnWrite = true;
|
|
309
|
-
try {
|
|
310
|
-
fn();
|
|
311
|
-
} finally {
|
|
312
|
-
applyingOwnWrite = false;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function syncedWriteKey(op) {
|
|
316
|
-
if (op.type !== "insert" && op.type !== "update") return null;
|
|
317
|
-
if (typeof op.key === "string") return op.key;
|
|
318
|
-
const id = op.value?.id;
|
|
319
|
-
return typeof id === "string" ? id : null;
|
|
320
|
-
}
|
|
321
|
-
function shouldDropSyncedWrite(op) {
|
|
322
|
-
const key = syncedWriteKey(op);
|
|
323
|
-
if (key === null) return false;
|
|
324
|
-
if (!applyingOwnWrite && hasPendingOptimisticMutation(key) && collection._state.syncedData.has(key)) {
|
|
325
|
-
logger.debug("Dropping synced write for optimistically-pending row", {
|
|
326
|
-
collectionName,
|
|
327
|
-
id: key
|
|
328
|
-
});
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
|
-
if (isStaleServerRecord(op.value)) {
|
|
332
|
-
logger.debug("Dropping stale synced write", { collectionName, id: key });
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
337
|
-
const innerSync = collectionOptions.sync.sync;
|
|
338
|
-
collectionOptions.sync = {
|
|
339
|
-
...collectionOptions.sync,
|
|
340
|
-
sync: (params) => {
|
|
341
|
-
const guardedWrite = (message) => {
|
|
342
|
-
if (shouldDropSyncedWrite(
|
|
343
|
-
message
|
|
344
|
-
)) {
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
return params.write(message);
|
|
348
|
-
};
|
|
349
|
-
return innerSync({ ...params, write: guardedWrite });
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
const collection = createCollection$1(collectionOptions);
|
|
353
|
-
function hasPendingOptimisticMutation(key) {
|
|
354
|
-
const state = collection._state;
|
|
355
|
-
return state.optimisticUpserts.has(key) || state.optimisticDeletes.has(key);
|
|
356
|
-
}
|
|
357
|
-
function recordUpdatedAt(record) {
|
|
358
|
-
const updated = record?.updated;
|
|
359
|
-
return typeof updated === "string" && updated !== "" ? updated : void 0;
|
|
360
|
-
}
|
|
361
|
-
function isStaleServerRecord(record) {
|
|
362
|
-
const id = record?.id;
|
|
363
|
-
if (typeof id !== "string") return false;
|
|
364
|
-
const incoming = recordUpdatedAt(record);
|
|
365
|
-
if (!incoming) return false;
|
|
366
|
-
const current = recordUpdatedAt(
|
|
367
|
-
collection._state.syncedData.get(id)
|
|
368
|
-
);
|
|
369
|
-
if (current === void 0) return false;
|
|
370
|
-
return incoming < current;
|
|
371
|
-
}
|
|
372
|
-
function isStaleEcho(event) {
|
|
373
|
-
if (event.action !== "create" && event.action !== "update") return false;
|
|
374
|
-
if (!isStaleServerRecord(event.record)) return false;
|
|
375
|
-
logger.debug("Ignoring stale realtime echo", {
|
|
376
|
-
collectionName,
|
|
377
|
-
id: event.record?.id
|
|
378
|
-
});
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
let unsubscribeFn = null;
|
|
382
|
-
let isSubscribed = false;
|
|
383
|
-
let subscriptionPromise = null;
|
|
384
|
-
let subscriptionResolve = null;
|
|
385
|
-
function markEchoPresence(event) {
|
|
386
|
-
if (event.action !== "delete") markConfirmedPresent([event.record]);
|
|
387
|
-
}
|
|
388
|
-
const handleRealtimeEvent = (event) => {
|
|
389
|
-
if (!collection.utils) return;
|
|
390
|
-
markEchoPresence(event);
|
|
391
|
-
if (isStaleEcho(event)) return;
|
|
392
|
-
try {
|
|
393
|
-
writeOwn(
|
|
394
|
-
() => collection.utils.writeBatch(() => {
|
|
395
|
-
switch (event.action) {
|
|
396
|
-
case "create":
|
|
397
|
-
collection.utils.writeInsert(event.record);
|
|
398
|
-
break;
|
|
399
|
-
case "update":
|
|
400
|
-
collection.utils.writeUpsert(event.record);
|
|
401
|
-
break;
|
|
402
|
-
case "delete":
|
|
403
|
-
if (event.record && "id" in event.record) {
|
|
404
|
-
collection.utils.writeDelete(
|
|
405
|
-
event.record.id
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
break;
|
|
409
|
-
}
|
|
410
|
-
})
|
|
411
|
-
);
|
|
412
|
-
} catch (error) {
|
|
413
|
-
if (error instanceof DeleteOperationItemNotFoundError) {
|
|
414
|
-
logger.debug("Ignoring delete echo for already-removed record", {
|
|
415
|
-
collectionName,
|
|
416
|
-
id: event.record?.id
|
|
417
|
-
});
|
|
418
|
-
} else {
|
|
419
|
-
throw error;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
const startSubscription = async () => {
|
|
424
|
-
if (isSubscribed) return;
|
|
425
|
-
if (!subscriptionPromise) {
|
|
426
|
-
subscriptionPromise = new Promise((resolve) => {
|
|
427
|
-
subscriptionResolve = resolve;
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
try {
|
|
431
|
-
unsubscribeFn = await pb.collection(collectionName).subscribe("*", handleRealtimeEvent, factoryOptions?.subscribeOptions?.());
|
|
432
|
-
isSubscribed = true;
|
|
433
|
-
logger.debug("Subscription started", { collectionName });
|
|
434
|
-
if (subscriptionResolve) {
|
|
435
|
-
subscriptionResolve();
|
|
436
|
-
}
|
|
437
|
-
} catch (error) {
|
|
438
|
-
logger.error("Failed to start subscription", { collectionName, error });
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
const stopSubscription = async () => {
|
|
442
|
-
if (!isSubscribed || !unsubscribeFn) return;
|
|
443
|
-
try {
|
|
444
|
-
await unsubscribeFn();
|
|
445
|
-
unsubscribeFn = null;
|
|
446
|
-
isSubscribed = false;
|
|
447
|
-
subscriptionPromise = null;
|
|
448
|
-
subscriptionResolve = null;
|
|
449
|
-
logger.debug("Subscription stopped", { collectionName });
|
|
450
|
-
} catch (error) {
|
|
451
|
-
logger.debug("Unsubscribe failed (expected if connection closed)", {
|
|
452
|
-
collectionName,
|
|
453
|
-
error
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
const waitForSubscription = async (timeout = 5e3) => {
|
|
458
|
-
if (isSubscribed) return;
|
|
459
|
-
if (!subscriptionPromise) {
|
|
460
|
-
await new Promise((resolve) => {
|
|
461
|
-
const checkInterval = setInterval(() => {
|
|
462
|
-
if (subscriptionPromise) {
|
|
463
|
-
clearInterval(checkInterval);
|
|
464
|
-
resolve();
|
|
465
|
-
}
|
|
466
|
-
}, 10);
|
|
467
|
-
setTimeout(() => {
|
|
468
|
-
clearInterval(checkInterval);
|
|
469
|
-
resolve();
|
|
470
|
-
}, timeout);
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
if (subscriptionPromise) {
|
|
474
|
-
await Promise.race([
|
|
475
|
-
subscriptionPromise,
|
|
476
|
-
new Promise(
|
|
477
|
-
(_, reject) => setTimeout(() => reject(new Error("Subscription timeout")), timeout)
|
|
478
|
-
)
|
|
479
|
-
]);
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
collection.on(
|
|
483
|
-
"subscribers:change",
|
|
484
|
-
(event) => {
|
|
485
|
-
const newCount = event.subscriberCount;
|
|
486
|
-
const previousCount = event.previousSubscriberCount;
|
|
487
|
-
if (newCount > 0 && previousCount === 0) {
|
|
488
|
-
startSubscription().catch(() => {
|
|
489
|
-
});
|
|
490
|
-
} else if (newCount === 0 && previousCount > 0) {
|
|
491
|
-
stopSubscription().catch(() => {
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
);
|
|
496
|
-
Object.assign(collection, {
|
|
497
|
-
collectionName,
|
|
498
|
-
waitForSubscription,
|
|
499
|
-
isSubscribed: () => isSubscribed
|
|
500
|
-
});
|
|
501
|
-
return collection;
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// src/util.ts
|
|
506
|
-
function newRecordId() {
|
|
507
|
-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
508
|
-
let result = "";
|
|
509
|
-
for (let i = 0; i < 15; i++) {
|
|
510
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
511
|
-
}
|
|
512
|
-
return result;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
export { createCollection, newRecordId, resetLogger, setLogger };
|
|
516
|
-
//# sourceMappingURL=chunk-P3KUBRZK.js.map
|
|
517
|
-
//# sourceMappingURL=chunk-P3KUBRZK.js.map
|