convex 1.41.0 → 1.42.1

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.
Files changed (191) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/browser.bundle.js +24 -6
  3. package/dist/browser.bundle.js.map +2 -2
  4. package/dist/cjs/browser/simple_client-node.js +85 -27
  5. package/dist/cjs/browser/simple_client-node.js.map +3 -3
  6. package/dist/cjs/browser/sync/authentication_manager.js +21 -4
  7. package/dist/cjs/browser/sync/authentication_manager.js.map +2 -2
  8. package/dist/cjs/browser/sync/client.js +2 -1
  9. package/dist/cjs/browser/sync/client.js.map +2 -2
  10. package/dist/cjs/cli/env.js +12 -5
  11. package/dist/cjs/cli/env.js.map +2 -2
  12. package/dist/cjs/cli/envDefault.js +12 -7
  13. package/dist/cjs/cli/envDefault.js.map +2 -2
  14. package/dist/cjs/cli/insights.js +11 -1
  15. package/dist/cjs/cli/insights.js.map +2 -2
  16. package/dist/cjs/cli/lib/env.js +5 -1
  17. package/dist/cjs/cli/lib/env.js.map +2 -2
  18. package/dist/cjs/cli/lib/login.js +16 -12
  19. package/dist/cjs/cli/lib/login.js.map +2 -2
  20. package/dist/cjs/cli/lib/logs.js +4 -1
  21. package/dist/cjs/cli/lib/logs.js.map +2 -2
  22. package/dist/cjs/cli/lib/run.js +27 -2
  23. package/dist/cjs/cli/lib/run.js.map +2 -2
  24. package/dist/cjs/cli/lib/typecheck.js +28 -10
  25. package/dist/cjs/cli/lib/typecheck.js.map +2 -2
  26. package/dist/cjs/cli/lib/utils/utils.js +30 -0
  27. package/dist/cjs/cli/lib/utils/utils.js.map +2 -2
  28. package/dist/cjs/cli/program.js +2 -1
  29. package/dist/cjs/cli/program.js.map +2 -2
  30. package/dist/cjs/cli/project.js +27 -0
  31. package/dist/cjs/cli/project.js.map +7 -0
  32. package/dist/cjs/cli/projectCreate.js +90 -0
  33. package/dist/cjs/cli/projectCreate.js.map +7 -0
  34. package/dist/cjs/index.js +1 -1
  35. package/dist/cjs/index.js.map +1 -1
  36. package/dist/cjs/server/database.js.map +1 -1
  37. package/dist/cjs/server/impl/meta_impl.js +6 -6
  38. package/dist/cjs/server/impl/meta_impl.js.map +2 -2
  39. package/dist/cjs/server/impl/registration_impl.js +9 -4
  40. package/dist/cjs/server/impl/registration_impl.js.map +2 -2
  41. package/dist/cjs/server/index.js.map +2 -2
  42. package/dist/cjs/server/meta.js.map +1 -1
  43. package/dist/cjs/server/query.js.map +1 -1
  44. package/dist/cjs/server/registration.js.map +1 -1
  45. package/dist/cjs/server/storage.js.map +1 -1
  46. package/dist/cjs/server/system_fields.js.map +1 -1
  47. package/dist/cjs-types/browser/sync/authentication_manager.d.ts +2 -0
  48. package/dist/cjs-types/browser/sync/authentication_manager.d.ts.map +1 -1
  49. package/dist/cjs-types/browser/sync/client.d.ts +16 -0
  50. package/dist/cjs-types/browser/sync/client.d.ts.map +1 -1
  51. package/dist/cjs-types/cli/env.d.ts.map +1 -1
  52. package/dist/cjs-types/cli/envDefault.d.ts.map +1 -1
  53. package/dist/cjs-types/cli/insights.d.ts +1 -0
  54. package/dist/cjs-types/cli/insights.d.ts.map +1 -1
  55. package/dist/cjs-types/cli/lib/env.d.ts +3 -1
  56. package/dist/cjs-types/cli/lib/env.d.ts.map +1 -1
  57. package/dist/cjs-types/cli/lib/env.test.d.ts +2 -0
  58. package/dist/cjs-types/cli/lib/env.test.d.ts.map +1 -0
  59. package/dist/cjs-types/cli/lib/login.d.ts.map +1 -1
  60. package/dist/cjs-types/cli/lib/logs.d.ts +1 -1
  61. package/dist/cjs-types/cli/lib/logs.d.ts.map +1 -1
  62. package/dist/cjs-types/cli/lib/run.d.ts +1 -1
  63. package/dist/cjs-types/cli/lib/run.d.ts.map +1 -1
  64. package/dist/cjs-types/cli/lib/utils/utils.d.ts +10 -0
  65. package/dist/cjs-types/cli/lib/utils/utils.d.ts.map +1 -1
  66. package/dist/cjs-types/cli/program.d.ts.map +1 -1
  67. package/dist/cjs-types/cli/project.d.ts +3 -0
  68. package/dist/cjs-types/cli/project.d.ts.map +1 -0
  69. package/dist/cjs-types/cli/projectCreate.d.ts +5 -0
  70. package/dist/cjs-types/cli/projectCreate.d.ts.map +1 -0
  71. package/dist/cjs-types/index.d.ts +1 -1
  72. package/dist/cjs-types/server/database.d.ts +21 -3
  73. package/dist/cjs-types/server/database.d.ts.map +1 -1
  74. package/dist/cjs-types/server/impl/meta_impl.d.ts.map +1 -1
  75. package/dist/cjs-types/server/impl/registration_impl.d.ts.map +1 -1
  76. package/dist/cjs-types/server/index.d.ts +1 -1
  77. package/dist/cjs-types/server/index.d.ts.map +1 -1
  78. package/dist/cjs-types/server/meta.d.ts +10 -0
  79. package/dist/cjs-types/server/meta.d.ts.map +1 -1
  80. package/dist/cjs-types/server/query.d.ts +1 -1
  81. package/dist/cjs-types/server/registration.d.ts +15 -3
  82. package/dist/cjs-types/server/registration.d.ts.map +1 -1
  83. package/dist/cjs-types/server/storage.d.ts +20 -3
  84. package/dist/cjs-types/server/storage.d.ts.map +1 -1
  85. package/dist/cli.bundle.cjs +1099 -634
  86. package/dist/cli.bundle.cjs.map +4 -4
  87. package/dist/esm/browser/simple_client-node.js +85 -27
  88. package/dist/esm/browser/simple_client-node.js.map +3 -3
  89. package/dist/esm/browser/sync/authentication_manager.js +21 -4
  90. package/dist/esm/browser/sync/authentication_manager.js.map +2 -2
  91. package/dist/esm/browser/sync/client.js +2 -1
  92. package/dist/esm/browser/sync/client.js.map +2 -2
  93. package/dist/esm/cli/env.js +12 -5
  94. package/dist/esm/cli/env.js.map +2 -2
  95. package/dist/esm/cli/envDefault.js +12 -7
  96. package/dist/esm/cli/envDefault.js.map +2 -2
  97. package/dist/esm/cli/insights.js +11 -1
  98. package/dist/esm/cli/insights.js.map +2 -2
  99. package/dist/esm/cli/lib/env.js +5 -1
  100. package/dist/esm/cli/lib/env.js.map +2 -2
  101. package/dist/esm/cli/lib/login.js +18 -13
  102. package/dist/esm/cli/lib/login.js.map +2 -2
  103. package/dist/esm/cli/lib/logs.js +5 -2
  104. package/dist/esm/cli/lib/logs.js.map +2 -2
  105. package/dist/esm/cli/lib/run.js +32 -3
  106. package/dist/esm/cli/lib/run.js.map +2 -2
  107. package/dist/esm/cli/lib/typecheck.js +28 -10
  108. package/dist/esm/cli/lib/typecheck.js.map +3 -3
  109. package/dist/esm/cli/lib/utils/utils.js +29 -0
  110. package/dist/esm/cli/lib/utils/utils.js.map +2 -2
  111. package/dist/esm/cli/program.js +2 -1
  112. package/dist/esm/cli/program.js.map +2 -2
  113. package/dist/esm/cli/project.js +5 -0
  114. package/dist/esm/cli/project.js.map +7 -0
  115. package/dist/esm/cli/projectCreate.js +68 -0
  116. package/dist/esm/cli/projectCreate.js.map +7 -0
  117. package/dist/esm/index.js +1 -1
  118. package/dist/esm/index.js.map +1 -1
  119. package/dist/esm/server/impl/meta_impl.js +6 -6
  120. package/dist/esm/server/impl/meta_impl.js.map +2 -2
  121. package/dist/esm/server/impl/registration_impl.js +9 -4
  122. package/dist/esm/server/impl/registration_impl.js.map +2 -2
  123. package/dist/esm/server/index.js.map +2 -2
  124. package/dist/esm-types/browser/sync/authentication_manager.d.ts +2 -0
  125. package/dist/esm-types/browser/sync/authentication_manager.d.ts.map +1 -1
  126. package/dist/esm-types/browser/sync/client.d.ts +16 -0
  127. package/dist/esm-types/browser/sync/client.d.ts.map +1 -1
  128. package/dist/esm-types/cli/env.d.ts.map +1 -1
  129. package/dist/esm-types/cli/envDefault.d.ts.map +1 -1
  130. package/dist/esm-types/cli/insights.d.ts +1 -0
  131. package/dist/esm-types/cli/insights.d.ts.map +1 -1
  132. package/dist/esm-types/cli/lib/env.d.ts +3 -1
  133. package/dist/esm-types/cli/lib/env.d.ts.map +1 -1
  134. package/dist/esm-types/cli/lib/env.test.d.ts +2 -0
  135. package/dist/esm-types/cli/lib/env.test.d.ts.map +1 -0
  136. package/dist/esm-types/cli/lib/login.d.ts.map +1 -1
  137. package/dist/esm-types/cli/lib/logs.d.ts +1 -1
  138. package/dist/esm-types/cli/lib/logs.d.ts.map +1 -1
  139. package/dist/esm-types/cli/lib/run.d.ts +1 -1
  140. package/dist/esm-types/cli/lib/run.d.ts.map +1 -1
  141. package/dist/esm-types/cli/lib/utils/utils.d.ts +10 -0
  142. package/dist/esm-types/cli/lib/utils/utils.d.ts.map +1 -1
  143. package/dist/esm-types/cli/program.d.ts.map +1 -1
  144. package/dist/esm-types/cli/project.d.ts +3 -0
  145. package/dist/esm-types/cli/project.d.ts.map +1 -0
  146. package/dist/esm-types/cli/projectCreate.d.ts +5 -0
  147. package/dist/esm-types/cli/projectCreate.d.ts.map +1 -0
  148. package/dist/esm-types/index.d.ts +1 -1
  149. package/dist/esm-types/server/database.d.ts +21 -3
  150. package/dist/esm-types/server/database.d.ts.map +1 -1
  151. package/dist/esm-types/server/impl/meta_impl.d.ts.map +1 -1
  152. package/dist/esm-types/server/impl/registration_impl.d.ts.map +1 -1
  153. package/dist/esm-types/server/index.d.ts +1 -1
  154. package/dist/esm-types/server/index.d.ts.map +1 -1
  155. package/dist/esm-types/server/meta.d.ts +10 -0
  156. package/dist/esm-types/server/meta.d.ts.map +1 -1
  157. package/dist/esm-types/server/query.d.ts +1 -1
  158. package/dist/esm-types/server/registration.d.ts +15 -3
  159. package/dist/esm-types/server/registration.d.ts.map +1 -1
  160. package/dist/esm-types/server/storage.d.ts +20 -3
  161. package/dist/esm-types/server/storage.d.ts.map +1 -1
  162. package/dist/react.bundle.js +24 -6
  163. package/dist/react.bundle.js.map +2 -2
  164. package/package.json +4 -4
  165. package/src/browser/sync/authentication_manager.ts +42 -14
  166. package/src/browser/sync/client.ts +17 -0
  167. package/src/cli/env.ts +16 -6
  168. package/src/cli/envDefault.ts +13 -7
  169. package/src/cli/insights.ts +12 -0
  170. package/src/cli/lib/env.test.ts +60 -0
  171. package/src/cli/lib/env.ts +9 -1
  172. package/src/cli/lib/login.ts +18 -18
  173. package/src/cli/lib/logs.ts +7 -2
  174. package/src/cli/lib/run.ts +37 -3
  175. package/src/cli/lib/typecheck.ts +26 -10
  176. package/src/cli/lib/utils/utils.ts +56 -0
  177. package/src/cli/program.ts +2 -0
  178. package/src/cli/project.ts +7 -0
  179. package/src/cli/projectCreate.ts +99 -0
  180. package/src/index.ts +1 -1
  181. package/src/react/ConvexAuthState.test.tsx +145 -0
  182. package/src/server/database.ts +21 -3
  183. package/src/server/impl/meta_impl.ts +7 -6
  184. package/src/server/impl/registration_impl.ts +11 -5
  185. package/src/server/index.ts +1 -0
  186. package/src/server/meta.ts +10 -0
  187. package/src/server/query.ts +1 -1
  188. package/src/server/registration.test.ts +25 -1
  189. package/src/server/registration.ts +16 -1
  190. package/src/server/storage.ts +22 -3
  191. package/src/server/system_fields.ts +1 -1
@@ -21,6 +21,7 @@ import { logout } from "./logout.js";
21
21
  import { logs } from "./logs.js";
22
22
  import { mcp } from "./mcp.js";
23
23
  import { networkTest } from "./network_test.js";
24
+ import { project } from "./project.js";
24
25
  import { reinit } from "./reinit.js";
25
26
  import { run } from "./run.js";
26
27
  import { typecheck } from "./typecheck.js";
@@ -51,6 +52,7 @@ export function buildProgram() {
51
52
  .addCommand(env)
52
53
  .addCommand(data)
53
54
  .addCommand(deployment)
55
+ .addCommand(project)
54
56
  .addCommand(codegen)
55
57
  .addCommand(update)
56
58
  .addCommand(logout)
@@ -0,0 +1,7 @@
1
+ import { Command } from "@commander-js/extra-typings";
2
+ import { projectCreate } from "./projectCreate.js";
3
+
4
+ export const project = new Command("project")
5
+ .summary("Manage projects")
6
+ .description("Manage projects in your team.")
7
+ .addCommand(projectCreate);
@@ -0,0 +1,99 @@
1
+ import { Command, Option } from "@commander-js/extra-typings";
2
+ import { oneoffContext } from "../bundler/context.js";
3
+ import { logFinishedStep, logMessage, showSpinner } from "../bundler/log.js";
4
+ import { chalkStderr } from "chalk";
5
+ import { createProject } from "./lib/api.js";
6
+ import { ensureAuthCanCreateDeployment } from "./lib/deploymentSelection.js";
7
+ import { validateOrSelectTeam } from "./lib/utils/utils.js";
8
+ import { promptString } from "./lib/utils/prompts.js";
9
+ import { projectDashboardUrl } from "./lib/dashboard.js";
10
+
11
+ type ProjectCreateOptions = {
12
+ team?: string | undefined;
13
+ };
14
+
15
+ async function runProjectCreate(
16
+ nameArg: string | undefined,
17
+ options: ProjectCreateOptions,
18
+ ): Promise<void> {
19
+ const ctx = await oneoffContext({
20
+ url: undefined,
21
+ adminKey: undefined,
22
+ envFile: undefined,
23
+ });
24
+
25
+ // Creating a project goes through the platform API, which accepts personal
26
+ // access tokens and project keys but not deployment/preview deploy keys.
27
+ // Fail fast (and clearly) for the unsupported keys rather than surfacing an
28
+ // opaque error later.
29
+ await ensureAuthCanCreateDeployment(ctx);
30
+
31
+ const { team } = await validateOrSelectTeam(ctx, options.team, "Team:");
32
+
33
+ let projectName = nameArg;
34
+ if (!projectName) {
35
+ if (!process.stdin.isTTY) {
36
+ return await ctx.crash({
37
+ exitCode: 1,
38
+ errorType: "fatal",
39
+ printedMessage:
40
+ "Specify a project name:\n" + " `npx convex project create my-app`",
41
+ });
42
+ }
43
+ projectName = await promptString(ctx, {
44
+ message: "Project name:",
45
+ });
46
+ }
47
+
48
+ showSpinner(`Creating project ${projectName}...`);
49
+ // `project create` only creates the project. Provisioning a deployment is a
50
+ // separate, explicit step (`npx convex deployment create`) so this command
51
+ // doesn't have to mirror every deployment setting (region, class, ...).
52
+ const { projectSlug } = await createProject(ctx, {
53
+ teamId: team.id,
54
+ projectName,
55
+ deploymentToProvision: null,
56
+ });
57
+
58
+ logFinishedStep(
59
+ `Created project ${chalkStderr.bold(projectSlug)} in team ` +
60
+ `${chalkStderr.bold(team.slug)}, manage it at ${chalkStderr.bold(
61
+ projectDashboardUrl(team.slug, projectSlug),
62
+ )}`,
63
+ );
64
+ logMessage(
65
+ chalkStderr.gray(
66
+ `Next, add a deployment with ` +
67
+ `\`npx convex deployment create ${team.slug}:${projectSlug}:dev --type dev\` ` +
68
+ `(pass \`--region us\` to choose a region).`,
69
+ ),
70
+ );
71
+ }
72
+
73
+ export const projectCreate = new Command("create")
74
+ .summary("Create a new project")
75
+ .description(
76
+ [
77
+ "Create a new project.",
78
+ "",
79
+ "Provisioning a deployment is a separate step — after creating the",
80
+ "project, run `npx convex deployment create` to add one.",
81
+ "",
82
+ "• Create a project in your only team: `npx convex project create my-app`",
83
+ "• Pick the team: `npx convex project create my-app --team my-team`",
84
+ ].join("\n"),
85
+ )
86
+ .allowExcessArguments(false)
87
+ .argument(
88
+ "[name]",
89
+ "The name of the new project. Prompted for when omitted in an " +
90
+ "interactive terminal; required otherwise.",
91
+ )
92
+ .addOption(
93
+ new Option(
94
+ "--team <team_slug>",
95
+ "The team to create the project in. Defaults to your only team, or " +
96
+ "prompts when you belong to several.",
97
+ ),
98
+ )
99
+ .action(runProjectCreate);
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export const version = "1.41.0";
1
+ export const version = "1.42.1";
@@ -198,6 +198,7 @@ test("Tokens are used to schedule refetch", async () => {
198
198
  function mockServerConfirmsAuth(
199
199
  client: ConvexReactClient,
200
200
  oldIdentityVersion: number,
201
+ clientClockSkew?: number,
201
202
  ) {
202
203
  act(() => {
203
204
  const querySetVersion = client.sync["remoteQuerySet"]["version"];
@@ -212,6 +213,7 @@ function mockServerConfirmsAuth(
212
213
  identity: oldIdentityVersion + 1,
213
214
  },
214
215
  modifications: [],
216
+ ...(clientClockSkew !== undefined ? { clientClockSkew } : {}),
215
217
  });
216
218
  });
217
219
  }
@@ -539,3 +541,146 @@ test("stop() during reauth notifies isRefreshing false", async () => {
539
541
 
540
542
  expect(onRefreshChange).toHaveBeenCalledWith(false);
541
543
  });
544
+
545
+ // --- initialAuthTokenReuse tests ---
546
+ // When enabled, the cached token is reused after server confirmation
547
+ // (no immediate fresh-token fetch), and the refetch is scheduled using
548
+ // the server's clientClockSkew to estimate remaining lifetime.
549
+ // The server computes clientClockSkew = clientTs - serverReceiveTs:
550
+ // negative means client behind, positive means ahead. Connect latency
551
+ // makes the skew more negative (conservative).
552
+
553
+ test("initialAuthTokenReuse: sends only one Authenticate", async () => {
554
+ const client = new ConvexReactClient("https://127.0.0.1:3001", {
555
+ initialAuthTokenReuse: true,
556
+ });
557
+ const tokenLifetimeSeconds = 60;
558
+ let tokenId = 0;
559
+ const tokenFetcher = vi.fn(async () =>
560
+ jwtEncode(
561
+ { iat: 1234500, exp: 1234500 + tokenLifetimeSeconds },
562
+ "secret" + tokenId++,
563
+ ),
564
+ );
565
+
566
+ const sendMessageSpy = vi.spyOn(
567
+ client.sync["webSocketManager"],
568
+ "sendMessage",
569
+ );
570
+
571
+ const onAuthChange = vi.fn();
572
+ void client.setAuth(tokenFetcher, onAuthChange);
573
+
574
+ await flushPromises();
575
+ mockServerConfirmsAuth(client, 0);
576
+ await flushPromises();
577
+
578
+ expect(onAuthChange).toHaveBeenCalledWith(true);
579
+
580
+ const authenticateMessages = sendMessageSpy.mock.calls.filter(
581
+ ([msg]) => msg.type === "Authenticate",
582
+ );
583
+ expect(authenticateMessages).toHaveLength(1);
584
+
585
+ await client.close();
586
+ });
587
+
588
+ test("initialAuthTokenReuse: clock skew adjusts schedule for partially-expired token", async () => {
589
+ const client = new ConvexReactClient("https://127.0.0.1:3001", {
590
+ initialAuthTokenReuse: true,
591
+ });
592
+ const defaultLeewaySeconds = 10;
593
+ const tokenLifetimeSeconds = 60;
594
+ const tokenAgeOnServer = 30;
595
+ const clientClockSkewMs = 0;
596
+ const clientNow = Date.now() / 1000;
597
+ const serverNow = clientNow;
598
+ const tokenFetcher = vi.fn(async () =>
599
+ jwtEncode(
600
+ {
601
+ iat: serverNow - tokenAgeOnServer,
602
+ exp: serverNow - tokenAgeOnServer + tokenLifetimeSeconds,
603
+ },
604
+ "secret",
605
+ ),
606
+ );
607
+ void client.setAuth(tokenFetcher, () => {});
608
+
609
+ await flushPromises();
610
+ mockServerConfirmsAuth(client, 0, clientClockSkewMs);
611
+
612
+ expect(tokenFetcher).toHaveBeenCalledTimes(1);
613
+
614
+ const expectedDelay =
615
+ (tokenLifetimeSeconds - tokenAgeOnServer - defaultLeewaySeconds) * 1000;
616
+
617
+ vi.advanceTimersByTime(expectedDelay - 1);
618
+ expect(tokenFetcher).toHaveBeenCalledTimes(1);
619
+
620
+ vi.advanceTimersByTime(1);
621
+ expect(tokenFetcher).toHaveBeenCalledTimes(2);
622
+ });
623
+
624
+ test("initialAuthTokenReuse: clock skew triggers immediate refetch for nearly-expired token", async () => {
625
+ const client = new ConvexReactClient("https://127.0.0.1:3001", {
626
+ initialAuthTokenReuse: true,
627
+ });
628
+ const tokenLifetimeSeconds = 60;
629
+ const clientClockSkewMs = -50_000;
630
+ const clientNow = Date.now() / 1000;
631
+ const serverNow = clientNow - clientClockSkewMs / 1000;
632
+ const tokenFetcher = vi.fn(async () =>
633
+ jwtEncode(
634
+ {
635
+ iat: serverNow - 55,
636
+ exp: serverNow - 55 + tokenLifetimeSeconds,
637
+ },
638
+ "secret",
639
+ ),
640
+ );
641
+ void client.setAuth(tokenFetcher, () => {});
642
+
643
+ await flushPromises();
644
+ mockServerConfirmsAuth(client, 0, clientClockSkewMs);
645
+
646
+ expect(tokenFetcher).toHaveBeenCalledTimes(1);
647
+
648
+ vi.advanceTimersByTime(1);
649
+ expect(tokenFetcher).toHaveBeenCalledTimes(2);
650
+ });
651
+
652
+ test("initialAuthTokenReuse: clock skew corrects for client clock behind server", async () => {
653
+ const client = new ConvexReactClient("https://127.0.0.1:3001", {
654
+ initialAuthTokenReuse: true,
655
+ });
656
+ const defaultLeewaySeconds = 10;
657
+ const tokenLifetimeSeconds = 60;
658
+ const tokenAgeOnServer = 30;
659
+ const clientClockSkewMs = -20_000;
660
+ const clientNow = Date.now() / 1000;
661
+ const serverNow = clientNow - clientClockSkewMs / 1000;
662
+ const tokenFetcher = vi.fn(async () =>
663
+ jwtEncode(
664
+ {
665
+ iat: serverNow - tokenAgeOnServer,
666
+ exp: serverNow - tokenAgeOnServer + tokenLifetimeSeconds,
667
+ },
668
+ "secret",
669
+ ),
670
+ );
671
+ void client.setAuth(tokenFetcher, () => {});
672
+
673
+ await flushPromises();
674
+ mockServerConfirmsAuth(client, 0, clientClockSkewMs);
675
+
676
+ expect(tokenFetcher).toHaveBeenCalledTimes(1);
677
+
678
+ const expectedDelay =
679
+ (tokenLifetimeSeconds - tokenAgeOnServer - defaultLeewaySeconds) * 1000;
680
+
681
+ vi.advanceTimersByTime(expectedDelay - 1);
682
+ expect(tokenFetcher).toHaveBeenCalledTimes(1);
683
+
684
+ vi.advanceTimersByTime(1);
685
+ expect(tokenFetcher).toHaveBeenCalledTimes(2);
686
+ });
@@ -14,7 +14,13 @@ import {
14
14
 
15
15
  interface BaseDatabaseReader<DataModel extends GenericDataModel> {
16
16
  /**
17
- * Fetch a single document from the database by its {@link values.GenericId}.
17
+ * Fetch a single document from the database by table name and
18
+ * {@link values.GenericId}.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const user = await ctx.db.get("users", userId);
23
+ * ```
18
24
  *
19
25
  * @param table - The name of the table to fetch the document from.
20
26
  * @param id - The {@link values.GenericId} of the document to fetch from the database.
@@ -28,6 +34,9 @@ interface BaseDatabaseReader<DataModel extends GenericDataModel> {
28
34
  /**
29
35
  * Fetch a single document from the database by its {@link values.GenericId}.
30
36
  *
37
+ * Supported for backwards compatibility. Prefer `db.get(tableName, id)` in
38
+ * new code, or `db.system.get(tableName, id)` for system tables.
39
+ *
31
40
  * @param id - The {@link values.GenericId} of the document to fetch from the database.
32
41
  * @returns - The {@link GenericDocument} of the document at the given {@link values.GenericId}, or `null` if it no longer exists.
33
42
  */
@@ -55,7 +64,7 @@ interface BaseDatabaseReader<DataModel extends GenericDataModel> {
55
64
  * This accepts the string ID format as well as the `.toString()` representation
56
65
  * of the legacy class-based ID format.
57
66
  *
58
- * This does not guarantee that the ID exists (i.e. `db.get(id)` may return `null`).
67
+ * This does not guarantee that the ID exists (i.e. `db.get(tableName, id)` may return `null`).
59
68
  *
60
69
  * @param tableName - The name of the table.
61
70
  * @param id - The ID string.
@@ -109,7 +118,7 @@ export interface BaseTableReader<
109
118
  *
110
119
  * The two entry points are:
111
120
  * - {@link GenericDatabaseReader.get}, which fetches a single document
112
- * by its {@link values.GenericId}.
121
+ * by table name and {@link values.GenericId}.
113
122
  * - {@link GenericDatabaseReader.query}, which starts building a query.
114
123
  *
115
124
  * @example
@@ -272,6 +281,9 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
272
281
  *
273
282
  * This method will throw if the document does not exist.
274
283
  *
284
+ * Supported for backwards compatibility. Prefer `db.patch(tableName, id, value)`
285
+ * in new code.
286
+ *
275
287
  * @param id - The {@link values.GenericId} of the document to patch.
276
288
  * @param value - The partial document to merge into the existing document.
277
289
  */
@@ -316,6 +328,9 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
316
328
  * Unlike `patch`, which does a shallow merge, `replace` overwrites the
317
329
  * entire document.
318
330
  *
331
+ * Supported for backwards compatibility. Prefer `db.replace(tableName, id, value)`
332
+ * in new code.
333
+ *
319
334
  * @param id - The {@link values.GenericId} of the document to replace.
320
335
  * @param value - The new document. System fields can be omitted.
321
336
  */
@@ -343,6 +358,9 @@ export interface GenericDatabaseWriter<DataModel extends GenericDataModel>
343
358
  /**
344
359
  * Delete an existing document.
345
360
  *
361
+ * Supported for backwards compatibility. Prefer `db.delete(tableName, id)` in
362
+ * new code.
363
+ *
346
364
  * **Note:** Convex queries do not support `.delete()` directly on query
347
365
  * results. To delete multiple documents, `.collect()` them first, then
348
366
  * delete each one individually.
@@ -34,7 +34,10 @@ async function getFunctionMetadata(): Promise<{
34
34
  "1.0/getFunctionMetadata",
35
35
  {},
36
36
  );
37
- return { name, componentPath };
37
+ return {
38
+ name,
39
+ componentPath,
40
+ };
38
41
  }
39
42
 
40
43
  async function getDeploymentMetadata(): Promise<DeploymentMetadata> {
@@ -51,11 +54,9 @@ async function getDeploymentMetadata(): Promise<DeploymentMetadata> {
51
54
  }
52
55
 
53
56
  async function getRequestMetadata(): Promise<RequestMetadata> {
54
- const { ip, userAgent, requestId } = await performAsyncSyscall(
55
- "1.0/getRequestMetadata",
56
- {},
57
- );
58
- return { ip, userAgent, requestId };
57
+ const { ip, userAgent, requestId, scheduledFunctionId } =
58
+ await performAsyncSyscall("1.0/getRequestMetadata", {});
59
+ return { ip, userAgent, requestId, scheduledFunctionId };
59
60
  }
60
61
 
61
62
  export function setupQueryMeta(
@@ -61,7 +61,9 @@ async function invokeMutation<
61
61
  meta: setupMutationMeta(visibility),
62
62
 
63
63
  runQuery: (reference: any, args?: any, options?: any) =>
64
- runUdf("query", reference, args, options?.transactionLimits),
64
+ options?.useStaleSnapshot
65
+ ? runUdf("snapshotQuery", reference, args, options?.transactionLimits)
66
+ : runUdf("query", reference, args, options?.transactionLimits),
65
67
  runMutation: (reference: any, args?: any, options?: any) =>
66
68
  runUdf("mutation", reference, args, options?.transactionLimits),
67
69
  };
@@ -336,8 +338,14 @@ async function invokeQuery<
336
338
  auth: setupAuth(requestId),
337
339
  storage: setupStorageReader(requestId),
338
340
  meta: setupQueryMeta(visibility),
339
- runQuery: (reference: any, args?: any, options?: any) =>
340
- runUdf("query", reference, args, options?.transactionLimits),
341
+ runQuery: (reference: any, args?: any, options?: any) => {
342
+ if (options?.useStaleSnapshot) {
343
+ throw new Error(
344
+ "`useStaleSnapshot` is only supported in mutations, not queries.",
345
+ );
346
+ }
347
+ return runUdf("query", reference, args, options?.transactionLimits);
348
+ },
341
349
  };
342
350
  const result = await invokeFunction(func, queryCtx, args as any);
343
351
  validateReturnValue(result);
@@ -492,7 +500,6 @@ export const internalQueryGeneric: QueryBuilder<any, "internal"> = ((
492
500
  async function invokeAction<
493
501
  F extends (ctx: GenericActionCtx<GenericDataModel>, ...args: any) => any,
494
502
  >(func: F, requestId: string, argsStr: string, visibility: FunctionVisibility) {
495
- (globalThis as any).Convex?.setupPerformance?.();
496
503
  const args = jsonToConvex(JSON.parse(argsStr));
497
504
  const calls = setupActionCalls(requestId);
498
505
  const ctx = {
@@ -674,7 +681,6 @@ export const internalActionGeneric: ActionBuilder<any, "internal"> = ((
674
681
  async function invokeHttpAction<
675
682
  F extends (ctx: GenericActionCtx<GenericDataModel>, request: Request) => any,
676
683
  >(func: F, request: Request) {
677
- (globalThis as any).Convex?.setupPerformance?.();
678
684
  // TODO(presley): Change the function signature and propagate the requestId from Rust.
679
685
  // Ok, to mock it out for now, since http endpoints are only running in V8.
680
686
  const requestId = "";
@@ -118,6 +118,7 @@ export type { IndexRange, IndexRangeBuilder } from "./index_range_builder.js";
118
118
  export * from "./pagination.js";
119
119
  export type { OrderedQuery, Query, QueryInitializer } from "./query.js";
120
120
  export type {
121
+ AdvancedRunQueryOptions,
121
122
  ArgsArray,
122
123
  DefaultFunctionArgs,
123
124
  FunctionVisibility,
@@ -104,6 +104,16 @@ export type RequestMetadata = {
104
104
  ip: string | null;
105
105
  userAgent: string | null;
106
106
  requestId: string;
107
+ /**
108
+ * The ID of the scheduled function document (in `_scheduled_functions`) that
109
+ * this execution belongs to, or `null` otherwise.
110
+ *
111
+ * This is set for the scheduled function itself and for any functions it
112
+ * calls (e.g. a mutation invoked via `runMutation` by a scheduled action),
113
+ * propagating the top-level scheduled function's ID down the call tree. It is
114
+ * `null` when the function was not scheduled.
115
+ */
116
+ scheduledFunctionId: string | null;
107
117
  };
108
118
 
109
119
  /**
@@ -106,7 +106,7 @@ export interface QueryInitializer<TableInfo extends GenericTableInfo>
106
106
  /**
107
107
  * The {@link Query} interface allows functions to read values out of the database.
108
108
  *
109
- * **If you only need to load an object by ID, use `db.get(id)` instead.**
109
+ * **If you only need to load an object by ID, use `db.get(tableName, id)` instead.**
110
110
  *
111
111
  * Executing a query consists of calling
112
112
  * 1. (Optional) {@link Query.order} to define the order
@@ -1,11 +1,14 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
2
  import { test, describe, expect } from "vitest";
3
- import { assert, Equals } from "../test/type_testing.js";
3
+ import { assert, assertFalse, Equals } from "../test/type_testing.js";
4
4
  import { v } from "../values/validator.js";
5
5
  import {
6
6
  ActionBuilder,
7
7
  ApiFromModules,
8
8
  DefaultFunctionArgs,
9
+ GenericDataModel,
10
+ GenericMutationCtx,
11
+ GenericQueryCtx,
9
12
  QueryBuilder,
10
13
  actionGeneric,
11
14
  mutationGeneric,
@@ -563,3 +566,24 @@ describe("argument and return value validators can be objects or validators", ()
563
566
  expect(JSON.parse(returnString)).toEqual(expectedReturnsExport);
564
567
  });
565
568
  });
569
+
570
+ describe("useStaleSnapshot availability", () => {
571
+ type MutationRunQueryOptions = NonNullable<
572
+ Parameters<GenericMutationCtx<GenericDataModel>["runQuery"]>[2]
573
+ >;
574
+ type QueryRunQueryOptions = NonNullable<
575
+ Parameters<GenericQueryCtx<GenericDataModel>["runQuery"]>[2]
576
+ >;
577
+
578
+ test("available on mutation ctx runQuery options", () => {
579
+ assert<
580
+ "useStaleSnapshot" extends keyof MutationRunQueryOptions ? true : false
581
+ >();
582
+ });
583
+
584
+ test("not available on query ctx runQuery options", () => {
585
+ assertFalse<
586
+ "useStaleSnapshot" extends keyof QueryRunQueryOptions ? true : false
587
+ >();
588
+ });
589
+ });
@@ -135,7 +135,7 @@ export interface GenericMutationCtx<DataModel extends GenericDataModel> {
135
135
  */
136
136
  runQuery: <Query extends FunctionReference<"query", "public" | "internal">>(
137
137
  query: Query,
138
- ...args: ArgsAndOptions<Query, { transactionLimits?: TransactionLimits }>
138
+ ...args: ArgsAndOptions<Query, AdvancedRunQueryOptions>
139
139
  ) => Promise<FunctionReturnType<Query>>;
140
140
 
141
141
  /**
@@ -1211,3 +1211,18 @@ export type ActionBuilder<
1211
1211
  export type HttpActionBuilder = (
1212
1212
  func: (ctx: GenericActionCtx<any>, request: Request) => Promise<Response>,
1213
1213
  ) => PublicHttpAction;
1214
+
1215
+ export interface AdvancedRunQueryOptions {
1216
+ transactionLimits?: TransactionLimits;
1217
+ /**
1218
+ * Run a query on a recent snapshot of the database that is not guaranteed
1219
+ * to be up-to-date when this transaction commits.
1220
+ *
1221
+ * This is an advanced feature which can introduce subtle race conditions,
1222
+ * so its use is generally discouraged except for specific use-cases where
1223
+ * database read conflicts are expected, e.g. reading from an append-only
1224
+ * table with immutable records where the only read conflicts are from
1225
+ * concurrent appends.
1226
+ */
1227
+ useStaleSnapshot?: boolean;
1228
+ }
@@ -1,16 +1,28 @@
1
1
  import { GenericId } from "../values/index.js";
2
2
 
3
3
  /**
4
- * A reference to a file in storage.
4
+ * @deprecated This ID format is no longer returned by stable file storage APIs. Use `Id<"_storage">` instead.
5
5
  *
6
- * This is used in the {@link StorageReader} and {@link StorageWriter} which are accessible in
7
- * Convex queries and mutations via {@link QueryCtx} and {@link MutationCtx} respectively.
6
+ * The old ID format used by Convex file storage.
7
+ *
8
+ * ⚠️ Security warning: Anyone that has knows to this ID can download the underlying file
9
+ * from `https://<deployment>.convex.cloud/api/storage/<storageId>`.
10
+ * (Note that it’s safe to share the new ID format, `Id<"_storage">`, to anyone).
8
11
  *
9
12
  * @public
10
13
  */
11
14
  export type StorageId = string;
15
+
16
+ /**
17
+ * @deprecated This type is only necessary for backwards compatibility with Convex versions that
18
+ * pre-date `convex@1.6.0`. Use `Id<"_storage">` instead.
19
+ */
12
20
  export type FileStorageId = GenericId<"_storage"> | StorageId;
21
+
13
22
  /**
23
+ * @deprecated This type is only returned by {@link StorageReader.getUrl | storage.getUrl}.
24
+ * To get the details of a document, use `ctx.db.system.get("_storage", storageId)` instead.
25
+ *
14
26
  * Metadata for a single file as returned by {@link StorageReader.getMetadata | storage.getMetadata}.
15
27
  *
16
28
  * @public
@@ -18,6 +30,13 @@ export type FileStorageId = GenericId<"_storage"> | StorageId;
18
30
  export type FileMetadata = {
19
31
  /**
20
32
  * ID for referencing the file (eg. via {@link StorageReader.getUrl | storage.getUrl})
33
+ *
34
+ * This is an older ID format that is no longer returned by stable file storage APIs.
35
+ * Consider using `Id<"_storage">` instead.
36
+ *
37
+ * ⚠️ Security warning: Anyone that has knows to this ID can download the underlying file
38
+ * from `https://<deployment>.convex.cloud/api/storage/<storageId>`.
39
+ * (Note that it’s safe to share the new ID format, `Id<"_storage">`, to anyone).
21
40
  */
22
41
  storageId: StorageId;
23
42
  /**
@@ -46,7 +46,7 @@ export type WithOptionalSystemFields<Document extends GenericDocument> = Expand<
46
46
  * @public
47
47
  */
48
48
  export type SystemIndexes = {
49
- // Note `db.get(id)` is simpler and equivalent to a query on `by_id`.
49
+ // Note `db.get(tableName, id)` is simpler and equivalent to a query on `by_id`.
50
50
  // Unless the query is being built dynamically, or doing manual pagination.
51
51
  by_id: ["_id"];
52
52