@rebasepro/client 0.20.0 → 0.21.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.es.js +90 -38
- package/dist/index.es.js.map +1 -1
- package/dist/realtime-channel.d.ts +11 -4
- package/dist/websocket.d.ts +2 -13
- package/package.json +4 -4
package/dist/index.es.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_STORAGE_SOURCE_KEY, EntityReference, EntityRelation, GeoPoint, PUBLIC_STORAGE_PREFIX, RebaseApiError, RebaseApiError as RebaseApiError$1, RebaseClientError, RebaseClientError as RebaseClientError$1, SCHEMA_VERSION_HEADER, Vector, hasFieldOperation, isPublicStoragePath, isUnsupported, parseRelationAggregateSort, sortKeyToString, toCanonicalOp, unsupportedMethod } from "@rebasepro/types";
|
|
2
2
|
import { COMPOSITE_ID_SEPARATOR, QueryBuilder, RebasePaginationError, and, buildCompositeId, collectAllPages, cond, mergeIncludeSpecs, normalizeOrderBy, not, or, paginateFind, resolveFindWindow, serializeFilter, serializeInclude, serializeLogicalCondition, serializeOrderBy } from "@rebasepro/common";
|
|
3
|
-
import { toSnakeCase } from "@rebasepro/utils";
|
|
3
|
+
import { toSnakeCase, unref } from "@rebasepro/utils";
|
|
4
4
|
//#region src/reviver.ts
|
|
5
5
|
function rebaseReviver(_key, value) {
|
|
6
6
|
if (value && typeof value === "object" && "__type" in value) {
|
|
@@ -2490,6 +2490,23 @@ var CHANNEL_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2490
2490
|
"channel_history"
|
|
2491
2491
|
]);
|
|
2492
2492
|
/**
|
|
2493
|
+
* The other direction: the frame types the *server* addresses by channel.
|
|
2494
|
+
*
|
|
2495
|
+
* Not the mirror of {@link CHANNEL_MESSAGE_TYPES} — a request and its answer
|
|
2496
|
+
* have different names (`presence_state` is the one type in both) — so the two
|
|
2497
|
+
* sets are listed separately rather than derived from each other. These are
|
|
2498
|
+
* exactly the members of `ChannelMessage`; adding one here without declaring it
|
|
2499
|
+
* there leaves the handler with a frame it cannot read.
|
|
2500
|
+
*/
|
|
2501
|
+
var CHANNEL_FRAME_TYPES = /* @__PURE__ */ new Set([
|
|
2502
|
+
"broadcast",
|
|
2503
|
+
"presence_state",
|
|
2504
|
+
"presence_diff",
|
|
2505
|
+
"channel_history",
|
|
2506
|
+
"error",
|
|
2507
|
+
"ERROR"
|
|
2508
|
+
]);
|
|
2509
|
+
/**
|
|
2493
2510
|
* Low-level realtime WebSocket client.
|
|
2494
2511
|
*
|
|
2495
2512
|
* @internal Not a stable app-facing API. `createRebaseClient()` constructs and
|
|
@@ -2500,6 +2517,28 @@ var CHANNEL_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2500
2517
|
* `@rebasepro/client-postgres` was removed; its surface may change without a
|
|
2501
2518
|
* major bump.
|
|
2502
2519
|
*/
|
|
2520
|
+
/**
|
|
2521
|
+
* Narrow a frame to the collection-update shape. See its use below.
|
|
2522
|
+
*
|
|
2523
|
+
* `WebSocketMessage.type` is `string`, so the message types that extend it do
|
|
2524
|
+
* not form a discriminated union and `type === "collection_update"` narrows
|
|
2525
|
+
* nothing on its own.
|
|
2526
|
+
*/
|
|
2527
|
+
function isCollectionUpdate(message) {
|
|
2528
|
+
return message.type === "collection_update";
|
|
2529
|
+
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Narrow a frame to one of the channel-addressed shapes.
|
|
2532
|
+
*
|
|
2533
|
+
* Both halves of the test are load-bearing. The type says which member of
|
|
2534
|
+
* {@link ChannelMessage} it is; `channel` is what a channel frame is *addressed
|
|
2535
|
+
* by*, and an `ERROR` from a server older than the change that names the
|
|
2536
|
+
* channel has none — it is a generic error, and falls through to the catch-all
|
|
2537
|
+
* warning rather than being delivered to a channel that was never named.
|
|
2538
|
+
*/
|
|
2539
|
+
function isChannelMessage(message) {
|
|
2540
|
+
return typeof message.channel === "string" && CHANNEL_FRAME_TYPES.has(message.type);
|
|
2541
|
+
}
|
|
2503
2542
|
var RebaseWebSocketClient = class {
|
|
2504
2543
|
websocketUrl;
|
|
2505
2544
|
ws = null;
|
|
@@ -2683,6 +2722,7 @@ var RebaseWebSocketClient = class {
|
|
|
2683
2722
|
clearTimeout(this.reconnectTimeout);
|
|
2684
2723
|
this.reconnectTimeout = null;
|
|
2685
2724
|
}
|
|
2725
|
+
this.suspendSubscribeWatchdogs();
|
|
2686
2726
|
if (this.ws) {
|
|
2687
2727
|
this.ws.onclose = null;
|
|
2688
2728
|
this.ws.onerror = null;
|
|
@@ -2864,7 +2904,7 @@ var RebaseWebSocketClient = class {
|
|
|
2864
2904
|
}
|
|
2865
2905
|
return;
|
|
2866
2906
|
}
|
|
2867
|
-
if (
|
|
2907
|
+
if (isChannelMessage(message)) {
|
|
2868
2908
|
const handlers = this.channelHandlers.get(message.channel);
|
|
2869
2909
|
if (handlers) for (const handler of [...handlers]) try {
|
|
2870
2910
|
handler(message);
|
|
@@ -2873,15 +2913,16 @@ var RebaseWebSocketClient = class {
|
|
|
2873
2913
|
}
|
|
2874
2914
|
return;
|
|
2875
2915
|
}
|
|
2876
|
-
if (subscriptionId &&
|
|
2916
|
+
if (subscriptionId && isCollectionUpdate(message)) {
|
|
2917
|
+
const collectionUpdate = message;
|
|
2877
2918
|
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
2878
2919
|
if (subscriptionKey) {
|
|
2879
2920
|
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
2880
2921
|
if (collectionSub) {
|
|
2881
2922
|
const incomingRows = message.rows || [];
|
|
2882
|
-
const updatePks =
|
|
2923
|
+
const updatePks = collectionUpdate.pks;
|
|
2883
2924
|
if (updatePks) collectionSub.pks = updatePks;
|
|
2884
|
-
const updateMeta =
|
|
2925
|
+
const updateMeta = collectionUpdate.meta;
|
|
2885
2926
|
if (updateMeta) collectionSub.latestMeta = updateMeta;
|
|
2886
2927
|
const rows = this.mergeRows(collectionSub.latestData, incomingRows, collectionSub.pks);
|
|
2887
2928
|
collectionSub.latestData = rows;
|
|
@@ -3747,7 +3788,7 @@ var RebaseRealtimeChannel = class {
|
|
|
3747
3788
|
this.catchUpInFlight = true;
|
|
3748
3789
|
if (this.catchUpTimeout) clearTimeout(this.catchUpTimeout);
|
|
3749
3790
|
this.catchUpTimeout = setTimeout(() => this.abandonCatchUp(), CATCH_UP_TIMEOUT_MS);
|
|
3750
|
-
this.catchUpTimeout
|
|
3791
|
+
unref(this.catchUpTimeout);
|
|
3751
3792
|
try {
|
|
3752
3793
|
await this.send("channel_history", {
|
|
3753
3794
|
sinceSeq: this.lastSeq,
|
|
@@ -3793,7 +3834,7 @@ var RebaseRealtimeChannel = class {
|
|
|
3793
3834
|
if (!this.trackedState) return;
|
|
3794
3835
|
this.send("presence_track", { state: this.trackedState }).catch(() => {});
|
|
3795
3836
|
}, PRESENCE_HEARTBEAT_MS);
|
|
3796
|
-
this.heartbeat
|
|
3837
|
+
unref(this.heartbeat);
|
|
3797
3838
|
}
|
|
3798
3839
|
}
|
|
3799
3840
|
/** Stop publishing presence, without leaving the channel. */
|
|
@@ -3921,12 +3962,11 @@ var RebaseRealtimeChannel = class {
|
|
|
3921
3962
|
handle(message) {
|
|
3922
3963
|
switch (message.type) {
|
|
3923
3964
|
case "presence_state":
|
|
3924
|
-
this.presences = message.presences
|
|
3965
|
+
this.presences = message.presences;
|
|
3925
3966
|
this.emitPresence();
|
|
3926
3967
|
break;
|
|
3927
3968
|
case "presence_diff": {
|
|
3928
|
-
const joins = message
|
|
3929
|
-
const leaves = message.leaves ?? {};
|
|
3969
|
+
const { joins, leaves } = message;
|
|
3930
3970
|
for (const [id, state] of Object.entries(joins)) this.presences[id] = state;
|
|
3931
3971
|
for (const id of Object.keys(leaves)) delete this.presences[id];
|
|
3932
3972
|
this.emitPresence({
|
|
@@ -3936,7 +3976,7 @@ var RebaseRealtimeChannel = class {
|
|
|
3936
3976
|
break;
|
|
3937
3977
|
}
|
|
3938
3978
|
case "broadcast": {
|
|
3939
|
-
const seq =
|
|
3979
|
+
const seq = message.seq;
|
|
3940
3980
|
const event = {
|
|
3941
3981
|
event: message.event,
|
|
3942
3982
|
payload: message.payload,
|
|
@@ -3961,9 +4001,7 @@ var RebaseRealtimeChannel = class {
|
|
|
3961
4001
|
clearTimeout(this.catchUpTimeout);
|
|
3962
4002
|
this.catchUpTimeout = null;
|
|
3963
4003
|
}
|
|
3964
|
-
const entries = message
|
|
3965
|
-
const retained = message.retained === true;
|
|
3966
|
-
const latestSeq = typeof message.latestSeq === "number" ? message.latestSeq : void 0;
|
|
4004
|
+
const { messages: entries, retained, latestSeq } = message;
|
|
3967
4005
|
for (const resolve of this.historyWaiters.splice(0)) resolve({
|
|
3968
4006
|
messages: entries,
|
|
3969
4007
|
retained,
|
|
@@ -3985,8 +4023,8 @@ var RebaseRealtimeChannel = class {
|
|
|
3985
4023
|
case "ERROR":
|
|
3986
4024
|
case "error": {
|
|
3987
4025
|
const raw = message.payload?.error;
|
|
3988
|
-
const asObject = typeof raw === "object"
|
|
3989
|
-
const text = asObject?.message ?? (typeof raw === "string" ? raw : void 0) ??
|
|
4026
|
+
const asObject = typeof raw === "object" ? raw : void 0;
|
|
4027
|
+
const text = asObject?.message ?? (typeof raw === "string" ? raw : void 0) ?? message.error ?? "The server refused a channel operation.";
|
|
3990
4028
|
const error = new RebaseApiError$1(text, { ...asObject?.code ? { code: asObject.code } : {} });
|
|
3991
4029
|
if (this.errorHandlers.size === 0) {
|
|
3992
4030
|
console.warn(`[Rebase] Channel "${this.name}" error${asObject?.code ? ` (${asObject.code})` : ""}: ${text}. Attach \`channel.onError(...)\` to handle it.`);
|
|
@@ -4285,7 +4323,7 @@ var ConnectivityMonitor = class {
|
|
|
4285
4323
|
this.timer = void 0;
|
|
4286
4324
|
this.onRetryDue?.();
|
|
4287
4325
|
}, delay);
|
|
4288
|
-
this.timer
|
|
4326
|
+
unref(this.timer);
|
|
4289
4327
|
}
|
|
4290
4328
|
clearPendingTimer() {
|
|
4291
4329
|
if (this.timer !== void 0) {
|
|
@@ -4948,6 +4986,31 @@ function generateOfflineNumericId() {
|
|
|
4948
4986
|
lastOfflineNumericId = lastOfflineNumericId === 0 ? candidate : Math.min(candidate, lastOfflineNumericId - 1);
|
|
4949
4987
|
return lastOfflineNumericId;
|
|
4950
4988
|
}
|
|
4989
|
+
/**
|
|
4990
|
+
* The row an optimistic write puts in the cache before the server has seen it.
|
|
4991
|
+
*
|
|
4992
|
+
* It is NOT an `M`, and saying so is the point of collecting this in one place.
|
|
4993
|
+
* `M` is the row as the database has it — every server default, every generated
|
|
4994
|
+
* column, every `afterRead` transform. What an optimistic write has is the
|
|
4995
|
+
* fields the caller supplied (plus, on an update, whatever was already cached)
|
|
4996
|
+
* and an id, which may itself be a locally-minted placeholder.
|
|
4997
|
+
*
|
|
4998
|
+
* The two are reconciled when the queued write drains and the server's row
|
|
4999
|
+
* replaces this one. Until then the SDK hands the caller this, under `M`,
|
|
5000
|
+
* because that is what the offline API's return type promises — and there is
|
|
5001
|
+
* nothing in this file that can make that promise true.
|
|
5002
|
+
*
|
|
5003
|
+
* So: one deliberate assertion, named, rather than five spread across the
|
|
5004
|
+
* create / createMany / update / upsert paths, each free to assemble the fields
|
|
5005
|
+
* differently. Fixing it properly means the offline surface saying what it
|
|
5006
|
+
* actually returns, which is a change to its public types.
|
|
5007
|
+
*/
|
|
5008
|
+
function optimisticRow(fields, id) {
|
|
5009
|
+
return {
|
|
5010
|
+
...fields,
|
|
5011
|
+
id
|
|
5012
|
+
};
|
|
5013
|
+
}
|
|
4951
5014
|
var MISSING = "\0missing";
|
|
4952
5015
|
/**
|
|
4953
5016
|
* Replays to spend on a mutation whose idempotency key the server is still
|
|
@@ -5034,7 +5097,7 @@ var OfflineManager = class {
|
|
|
5034
5097
|
if ((config.crossTab ?? this.store instanceof IndexedDBOfflineStore) && typeof BroadcastChannel !== "undefined") try {
|
|
5035
5098
|
this.channel = new BroadcastChannel("rebase-offline");
|
|
5036
5099
|
this.channel.onmessage = (event) => this.onBroadcast(event.data);
|
|
5037
|
-
this.channel
|
|
5100
|
+
unref(this.channel);
|
|
5038
5101
|
} catch {}
|
|
5039
5102
|
this.api = {
|
|
5040
5103
|
sync: () => this.sync(),
|
|
@@ -5197,10 +5260,7 @@ var OfflineManager = class {
|
|
|
5197
5260
|
}
|
|
5198
5261
|
const providedId = id ?? data.id;
|
|
5199
5262
|
const rowId = providedId ?? this.mintOfflineId(slug);
|
|
5200
|
-
const row =
|
|
5201
|
-
...data,
|
|
5202
|
-
id: rowId
|
|
5203
|
-
};
|
|
5263
|
+
const row = optimisticRow(data, rowId);
|
|
5204
5264
|
await this.enqueue({
|
|
5205
5265
|
collection: slug,
|
|
5206
5266
|
type: "create",
|
|
@@ -5228,10 +5288,7 @@ var OfflineManager = class {
|
|
|
5228
5288
|
if (!isNetworkError(error)) throw error;
|
|
5229
5289
|
this.connectivity.markFailure();
|
|
5230
5290
|
}
|
|
5231
|
-
const rows = data.map((r) => (
|
|
5232
|
-
...r,
|
|
5233
|
-
id: r.id ?? this.mintOfflineId(slug)
|
|
5234
|
-
}));
|
|
5291
|
+
const rows = data.map((r) => optimisticRow(r, r.id ?? this.mintOfflineId(slug)));
|
|
5235
5292
|
const rollback = {};
|
|
5236
5293
|
for (const row of rows) {
|
|
5237
5294
|
const key = String(row.id);
|
|
@@ -5274,10 +5331,7 @@ var OfflineManager = class {
|
|
|
5274
5331
|
}
|
|
5275
5332
|
const rowId = data.id;
|
|
5276
5333
|
if (options?.onConflict?.length || rowId === void 0) throw new RebaseClientError(`Cannot upsert into "${slug}" while offline: the row it would replace can only be found by the server. Upserting on the primary key, with the key in the row, is queued; upserting on a natural key is not.`, { code: "OFFLINE_UPSERT_UNSUPPORTED" });
|
|
5277
|
-
const row =
|
|
5278
|
-
...data,
|
|
5279
|
-
id: rowId
|
|
5280
|
-
};
|
|
5334
|
+
const row = optimisticRow(data, rowId);
|
|
5281
5335
|
await this.enqueue({
|
|
5282
5336
|
collection: slug,
|
|
5283
5337
|
type: "createMany",
|
|
@@ -5309,11 +5363,10 @@ var OfflineManager = class {
|
|
|
5309
5363
|
for (const { id, data } of updates) {
|
|
5310
5364
|
const base = this.rawLocalRow(slug, id);
|
|
5311
5365
|
rollback[String(id)] = base ?? null;
|
|
5312
|
-
optimistic.push({
|
|
5366
|
+
optimistic.push(optimisticRow({
|
|
5313
5367
|
...base ?? {},
|
|
5314
|
-
...data
|
|
5315
|
-
|
|
5316
|
-
});
|
|
5368
|
+
...data
|
|
5369
|
+
}, id));
|
|
5317
5370
|
}
|
|
5318
5371
|
await this.enqueue({
|
|
5319
5372
|
collection: slug,
|
|
@@ -5376,11 +5429,10 @@ var OfflineManager = class {
|
|
|
5376
5429
|
data,
|
|
5377
5430
|
rollback: { rows: { [String(id)]: base ?? null } }
|
|
5378
5431
|
});
|
|
5379
|
-
const optimistic = {
|
|
5432
|
+
const optimistic = optimisticRow({
|
|
5380
5433
|
...base ?? {},
|
|
5381
|
-
...data
|
|
5382
|
-
|
|
5383
|
-
};
|
|
5434
|
+
...data
|
|
5435
|
+
}, id);
|
|
5384
5436
|
this.setLocalRow(slug, id, optimistic);
|
|
5385
5437
|
this.notifyCollection(slug);
|
|
5386
5438
|
return optimistic;
|