@rebasepro/client 0.13.0 → 0.13.1-canary.g249daa1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +331 -79
- package/dist/index.es.js.map +1 -1
- package/dist/offline-query.d.ts +18 -3
- package/dist/offline-store.d.ts +8 -1
- package/dist/query-contract.types.d.ts +84 -0
- package/dist/sdk_query_builder.d.ts +45 -3
- package/package.json +4 -4
- package/src/auth-listener-errors.test.ts +57 -0
- package/src/auth-refresh-overflow.test.ts +89 -0
- package/src/auth.ts +30 -1
- package/src/collection-listen-meta.test.ts +105 -0
- package/src/collection-observe.test.ts +138 -0
- package/src/collection.ts +198 -55
- package/src/index.ts +51 -15
- package/src/like-pattern-redos.test.ts +61 -0
- package/src/offline-query.test.ts +28 -2
- package/src/offline-query.ts +40 -8
- package/src/offline-store.ts +5 -1
- package/src/offline.test.ts +114 -1
- package/src/offline.ts +123 -8
- package/src/query-contract.types.ts +114 -0
- package/src/realtime-optout.test.ts +15 -15
- package/src/realtime-subscription-key.test.ts +92 -0
- package/src/sdk_query_builder.ts +55 -3
- package/src/transport-baseurl.test.ts +49 -1
- package/src/transport.ts +34 -0
- package/src/vector-search-query.test.ts +55 -0
- package/src/websocket-url.test.ts +97 -0
- package/src/websocket.ts +18 -9
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { type OfflineApi, type OfflineConfig } from "./offline";
|
|
|
12
12
|
import { InsertOf, RebaseClient, RebaseSdkData, RowOf, StorageSource, StorageSourceDefinition, StorageSourceRegistry, UpdateOf } from "@rebasepro/types";
|
|
13
13
|
export { RebaseApiError } from "./transport";
|
|
14
14
|
export { RebaseClientError } from "./errors";
|
|
15
|
+
export type { RebaseErrorCode } from "@rebasepro/types";
|
|
15
16
|
export type { RebaseClientConfig, FindParams, FindResponse } from "./transport";
|
|
16
17
|
export type { CollectionClient } from "./collection";
|
|
17
18
|
export type { FindResult, SDKCollectionClient, SDKQueryBuilderInterface, PaginationMeta } from "@rebasepro/types";
|
package/dist/index.es.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, RebaseClientError as RebaseClientError$1, Vector, isPublicStoragePath, toCanonicalOp } from "@rebasepro/types";
|
|
2
|
-
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, or, paginateFind, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
2
|
+
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, or, paginateFind, resolveFindWindow, serializeFilter, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
4
4
|
//#region src/reviver.ts
|
|
5
5
|
function rebaseReviver(_key, value) {
|
|
@@ -91,7 +91,17 @@ function buildQueryString(params) {
|
|
|
91
91
|
const wire = serializeOrderBy(params.orderBy);
|
|
92
92
|
if (wire) parts.push(`orderBy=${encodeURIComponent(wire)}`);
|
|
93
93
|
}
|
|
94
|
-
if (params.searchString)
|
|
94
|
+
if (params.searchString) {
|
|
95
|
+
parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
|
|
96
|
+
if (params.searchExplain) parts.push("searchExplain=true");
|
|
97
|
+
}
|
|
98
|
+
if (params.vectorSearch) {
|
|
99
|
+
const vs = params.vectorSearch;
|
|
100
|
+
parts.push(`vector_search=${encodeURIComponent(vs.property)}`);
|
|
101
|
+
parts.push(`vector=${encodeURIComponent(JSON.stringify(vs.vector))}`);
|
|
102
|
+
if (vs.distance) parts.push(`vector_distance=${encodeURIComponent(vs.distance)}`);
|
|
103
|
+
if (vs.threshold !== void 0) parts.push(`vector_threshold=${encodeURIComponent(String(vs.threshold))}`);
|
|
104
|
+
}
|
|
95
105
|
if (params.include && params.include.length > 0) parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
|
|
96
106
|
if (params.logical) {
|
|
97
107
|
const root = params.logical;
|
|
@@ -131,6 +141,13 @@ function resolveBaseUrl(configured) {
|
|
|
131
141
|
function createTransport(config, environment) {
|
|
132
142
|
const fetchFn = config.fetch || globalThis.fetch;
|
|
133
143
|
const apiPath = config.apiPath || "/api";
|
|
144
|
+
for (const field of ["baseUrl", "storageUrlOrigin"]) {
|
|
145
|
+
const value = config[field];
|
|
146
|
+
if (!value || !apiPath) continue;
|
|
147
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
148
|
+
if (!trimmed.endsWith(apiPath)) continue;
|
|
149
|
+
console.warn(`[Rebase] ${field} ${JSON.stringify(value)} already ends with the API path ${JSON.stringify(apiPath)}, which is appended to it — requests will go to ${trimmed}${apiPath}/… and 404. Pass the origin only (${JSON.stringify(trimmed.slice(0, trimmed.length - apiPath.length) || "/")}), or set \`apiPath\` if the server really does mount the API one level deeper.`);
|
|
150
|
+
}
|
|
134
151
|
let token = config.token;
|
|
135
152
|
let tokenGetter;
|
|
136
153
|
let onUnauthorizedHandler = config.onUnauthorized;
|
|
@@ -316,6 +333,11 @@ function createAuth(transport, options) {
|
|
|
316
333
|
const authFlowMode = opts.authFlowMode || "json";
|
|
317
334
|
const STORAGE_KEY = "rebase_auth";
|
|
318
335
|
const REFRESH_BUFFER_MS = 12e4;
|
|
336
|
+
/**
|
|
337
|
+
* The largest delay `setTimeout` can hold — 2^31 - 1 ms, about 24.8 days.
|
|
338
|
+
* Anything larger is silently clamped to 1ms by Node and every browser.
|
|
339
|
+
*/
|
|
340
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
319
341
|
const MAX_REFRESH_RETRIES = 5;
|
|
320
342
|
const REFRESH_RETRY_BASE_MS = 1e3;
|
|
321
343
|
const REFRESH_RETRY_MAX_MS = 3e4;
|
|
@@ -343,7 +365,9 @@ function createAuth(transport, options) {
|
|
|
343
365
|
function emit(event, session) {
|
|
344
366
|
for (const fn of listeners) try {
|
|
345
367
|
fn(event, session);
|
|
346
|
-
} catch (e) {
|
|
368
|
+
} catch (e) {
|
|
369
|
+
console.error("Error in auth state change listener:", e);
|
|
370
|
+
}
|
|
347
371
|
}
|
|
348
372
|
function saveSession(session) {
|
|
349
373
|
if (!persistSession || authFlowMode === "cookie") return;
|
|
@@ -447,6 +471,10 @@ function createAuth(transport, options) {
|
|
|
447
471
|
attemptScheduledRefresh(0);
|
|
448
472
|
return;
|
|
449
473
|
}
|
|
474
|
+
if (delay > MAX_TIMER_DELAY_MS) {
|
|
475
|
+
refreshTimeout = setTimeout(() => scheduleRefresh(expiresAt), MAX_TIMER_DELAY_MS);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
450
478
|
refreshTimeout = setTimeout(() => {
|
|
451
479
|
attemptScheduledRefresh(0);
|
|
452
480
|
}, delay);
|
|
@@ -1230,9 +1258,55 @@ var SDKQueryBuilder = class {
|
|
|
1230
1258
|
}
|
|
1231
1259
|
/**
|
|
1232
1260
|
* Set a free-text search string if supported by the backend.
|
|
1261
|
+
*
|
|
1262
|
+
* By default this is a substring match across the collection's top-level
|
|
1263
|
+
* string properties. A Postgres collection that declares a `search` block
|
|
1264
|
+
* gets ranked full-text matching over the fields it named instead, and each
|
|
1265
|
+
* row comes back with a `_score` you can sort on:
|
|
1266
|
+
*
|
|
1267
|
+
* ```ts
|
|
1268
|
+
* client.data.talents.search("auditor iso 14001").orderBy("_score", "desc").find()
|
|
1269
|
+
* ```
|
|
1270
|
+
*
|
|
1271
|
+
* Pass `{ explain: true }` to have each row report which of the declared
|
|
1272
|
+
* fields matched, with a highlighted snippet, on `_matches`:
|
|
1273
|
+
*
|
|
1274
|
+
* ```ts
|
|
1275
|
+
* const { data } = await client.data.talents.search("iso 14001", { explain: true }).find();
|
|
1276
|
+
* data[0]._matches
|
|
1277
|
+
* // [{ field: "questionnaire.certifications", snippet: "<mark>ISO</mark> <mark>14001</mark> Lead Auditor" }]
|
|
1278
|
+
* ```
|
|
1233
1279
|
*/
|
|
1234
|
-
search(searchString) {
|
|
1280
|
+
search(searchString, options) {
|
|
1235
1281
|
this.params.searchString = searchString;
|
|
1282
|
+
if (options?.explain !== void 0) this.params.searchExplain = options.explain;
|
|
1283
|
+
return this;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Order rows by nearest-neighbour distance to `vector`.
|
|
1287
|
+
*
|
|
1288
|
+
* The server has supported this from the REST layer since vectors landed;
|
|
1289
|
+
* this is the SDK reaching it. Results come back closest-first with a
|
|
1290
|
+
* `_distance` on each row, and any `where` / `orderBy` on the same query is
|
|
1291
|
+
* a filter applied before the ordering — distance decides the order.
|
|
1292
|
+
*
|
|
1293
|
+
* You supply the query vector. Rebase stores and searches embeddings; it
|
|
1294
|
+
* does not produce them, so this is where whatever model you already use
|
|
1295
|
+
* for the stored vectors gets called.
|
|
1296
|
+
*
|
|
1297
|
+
* @param property - Name of the `vector` property to compare against.
|
|
1298
|
+
* @param vector - The query embedding. Its length must match the property's
|
|
1299
|
+
* declared `dimensions`, or the server answers 400.
|
|
1300
|
+
* @example
|
|
1301
|
+
* client.data.docs.vectorSearch("embedding", queryVector, { threshold: 0.35 }).limit(10).find()
|
|
1302
|
+
*/
|
|
1303
|
+
vectorSearch(property, vector, options) {
|
|
1304
|
+
this.params.vectorSearch = {
|
|
1305
|
+
property,
|
|
1306
|
+
vector,
|
|
1307
|
+
...options?.distance !== void 0 && { distance: options.distance },
|
|
1308
|
+
...options?.threshold !== void 0 && { threshold: options.threshold }
|
|
1309
|
+
};
|
|
1236
1310
|
return this;
|
|
1237
1311
|
}
|
|
1238
1312
|
/**
|
|
@@ -1270,6 +1344,11 @@ var SDKQueryBuilder = class {
|
|
|
1270
1344
|
};
|
|
1271
1345
|
//#endregion
|
|
1272
1346
|
//#region src/collection.ts
|
|
1347
|
+
/**
|
|
1348
|
+
* Counts currently in flight, keyed by the exact request they issue. Entries
|
|
1349
|
+
* live only for the duration of the request — see `count()` for why.
|
|
1350
|
+
*/
|
|
1351
|
+
var inflightCounts = /* @__PURE__ */ new Map();
|
|
1273
1352
|
function createCollectionClient(transport, slug, ws) {
|
|
1274
1353
|
const basePath = `/data/${slug}`;
|
|
1275
1354
|
const client = {
|
|
@@ -1314,18 +1393,63 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1314
1393
|
body: JSON.stringify({
|
|
1315
1394
|
rows: data,
|
|
1316
1395
|
...options?.upsert ? { upsert: true } : {}
|
|
1317
|
-
})
|
|
1396
|
+
}),
|
|
1397
|
+
...options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : {}
|
|
1318
1398
|
})).data || [];
|
|
1319
1399
|
},
|
|
1400
|
+
/**
|
|
1401
|
+
* Still `PUT`, deliberately, even though the server now serves `PATCH`
|
|
1402
|
+
* on the same handler and `PATCH` is the honest verb for a merge.
|
|
1403
|
+
*
|
|
1404
|
+
* The two are interchangeable server-side, so switching buys nothing at
|
|
1405
|
+
* runtime — and it costs compatibility in the direction that fails
|
|
1406
|
+
* quietly. A 0.14 client talking to a 0.13 server would send `PATCH` to
|
|
1407
|
+
* a route that does not exist and get a **404**, which is
|
|
1408
|
+
* indistinguishable from "that row is gone". Every write would look like
|
|
1409
|
+
* a missing record.
|
|
1410
|
+
*
|
|
1411
|
+
* `PATCH` is what the OpenAPI spec advertises, so anyone generating a
|
|
1412
|
+
* client gets the correct verb; this stays on `PUT` until the oldest
|
|
1413
|
+
* supported server is one that serves both.
|
|
1414
|
+
*/
|
|
1320
1415
|
async update(id, data) {
|
|
1321
1416
|
return await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
1322
1417
|
method: "PUT",
|
|
1323
1418
|
body: JSON.stringify(data)
|
|
1324
1419
|
});
|
|
1325
1420
|
},
|
|
1421
|
+
async updateMany(updates, options) {
|
|
1422
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
1423
|
+
if (updates.length === 0) return [];
|
|
1424
|
+
return (await transport.request(`${basePath}/bulk`, {
|
|
1425
|
+
method: "PATCH",
|
|
1426
|
+
body: JSON.stringify({ updates }),
|
|
1427
|
+
...options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : {}
|
|
1428
|
+
})).data || [];
|
|
1429
|
+
},
|
|
1326
1430
|
async delete(id) {
|
|
1327
1431
|
await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
|
|
1328
1432
|
},
|
|
1433
|
+
/**
|
|
1434
|
+
* `POST .../bulk/delete`, not `DELETE .../bulk`.
|
|
1435
|
+
*
|
|
1436
|
+
* The honest verb would take the ids in a DELETE body, and that is the
|
|
1437
|
+
* one request shape the HTTP ecosystem handles unreliably: bodies on
|
|
1438
|
+
* DELETE are permitted but widely dropped by proxies and CDNs, and
|
|
1439
|
+
* several OpenAPI generators ignore `requestBody` on a DELETE
|
|
1440
|
+
* operation, so a generated client would send the request without its
|
|
1441
|
+
* ids. A backend deployed behind arbitrary ingress cannot take that
|
|
1442
|
+
* bet. Same reason `:batchDelete` exists in Google's API guidelines.
|
|
1443
|
+
*/
|
|
1444
|
+
async deleteMany(ids, options) {
|
|
1445
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
1446
|
+
if (ids.length === 0) return;
|
|
1447
|
+
await transport.request(`${basePath}/bulk/delete`, {
|
|
1448
|
+
method: "POST",
|
|
1449
|
+
body: JSON.stringify({ ids }),
|
|
1450
|
+
...options?.idempotencyKey ? { headers: { "Idempotency-Key": options.idempotencyKey } } : {}
|
|
1451
|
+
});
|
|
1452
|
+
},
|
|
1329
1453
|
async count(params) {
|
|
1330
1454
|
const qs = buildQueryString({
|
|
1331
1455
|
...params,
|
|
@@ -1333,12 +1457,28 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1333
1457
|
offset: void 0,
|
|
1334
1458
|
include: void 0
|
|
1335
1459
|
});
|
|
1336
|
-
|
|
1460
|
+
const key = basePath + "/count" + qs;
|
|
1461
|
+
const inflight = inflightCounts.get(key);
|
|
1462
|
+
if (inflight) return inflight;
|
|
1463
|
+
const request = transport.request(key, { method: "GET" }).then((raw) => raw.count ?? 0);
|
|
1464
|
+
inflightCounts.set(key, request);
|
|
1465
|
+
try {
|
|
1466
|
+
return await request;
|
|
1467
|
+
} finally {
|
|
1468
|
+
inflightCounts.delete(key);
|
|
1469
|
+
}
|
|
1337
1470
|
},
|
|
1338
1471
|
observe(params, onResult, onError, options) {
|
|
1339
1472
|
let closed = false;
|
|
1340
|
-
|
|
1473
|
+
let liveDelivered = false;
|
|
1474
|
+
let signature;
|
|
1475
|
+
const deliver = (result, fromLive) => {
|
|
1341
1476
|
if (closed) return;
|
|
1477
|
+
if (fromLive) liveDelivered = true;
|
|
1478
|
+
else if (liveDelivered) return;
|
|
1479
|
+
const next = `${result.meta?.total ?? ""}|${JSON.stringify(result.data)}`;
|
|
1480
|
+
if (signature !== void 0 && next === signature) return;
|
|
1481
|
+
signature = next;
|
|
1342
1482
|
onResult({
|
|
1343
1483
|
...result,
|
|
1344
1484
|
fromCache: false,
|
|
@@ -1346,10 +1486,10 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1346
1486
|
partial: false
|
|
1347
1487
|
});
|
|
1348
1488
|
};
|
|
1349
|
-
client.find(params).then(
|
|
1489
|
+
client.find(params).then((result) => deliver(result, false)).catch((error) => {
|
|
1350
1490
|
if (!closed) onError?.(error);
|
|
1351
1491
|
});
|
|
1352
|
-
const live = options?.realtime !== false && client.listen ? client.listen(params,
|
|
1492
|
+
const live = options?.realtime !== false && client.listen ? client.listen(params, (result) => deliver(result, true), onError) : void 0;
|
|
1353
1493
|
return () => {
|
|
1354
1494
|
closed = true;
|
|
1355
1495
|
live?.();
|
|
@@ -1357,17 +1497,24 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1357
1497
|
},
|
|
1358
1498
|
observeById(id, onResult, onError, options) {
|
|
1359
1499
|
let closed = false;
|
|
1360
|
-
|
|
1500
|
+
let liveDelivered = false;
|
|
1501
|
+
let signature;
|
|
1502
|
+
const deliver = (row, fromLive) => {
|
|
1361
1503
|
if (closed) return;
|
|
1504
|
+
if (fromLive) liveDelivered = true;
|
|
1505
|
+
else if (liveDelivered) return;
|
|
1506
|
+
const next = row === void 0 ? "\0missing" : JSON.stringify(row);
|
|
1507
|
+
if (signature !== void 0 && next === signature) return;
|
|
1508
|
+
signature = next;
|
|
1362
1509
|
onResult(row, {
|
|
1363
1510
|
fromCache: false,
|
|
1364
1511
|
hasPendingWrites: false
|
|
1365
1512
|
});
|
|
1366
1513
|
};
|
|
1367
|
-
client.findById(id).then(
|
|
1514
|
+
client.findById(id).then((row) => deliver(row, false)).catch((error) => {
|
|
1368
1515
|
if (!closed) onError?.(error);
|
|
1369
1516
|
});
|
|
1370
|
-
const live = options?.realtime !== false && client.listenById ? client.listenById(id,
|
|
1517
|
+
const live = options?.realtime !== false && client.listenById ? client.listenById(id, (row) => deliver(row, true), onError) : void 0;
|
|
1371
1518
|
return () => {
|
|
1372
1519
|
closed = true;
|
|
1373
1520
|
live?.();
|
|
@@ -1387,8 +1534,11 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1387
1534
|
offset(count) {
|
|
1388
1535
|
return new SDKQueryBuilder(client).offset(count);
|
|
1389
1536
|
},
|
|
1390
|
-
search(searchString) {
|
|
1391
|
-
return new SDKQueryBuilder(client).search(searchString);
|
|
1537
|
+
search(searchString, options) {
|
|
1538
|
+
return new SDKQueryBuilder(client).search(searchString, options);
|
|
1539
|
+
},
|
|
1540
|
+
vectorSearch(property, vector, options) {
|
|
1541
|
+
return new SDKQueryBuilder(client).vectorSearch(property, vector, options);
|
|
1392
1542
|
},
|
|
1393
1543
|
include(...relations) {
|
|
1394
1544
|
return new SDKQueryBuilder(client).include(...relations);
|
|
@@ -1398,51 +1548,44 @@ function createCollectionClient(transport, slug, ws) {
|
|
|
1398
1548
|
client.listen = (params, onUpdate, onError) => {
|
|
1399
1549
|
let active = true;
|
|
1400
1550
|
let lastUpdateId = 0;
|
|
1551
|
+
let lastKnownTotal;
|
|
1552
|
+
const window = resolveFindWindow(params);
|
|
1401
1553
|
const unsub = ws.listenCollection({
|
|
1402
1554
|
path: slug,
|
|
1403
1555
|
filter: params?.where,
|
|
1556
|
+
logical: params?.logical,
|
|
1404
1557
|
limit: params?.limit,
|
|
1405
|
-
|
|
1558
|
+
offset: window.driverOffset,
|
|
1406
1559
|
orderBy: params?.orderBy?.[0],
|
|
1407
1560
|
order: params?.orderBy?.[1],
|
|
1408
|
-
searchString: params?.searchString
|
|
1561
|
+
searchString: params?.searchString,
|
|
1562
|
+
searchExplain: params?.searchExplain
|
|
1409
1563
|
}, (incomingRows) => {
|
|
1410
1564
|
const currentUpdateId = ++lastUpdateId;
|
|
1411
|
-
const requestedLimit =
|
|
1412
|
-
const offset =
|
|
1565
|
+
const requestedLimit = window.limit;
|
|
1566
|
+
const offset = window.offset;
|
|
1413
1567
|
const rows = incomingRows;
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (active && currentUpdateId === lastUpdateId) onUpdate({
|
|
1568
|
+
const emit = (total, hasMore) => {
|
|
1569
|
+
if (!active || currentUpdateId !== lastUpdateId) return;
|
|
1570
|
+
onUpdate({
|
|
1418
1571
|
data: rows,
|
|
1419
1572
|
meta: {
|
|
1420
1573
|
total,
|
|
1421
1574
|
limit: requestedLimit,
|
|
1422
1575
|
offset,
|
|
1423
|
-
hasMore
|
|
1576
|
+
hasMore
|
|
1424
1577
|
}
|
|
1425
1578
|
});
|
|
1579
|
+
};
|
|
1580
|
+
const emitWithoutCount = () => emit(offset + rows.length, rows.length >= requestedLimit);
|
|
1581
|
+
if (client.count) client.count(params).then((total) => {
|
|
1582
|
+
lastKnownTotal = total;
|
|
1583
|
+
emit(total, offset + rows.length < total);
|
|
1426
1584
|
}).catch(() => {
|
|
1427
|
-
if (
|
|
1428
|
-
|
|
1429
|
-
meta: {
|
|
1430
|
-
total: heuristicTotal,
|
|
1431
|
-
limit: requestedLimit,
|
|
1432
|
-
offset,
|
|
1433
|
-
hasMore: heuristicHasMore
|
|
1434
|
-
}
|
|
1435
|
-
});
|
|
1436
|
-
});
|
|
1437
|
-
else onUpdate({
|
|
1438
|
-
data: rows,
|
|
1439
|
-
meta: {
|
|
1440
|
-
total: heuristicTotal,
|
|
1441
|
-
limit: requestedLimit,
|
|
1442
|
-
offset,
|
|
1443
|
-
hasMore: heuristicHasMore
|
|
1444
|
-
}
|
|
1585
|
+
if (lastKnownTotal !== void 0) emit(lastKnownTotal, offset + rows.length < lastKnownTotal);
|
|
1586
|
+
else emitWithoutCount();
|
|
1445
1587
|
});
|
|
1588
|
+
else emitWithoutCount();
|
|
1446
1589
|
}, onError);
|
|
1447
1590
|
return () => {
|
|
1448
1591
|
active = false;
|
|
@@ -2833,15 +2976,10 @@ var RebaseWebSocketClient = class {
|
|
|
2833
2976
|
}
|
|
2834
2977
|
}
|
|
2835
2978
|
createCollectionSubscriptionKey(props) {
|
|
2979
|
+
const { collection, ...query } = props;
|
|
2836
2980
|
const key = {
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
limit: props.limit,
|
|
2840
|
-
startAfter: props.startAfter,
|
|
2841
|
-
orderBy: props.orderBy,
|
|
2842
|
-
order: props.order,
|
|
2843
|
-
searchString: props.searchString,
|
|
2844
|
-
collection: props.collection?.name
|
|
2981
|
+
...query,
|
|
2982
|
+
collection: collection?.name
|
|
2845
2983
|
};
|
|
2846
2984
|
return JSON.stringify(key, (_, value) => {
|
|
2847
2985
|
if (value && typeof value === "object" && !Array.isArray(value)) return Object.keys(value).sort().reduce((sorted, k) => {
|
|
@@ -3764,14 +3902,23 @@ function looseEquals(a, b) {
|
|
|
3764
3902
|
*/
|
|
3765
3903
|
function likeToRegExp(pattern, caseInsensitive) {
|
|
3766
3904
|
let source = "^";
|
|
3905
|
+
let lastWasWildcard = false;
|
|
3767
3906
|
for (let i = 0; i < pattern.length; i++) {
|
|
3768
3907
|
const char = pattern[i];
|
|
3769
3908
|
if (char === "\\" && i + 1 < pattern.length) {
|
|
3770
3909
|
source += pattern[i + 1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3771
3910
|
i++;
|
|
3772
|
-
|
|
3773
|
-
else if (char === "
|
|
3774
|
-
|
|
3911
|
+
lastWasWildcard = false;
|
|
3912
|
+
} else if (char === "%") {
|
|
3913
|
+
if (!lastWasWildcard) source += "[\\s\\S]*";
|
|
3914
|
+
lastWasWildcard = true;
|
|
3915
|
+
} else if (char === "_") {
|
|
3916
|
+
source += "[\\s\\S]";
|
|
3917
|
+
lastWasWildcard = false;
|
|
3918
|
+
} else {
|
|
3919
|
+
source += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3920
|
+
lastWasWildcard = false;
|
|
3921
|
+
}
|
|
3775
3922
|
}
|
|
3776
3923
|
return new RegExp(source + "$", caseInsensitive ? "i" : "");
|
|
3777
3924
|
}
|
|
@@ -3903,12 +4050,20 @@ function sortRows(rows, orderBy) {
|
|
|
3903
4050
|
function tiebreak(a, b) {
|
|
3904
4051
|
return compareValues(a.id, b.id) ?? 0;
|
|
3905
4052
|
}
|
|
3906
|
-
/**
|
|
4053
|
+
/**
|
|
4054
|
+
* Resolve `page`/`offset`/`limit` the way the server does.
|
|
4055
|
+
*
|
|
4056
|
+
* It did not: this defaulted an absent limit to 20 while `/api/data` pages by
|
|
4057
|
+
* 50, so the same `observe()` answered with 20 rows from the local database and
|
|
4058
|
+
* 50 from the network — a list that changed length depending on which side
|
|
4059
|
+
* answered, with `page` striding differently on each. Delegated now, so the
|
|
4060
|
+
* sentence above is true by construction rather than by agreement.
|
|
4061
|
+
*/
|
|
3907
4062
|
function resolvePagination(params) {
|
|
3908
|
-
const limit = params
|
|
4063
|
+
const { limit, offset } = resolveFindWindow(params);
|
|
3909
4064
|
return {
|
|
3910
4065
|
limit,
|
|
3911
|
-
offset
|
|
4066
|
+
offset
|
|
3912
4067
|
};
|
|
3913
4068
|
}
|
|
3914
4069
|
/**
|
|
@@ -3923,6 +4078,7 @@ function isExactlyEvaluable(params) {
|
|
|
3923
4078
|
if (!params) return true;
|
|
3924
4079
|
if (params.include && params.include.length > 0) return false;
|
|
3925
4080
|
if (params.searchString) return false;
|
|
4081
|
+
if (params.vectorSearch) return false;
|
|
3926
4082
|
return true;
|
|
3927
4083
|
}
|
|
3928
4084
|
/** Run a full query — filter, sort, paginate — over a set of rows. */
|
|
@@ -4238,6 +4394,72 @@ var OfflineManager = class {
|
|
|
4238
4394
|
this.notifyCollection(slug);
|
|
4239
4395
|
return rows;
|
|
4240
4396
|
},
|
|
4397
|
+
updateMany: async (updates, options) => {
|
|
4398
|
+
await this.ensureCollection(slug);
|
|
4399
|
+
if (!Array.isArray(updates)) throw new TypeError("updateMany expects an array of { id, data } entries.");
|
|
4400
|
+
if (updates.length === 0) return [];
|
|
4401
|
+
const anyPending = updates.some((u) => this.hasPending(slug, u.id));
|
|
4402
|
+
if (this.connectivity.shouldAttempt() && !anyPending) try {
|
|
4403
|
+
const rows = await inner.updateMany(updates, options);
|
|
4404
|
+
this.connectivity.markSuccess();
|
|
4405
|
+
await this.ingest(slug, rows);
|
|
4406
|
+
this.notifyCollection(slug);
|
|
4407
|
+
return rows;
|
|
4408
|
+
} catch (error) {
|
|
4409
|
+
if (!isNetworkError(error)) throw error;
|
|
4410
|
+
this.connectivity.markFailure();
|
|
4411
|
+
}
|
|
4412
|
+
const rollback = {};
|
|
4413
|
+
const optimistic = [];
|
|
4414
|
+
for (const { id, data } of updates) {
|
|
4415
|
+
const base = this.rawLocalRow(slug, id);
|
|
4416
|
+
rollback[String(id)] = base ?? null;
|
|
4417
|
+
optimistic.push({
|
|
4418
|
+
...base ?? {},
|
|
4419
|
+
...data,
|
|
4420
|
+
id
|
|
4421
|
+
});
|
|
4422
|
+
}
|
|
4423
|
+
await this.enqueue({
|
|
4424
|
+
collection: slug,
|
|
4425
|
+
type: "updateMany",
|
|
4426
|
+
updates: updates.map((u) => ({
|
|
4427
|
+
id: u.id,
|
|
4428
|
+
data: u.data
|
|
4429
|
+
})),
|
|
4430
|
+
rollback: { rows: rollback }
|
|
4431
|
+
});
|
|
4432
|
+
for (const row of optimistic) this.setLocalRow(slug, row.id, row);
|
|
4433
|
+
this.notifyCollection(slug);
|
|
4434
|
+
return optimistic;
|
|
4435
|
+
},
|
|
4436
|
+
deleteMany: async (ids, options) => {
|
|
4437
|
+
await this.ensureCollection(slug);
|
|
4438
|
+
if (!Array.isArray(ids)) throw new TypeError("deleteMany expects an array of ids.");
|
|
4439
|
+
if (ids.length === 0) return;
|
|
4440
|
+
const anyPending = ids.some((id) => this.hasPending(slug, id));
|
|
4441
|
+
if (this.connectivity.shouldAttempt() && !anyPending) try {
|
|
4442
|
+
await inner.deleteMany(ids, options);
|
|
4443
|
+
this.connectivity.markSuccess();
|
|
4444
|
+
for (const id of ids) this.removeLocalRow(slug, id, true);
|
|
4445
|
+
this.notifyCollection(slug);
|
|
4446
|
+
this.scheduleRefresh(slug);
|
|
4447
|
+
return;
|
|
4448
|
+
} catch (error) {
|
|
4449
|
+
if (!isNetworkError(error)) throw error;
|
|
4450
|
+
this.connectivity.markFailure();
|
|
4451
|
+
}
|
|
4452
|
+
const rollback = {};
|
|
4453
|
+
for (const id of ids) rollback[String(id)] = this.rawLocalRow(slug, id) ?? null;
|
|
4454
|
+
await this.enqueue({
|
|
4455
|
+
collection: slug,
|
|
4456
|
+
type: "deleteMany",
|
|
4457
|
+
ids,
|
|
4458
|
+
rollback: { rows: rollback }
|
|
4459
|
+
});
|
|
4460
|
+
for (const id of ids) this.removeLocalRow(slug, id, false);
|
|
4461
|
+
this.notifyCollection(slug);
|
|
4462
|
+
},
|
|
4241
4463
|
update: async (id, data) => {
|
|
4242
4464
|
await this.ensureCollection(slug);
|
|
4243
4465
|
if (this.connectivity.shouldAttempt() && !this.hasPending(slug, id)) try {
|
|
@@ -4316,7 +4538,8 @@ var OfflineManager = class {
|
|
|
4316
4538
|
orderBy: (column, direction) => new SDKQueryBuilder(wrapped).orderBy(column, direction),
|
|
4317
4539
|
limit: (count) => new SDKQueryBuilder(wrapped).limit(count),
|
|
4318
4540
|
offset: (count) => new SDKQueryBuilder(wrapped).offset(count),
|
|
4319
|
-
search: (searchString) => new SDKQueryBuilder(wrapped).search(searchString),
|
|
4541
|
+
search: (searchString, options) => new SDKQueryBuilder(wrapped).search(searchString, options),
|
|
4542
|
+
vectorSearch: (property, vector, options) => new SDKQueryBuilder(wrapped).vectorSearch(property, vector, options),
|
|
4320
4543
|
include: (...relations) => new SDKQueryBuilder(wrapped).include(...relations)
|
|
4321
4544
|
};
|
|
4322
4545
|
if (inner.listen) wrapped.listen = (params, onUpdate, onError) => inner.listen(params, (response) => {
|
|
@@ -4536,9 +4759,8 @@ var OfflineManager = class {
|
|
|
4536
4759
|
if (!state) return {
|
|
4537
4760
|
data: [],
|
|
4538
4761
|
meta: {
|
|
4762
|
+
...resolvePagination(params),
|
|
4539
4763
|
total: 0,
|
|
4540
|
-
limit: params?.limit ?? 20,
|
|
4541
|
-
offset: params?.offset ?? 0,
|
|
4542
4764
|
hasMore: false
|
|
4543
4765
|
},
|
|
4544
4766
|
fromCache: true,
|
|
@@ -4574,16 +4796,14 @@ var OfflineManager = class {
|
|
|
4574
4796
|
}
|
|
4575
4797
|
let added = 0;
|
|
4576
4798
|
const offset = snapshot.offset ?? 0;
|
|
4577
|
-
if (exact && offset === 0) {
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
added++;
|
|
4584
|
-
}
|
|
4585
|
-
if (added > 0 && params?.orderBy) sortRows(rows, params.orderBy);
|
|
4799
|
+
if (exact && offset === 0) for (const [key, entry] of state.rows) {
|
|
4800
|
+
if (seen.has(key) || !this.hasPending(slug, key)) continue;
|
|
4801
|
+
if (!this.isLocallyCreated(slug, key)) continue;
|
|
4802
|
+
if (!matchesParams(entry.row, params)) continue;
|
|
4803
|
+
rows.push(entry.row);
|
|
4804
|
+
added++;
|
|
4586
4805
|
}
|
|
4806
|
+
if (params?.orderBy) sortRows(rows, params.orderBy);
|
|
4587
4807
|
return {
|
|
4588
4808
|
data: rows,
|
|
4589
4809
|
meta: {
|
|
@@ -4730,17 +4950,17 @@ var OfflineManager = class {
|
|
|
4730
4950
|
return row;
|
|
4731
4951
|
}
|
|
4732
4952
|
recordSnapshot(slug, params, result) {
|
|
4953
|
+
const window = resolvePagination(params);
|
|
4733
4954
|
const meta = result.meta ?? {
|
|
4734
4955
|
total: result.data?.length ?? 0,
|
|
4735
|
-
|
|
4736
|
-
offset: 0,
|
|
4956
|
+
...window,
|
|
4737
4957
|
hasMore: false
|
|
4738
4958
|
};
|
|
4739
4959
|
const snapshot = {
|
|
4740
4960
|
ids: (result.data ?? []).map((row) => row.id).filter((id) => id !== void 0),
|
|
4741
4961
|
total: meta.total ?? result.data?.length ?? 0,
|
|
4742
|
-
limit: meta.limit ??
|
|
4743
|
-
offset: meta.offset ??
|
|
4962
|
+
limit: meta.limit ?? window.limit,
|
|
4963
|
+
offset: meta.offset ?? window.offset,
|
|
4744
4964
|
hasMore: meta.hasMore ?? false
|
|
4745
4965
|
};
|
|
4746
4966
|
const state = this.collectionState(slug);
|
|
@@ -4955,11 +5175,25 @@ var OfflineManager = class {
|
|
|
4955
5175
|
await this.adoptServerRow(op, op.id, row);
|
|
4956
5176
|
} else if (op.type === "createMany") {
|
|
4957
5177
|
const queued = op.data ?? [];
|
|
4958
|
-
const rows = await inner.createMany(queued,
|
|
5178
|
+
const rows = await inner.createMany(queued, {
|
|
5179
|
+
...op.upsert ? { upsert: true } : {},
|
|
5180
|
+
idempotencyKey: op.mutationId
|
|
5181
|
+
});
|
|
4959
5182
|
for (let i = 0; i < rows.length; i++) await this.adoptServerRow(op, queued[i]?.id, rows[i]);
|
|
5183
|
+
} else if (op.type === "updateMany") {
|
|
5184
|
+
const queued = op.updates ?? [];
|
|
5185
|
+
const rows = await inner.updateMany(queued.map((u) => ({
|
|
5186
|
+
id: u.id,
|
|
5187
|
+
data: u.data
|
|
5188
|
+
})), { idempotencyKey: op.mutationId });
|
|
5189
|
+
for (let i = 0; i < rows.length; i++) await this.ingestReplaced(op, queued[i].id, rows[i]);
|
|
4960
5190
|
} else if (op.type === "update") {
|
|
4961
5191
|
const row = await inner.update(op.id, op.data);
|
|
4962
5192
|
await this.ingestReplaced(op, op.id, row);
|
|
5193
|
+
} else if (op.type === "deleteMany") {
|
|
5194
|
+
const ids = op.ids ?? [];
|
|
5195
|
+
await inner.deleteMany(ids, { idempotencyKey: op.mutationId });
|
|
5196
|
+
for (const id of ids) this.removeLocalRow(op.collection, id, true);
|
|
4963
5197
|
} else if (op.type === "delete") {
|
|
4964
5198
|
await inner.delete(op.id);
|
|
4965
5199
|
this.removeLocalRow(op.collection, op.id, true);
|
|
@@ -5204,23 +5438,37 @@ var OfflineManager = class {
|
|
|
5204
5438
|
/**
|
|
5205
5439
|
* Derive a WebSocket URL from an HTTP base URL.
|
|
5206
5440
|
* `http://` → `ws://`, `https://` → `wss://`.
|
|
5441
|
+
*
|
|
5442
|
+
* A backend mounted under a path is the reason `baseUrl` accepts one, so the
|
|
5443
|
+
* path is kept. It used to be kept for an absolute `baseUrl` and dropped for a
|
|
5444
|
+
* relative one — resolved through `.origin` — so one deployment dialled two
|
|
5445
|
+
* different sockets depending on whether its config said `"/backend"` or
|
|
5446
|
+
* `"https://app.example.com/backend"`.
|
|
5447
|
+
*
|
|
5448
|
+
* Returns `""` when there is nothing to resolve against: a relative `baseUrl`
|
|
5449
|
+
* outside a browser has no origin, and inventing one would dial somewhere
|
|
5450
|
+
* arbitrary. The caller warns rather than leaving that silent.
|
|
5207
5451
|
*/
|
|
5208
5452
|
function deriveWebSocketUrl(baseUrl) {
|
|
5453
|
+
const toWsProtocol = (url) => {
|
|
5454
|
+
const secure = /^(https|wss):/i.test(url);
|
|
5455
|
+
return url.replace(/^https?:\/\//i, secure ? "wss://" : "ws://").replace(/^wss?:\/\//i, secure ? "wss://" : "ws://").replace(/\/$/, "");
|
|
5456
|
+
};
|
|
5209
5457
|
if (typeof window !== "undefined") {
|
|
5210
|
-
let absoluteUrl
|
|
5458
|
+
let absoluteUrl;
|
|
5211
5459
|
if (!baseUrl) absoluteUrl = window.location.origin;
|
|
5212
5460
|
else if (/^https?:\/\//i.test(baseUrl) || /^wss?:\/\//i.test(baseUrl)) absoluteUrl = baseUrl;
|
|
5213
5461
|
else try {
|
|
5214
|
-
|
|
5462
|
+
const resolved = new URL(baseUrl, window.location.href);
|
|
5463
|
+
absoluteUrl = resolved.origin + resolved.pathname;
|
|
5215
5464
|
} catch {
|
|
5216
5465
|
absoluteUrl = window.location.origin;
|
|
5217
5466
|
}
|
|
5218
|
-
|
|
5219
|
-
return absoluteUrl.replace(/^https?:\/\//i, `${protocol}//`).replace(/^wss?:\/\//i, `${protocol}//`).replace(/\/$/, "");
|
|
5467
|
+
return toWsProtocol(absoluteUrl);
|
|
5220
5468
|
}
|
|
5221
5469
|
if (!baseUrl) return "";
|
|
5222
5470
|
if (!/^https?:\/\//i.test(baseUrl) && !/^wss?:\/\//i.test(baseUrl)) return "";
|
|
5223
|
-
return baseUrl
|
|
5471
|
+
return toWsProtocol(baseUrl);
|
|
5224
5472
|
}
|
|
5225
5473
|
function createRebaseClient(options) {
|
|
5226
5474
|
const transport = createTransport(options, { credentialOutOfBand: options.auth?.authFlowMode === "cookie" });
|
|
@@ -5248,7 +5496,11 @@ function createRebaseClient(options) {
|
|
|
5248
5496
|
});
|
|
5249
5497
|
return storageSourcesPromise;
|
|
5250
5498
|
};
|
|
5251
|
-
const
|
|
5499
|
+
const realtimeEnabled = options.realtime !== false;
|
|
5500
|
+
const resolvedWsUrl = realtimeEnabled ? options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl) : void 0;
|
|
5501
|
+
const realtimeUnreachable = realtimeEnabled && !resolvedWsUrl;
|
|
5502
|
+
const unreachableReason = `no WebSocket URL could be derived from baseUrl ${JSON.stringify(options.baseUrl ?? null)} — outside a browser there is no page origin to resolve a relative URL against. Pass an absolute \`baseUrl\`, set \`websocketUrl\` explicitly, or pass \`realtime: false\` to say this was intended.`;
|
|
5503
|
+
if (realtimeUnreachable) console.warn(`[Rebase] Realtime is enabled but ${unreachableReason} Live queries will fall back to a single fetch and channels will throw.`);
|
|
5252
5504
|
let ws;
|
|
5253
5505
|
/** One channel object per name — see `realtime.channel`. */
|
|
5254
5506
|
const realtimeChannels = /* @__PURE__ */ new Map();
|
|
@@ -5368,7 +5620,7 @@ function createRebaseClient(options) {
|
|
|
5368
5620
|
* cut off the others.
|
|
5369
5621
|
*/
|
|
5370
5622
|
channel: (name, options) => {
|
|
5371
|
-
if (!ws) throw new RebaseClientError("Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
5623
|
+
if (!ws) throw new RebaseClientError(realtimeUnreachable ? `Realtime is enabled but ${unreachableReason}` : "Realtime is disabled on this client (realtime: false), so channels are unavailable.");
|
|
5372
5624
|
let existing = realtimeChannels.get(name);
|
|
5373
5625
|
if (!existing) {
|
|
5374
5626
|
existing = new RebaseRealtimeChannel(name, ws, options);
|