authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 03:49:14-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 03:49:14-07:00
log21af72f7a57c0dbe668f0e0bf80166c8e9873ab3
tree3ebb0b3638d6efc23ddb3a9ef11147593b8fa0f5
parent519d64f6cec0a0f9ed18fbca4a3f8695cac44506
signature Signed by PGP key B5690EEEBB952194

perf(client-runtime): merged pull requests are no longer re-read every minute (#19374)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

3 files changed, 58 insertions(+), 6 deletions(-)

packages/client-runtime/src/state/pullRequests.ts+2-1
......@@ -115,7 +115,7 @@ function createPullRequestRefreshAtomFamily<R, E>(
115115 });
116116}
117117
118/** Refresh only the live fields a linked thread renders. */
118/** Refresh only the live fields a linked thread renders. A merged pull request is not re-read. */
119119export function createLinkedPullRequestSummaryAtomFamily<R, E>(
120120 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,
121121 refreshes = createPullRequestRefreshAtomFamily(runtime),
......@@ -129,6 +129,7 @@ export function createLinkedPullRequestSummaryAtomFamily<R, E>(
129129 refreshIntervalMs: 60_000,
130130 idleTtlMs: LINKED_PULL_REQUEST_IDLE_TTL_MS,
131131 refreshTrigger: ({ environmentId }) => refreshes({ environmentId, input: {} }),
132 isFinal: (summary) => summary.state === "merged",
132133 });
133134}
134135
packages/client-runtime/src/state/runtime.test.ts+27
......@@ -88,8 +88,10 @@ const makeEnvironmentQueryHarness = Effect.fn("TestEnvironmentQuery.makeHarness"
8888 idleTtlMs,
8989 persistence,
9090 initialState,
91 isFinal,
9192 }: {
9293 readonly refreshTrigger?: Atom.Atom<unknown>;
94 readonly isFinal?: (value: A) => boolean;
9395 readonly idleTtlMs?: number;
9496 readonly persistence?: QueryPersistence<A>;
9597 readonly initialState?: SupervisorConnectionState;
......@@ -127,6 +129,7 @@ const makeEnvironmentQueryHarness = Effect.fn("TestEnvironmentQuery.makeHarness"
127129 execute: () => execute,
128130 ...(refreshTrigger === undefined ? {} : { refreshTrigger: () => refreshTrigger }),
129131 ...(idleTtlMs === undefined ? {} : { idleTtlMs }),
132 ...(isFinal === undefined ? {} : { isFinal }),
130133 });
131134
132135 return {
......@@ -675,6 +678,30 @@ describe("environment query lifecycle", () => {
675678 ),
676679 );
677680
681 it.effect("stops refreshing on its trigger once the value is final", () =>
682 Effect.scoped(
683 Effect.gen(function* () {
684 let executions = 0;
685 const trigger = Atom.make(0).pipe(Atom.keepAlive);
686 const harness = yield* makeEnvironmentQueryHarness(
687 Effect.sync(() => (executions += 1)),
688 { refreshTrigger: trigger, isFinal: (value) => value >= 2 },
689 );
690 const registry = AtomRegistry.make();
691 yield* Effect.addFinalizer(() => Effect.sync(() => registry.dispose()));
692 const read = readEnvironmentQuery(registry, harness.atom);
693
694 registry.mount(harness.atom);
695 expect(yield* read).toBe(1);
696 registry.set(trigger, 1);
697 expect(yield* read).toBe(2);
698 registry.set(trigger, 2);
699 expect(yield* read).toBe(2);
700 expect(executions).toBe(2);
701 }),
702 ),
703 );
704
678705 it.effect("keeps an idle query when its trigger recomputes to the same value", () =>
679706 Effect.scoped(
680707 Effect.gen(function* () {
packages/client-runtime/src/state/runtime.ts+29-5
......@@ -74,6 +74,8 @@ interface EnvironmentQueryAtomOptions<Input, A, E, R> extends Omit<
7474 readonly environmentId: EnvironmentIdType;
7575 readonly input: Input;
7676 }) => Atom.Atom<unknown> | undefined;
77 /** A value that can no longer change. It stops the interval and `refreshTrigger` re-reads. */
78 readonly isFinal?: (value: A) => boolean;
7779}
7880
7981interface EnvironmentSubscriptionAtomOptions<Input, A, E, R> {
......@@ -485,8 +487,8 @@ function runInEnvironment<A, E, R>(
485487 * refreshes on its next read.
486488 */
487489const refreshOnSignalWhileRead =
488 (signal: Atom.Atom<unknown>) =>
489 <A>(self: Atom.Atom<A>): Atom.Atom<A> => {
490 <V>(signal: Atom.Atom<unknown>, isFinal: (value: V) => boolean) =>
491 <A extends V>(self: Atom.Atom<A>): Atom.Atom<A> => {
490492 // The signal value each registry's data was read under. Held outside the node because the
491493 // registry can sweep this node while `self` keeps the data, and the rebuilt node must still
492494 // see a change it missed.
......@@ -499,13 +501,31 @@ const refreshOnSignalWhileRead =
499501 const changed =
500502 readUnder.has(get.registry) && !Object.is(readUnder.get(get.registry), current);
501503 readUnder.set(get.registry, current);
502 if (changed) get.refresh(self);
504 if (changed && !isFinal(get.once(self))) get.refresh(self);
503505 return get.once(self);
504506 },
505507 { initialValueTarget: self },
506508 );
507509 };
508510
511/** `Atom.withRefresh`, except the timer stops once the value is final. */
512const refreshEveryUntilFinal =
513 <V>(intervalMs: number, isFinal: (value: V) => boolean) =>
514 <A extends V>(self: Atom.Atom<A>): Atom.Atom<A> =>
515 Atom.transform(
516 self,
517 (get) => {
518 const value = get(self);
519 if (!isFinal(value)) {
520 // @effect-diagnostics-next-line globalTimers:off - Mirrors `Atom.withRefresh`, outside an Effect runtime.
521 const handle = setTimeout(() => get.refresh(self), intervalMs);
522 get.addFinalizer(() => clearTimeout(handle));
523 }
524 return value;
525 },
526 { initialValueTarget: self },
527 );
528
509529export function createEnvironmentQueryAtomFamily<R, ER, Input, A, E>(
510530 runtime: Atom.AtomRuntime<EnvironmentRegistry.EnvironmentRegistry | R, ER>,
511531 options: EnvironmentQueryAtomOptions<
......@@ -626,15 +646,17 @@ export function createEnvironmentQueryAtomFamily<R, ER, Input, A, E>(
626646 }),
627647 Atom.setIdleTTL(idleTtlMs),
628648 );
649 const isFinal = (result: AsyncResult.AsyncResult<A, unknown>) =>
650 options.isFinal !== undefined && Option.exists(AsyncResult.value(result), options.isFinal);
629651 const intervalQuery =
630652 options.refreshIntervalMs === undefined
631653 ? queryAtom
632 : queryAtom.pipe(Atom.withRefresh(options.refreshIntervalMs));
654 : queryAtom.pipe(refreshEveryUntilFinal(options.refreshIntervalMs, isFinal));
633655 const refreshTrigger = options.refreshTrigger?.(target);
634656 return (
635657 refreshTrigger === undefined
636658 ? intervalQuery
637 : intervalQuery.pipe(refreshOnSignalWhileRead(refreshTrigger))
659 : intervalQuery.pipe(refreshOnSignalWhileRead(refreshTrigger, isFinal))
638660 ).pipe(Atom.setIdleTTL(idleTtlMs), Atom.withLabel(`${options.label}:${key}`));
639661 });
640662 return (target) => family(environmentRpcKey(target));
......@@ -716,6 +738,7 @@ export function createEnvironmentRpcQueryAtomFamily<
716738 readonly environmentId: EnvironmentIdType;
717739 readonly input: EnvironmentRpcInput<TTag>;
718740 }) => Atom.Atom<unknown> | undefined;
741 readonly isFinal?: (value: EnvironmentRpcSuccess<TTag>) => boolean;
719742 },
720743) {
721744 return createEnvironmentQueryAtomFamily(runtime, {
......@@ -727,6 +750,7 @@ export function createEnvironmentRpcQueryAtomFamily<
727750 ? {}
728751 : { refreshIntervalMs: options.refreshIntervalMs }),
729752 ...(options.refreshTrigger === undefined ? {} : { refreshTrigger: options.refreshTrigger }),
753 ...(options.isFinal === undefined ? {} : { isFinal: options.isFinal }),
730754 execute: (
731755 input: EnvironmentRpcInput<TTag>,
732756 emit: (value: EnvironmentRpcSuccess<TTag>) => Effect.Effect<void>,