@rebasepro/client 0.13.0 → 0.13.1-canary.g18cfeb7
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 +263 -73
- 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/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 +184 -50
- 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 +35 -8
- package/src/offline-store.ts +5 -1
- package/src/offline.test.ts +114 -1
- package/src/offline.ts +121 -7
- package/src/realtime-optout.test.ts +15 -15
- package/src/realtime-subscription-key.test.ts +92 -0
- package/src/transport-baseurl.test.ts +49 -1
- package/src/transport.ts +22 -0
- package/src/websocket-url.test.ts +97 -0
- package/src/websocket.ts +18 -9
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { createRebaseClient } from "./index";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where the realtime socket dials, given a `baseUrl`.
|
|
6
|
+
*
|
|
7
|
+
* The socket URL is derived rather than configured, so every disagreement
|
|
8
|
+
* between how it is derived and where the API actually lives shows up only as
|
|
9
|
+
* realtime not working — never as an error naming a cause.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function setWindow(origin: string | undefined) {
|
|
13
|
+
if (origin === undefined) {
|
|
14
|
+
delete (globalThis as never as { window?: unknown }).window;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
(globalThis as never as { window: unknown }).window = {
|
|
18
|
+
location: { origin, href: origin + "/" }
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function trackingWebSocket() {
|
|
23
|
+
const opened: string[] = [];
|
|
24
|
+
class FakeWebSocket {
|
|
25
|
+
static readonly OPEN = 1;
|
|
26
|
+
readyState = 0;
|
|
27
|
+
onopen: (() => void) | null = null;
|
|
28
|
+
onclose: (() => void) | null = null;
|
|
29
|
+
onerror: (() => void) | null = null;
|
|
30
|
+
onmessage: (() => void) | null = null;
|
|
31
|
+
constructor(public url: string) { opened.push(url); }
|
|
32
|
+
close() { /* no-op */ }
|
|
33
|
+
send() { /* no-op */ }
|
|
34
|
+
}
|
|
35
|
+
return { FakeWebSocket: FakeWebSocket as unknown as typeof WebSocket, opened };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("realtime socket URL", () => {
|
|
39
|
+
const originalWebSocket = globalThis.WebSocket;
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
globalThis.WebSocket = originalWebSocket;
|
|
43
|
+
setWindow(undefined);
|
|
44
|
+
jest.restoreAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("keeps the path of a relative baseUrl, as it does for an absolute one", () => {
|
|
48
|
+
// A backend mounted under a path is the reason `baseUrl` accepts one.
|
|
49
|
+
// Resolving the relative form to `.origin` dropped it, so the same
|
|
50
|
+
// deployment dialled two different URLs depending on whether its
|
|
51
|
+
// baseUrl was written as "/backend" or "https://app.example.com/backend".
|
|
52
|
+
const { FakeWebSocket, opened } = trackingWebSocket();
|
|
53
|
+
globalThis.WebSocket = FakeWebSocket;
|
|
54
|
+
setWindow("https://app.example.com");
|
|
55
|
+
|
|
56
|
+
const relative = createRebaseClient({ baseUrl: "/backend" });
|
|
57
|
+
relative.collection("posts").listen!(undefined, () => {});
|
|
58
|
+
|
|
59
|
+
const absolute = createRebaseClient({ baseUrl: "https://app.example.com/backend" });
|
|
60
|
+
absolute.collection("posts").listen!(undefined, () => {});
|
|
61
|
+
|
|
62
|
+
expect(opened[0]).toBe("wss://app.example.com/backend");
|
|
63
|
+
expect(opened[0]).toBe(opened[1]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("dials the page origin when no baseUrl is set in a browser", () => {
|
|
67
|
+
const { FakeWebSocket, opened } = trackingWebSocket();
|
|
68
|
+
globalThis.WebSocket = FakeWebSocket;
|
|
69
|
+
setWindow("https://app.example.com");
|
|
70
|
+
|
|
71
|
+
const client = createRebaseClient({});
|
|
72
|
+
client.collection("posts").listen!(undefined, () => {});
|
|
73
|
+
|
|
74
|
+
expect(opened[0]).toBe("wss://app.example.com");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("says why realtime is unavailable when no URL could be derived", () => {
|
|
78
|
+
// Node, with a relative baseUrl there is nothing to resolve against.
|
|
79
|
+
// The client silently had no socket, and `realtime.channel()` then
|
|
80
|
+
// blamed `realtime: false` — an option the caller never passed.
|
|
81
|
+
setWindow(undefined);
|
|
82
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
83
|
+
const client = createRebaseClient({ baseUrl: "/backend" });
|
|
84
|
+
|
|
85
|
+
expect(client.ws).toBeUndefined();
|
|
86
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("websocketUrl"));
|
|
87
|
+
expect(() => client.realtime.channel("room")).toThrow(/no WebSocket URL could be derived/i);
|
|
88
|
+
// It may still *suggest* `realtime: false` as the way to make this
|
|
89
|
+
// deliberate; what it must not do is claim that is what happened.
|
|
90
|
+
expect(() => client.realtime.channel("room")).not.toThrow(/disabled on this client/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("still blames the opt-out when realtime really was switched off", () => {
|
|
94
|
+
const client = createRebaseClient({ baseUrl: "https://api.example.com", realtime: false });
|
|
95
|
+
expect(() => client.realtime.channel("room")).toThrow(/realtime: false/);
|
|
96
|
+
});
|
|
97
|
+
});
|
package/src/websocket.ts
CHANGED
|
@@ -1288,6 +1288,9 @@ options }
|
|
|
1288
1288
|
|
|
1289
1289
|
const obj = val as Record<string, unknown>;
|
|
1290
1290
|
if (obj.__type === "relation") {
|
|
1291
|
+
// `data` is dropped on purpose: a relation compares by the
|
|
1292
|
+
// reference it holds, not by the row it happens to have loaded.
|
|
1293
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1291
1294
|
const { data, ...rest } = obj;
|
|
1292
1295
|
return rest;
|
|
1293
1296
|
}
|
|
@@ -1803,16 +1806,22 @@ onError });
|
|
|
1803
1806
|
}
|
|
1804
1807
|
|
|
1805
1808
|
private createCollectionSubscriptionKey(props: FetchCollectionProps): string {
|
|
1806
|
-
//
|
|
1809
|
+
// Derived from the props, not a hand-listed subset of them.
|
|
1810
|
+
//
|
|
1811
|
+
// Two subscriptions share one server subscription when their keys
|
|
1812
|
+
// match, so a field left off the key makes two different queries
|
|
1813
|
+
// collide and hands the second listener the first one's rows. `offset`
|
|
1814
|
+
// and `logical` were both missing: page two of a live list showed page
|
|
1815
|
+
// one, and two views filtered by different `or(...)` groups saw the
|
|
1816
|
+
// same rows. Listing fields by hand is what let that happen, so the key
|
|
1817
|
+
// now covers whatever `FetchCollectionProps` carries.
|
|
1818
|
+
//
|
|
1819
|
+
// `collection` is the exception: it is the whole collection config,
|
|
1820
|
+
// property thunks and all, so it contributes its name as before.
|
|
1821
|
+
const { collection, ...query } = props as FetchCollectionProps & Record<string, unknown>;
|
|
1807
1822
|
const key = {
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
limit: props.limit,
|
|
1811
|
-
startAfter: props.startAfter,
|
|
1812
|
-
orderBy: props.orderBy,
|
|
1813
|
-
order: props.order,
|
|
1814
|
-
searchString: props.searchString,
|
|
1815
|
-
collection: props.collection?.name
|
|
1823
|
+
...query,
|
|
1824
|
+
collection: collection?.name
|
|
1816
1825
|
};
|
|
1817
1826
|
// Use replacer function (not array) to sort keys at all levels for deterministic output
|
|
1818
1827
|
return JSON.stringify(key, (_, value) => {
|