convex 1.42.2-alpha.0 → 1.42.3
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/CHANGELOG.md +24 -0
- package/dist/browser.bundle.js +1 -1
- package/dist/browser.bundle.js.map +2 -2
- package/dist/cjs/browser/index.js.map +2 -2
- package/dist/cjs/browser/sync/client.js.map +1 -1
- package/dist/cjs/browser/sync/paginated_query_client.js.map +2 -2
- package/dist/cjs/cli/codegen_templates/common.js +6 -0
- package/dist/cjs/cli/codegen_templates/common.js.map +2 -2
- package/dist/cjs/cli/codegen_templates/component_api.js +2 -2
- package/dist/cjs/cli/codegen_templates/component_api.js.map +2 -2
- package/dist/cjs/cli/lib/codegen.js +2 -1
- package/dist/cjs/cli/lib/codegen.js.map +2 -2
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/client.js.map +2 -2
- package/dist/cjs/server/meta.js.map +1 -1
- package/dist/cjs-types/browser/index.d.ts.map +1 -1
- package/dist/cjs-types/browser/sync/paginated_query_client.d.ts +1 -38
- package/dist/cjs-types/browser/sync/paginated_query_client.d.ts.map +1 -1
- package/dist/cjs-types/cli/codegen_templates/common.d.ts +12 -0
- package/dist/cjs-types/cli/codegen_templates/common.d.ts.map +1 -1
- package/dist/cjs-types/cli/codegen_templates/common.test.d.ts +2 -0
- package/dist/cjs-types/cli/codegen_templates/common.test.d.ts.map +1 -0
- package/dist/cjs-types/cli/codegen_templates/component_api.d.ts.map +1 -1
- package/dist/cjs-types/cli/lib/codegen.d.ts.map +1 -1
- package/dist/cjs-types/index.d.ts +1 -1
- package/dist/cjs-types/index.d.ts.map +1 -1
- package/dist/cjs-types/react/client.d.ts.map +1 -1
- package/dist/cjs-types/server/meta.d.ts +10 -2
- package/dist/cjs-types/server/meta.d.ts.map +1 -1
- package/dist/cli.bundle.cjs +9 -4
- package/dist/cli.bundle.cjs.map +2 -2
- package/dist/esm/browser/index.js.map +2 -2
- package/dist/esm/browser/sync/client.js.map +1 -1
- package/dist/esm/browser/sync/paginated_query_client.js.map +2 -2
- package/dist/esm/cli/codegen_templates/common.js +5 -0
- package/dist/esm/cli/codegen_templates/common.js.map +2 -2
- package/dist/esm/cli/codegen_templates/component_api.js +8 -3
- package/dist/esm/cli/codegen_templates/component_api.js.map +2 -2
- package/dist/esm/cli/lib/codegen.js +2 -1
- package/dist/esm/cli/lib/codegen.js.map +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/client.js.map +2 -2
- package/dist/esm-types/browser/index.d.ts.map +1 -1
- package/dist/esm-types/browser/sync/paginated_query_client.d.ts +1 -38
- package/dist/esm-types/browser/sync/paginated_query_client.d.ts.map +1 -1
- package/dist/esm-types/cli/codegen_templates/common.d.ts +12 -0
- package/dist/esm-types/cli/codegen_templates/common.d.ts.map +1 -1
- package/dist/esm-types/cli/codegen_templates/common.test.d.ts +2 -0
- package/dist/esm-types/cli/codegen_templates/common.test.d.ts.map +1 -0
- package/dist/esm-types/cli/codegen_templates/component_api.d.ts.map +1 -1
- package/dist/esm-types/cli/lib/codegen.d.ts.map +1 -1
- package/dist/esm-types/index.d.ts +1 -1
- package/dist/esm-types/index.d.ts.map +1 -1
- package/dist/esm-types/react/client.d.ts.map +1 -1
- package/dist/esm-types/server/meta.d.ts +10 -2
- package/dist/esm-types/server/meta.d.ts.map +1 -1
- package/dist/react.bundle.js +1 -1
- package/dist/react.bundle.js.map +2 -2
- package/package.json +1 -1
- package/src/browser/index.ts +2 -0
- package/src/browser/sync/client.test.ts +8 -1
- package/src/browser/sync/client.ts +63 -0
- package/src/browser/sync/paginated_query_client.ts +3 -2
- package/src/cli/codegen_templates/common.test.ts +52 -0
- package/src/cli/codegen_templates/common.ts +17 -0
- package/src/cli/codegen_templates/component_api.ts +8 -3
- package/src/cli/lib/codegen.ts +2 -1
- package/src/index.ts +1 -1
- package/src/react/ConvexAuthState.test.tsx +24 -8
- package/src/react/auth_websocket.test.tsx +39 -29
- package/src/react/client.ts +3 -2
- package/src/react/client_provided_base_client.test.tsx +154 -36
- package/src/server/meta.ts +2 -4
package/package.json
CHANGED
package/src/browser/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ export type {
|
|
|
24
24
|
ConnectionState,
|
|
25
25
|
AuthTokenFetcher,
|
|
26
26
|
} from "./sync/client.js";
|
|
27
|
+
/** @internal */
|
|
28
|
+
export type { BaseConvexClientInterface } from "./sync/client.js";
|
|
27
29
|
export type { PaginationStatus } from "./sync/pagination.js";
|
|
28
30
|
export type { ConvexClientOptions } from "./simple_client.js";
|
|
29
31
|
export { ConvexClient } from "./simple_client.js";
|
|
@@ -4,9 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
import { test, expect } from "vitest";
|
|
6
6
|
|
|
7
|
-
import { BaseConvexClient } from "./client.js";
|
|
7
|
+
import { BaseConvexClient, BaseConvexClientInterface } from "./client.js";
|
|
8
8
|
import { anyApi } from "../../server/api.js";
|
|
9
9
|
|
|
10
|
+
// Compile-time assertion that `BaseConvexClient` satisfies
|
|
11
|
+
// `BaseConvexClientInterface`. We can't use an `implements` clause on the class
|
|
12
|
+
// because `BaseConvexClientInterface` is `@internal`: that would leak the
|
|
13
|
+
// interface name into the public `.d.ts` where it has been stripped.
|
|
14
|
+
const _assertBaseConvexClientImplementsInterface: BaseConvexClientInterface =
|
|
15
|
+
undefined as unknown as BaseConvexClient;
|
|
16
|
+
|
|
10
17
|
test("localQueryResult reflects optimistic results", async () => {
|
|
11
18
|
const client = new BaseConvexClient("http://127.0.0.1:8000", () => {
|
|
12
19
|
// ignore updates.
|
|
@@ -1126,3 +1126,66 @@ export class BaseConvexClient {
|
|
|
1126
1126
|
});
|
|
1127
1127
|
}
|
|
1128
1128
|
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* The public API of {@link BaseConvexClient}.
|
|
1132
|
+
*
|
|
1133
|
+
* @internal
|
|
1134
|
+
*/
|
|
1135
|
+
export interface BaseConvexClientInterface {
|
|
1136
|
+
addOnTransitionHandler(fn: (transition: Transition) => void): () => void;
|
|
1137
|
+
|
|
1138
|
+
setAuth(
|
|
1139
|
+
fetchToken: AuthTokenFetcher,
|
|
1140
|
+
onChange: (isAuthenticated: boolean) => void,
|
|
1141
|
+
onRefreshChange?: (isRefreshing: boolean) => void,
|
|
1142
|
+
): void;
|
|
1143
|
+
|
|
1144
|
+
/** @internal */
|
|
1145
|
+
setAdminAuth(value: string, fakeUserIdentity?: UserIdentityAttributes): void;
|
|
1146
|
+
|
|
1147
|
+
clearAuth(): void;
|
|
1148
|
+
|
|
1149
|
+
subscribe(
|
|
1150
|
+
name: string,
|
|
1151
|
+
args?: Record<string, Value>,
|
|
1152
|
+
options?: SubscribeOptions,
|
|
1153
|
+
): { queryToken: QueryToken; unsubscribe: () => void };
|
|
1154
|
+
|
|
1155
|
+
localQueryResult(
|
|
1156
|
+
udfPath: string,
|
|
1157
|
+
args?: Record<string, Value>,
|
|
1158
|
+
): Value | undefined;
|
|
1159
|
+
|
|
1160
|
+
localQueryResultByToken(queryToken: QueryToken): Value | undefined;
|
|
1161
|
+
|
|
1162
|
+
hasLocalQueryResultByToken(queryToken: QueryToken): boolean;
|
|
1163
|
+
|
|
1164
|
+
localQueryLogs(
|
|
1165
|
+
udfPath: string,
|
|
1166
|
+
args?: Record<string, Value>,
|
|
1167
|
+
): string[] | undefined;
|
|
1168
|
+
|
|
1169
|
+
queryJournal(
|
|
1170
|
+
name: string,
|
|
1171
|
+
args?: Record<string, Value>,
|
|
1172
|
+
): QueryJournal | undefined;
|
|
1173
|
+
|
|
1174
|
+
connectionState(): ConnectionState;
|
|
1175
|
+
|
|
1176
|
+
subscribeToConnectionState(
|
|
1177
|
+
cb: (connectionState: ConnectionState) => void,
|
|
1178
|
+
): () => void;
|
|
1179
|
+
|
|
1180
|
+
mutation(
|
|
1181
|
+
name: string,
|
|
1182
|
+
args?: Record<string, Value>,
|
|
1183
|
+
options?: MutationOptions,
|
|
1184
|
+
): Promise<any>;
|
|
1185
|
+
|
|
1186
|
+
action(name: string, args?: Record<string, Value>): Promise<any>;
|
|
1187
|
+
|
|
1188
|
+
close(): Promise<void>;
|
|
1189
|
+
|
|
1190
|
+
readonly url: string;
|
|
1191
|
+
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
serializePaginatedPathAndArgs,
|
|
18
18
|
canonicalizeUdfPath,
|
|
19
19
|
} from "./udf_path_utils.js";
|
|
20
|
-
import {
|
|
20
|
+
import { BaseConvexClientInterface, Transition } from "./client.js";
|
|
21
21
|
import {
|
|
22
22
|
PaginatedQueryResult,
|
|
23
23
|
PaginationStatus,
|
|
@@ -74,6 +74,7 @@ export type ExtendedTransition = Transition & {
|
|
|
74
74
|
}>;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
/** @internal */
|
|
77
78
|
export class PaginatedQueryClient {
|
|
78
79
|
private paginatedQuerySet: Map<PaginatedQueryToken, LocalPaginatedQuery> =
|
|
79
80
|
new Map();
|
|
@@ -81,7 +82,7 @@ export class PaginatedQueryClient {
|
|
|
81
82
|
private lastTransitionTs: TS;
|
|
82
83
|
|
|
83
84
|
constructor(
|
|
84
|
-
private client:
|
|
85
|
+
private client: BaseConvexClientInterface,
|
|
85
86
|
private onTransition: (transition: ExtendedTransition) => void,
|
|
86
87
|
) {
|
|
87
88
|
// Nonsense initial value to construct synthetic Transitions
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { compareModulePaths } from "./common.js";
|
|
3
|
+
|
|
4
|
+
describe("compareModulePaths", () => {
|
|
5
|
+
test("matches default sort order for forward slash paths", () => {
|
|
6
|
+
const paths = ["fooBaz.ts", "foo/bar.ts", "foo.ts"];
|
|
7
|
+
expect([...paths].sort(compareModulePaths)).toEqual([...paths].sort());
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("orders Windows paths identically to their POSIX equivalents", () => {
|
|
11
|
+
const posixPaths = ["fooBaz.ts", "foo/bar.ts", "foo.ts"];
|
|
12
|
+
const windowsPaths = posixPaths.map((p) => p.replace(/\//g, "\\"));
|
|
13
|
+
|
|
14
|
+
const sortedPosix = [...posixPaths].sort(compareModulePaths);
|
|
15
|
+
const sortedWindows = [...windowsPaths].sort(compareModulePaths);
|
|
16
|
+
|
|
17
|
+
expect(sortedPosix).toEqual(["foo.ts", "foo/bar.ts", "fooBaz.ts"]);
|
|
18
|
+
expect(sortedWindows.map((p) => p.replace(/\\/g, "/"))).toEqual(
|
|
19
|
+
sortedPosix,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("default sort diverges on this input, demonstrating the bug", () => {
|
|
24
|
+
// "/" (0x2F) sorts before letters while "\" (0x5C) sorts after them,
|
|
25
|
+
// so the OS-native forms of the same file set sort differently.
|
|
26
|
+
const windowsPaths = ["fooBaz.ts", "foo\\bar.ts", "foo.ts"];
|
|
27
|
+
expect([...windowsPaths].sort()).toEqual([
|
|
28
|
+
"foo.ts",
|
|
29
|
+
"fooBaz.ts",
|
|
30
|
+
"foo\\bar.ts",
|
|
31
|
+
]);
|
|
32
|
+
expect([...windowsPaths].sort(compareModulePaths)).toEqual([
|
|
33
|
+
"foo.ts",
|
|
34
|
+
"foo\\bar.ts",
|
|
35
|
+
"fooBaz.ts",
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("sorts nested directories before longer sibling file names", () => {
|
|
40
|
+
const paths = ["a/b/c.ts", "aZ.ts", "a.ts", "a/b.ts"];
|
|
41
|
+
expect([...paths].sort(compareModulePaths)).toEqual([
|
|
42
|
+
"a.ts",
|
|
43
|
+
"a/b.ts",
|
|
44
|
+
"a/b/c.ts",
|
|
45
|
+
"aZ.ts",
|
|
46
|
+
]);
|
|
47
|
+
const windows = paths.map((p) => p.replace(/\//g, "\\"));
|
|
48
|
+
expect(
|
|
49
|
+
[...windows].sort(compareModulePaths).map((p) => p.replace(/\\/g, "/")),
|
|
50
|
+
).toEqual(["a.ts", "a/b.ts", "a/b/c.ts", "aZ.ts"]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -43,3 +43,20 @@ const collator = new Intl.Collator("en-US", {
|
|
|
43
43
|
export function compareStrings(a: string, b: string): number {
|
|
44
44
|
return collator.compare(a, b);
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Comparison function for sorting module paths in codegen output.
|
|
49
|
+
*
|
|
50
|
+
* Compares the forward slash normalized form of each path so the resulting
|
|
51
|
+
* order is identical on every platform. Sorting OS-native paths directly
|
|
52
|
+
* diverges on Windows because "\" (0x5C) sorts after letters while "/"
|
|
53
|
+
* (0x2F) sorts before them. Uses plain code unit comparison to match the
|
|
54
|
+
* default Array.prototype.sort() order these paths sorted with on POSIX.
|
|
55
|
+
*
|
|
56
|
+
* Usage: modulePaths.sort(compareModulePaths)
|
|
57
|
+
*/
|
|
58
|
+
export function compareModulePaths(a: string, b: string): number {
|
|
59
|
+
const aNormalized = a.replace(/\\/g, "/");
|
|
60
|
+
const bNormalized = b.replace(/\\/g, "/");
|
|
61
|
+
return aNormalized < bNormalized ? -1 : aNormalized > bNormalized ? 1 : 0;
|
|
62
|
+
}
|
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
} from "../lib/components/definition/directoryStructure.js";
|
|
10
10
|
import { StartPushResponse } from "../lib/deployApi/startPush.js";
|
|
11
11
|
import { importPath, moduleIdentifier } from "./api.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
apiComment,
|
|
14
|
+
compareModulePaths,
|
|
15
|
+
compareStrings,
|
|
16
|
+
header,
|
|
17
|
+
} from "./common.js";
|
|
13
18
|
import {
|
|
14
19
|
ComponentExports,
|
|
15
20
|
EvaluatedComponentDefinition,
|
|
@@ -346,7 +351,7 @@ async function* codegenDynamicApiObjects(
|
|
|
346
351
|
const absModulePaths = await entryPoints(ctx, componentDirectory.path);
|
|
347
352
|
const modulePaths = absModulePaths
|
|
348
353
|
.map((p) => path.relative(componentDirectory.path, p))
|
|
349
|
-
.sort();
|
|
354
|
+
.sort(compareModulePaths);
|
|
350
355
|
for (const modulePath of modulePaths) {
|
|
351
356
|
const ident = moduleIdentifier(modulePath);
|
|
352
357
|
const path = importPath(modulePath);
|
|
@@ -382,7 +387,7 @@ async function* codegenDynamicApiObjectsTS(
|
|
|
382
387
|
const absModulePaths = await entryPoints(ctx, componentDirectory.path);
|
|
383
388
|
const modulePaths = absModulePaths
|
|
384
389
|
.map((p) => path.relative(componentDirectory.path, p))
|
|
385
|
-
.sort();
|
|
390
|
+
.sort(compareModulePaths);
|
|
386
391
|
for (const modulePath of modulePaths) {
|
|
387
392
|
const ident = moduleIdentifier(modulePath);
|
|
388
393
|
const path = importPath(modulePath);
|
package/src/cli/lib/codegen.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { withTmpDir, TempDir } from "../../bundler/fs.js";
|
|
|
4
4
|
import { entryPoints } from "../../bundler/index.js";
|
|
5
5
|
import { apiCodegen } from "../codegen_templates/api.js";
|
|
6
6
|
import { apiCjsCodegen } from "../codegen_templates/api_cjs.js";
|
|
7
|
+
import { compareModulePaths } from "../codegen_templates/common.js";
|
|
7
8
|
import {
|
|
8
9
|
dynamicDataModelDTS,
|
|
9
10
|
dynamicDataModelTS,
|
|
@@ -846,7 +847,7 @@ async function doApiCodegen(
|
|
|
846
847
|
const absModulePaths = await entryPoints(ctx, functionsDir);
|
|
847
848
|
const modulePaths = absModulePaths
|
|
848
849
|
.map((p) => path.relative(functionsDir, p))
|
|
849
|
-
.sort();
|
|
850
|
+
.sort(compareModulePaths);
|
|
850
851
|
|
|
851
852
|
const writtenFiles: string[] = [];
|
|
852
853
|
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "1.42.
|
|
1
|
+
export const version = "1.42.3";
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
ConvexReactClient,
|
|
11
11
|
useConvexAuth,
|
|
12
12
|
} from "./index.js";
|
|
13
|
+
import {
|
|
14
|
+
BaseConvexClient,
|
|
15
|
+
BaseConvexClientInterface,
|
|
16
|
+
} from "../browser/index.js";
|
|
13
17
|
|
|
14
18
|
vi.useFakeTimers();
|
|
15
19
|
|
|
@@ -18,6 +22,16 @@ const flushPromises = async () => {
|
|
|
18
22
|
await act(() => new Promise((timers as any).setImmediate));
|
|
19
23
|
};
|
|
20
24
|
|
|
25
|
+
// Test helpers reach into `BaseConvexClient`'s private internals; the tests
|
|
26
|
+
// only ever construct `ConvexReactClient` without a `baseClient` override, so
|
|
27
|
+
// `sync` is always a real `BaseConvexClient` under the hood.
|
|
28
|
+
const syncAsBaseConvexClient = (sync: BaseConvexClientInterface) => {
|
|
29
|
+
if (!(sync instanceof BaseConvexClient)) {
|
|
30
|
+
throw new Error("Expected `sync` to be a `BaseConvexClient`");
|
|
31
|
+
}
|
|
32
|
+
return sync;
|
|
33
|
+
};
|
|
34
|
+
|
|
21
35
|
test("setAuth legacy signature typechecks and doesn't throw", async () => {
|
|
22
36
|
const convex = new ConvexReactClient("https://127.0.0.1:3001");
|
|
23
37
|
// We're moving towards removing the Promise, but for backwards compatibility
|
|
@@ -201,8 +215,10 @@ function mockServerConfirmsAuth(
|
|
|
201
215
|
clientClockSkew?: number,
|
|
202
216
|
) {
|
|
203
217
|
act(() => {
|
|
204
|
-
const querySetVersion = client.sync[
|
|
205
|
-
|
|
218
|
+
const querySetVersion = syncAsBaseConvexClient(client.sync)[
|
|
219
|
+
"remoteQuerySet"
|
|
220
|
+
]["version"];
|
|
221
|
+
syncAsBaseConvexClient(client.sync)["authenticationManager"].onTransition({
|
|
206
222
|
type: "Transition",
|
|
207
223
|
startVersion: {
|
|
208
224
|
...querySetVersion,
|
|
@@ -311,7 +327,7 @@ test("isRefreshing reflects reauthentication in React tree", async () => {
|
|
|
311
327
|
|
|
312
328
|
// Server rejects a token mid-session, triggering reauthentication.
|
|
313
329
|
act(() => {
|
|
314
|
-
convex.sync["authenticationManager"].onAuthError({
|
|
330
|
+
syncAsBaseConvexClient(convex.sync)["authenticationManager"].onAuthError({
|
|
315
331
|
type: "AuthError",
|
|
316
332
|
error: "token expired",
|
|
317
333
|
baseVersion: 2,
|
|
@@ -362,7 +378,7 @@ test("isRefreshing becomes false when reauth fetch fails", async () => {
|
|
|
362
378
|
|
|
363
379
|
// Server rejects the token; tryToReauthenticate runs but the fetcher
|
|
364
380
|
// returns null on its retry.
|
|
365
|
-
client.sync["authenticationManager"].onAuthError({
|
|
381
|
+
syncAsBaseConvexClient(client.sync)["authenticationManager"].onAuthError({
|
|
366
382
|
type: "AuthError",
|
|
367
383
|
error: "token expired",
|
|
368
384
|
baseVersion: 1,
|
|
@@ -435,7 +451,7 @@ test("isFromOutdatedConfig during reauth resets isRefreshing via resetAuthState"
|
|
|
435
451
|
onRefreshChange.mockClear();
|
|
436
452
|
|
|
437
453
|
// Server rejects the token, kicking off tryToReauthenticate.
|
|
438
|
-
client.sync["authenticationManager"].onAuthError({
|
|
454
|
+
syncAsBaseConvexClient(client.sync)["authenticationManager"].onAuthError({
|
|
439
455
|
type: "AuthError",
|
|
440
456
|
error: "token expired",
|
|
441
457
|
baseVersion: 1,
|
|
@@ -482,7 +498,7 @@ test("isRefreshing is true during reauthentication after auth error", async () =
|
|
|
482
498
|
|
|
483
499
|
// Simulate auth error from server
|
|
484
500
|
act(() => {
|
|
485
|
-
client.sync["authenticationManager"].onAuthError({
|
|
501
|
+
syncAsBaseConvexClient(client.sync)["authenticationManager"].onAuthError({
|
|
486
502
|
type: "AuthError",
|
|
487
503
|
error: "token expired",
|
|
488
504
|
baseVersion: 1,
|
|
@@ -527,7 +543,7 @@ test("stop() during reauth notifies isRefreshing false", async () => {
|
|
|
527
543
|
onRefreshChange.mockClear();
|
|
528
544
|
|
|
529
545
|
// Server rejects the token so isRefreshing becomes true.
|
|
530
|
-
client.sync["authenticationManager"].onAuthError({
|
|
546
|
+
syncAsBaseConvexClient(client.sync)["authenticationManager"].onAuthError({
|
|
531
547
|
type: "AuthError",
|
|
532
548
|
error: "token expired",
|
|
533
549
|
baseVersion: 1,
|
|
@@ -564,7 +580,7 @@ test("initialAuthTokenReuse: sends only one Authenticate", async () => {
|
|
|
564
580
|
);
|
|
565
581
|
|
|
566
582
|
const sendMessageSpy = vi.spyOn(
|
|
567
|
-
client.sync["webSocketManager"],
|
|
583
|
+
syncAsBaseConvexClient(client.sync)["webSocketManager"],
|
|
568
584
|
"sendMessage",
|
|
569
585
|
);
|
|
570
586
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
withInMemoryWebSocket,
|
|
9
9
|
} from "../browser/sync/client_node_test_helpers.js";
|
|
10
10
|
import { ConvexReactClient, ConvexReactClientOptions } from "./index.js";
|
|
11
|
+
import { BaseConvexClient } from "../browser/index.js";
|
|
11
12
|
import waitForExpect from "wait-for-expect";
|
|
12
13
|
import { anyApi } from "../server/index.js";
|
|
13
14
|
import { Long } from "../vendor/long.js";
|
|
@@ -17,12 +18,21 @@ import {
|
|
|
17
18
|
WireServerMessage,
|
|
18
19
|
} from "../browser/sync/protocol.js";
|
|
19
20
|
|
|
20
|
-
const testReactClient = (
|
|
21
|
-
|
|
21
|
+
const testReactClient = (
|
|
22
|
+
address: string,
|
|
23
|
+
options?: ConvexReactClientOptions,
|
|
24
|
+
) => {
|
|
25
|
+
const client = new ConvexReactClient(address, {
|
|
22
26
|
webSocketConstructor: nodeWebSocket,
|
|
23
27
|
unsavedChangesWarning: false,
|
|
24
28
|
...options,
|
|
25
29
|
});
|
|
30
|
+
const baseClient = client.sync;
|
|
31
|
+
if (!(baseClient instanceof BaseConvexClient)) {
|
|
32
|
+
throw new Error("Expected `sync` to be a `BaseConvexClient`");
|
|
33
|
+
}
|
|
34
|
+
return { client, baseClient };
|
|
35
|
+
};
|
|
26
36
|
|
|
27
37
|
// Disabled due to flakes in CI
|
|
28
38
|
// https://linear.app/convex/issue/ENG-7052/re-enable-auth-websocket-client-tests
|
|
@@ -33,7 +43,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
33
43
|
// with a constant token provider.
|
|
34
44
|
test("Authenticate via valid static token", async () => {
|
|
35
45
|
await withInMemoryWebSocket(async ({ address, receive, send }) => {
|
|
36
|
-
const client = testReactClient(address);
|
|
46
|
+
const { client, baseClient } = testReactClient(address);
|
|
37
47
|
|
|
38
48
|
const tokenFetcher = vi.fn(async () =>
|
|
39
49
|
jwtEncode({ iat: 1234500, exp: 1244500 }, "secret"),
|
|
@@ -45,7 +55,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
45
55
|
expect((await receive()).type).toEqual("Authenticate");
|
|
46
56
|
expect((await receive()).type).toEqual("ModifyQuerySet");
|
|
47
57
|
|
|
48
|
-
const querySetVersion =
|
|
58
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
49
59
|
|
|
50
60
|
send({
|
|
51
61
|
type: "Transition",
|
|
@@ -73,7 +83,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
73
83
|
// This happens when a user opens a page after their cached token expired
|
|
74
84
|
test("Reauthenticate after token expiration with versioning", async () => {
|
|
75
85
|
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
|
|
76
|
-
const client = testReactClient(address);
|
|
86
|
+
const { client, baseClient } = testReactClient(address);
|
|
77
87
|
|
|
78
88
|
let token = jwtEncode({ iat: 1234500, exp: 1244500 }, "wobabloobla");
|
|
79
89
|
const fetchToken = async () => token;
|
|
@@ -106,7 +116,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
106
116
|
token: token,
|
|
107
117
|
});
|
|
108
118
|
|
|
109
|
-
const querySetVersion =
|
|
119
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
110
120
|
|
|
111
121
|
// Server accepts new token
|
|
112
122
|
send({
|
|
@@ -138,7 +148,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
138
148
|
// fetch from a cache (say due to Prevent Cross Site tracking)
|
|
139
149
|
test("Reauthenticate after token cache failure", async () => {
|
|
140
150
|
await withInMemoryWebSocket(async ({ address, receive, send }) => {
|
|
141
|
-
const client = testReactClient(address);
|
|
151
|
+
const { client, baseClient } = testReactClient(address);
|
|
142
152
|
|
|
143
153
|
const freshToken = jwtEncode({ iat: 1234500, exp: 1244500 }, "secret");
|
|
144
154
|
const tokenFetcher = vi.fn(
|
|
@@ -158,7 +168,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
158
168
|
token: freshToken,
|
|
159
169
|
});
|
|
160
170
|
|
|
161
|
-
const querySetVersion =
|
|
171
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
162
172
|
|
|
163
173
|
// Server accepts new token
|
|
164
174
|
send({
|
|
@@ -189,7 +199,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
189
199
|
// This is usually a misconfigured server rejecting any token
|
|
190
200
|
test("Fail when tokens are always rejected", async () => {
|
|
191
201
|
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
|
|
192
|
-
const client = testReactClient(address);
|
|
202
|
+
const { client } = testReactClient(address);
|
|
193
203
|
|
|
194
204
|
const consoleSpy = vi
|
|
195
205
|
.spyOn(global.console, "error")
|
|
@@ -287,7 +297,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
287
297
|
// it's authed but it cannot fetch a token at all.
|
|
288
298
|
test("Fail when tokens cannot be fetched", async () => {
|
|
289
299
|
await withInMemoryWebSocket(async ({ address, receive }) => {
|
|
290
|
-
const client = testReactClient(address);
|
|
300
|
+
const { client } = testReactClient(address);
|
|
291
301
|
const tokenFetcher = vi.fn(async () => null);
|
|
292
302
|
const onAuthChange = vi.fn();
|
|
293
303
|
void client.setAuth(tokenFetcher, onAuthChange);
|
|
@@ -307,7 +317,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
307
317
|
|
|
308
318
|
test("Client is protected against token rejection race", async () => {
|
|
309
319
|
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
|
|
310
|
-
const client = testReactClient(address);
|
|
320
|
+
const { client, baseClient } = testReactClient(address);
|
|
311
321
|
|
|
312
322
|
const badToken = jwtEncode({ iat: 1234500, exp: 1244500 }, "wobalooba");
|
|
313
323
|
const badTokenFetcher = vi.fn(async () => badToken);
|
|
@@ -319,7 +329,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
319
329
|
token: badToken,
|
|
320
330
|
});
|
|
321
331
|
|
|
322
|
-
const querySetVersion =
|
|
332
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
323
333
|
|
|
324
334
|
const goodToken = jwtEncode({ iat: 1234500, exp: 1244500 }, "secret");
|
|
325
335
|
const goodTokenFetcher = vi.fn(async () => goodToken);
|
|
@@ -379,7 +389,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
379
389
|
// comes back while the client is waiting for server validation of the new token.
|
|
380
390
|
test("Client ignores non-auth responses for token validation", async () => {
|
|
381
391
|
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
|
|
382
|
-
const client = testReactClient(address);
|
|
392
|
+
const { client, baseClient } = testReactClient(address);
|
|
383
393
|
const ts = Math.ceil(Date.now() / 1000);
|
|
384
394
|
const initialToken = jwtEncode({ iat: ts, exp: ts + 1000 }, "token1");
|
|
385
395
|
const freshToken = jwtEncode({ iat: ts, exp: ts + 1000 }, "token2");
|
|
@@ -398,7 +408,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
398
408
|
token: initialToken,
|
|
399
409
|
});
|
|
400
410
|
|
|
401
|
-
const querySetVersion =
|
|
411
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
402
412
|
|
|
403
413
|
send({
|
|
404
414
|
type: "Transition",
|
|
@@ -445,7 +455,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
445
455
|
token: freshToken,
|
|
446
456
|
});
|
|
447
457
|
|
|
448
|
-
const querySetVersion2 =
|
|
458
|
+
const querySetVersion2 = baseClient["remoteQuerySet"]["version"];
|
|
449
459
|
|
|
450
460
|
send({
|
|
451
461
|
type: "Transition",
|
|
@@ -478,7 +488,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
478
488
|
test("Client maintains connection when refetch occurs during reauth attempt", async () => {
|
|
479
489
|
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
|
|
480
490
|
vi.useFakeTimers();
|
|
481
|
-
const client = testReactClient(address);
|
|
491
|
+
const { client, baseClient } = testReactClient(address);
|
|
482
492
|
// In this test we're going to simulate `fetchToken` taking a long time (1s)
|
|
483
493
|
// so we can test race conditions, so the issued time will be 1s after the
|
|
484
494
|
// fetched time.
|
|
@@ -543,7 +553,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
543
553
|
baseVersion: 0,
|
|
544
554
|
token: initialToken,
|
|
545
555
|
});
|
|
546
|
-
const querySetVersion =
|
|
556
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
547
557
|
|
|
548
558
|
send({
|
|
549
559
|
type: "Transition",
|
|
@@ -659,7 +669,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
659
669
|
// unauthenticated state.
|
|
660
670
|
test("Client retries token validation on error", async () => {
|
|
661
671
|
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
|
|
662
|
-
const client = testReactClient(address);
|
|
672
|
+
const { client, baseClient } = testReactClient(address);
|
|
663
673
|
const ts = Math.ceil(Date.now() / 1000);
|
|
664
674
|
const token1 = jwtEncode({ iat: ts, exp: ts + 60 }, "token1");
|
|
665
675
|
const token2 = jwtEncode({ iat: ts, exp: ts + 60 }, "token2");
|
|
@@ -674,7 +684,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
674
684
|
token: token1,
|
|
675
685
|
});
|
|
676
686
|
|
|
677
|
-
const querySetVersion =
|
|
687
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
678
688
|
|
|
679
689
|
send({
|
|
680
690
|
type: "Transition",
|
|
@@ -751,7 +761,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
751
761
|
|
|
752
762
|
test("Authentication runs first", async () => {
|
|
753
763
|
await withInMemoryWebSocket(async ({ address, receive, send }) => {
|
|
754
|
-
const client = testReactClient(address);
|
|
764
|
+
const { client, baseClient } = testReactClient(address);
|
|
755
765
|
|
|
756
766
|
const token1 = jwtEncode({ iat: 1234500, exp: 1244500 }, "secret");
|
|
757
767
|
const tokenFetcher = vi.fn(async () => token1);
|
|
@@ -762,7 +772,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
762
772
|
token: token1,
|
|
763
773
|
});
|
|
764
774
|
|
|
765
|
-
const querySetVersion =
|
|
775
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
766
776
|
|
|
767
777
|
send({
|
|
768
778
|
type: "Transition",
|
|
@@ -792,7 +802,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
792
802
|
|
|
793
803
|
test("Auth pause doesn't prevent unsubscribing from queries", async () => {
|
|
794
804
|
await withInMemoryWebSocket(async ({ address, receive, send }) => {
|
|
795
|
-
const client = testReactClient(address);
|
|
805
|
+
const { client, baseClient } = testReactClient(address);
|
|
796
806
|
|
|
797
807
|
const token1 = jwtEncode({ iat: 1234500, exp: 1244500 }, "secret");
|
|
798
808
|
const tokenFetcher = vi.fn(async () => token1);
|
|
@@ -807,7 +817,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
807
817
|
token: token1,
|
|
808
818
|
});
|
|
809
819
|
|
|
810
|
-
const querySetVersion =
|
|
820
|
+
const querySetVersion = baseClient["remoteQuerySet"]["version"];
|
|
811
821
|
|
|
812
822
|
send({
|
|
813
823
|
type: "Transition",
|
|
@@ -829,7 +839,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
829
839
|
});
|
|
830
840
|
|
|
831
841
|
await waitForExpect(() => {
|
|
832
|
-
expect(
|
|
842
|
+
expect(baseClient["remoteQuerySet"]["version"].identity).toEqual(1);
|
|
833
843
|
});
|
|
834
844
|
|
|
835
845
|
let resolve: (value: string) => void;
|
|
@@ -882,7 +892,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
882
892
|
|
|
883
893
|
test("Local state resume doesn't cause duplicate AddQuery", async () => {
|
|
884
894
|
await withInMemoryWebSocket(async ({ address, receive }) => {
|
|
885
|
-
const client = testReactClient(address);
|
|
895
|
+
const { client } = testReactClient(address);
|
|
886
896
|
|
|
887
897
|
// First we subscribe
|
|
888
898
|
client.watchQuery(anyApi.myQuery.default).onUpdate(() => {});
|
|
@@ -923,7 +933,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
923
933
|
|
|
924
934
|
test("Local state resume doesn't send both Add and Remove", async () => {
|
|
925
935
|
await withInMemoryWebSocket(async ({ address, receive }) => {
|
|
926
|
-
const client = testReactClient(address);
|
|
936
|
+
const { client } = testReactClient(address);
|
|
927
937
|
|
|
928
938
|
// First we subscribe to kick off connect.
|
|
929
939
|
client.watchQuery(anyApi.myQuery.default).onUpdate(() => {});
|
|
@@ -978,7 +988,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
978
988
|
|
|
979
989
|
test("Local state resume refcounts", async () => {
|
|
980
990
|
await withInMemoryWebSocket(async ({ address, receive }) => {
|
|
981
|
-
const client = testReactClient(address);
|
|
991
|
+
const { client } = testReactClient(address);
|
|
982
992
|
|
|
983
993
|
// First we subscribe to kick off connect.
|
|
984
994
|
client.watchQuery(anyApi.myQuery.default).onUpdate(() => {});
|
|
@@ -1034,7 +1044,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
1034
1044
|
|
|
1035
1045
|
test("Local state restart doesn't send both Add and Remove", async () => {
|
|
1036
1046
|
await withInMemoryWebSocket(async ({ address, receive }) => {
|
|
1037
|
-
const client = testReactClient(address);
|
|
1047
|
+
const { client } = testReactClient(address);
|
|
1038
1048
|
|
|
1039
1049
|
// Set slow auth, causing pause while connecting
|
|
1040
1050
|
let resolve: (value: string) => void;
|
|
@@ -1073,7 +1083,7 @@ describe.sequential.skip("auth websocket tests", () => {
|
|
|
1073
1083
|
describe.sequential("authMode WebSocket", () => {
|
|
1074
1084
|
test.each([false, true])("expectAuth: %s", async (expectAuth: boolean) => {
|
|
1075
1085
|
await withInMemoryWebSocket(async ({ address, receive, close }) => {
|
|
1076
|
-
const client = testReactClient(
|
|
1086
|
+
const { client } = testReactClient(
|
|
1077
1087
|
address,
|
|
1078
1088
|
expectAuth ? { expectAuth: true } : {},
|
|
1079
1089
|
);
|
package/src/react/client.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
AuthTokenFetcher,
|
|
13
13
|
BaseConvexClientOptions,
|
|
14
14
|
ConnectionState,
|
|
15
|
+
BaseConvexClientInterface,
|
|
15
16
|
} from "../browser/sync/client.js";
|
|
16
17
|
import type { UserIdentityAttributes } from "../browser/sync/protocol.js";
|
|
17
18
|
import { RequestForQueries, useQueries } from "./use_queries.js";
|
|
@@ -295,7 +296,7 @@ export interface ConvexReactClientOptions extends BaseConvexClientOptions {
|
|
|
295
296
|
*
|
|
296
297
|
* @internal
|
|
297
298
|
*/
|
|
298
|
-
baseClient?:
|
|
299
|
+
baseClient?: BaseConvexClientInterface;
|
|
299
300
|
|
|
300
301
|
// FIXME: Ideally we define `ConvexReactClientOptions` as
|
|
301
302
|
// `type ConvexReactClientOptions = BaseConvexClientOptions | { baseClient: BaseConvexClient }`
|
|
@@ -313,7 +314,7 @@ export interface ConvexReactClientOptions extends BaseConvexClientOptions {
|
|
|
313
314
|
*/
|
|
314
315
|
export class ConvexReactClient {
|
|
315
316
|
private address: string;
|
|
316
|
-
private cachedSync?:
|
|
317
|
+
private cachedSync?: BaseConvexClientInterface | undefined;
|
|
317
318
|
private cachedPaginatedQueryClient?: PaginatedQueryClient | undefined;
|
|
318
319
|
private listeners: Map<QueryToken | PaginatedQueryToken, Set<() => void>>;
|
|
319
320
|
private options: ConvexReactClientOptions;
|