@agent-native/core 0.109.2 → 0.109.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.
Files changed (97) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/create.ts +18 -12
  5. package/corpus/core/src/client/require-session.tsx +19 -3
  6. package/corpus/core/src/client/use-action.ts +145 -2
  7. package/corpus/core/src/client/use-session.ts +79 -48
  8. package/corpus/core/src/db/client.ts +13 -7
  9. package/corpus/core/src/db/create-get-db.ts +20 -13
  10. package/corpus/core/src/db/request-telemetry.ts +154 -0
  11. package/corpus/core/src/integrations/index.ts +1 -0
  12. package/corpus/core/src/integrations/plugin.ts +9 -3
  13. package/corpus/core/src/integrations/remote-push-delivery-job.ts +48 -0
  14. package/corpus/core/src/integrations/remote-push-delivery.ts +370 -0
  15. package/corpus/core/src/integrations/remote-push-store.ts +275 -26
  16. package/corpus/core/src/server/better-auth-instance.ts +11 -3
  17. package/corpus/core/src/server/core-routes-plugin.ts +36 -14
  18. package/corpus/core/src/server/framework-request-handler.ts +11 -1
  19. package/corpus/core/src/server/http-response-telemetry.ts +233 -31
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  21. package/corpus/templates/clips/actions/finalize-recording.ts +3 -1
  22. package/corpus/templates/clips/actions/set-mobile-capture-state.ts +4 -1
  23. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -6
  24. package/corpus/templates/clips/changelog/2026-07-18-new-recording-is-now-emphasized-as-the-primary-action-in-the.md +6 -0
  25. package/corpus/templates/clips/changelog/2026-07-18-recordings-now-remain-safely-recoverable-until-the-stored-me.md +6 -0
  26. package/corpus/templates/tasks/AGENTS.md +3 -0
  27. package/corpus/templates/tasks/actions/render-task-list-inline.ts +202 -0
  28. package/corpus/templates/tasks/changelog/2026-07-18-task-lists-can-now-appear-as-interactive-widgets-inline-in-c.md +6 -0
  29. package/corpus/templates/tasks/docs/features/f8-chat.mdx +2 -0
  30. package/corpus/templates/tasks/server/plugins/agent-chat.ts +2 -0
  31. package/dist/cli/create.d.ts +10 -1
  32. package/dist/cli/create.d.ts.map +1 -1
  33. package/dist/cli/create.js +17 -13
  34. package/dist/cli/create.js.map +1 -1
  35. package/dist/client/require-session.d.ts.map +1 -1
  36. package/dist/client/require-session.js +6 -3
  37. package/dist/client/require-session.js.map +1 -1
  38. package/dist/client/use-action.d.ts.map +1 -1
  39. package/dist/client/use-action.js +117 -1
  40. package/dist/client/use-action.js.map +1 -1
  41. package/dist/client/use-session.d.ts.map +1 -1
  42. package/dist/client/use-session.js +72 -50
  43. package/dist/client/use-session.js.map +1 -1
  44. package/dist/collab/routes.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/db/client.d.ts.map +1 -1
  47. package/dist/db/client.js +7 -1
  48. package/dist/db/client.js.map +1 -1
  49. package/dist/db/create-get-db.d.ts +3 -6
  50. package/dist/db/create-get-db.d.ts.map +1 -1
  51. package/dist/db/create-get-db.js +9 -2
  52. package/dist/db/create-get-db.js.map +1 -1
  53. package/dist/db/request-telemetry.d.ts +23 -0
  54. package/dist/db/request-telemetry.d.ts.map +1 -0
  55. package/dist/db/request-telemetry.js +95 -0
  56. package/dist/db/request-telemetry.js.map +1 -0
  57. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  58. package/dist/integrations/index.d.ts +1 -0
  59. package/dist/integrations/index.d.ts.map +1 -1
  60. package/dist/integrations/index.js +1 -0
  61. package/dist/integrations/index.js.map +1 -1
  62. package/dist/integrations/plugin.d.ts.map +1 -1
  63. package/dist/integrations/plugin.js +8 -3
  64. package/dist/integrations/plugin.js.map +1 -1
  65. package/dist/integrations/remote-push-delivery-job.d.ts +4 -0
  66. package/dist/integrations/remote-push-delivery-job.d.ts.map +1 -0
  67. package/dist/integrations/remote-push-delivery-job.js +44 -0
  68. package/dist/integrations/remote-push-delivery-job.js.map +1 -0
  69. package/dist/integrations/remote-push-delivery.d.ts +11 -0
  70. package/dist/integrations/remote-push-delivery.d.ts.map +1 -0
  71. package/dist/integrations/remote-push-delivery.js +265 -0
  72. package/dist/integrations/remote-push-delivery.js.map +1 -0
  73. package/dist/integrations/remote-push-store.d.ts +33 -0
  74. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  75. package/dist/integrations/remote-push-store.js +195 -26
  76. package/dist/integrations/remote-push-store.js.map +1 -1
  77. package/dist/progress/routes.d.ts +1 -1
  78. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  79. package/dist/resources/handlers.d.ts +3 -3
  80. package/dist/secrets/routes.d.ts +9 -9
  81. package/dist/server/better-auth-instance.js +7 -3
  82. package/dist/server/better-auth-instance.js.map +1 -1
  83. package/dist/server/core-routes-plugin.d.ts +8 -0
  84. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  85. package/dist/server/core-routes-plugin.js +23 -8
  86. package/dist/server/core-routes-plugin.js.map +1 -1
  87. package/dist/server/framework-request-handler.d.ts.map +1 -1
  88. package/dist/server/framework-request-handler.js +9 -1
  89. package/dist/server/framework-request-handler.js.map +1 -1
  90. package/dist/server/http-response-telemetry.d.ts +3 -2
  91. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  92. package/dist/server/http-response-telemetry.js +160 -27
  93. package/dist/server/http-response-telemetry.js.map +1 -1
  94. package/dist/server/transcribe-voice.d.ts +1 -1
  95. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  96. package/package.json +1 -1
  97. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 2309
31
- - template files: 6148
30
+ - core files: 2312
31
+ - template files: 6152
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.109.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 5199e0b: Make Chat the first and default starting point in the new-app CLI picker.
8
+ - 5199e0b: Track browser, server, cold-start, framework-readiness, and database latency for app requests; retain action 4xx failures; parallelize legacy route bootstrap reads; and deduplicate session lookups without treating transient auth failures as signed-out sessions.
9
+ - 5199e0b: Deliver remote-session Expo push notifications through a retrying, batched outbox worker and verify provider receipts before marking delivery complete.
10
+
3
11
  ## 0.109.2
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.109.2",
3
+ "version": "0.109.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -200,31 +200,36 @@ export async function createApp(
200
200
  async function promptStartShape(
201
201
  clack: typeof import("@clack/prompts"),
202
202
  ): Promise<"template" | "chat" | "headless"> {
203
- const choice = await clack.select({
203
+ const choice = await clack.select(startShapePromptOptions());
204
+ if (clack.isCancel(choice)) {
205
+ clack.cancel("Cancelled.");
206
+ process.exit(0);
207
+ }
208
+ return choice as "template" | "chat" | "headless";
209
+ }
210
+
211
+ function startShapePromptOptions() {
212
+ return {
204
213
  message: "How do you want to start?",
214
+ initialValue: "chat",
205
215
  options: [
206
- {
207
- value: "template",
208
- label: "Full template(s)",
209
- hint: "Clone complete apps (Mail, Calendar, Slides, ...) into a workspace",
210
- },
211
216
  {
212
217
  value: "chat",
213
218
  label: "Chat",
214
219
  hint: "A single app with a minimal chat UI and the browser shell wired up",
215
220
  },
221
+ {
222
+ value: "template",
223
+ label: "Full template(s)",
224
+ hint: "Clone complete apps (Mail, Calendar, Slides, ...) into a workspace",
225
+ },
216
226
  {
217
227
  value: "headless",
218
228
  label: "Headless",
219
229
  hint: "A single action-first app with one primitive and no UI shell",
220
230
  },
221
231
  ],
222
- });
223
- if (clack.isCancel(choice)) {
224
- clack.cancel("Cancelled.");
225
- process.exit(0);
226
- }
227
- return choice as "template" | "chat" | "headless";
232
+ };
228
233
  }
229
234
 
230
235
  /**
@@ -1348,6 +1353,7 @@ export {
1348
1353
  githubTarballUrl as _githubTarballUrl,
1349
1354
  findLocalTemplateFrom as _findLocalTemplateFrom,
1350
1355
  workspaceAppNameForTemplateSelection as _workspaceAppNameForTemplateSelection,
1356
+ startShapePromptOptions as _startShapePromptOptions,
1351
1357
  shouldSkipScaffoldEntry as _shouldSkipScaffoldEntry,
1352
1358
  tarExtractArgs as _tarExtractArgs,
1353
1359
  };
@@ -95,14 +95,31 @@ export function RequireSession({
95
95
  signedOut,
96
96
  bypass = false,
97
97
  }: RequireSessionProps) {
98
+ if (bypass) return <>{children}</>;
99
+ return (
100
+ <ResolvedSessionGate
101
+ fallback={fallback}
102
+ redirect={redirect}
103
+ signedOut={signedOut}
104
+ >
105
+ {children}
106
+ </ResolvedSessionGate>
107
+ );
108
+ }
109
+
110
+ function ResolvedSessionGate({
111
+ children,
112
+ fallback,
113
+ redirect = true,
114
+ signedOut,
115
+ }: Omit<RequireSessionProps, "bypass">) {
98
116
  const { session, isLoading } = useSession();
99
117
  // Guard against firing the redirect more than once (effect re-runs, React
100
118
  // StrictMode double-invoke) — a second navigation while the first is in
101
119
  // flight is harmless but noisy.
102
120
  const redirectedRef = useRef(false);
103
121
 
104
- const mustRedirect =
105
- !bypass && !isLoading && !session && redirect && !isOnSignInPage();
122
+ const mustRedirect = !isLoading && !session && redirect && !isOnSignInPage();
106
123
 
107
124
  useEffect(() => {
108
125
  if (!mustRedirect) return;
@@ -114,7 +131,6 @@ export function RequireSession({
114
131
  window.location.replace(buildSignInReturnHref());
115
132
  }, [mustRedirect]);
116
133
 
117
- if (bypass) return <>{children}</>;
118
134
  // Still resolving, or redirect already in flight: show the loading fallback
119
135
  // rather than flashing app chrome the visitor can't use.
120
136
  if (isLoading) return <>{fallback ?? <DefaultSpinner />}</>;
@@ -27,6 +27,7 @@ import type {
27
27
  UseMutationOptions,
28
28
  } from "@tanstack/react-query";
29
29
 
30
+ import { trackEvent } from "./analytics.js";
30
31
  import { agentNativePath } from "./api-path.js";
31
32
  import { getBrowserTabId } from "./browser-tab-id.js";
32
33
  import { ensureEmbedAuthFetchInterceptor } from "./embed-auth.js";
@@ -221,6 +222,10 @@ export interface ActionFetchOptions {
221
222
  includeRequestSource?: boolean;
222
223
  }
223
224
 
225
+ type InternalActionFetchOptions = ActionFetchOptions & {
226
+ onResponse?: (response: Response) => void;
227
+ };
228
+
224
229
  /**
225
230
  * Conservative per-document keepalive body budget. Browsers commonly enforce
226
231
  * an approximately 64 KiB aggregate limit across every in-flight keepalive
@@ -251,11 +256,11 @@ function utf8ByteLength(value: string): number {
251
256
  return bytes;
252
257
  }
253
258
 
254
- async function actionFetch<T>(
259
+ async function performActionFetch<T>(
255
260
  name: string,
256
261
  method: string,
257
262
  params?: Record<string, any>,
258
- options?: ActionFetchOptions,
263
+ options?: InternalActionFetchOptions,
259
264
  ): Promise<T> {
260
265
  ensureEmbedAuthFetchInterceptor();
261
266
  let url = `${ACTION_PREFIX}/${name}`;
@@ -332,6 +337,7 @@ async function actionFetch<T>(
332
337
  try {
333
338
  try {
334
339
  res = await fetch(url, init);
340
+ options?.onResponse?.(res);
335
341
  } catch (err) {
336
342
  if (timedOut) throwTimeout();
337
343
  // Caller-initiated cancellation — rethrow untouched so React Query
@@ -418,6 +424,143 @@ async function actionFetch<T>(
418
424
  return (data ?? (null as unknown)) as T;
419
425
  }
420
426
 
427
+ function actionTelemetryNow(): number {
428
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
429
+ }
430
+
431
+ function parseServerTiming(
432
+ response: Response | undefined,
433
+ ): Map<string, number> {
434
+ const timings = new Map<string, number>();
435
+ const value = response?.headers.get("server-timing");
436
+ if (!value) return timings;
437
+
438
+ for (const entry of value.split(",")) {
439
+ const [rawName, ...params] = entry.trim().split(";");
440
+ const name = rawName?.trim();
441
+ if (!name) continue;
442
+ const duration = params
443
+ .map((param) => /^dur=(.+)$/i.exec(param.trim())?.[1])
444
+ .find(Boolean);
445
+ const parsed = duration === undefined ? NaN : Number(duration);
446
+ if (Number.isFinite(parsed)) timings.set(name, parsed);
447
+ }
448
+ return timings;
449
+ }
450
+
451
+ function shouldTrackActionResponse(
452
+ error: unknown,
453
+ durationMs: number,
454
+ response: Response | undefined,
455
+ ): boolean {
456
+ if (error || durationMs >= 1_000) return true;
457
+ if (response && response.status >= 400 && response.status < 500) return true;
458
+ if (
459
+ /\bstartup(?:-db)?\s*;/i.test(response?.headers.get("server-timing") ?? "")
460
+ ) {
461
+ return true;
462
+ }
463
+ const raw = (import.meta.env as Record<string, string | undefined>)
464
+ ?.VITE_AGENT_NATIVE_ACTION_TELEMETRY_SAMPLE_RATE;
465
+ const parsed = raw === undefined ? 0.1 : Number(raw);
466
+ const rate = Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : 0.1;
467
+ return Math.random() < rate;
468
+ }
469
+
470
+ async function actionFetch<T>(
471
+ name: string,
472
+ method: string,
473
+ params?: Record<string, any>,
474
+ options?: ActionFetchOptions,
475
+ ): Promise<T> {
476
+ const startedAt = actionTelemetryNow();
477
+ let response: Response | undefined;
478
+ let responseAt: number | undefined;
479
+ let error: unknown;
480
+
481
+ try {
482
+ return await performActionFetch<T>(name, method, params, {
483
+ ...options,
484
+ onResponse: (nextResponse) => {
485
+ response = nextResponse;
486
+ responseAt = actionTelemetryNow();
487
+ },
488
+ });
489
+ } catch (caught) {
490
+ error = caught;
491
+ throw caught;
492
+ } finally {
493
+ try {
494
+ const completedAt = actionTelemetryNow();
495
+ const durationMs = Math.max(0, completedAt - startedAt);
496
+ if (shouldTrackActionResponse(error, durationMs, response)) {
497
+ const ttfbMs =
498
+ responseAt === undefined
499
+ ? undefined
500
+ : Math.max(0, responseAt - startedAt);
501
+ const serverTiming = parseServerTiming(response);
502
+ const serverDurationMs = serverTiming.get("app");
503
+ const errorStatus = Number(
504
+ (error as { status?: unknown } | undefined)?.status,
505
+ );
506
+ const statusCode =
507
+ response?.status ??
508
+ (Number.isFinite(errorStatus) ? errorStatus : undefined);
509
+ const timedOut =
510
+ (error as { timedOut?: unknown } | undefined)?.timedOut === true;
511
+ const cancelled = options?.signal?.aborted === true && !timedOut;
512
+ const contentLength = Number(response?.headers.get("content-length"));
513
+
514
+ trackEvent("action.response", {
515
+ request_id:
516
+ response?.headers.get("x-agent-native-request-id") ?? undefined,
517
+ action: name,
518
+ method,
519
+ status_code: statusCode,
520
+ status_class:
521
+ statusCode === undefined
522
+ ? "network"
523
+ : `${Math.floor(statusCode / 100)}xx`,
524
+ success: !error,
525
+ outcome: !error
526
+ ? "success"
527
+ : timedOut
528
+ ? "timeout"
529
+ : cancelled
530
+ ? "cancelled"
531
+ : response
532
+ ? "http-error"
533
+ : "network-error",
534
+ duration_ms: Math.round(durationMs),
535
+ ttfb_ms: ttfbMs === undefined ? undefined : Math.round(ttfbMs),
536
+ body_ms:
537
+ ttfbMs === undefined ? undefined : Math.round(durationMs - ttfbMs),
538
+ server_duration_ms:
539
+ serverDurationMs === undefined
540
+ ? undefined
541
+ : Math.round(serverDurationMs),
542
+ network_overhead_ms:
543
+ ttfbMs === undefined || serverDurationMs === undefined
544
+ ? undefined
545
+ : Math.max(0, Math.round(ttfbMs - serverDurationMs)),
546
+ framework_ready_wait_ms: serverTiming.get("startup"),
547
+ db_operation_wall_ms: serverTiming.get("db"),
548
+ db_connect_total_ms: serverTiming.get("db-connect"),
549
+ db_slowest_operation_ms: serverTiming.get("db-slowest"),
550
+ startup_db_operation_wall_ms: serverTiming.get("startup-db"),
551
+ startup_db_connect_total_ms: serverTiming.get("startup-db-connect"),
552
+ response_bytes:
553
+ Number.isFinite(contentLength) && contentLength >= 0
554
+ ? contentLength
555
+ : undefined,
556
+ });
557
+ }
558
+ } catch {
559
+ // Performance telemetry must never change the action result.
560
+ }
561
+ }
562
+ }
563
+
421
564
  /**
422
565
  * Imperatively call an action from browser/client code.
423
566
  *
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef } from "react";
1
+ import { useEffect, useState } from "react";
2
2
 
3
3
  import type { AuthSession } from "../server/auth.js";
4
4
  import { setSentryUser, trackSessionStatus } from "./analytics.js";
@@ -11,6 +11,65 @@ interface UseSessionResult {
11
11
  isLoading: boolean;
12
12
  }
13
13
 
14
+ const SESSION_CACHE_TTL_MS = 30_000;
15
+ const SESSION_RETRY_DELAY_MS = 1_000;
16
+ let cachedSession: AuthSession | null | undefined;
17
+ let cachedSessionAt = 0;
18
+ let sessionRequest: Promise<AuthSession | null | undefined> | undefined;
19
+ let trackedSessionIdentity: string | null | undefined;
20
+
21
+ function hasFreshSessionCache(): boolean {
22
+ return (
23
+ cachedSession !== undefined &&
24
+ Date.now() - cachedSessionAt < SESSION_CACHE_TTL_MS
25
+ );
26
+ }
27
+
28
+ function publishSessionIdentity(session: AuthSession | null): void {
29
+ const identity = session?.userId ?? session?.email ?? null;
30
+ if (trackedSessionIdentity !== identity) {
31
+ trackedSessionIdentity = identity;
32
+ if (session) {
33
+ setSentryUser(
34
+ {
35
+ id: session.userId,
36
+ email: session.email,
37
+ username: session.name,
38
+ },
39
+ session.orgId ?? null,
40
+ );
41
+ } else {
42
+ setSentryUser(null, null);
43
+ }
44
+ }
45
+ trackSessionStatus(Boolean(session));
46
+ }
47
+
48
+ function fetchSharedSession(): Promise<AuthSession | null | undefined> {
49
+ if (hasFreshSessionCache()) return Promise.resolve(cachedSession ?? null);
50
+ if (sessionRequest) return sessionRequest;
51
+
52
+ sessionRequest = (async () => {
53
+ try {
54
+ const res = await fetch(agentNativePath("/_agent-native/auth/session"));
55
+ if (!res.ok) return undefined;
56
+
57
+ const data = await res.json();
58
+ const session = data.error ? null : (data as AuthSession);
59
+ cachedSession = session;
60
+ cachedSessionAt = Date.now();
61
+ publishSessionIdentity(session);
62
+ return session;
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ })().finally(() => {
67
+ sessionRequest = undefined;
68
+ });
69
+
70
+ return sessionRequest;
71
+ }
72
+
14
73
  /**
15
74
  * Client-side hook to get the current auth session.
16
75
  *
@@ -21,61 +80,33 @@ interface UseSessionResult {
21
80
  * Templates should use this instead of building their own auth context.
22
81
  */
23
82
  export function useSession(): UseSessionResult {
24
- const [session, setSession] = useState<AuthSession | null>(null);
25
- const [isLoading, setIsLoading] = useState(true);
26
- const trackedRef = useRef(false);
83
+ const cached = hasFreshSessionCache() ? (cachedSession ?? null) : null;
84
+ const [session, setSession] = useState<AuthSession | null>(cached);
85
+ const [isLoading, setIsLoading] = useState(!hasFreshSessionCache());
27
86
 
28
87
  useEffect(() => {
29
88
  let cancelled = false;
89
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
30
90
 
31
- async function fetchSession() {
32
- let signedIn = false;
33
- let resolved: AuthSession | null = null;
34
- try {
35
- const res = await fetch(agentNativePath("/_agent-native/auth/session"));
36
- if (!res.ok) {
37
- setSession(null);
38
- return;
39
- }
40
- const data = await res.json();
41
- if (!cancelled) {
42
- // The endpoint returns { error: "..." } when not authenticated
43
- if (data.error) {
44
- setSession(null);
45
- } else {
46
- resolved = data as AuthSession;
47
- setSession(resolved);
48
- signedIn = true;
49
- }
50
- }
51
- } catch {
52
- if (!cancelled) setSession(null);
53
- } finally {
54
- if (!cancelled) {
55
- setIsLoading(false);
56
- if (resolved) {
57
- setSentryUser(
58
- {
59
- id: resolved.userId,
60
- email: resolved.email,
61
- username: resolved.name,
62
- },
63
- resolved.orgId ?? null,
64
- );
65
- } else {
66
- setSentryUser(null, null);
67
- }
68
- if (!trackedRef.current) {
69
- trackedRef.current = true;
70
- trackSessionStatus(signedIn);
71
- }
72
- }
91
+ const resolveSession = async () => {
92
+ const resolved = await fetchSharedSession();
93
+ if (cancelled) return;
94
+
95
+ if (resolved === undefined) {
96
+ retryTimer = setTimeout(() => {
97
+ void resolveSession();
98
+ }, SESSION_RETRY_DELAY_MS);
99
+ return;
73
100
  }
74
- }
75
101
 
76
- fetchSession();
102
+ setSession(resolved);
103
+ setIsLoading(false);
104
+ };
105
+
106
+ void resolveSession();
77
107
  return () => {
78
108
  cancelled = true;
109
+ if (retryTimer) clearTimeout(retryTimer);
79
110
  };
80
111
  }, []);
81
112
 
@@ -10,6 +10,11 @@
10
10
  */
11
11
  import path from "path";
12
12
 
13
+ import {
14
+ beginDatabaseOperation,
15
+ recordDatabaseRetry,
16
+ } from "./request-telemetry.js";
17
+
13
18
  const recyclingPostgresPools = new WeakSet<object>();
14
19
  const loggedNeonPools = new WeakSet<object>();
15
20
 
@@ -735,6 +740,7 @@ export async function retryOnConnectionError<T>(
735
740
  } catch (e) {
736
741
  last = e;
737
742
  if (!isConnectionError(e) || attempt === maxAttempts - 1) throw e;
743
+ recordDatabaseRetry();
738
744
  await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
739
745
  }
740
746
  }
@@ -790,6 +796,9 @@ export async function withDbTimeout<T>(
790
796
  ): Promise<T> {
791
797
  let timer: ReturnType<typeof setTimeout> | undefined;
792
798
  let settled = false;
799
+ const finishTelemetry = beginDatabaseOperation(
800
+ op === "connect" ? "connect" : "query",
801
+ );
793
802
 
794
803
  const runCleanup = async () => {
795
804
  if (!onTimeout) return;
@@ -811,12 +820,14 @@ export async function withDbTimeout<T>(
811
820
  if (settled) return;
812
821
  settled = true;
813
822
  if (timer) clearTimeout(timer);
823
+ finishTelemetry("success");
814
824
  complete(value);
815
825
  };
816
826
  const fail = (err: unknown) => {
817
827
  if (settled) return;
818
828
  settled = true;
819
829
  if (timer) clearTimeout(timer);
830
+ finishTelemetry("error");
820
831
  reject(err);
821
832
  };
822
833
 
@@ -825,6 +836,7 @@ export async function withDbTimeout<T>(
825
836
  settled = true;
826
837
  void (async () => {
827
838
  await runCleanup();
839
+ finishTelemetry("timeout");
828
840
  reject(new DbTimeoutError(op, ms));
829
841
  })();
830
842
  }, ms);
@@ -1171,13 +1183,7 @@ async function createDbExecInternal(
1171
1183
  return retryOnConnectionError<{
1172
1184
  rows: unknown[];
1173
1185
  rowsAffected: number;
1174
- }>(() =>
1175
- withDbTimeout(
1176
- "http-query",
1177
- () => queryNeonClient(pool, sql),
1178
- dbOpTimeoutMs(),
1179
- ),
1180
- );
1186
+ }>(() => queryNeonClient(pool, sql));
1181
1187
  }
1182
1188
  const result = await retryOnConnectionError<{
1183
1189
  rows: unknown[];
@@ -84,23 +84,28 @@ export function isSqlRead(sql: string): boolean {
84
84
  * connection the transaction needs. The pool-level error logger still fires
85
85
  * on idle-client drops inside transactions.
86
86
  */
87
- export function buildResilientNeonPool(pool: {
88
- connect(): Promise<any>;
89
- query(
90
- sql: string,
91
- args?: any[],
92
- ): Promise<{ rows: unknown[]; rowCount?: number }>;
93
- end(): Promise<void>;
94
- on(event: string, listener: (...args: any[]) => void): unknown;
95
- }): typeof pool {
87
+ export function buildResilientNeonPool<
88
+ T extends {
89
+ connect(): Promise<any>;
90
+ query(...args: any[]): Promise<any>;
91
+ end(): Promise<void>;
92
+ on(event: string, listener: (...args: any[]) => void): unknown;
93
+ },
94
+ >(pool: T): T {
96
95
  // Preserve all original pool methods and properties; only override `connect`
97
96
  // and `query` at the Pool level (used by drizzle's neon-serverless adapter
98
97
  // when it calls pool.query() directly, e.g. outside a transaction).
99
98
  const resilientQuery = async (
100
- sql: string,
99
+ sql: string | { text?: unknown },
101
100
  args?: any[],
102
101
  ): Promise<{ rows: unknown[]; rowCount?: number }> => {
103
- const isRead = isSqlRead(sql);
102
+ const sqlText =
103
+ typeof sql === "string"
104
+ ? sql
105
+ : typeof sql?.text === "string"
106
+ ? sql.text
107
+ : "";
108
+ const isRead = isSqlRead(sqlText);
104
109
 
105
110
  const runAttempt = async (): Promise<{
106
111
  rows: unknown[];
@@ -135,7 +140,9 @@ export function buildResilientNeonPool(pool: {
135
140
  const result = await withDbTimeout(
136
141
  "query",
137
142
  () =>
138
- client.query(sql, args ?? []) as Promise<{
143
+ (args === undefined
144
+ ? client.query(sql)
145
+ : client.query(sql, args)) as Promise<{
139
146
  rows: unknown[];
140
147
  rowCount?: number;
141
148
  }>,
@@ -180,7 +187,7 @@ export function buildResilientNeonPool(pool: {
180
187
  const val = (target as any)[prop];
181
188
  return typeof val === "function" ? val.bind(target) : val;
182
189
  },
183
- });
190
+ }) as T;
184
191
  }
185
192
 
186
193
  /**