@agent-native/core 0.109.1 → 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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/docs/content/integrations.mdx +87 -253
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/create.ts +18 -12
- package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
- package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
- package/corpus/core/src/client/require-session.tsx +19 -3
- package/corpus/core/src/client/use-action.ts +145 -2
- package/corpus/core/src/client/use-session.ts +79 -48
- package/corpus/core/src/db/client.ts +13 -7
- package/corpus/core/src/db/create-get-db.ts +20 -13
- package/corpus/core/src/db/request-telemetry.ts +154 -0
- package/corpus/core/src/deploy/build.ts +63 -3
- package/corpus/core/src/integrations/index.ts +1 -0
- package/corpus/core/src/integrations/plugin.ts +9 -3
- package/corpus/core/src/integrations/remote-push-delivery-job.ts +48 -0
- package/corpus/core/src/integrations/remote-push-delivery.ts +370 -0
- package/corpus/core/src/integrations/remote-push-store.ts +275 -26
- package/corpus/core/src/server/better-auth-instance.ts +11 -3
- package/corpus/core/src/server/core-routes-plugin.ts +36 -14
- package/corpus/core/src/server/framework-request-handler.ts +11 -1
- package/corpus/core/src/server/http-response-telemetry.ts +233 -31
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
- package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
- package/corpus/templates/clips/AGENTS.md +10 -0
- package/corpus/templates/clips/actions/create-dictation.ts +29 -4
- package/corpus/templates/clips/actions/finalize-recording.ts +1 -1
- package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
- package/corpus/templates/clips/actions/set-mobile-capture-state.ts +40 -0
- package/corpus/templates/clips/actions/update-dictation.ts +9 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -6
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -2
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +13 -2
- package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-new-recording-is-now-emphasized-as-the-primary-action-in-the.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-recordings-now-remain-safely-recoverable-until-the-stored-me.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +17 -0
- package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +60 -35
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +12 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +14 -0
- package/corpus/templates/clips/server/db/schema.ts +9 -1
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +59 -7
- package/corpus/templates/clips/server/lib/media-verification-state.ts +17 -0
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
- package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +3 -0
- package/corpus/templates/tasks/AGENTS.md +3 -0
- package/corpus/templates/tasks/actions/render-task-list-inline.ts +202 -0
- package/corpus/templates/tasks/changelog/2026-07-18-task-lists-can-now-appear-as-interactive-widgets-inline-in-c.md +6 -0
- package/corpus/templates/tasks/docs/features/f8-chat.mdx +2 -0
- package/corpus/templates/tasks/server/plugins/agent-chat.ts +2 -0
- package/dist/cli/create.d.ts +10 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +17 -13
- package/dist/cli/create.js.map +1 -1
- package/dist/client/blocks/library/diagram.config.d.ts +2 -0
- package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.config.js +8 -1
- package/dist/client/blocks/library/diagram.config.js.map +1 -1
- package/dist/client/blocks/library/diagram.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.js +9 -4
- package/dist/client/blocks/library/diagram.js.map +1 -1
- package/dist/client/require-session.d.ts.map +1 -1
- package/dist/client/require-session.js +6 -3
- package/dist/client/require-session.js.map +1 -1
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +117 -1
- package/dist/client/use-action.js.map +1 -1
- package/dist/client/use-session.d.ts.map +1 -1
- package/dist/client/use-session.js +72 -50
- package/dist/client/use-session.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +7 -1
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts +3 -6
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +9 -2
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/db/request-telemetry.d.ts +23 -0
- package/dist/db/request-telemetry.d.ts.map +1 -0
- package/dist/db/request-telemetry.js +95 -0
- package/dist/db/request-telemetry.js.map +1 -0
- package/dist/deploy/build.d.ts +3 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +45 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/index.d.ts +1 -0
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +1 -0
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +8 -3
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/remote-push-delivery-job.d.ts +4 -0
- package/dist/integrations/remote-push-delivery-job.d.ts.map +1 -0
- package/dist/integrations/remote-push-delivery-job.js +44 -0
- package/dist/integrations/remote-push-delivery-job.js.map +1 -0
- package/dist/integrations/remote-push-delivery.d.ts +11 -0
- package/dist/integrations/remote-push-delivery.d.ts.map +1 -0
- package/dist/integrations/remote-push-delivery.js +265 -0
- package/dist/integrations/remote-push-delivery.js.map +1 -0
- package/dist/integrations/remote-push-store.d.ts +33 -0
- package/dist/integrations/remote-push-store.d.ts.map +1 -1
- package/dist/integrations/remote-push-store.js +195 -26
- package/dist/integrations/remote-push-store.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/better-auth-instance.js +7 -3
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +8 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +23 -8
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/framework-request-handler.d.ts.map +1 -1
- package/dist/server/framework-request-handler.js +9 -1
- package/dist/server/framework-request-handler.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts +3 -2
- package/dist/server/http-response-telemetry.d.ts.map +1 -1
- package/dist/server/http-response-telemetry.js +160 -27
- package/dist/server/http-response-telemetry.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
- package/docs/content/integrations.mdx +87 -253
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
|
@@ -88,7 +88,9 @@ function HtmlDiagram({
|
|
|
88
88
|
}) {
|
|
89
89
|
const ref = useRef<HTMLDivElement>(null);
|
|
90
90
|
const isDark = useIsDark();
|
|
91
|
-
const
|
|
91
|
+
const preferredStyle = useWireframeStyle();
|
|
92
|
+
const designMode = data.renderMode === "design";
|
|
93
|
+
const style = designMode ? "clean" : preferredStyle;
|
|
92
94
|
const showFrame = resolveVisualFrame(data.frame, ctx);
|
|
93
95
|
const scopeId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
94
96
|
// Sanitize author HTML/CSS at the render point (defense-in-depth against
|
|
@@ -127,7 +129,7 @@ function HtmlDiagram({
|
|
|
127
129
|
</div>
|
|
128
130
|
<RoughOverlay
|
|
129
131
|
scopeRef={ref}
|
|
130
|
-
enabled={style === "sketchy"}
|
|
132
|
+
enabled={!designMode && style === "sketchy"}
|
|
131
133
|
drawFrame={showFrame}
|
|
132
134
|
selector={DIAGRAM_ROUGH_SELECTOR}
|
|
133
135
|
/>
|
|
@@ -650,7 +652,8 @@ function ExpandableDiagramBody({
|
|
|
650
652
|
compact?: boolean;
|
|
651
653
|
}) {
|
|
652
654
|
const [expanded, setExpanded] = useState(false);
|
|
653
|
-
const
|
|
655
|
+
const designMode = data.renderMode === "design";
|
|
656
|
+
const supportsStyleToggle = Boolean(data.html) && !designMode;
|
|
654
657
|
const style = useWireframeStyle();
|
|
655
658
|
const copy = useBlockCopy();
|
|
656
659
|
const sketchy = style === "sketchy";
|
|
@@ -794,7 +797,9 @@ export function DiagramEdit({
|
|
|
794
797
|
onChange({
|
|
795
798
|
html: html.trim() || undefined,
|
|
796
799
|
css: css.trim() || undefined,
|
|
800
|
+
renderMode: data.renderMode,
|
|
797
801
|
caption: caption.trim() || undefined,
|
|
802
|
+
frame: data.frame,
|
|
798
803
|
nodes: data.nodes,
|
|
799
804
|
edges: data.edges,
|
|
800
805
|
notes: data.notes,
|
|
@@ -946,7 +951,7 @@ export const diagramBlock = defineBlock<DiagramData>({
|
|
|
946
951
|
editSurface: "panel",
|
|
947
952
|
label: "Diagram",
|
|
948
953
|
description:
|
|
949
|
-
"A flexible inline architecture/code diagram. Prefer html/css with SVG or semantic HTML for polished two-dimensional layouts; use .diagram-* primitives and --wf-* tokens for theme/sketch compatibility. Legacy nodes/edges are only for simple previews.",
|
|
954
|
+
"A flexible inline architecture/code diagram. Prefer html/css with SVG or semantic HTML for polished two-dimensional layouts; use renderMode design for clean presentation or .diagram-* primitives and --wf-* tokens for theme/sketch compatibility. Legacy nodes/edges are only for simple previews.",
|
|
950
955
|
// Seed the legacy fallback shape so a fresh block validates while agents can
|
|
951
956
|
// replace it with html/css when layout quality matters.
|
|
952
957
|
empty: () => ({ nodes: [{ id: "n1", label: "Module" }], edges: [] }),
|
|
@@ -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
|
|
259
|
+
async function performActionFetch<T>(
|
|
255
260
|
name: string,
|
|
256
261
|
method: string,
|
|
257
262
|
params?: Record<string, any>,
|
|
258
|
-
options?:
|
|
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 {
|
|
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
|
|
25
|
-
const [
|
|
26
|
-
const
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
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
|
/**
|