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>(...@@ -115,7 +115,7 @@ function createPullRequestRefreshAtomFamily<R, E>(
115 });115 });
116}116}
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. */
119export function createLinkedPullRequestSummaryAtomFamily<R, E>(119export function createLinkedPullRequestSummaryAtomFamily<R, E>(
120 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,120 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,
121 refreshes = createPullRequestRefreshAtomFamily(runtime),121 refreshes = createPullRequestRefreshAtomFamily(runtime),
...@@ -129,6 +129,7 @@ export function createLinkedPullRequestSummaryAtomFamily<R, E>(...@@ -129,6 +129,7 @@ export function createLinkedPullRequestSummaryAtomFamily<R, E>(
129 refreshIntervalMs: 60_000,129 refreshIntervalMs: 60_000,
130 idleTtlMs: LINKED_PULL_REQUEST_IDLE_TTL_MS,130 idleTtlMs: LINKED_PULL_REQUEST_IDLE_TTL_MS,
131 refreshTrigger: ({ environmentId }) => refreshes({ environmentId, input: {} }),131 refreshTrigger: ({ environmentId }) => refreshes({ environmentId, input: {} }),
132 isFinal: (summary) => summary.state === "merged",
132 });133 });
133}134}
134135
packages/client-runtime/src/state/runtime.test.ts+27
...@@ -88,8 +88,10 @@ const makeEnvironmentQueryHarness = Effect.fn("TestEnvironmentQuery.makeHarness"...@@ -88,8 +88,10 @@ const makeEnvironmentQueryHarness = Effect.fn("TestEnvironmentQuery.makeHarness"
88 idleTtlMs,88 idleTtlMs,
89 persistence,89 persistence,
90 initialState,90 initialState,
91 isFinal,
91 }: {92 }: {
92 readonly refreshTrigger?: Atom.Atom<unknown>;93 readonly refreshTrigger?: Atom.Atom<unknown>;
94 readonly isFinal?: (value: A) => boolean;
93 readonly idleTtlMs?: number;95 readonly idleTtlMs?: number;
94 readonly persistence?: QueryPersistence<A>;96 readonly persistence?: QueryPersistence<A>;
95 readonly initialState?: SupervisorConnectionState;97 readonly initialState?: SupervisorConnectionState;
...@@ -127,6 +129,7 @@ const makeEnvironmentQueryHarness = Effect.fn("TestEnvironmentQuery.makeHarness"...@@ -127,6 +129,7 @@ const makeEnvironmentQueryHarness = Effect.fn("TestEnvironmentQuery.makeHarness"
127 execute: () => execute,129 execute: () => execute,
128 ...(refreshTrigger === undefined ? {} : { refreshTrigger: () => refreshTrigger }),130 ...(refreshTrigger === undefined ? {} : { refreshTrigger: () => refreshTrigger }),
129 ...(idleTtlMs === undefined ? {} : { idleTtlMs }),131 ...(idleTtlMs === undefined ? {} : { idleTtlMs }),
132 ...(isFinal === undefined ? {} : { isFinal }),
130 });133 });
131134
132 return {135 return {
...@@ -675,6 +678,30 @@ describe("environment query lifecycle", () => {...@@ -675,6 +678,30 @@ describe("environment query lifecycle", () => {
675 ),678 ),
676 );679 );
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
678 it.effect("keeps an idle query when its trigger recomputes to the same value", () =>705 it.effect("keeps an idle query when its trigger recomputes to the same value", () =>
679 Effect.scoped(706 Effect.scoped(
680 Effect.gen(function* () {707 Effect.gen(function* () {
packages/client-runtime/src/state/runtime.ts+29-5
...@@ -74,6 +74,8 @@ interface EnvironmentQueryAtomOptions<Input, A, E, R> extends Omit<...@@ -74,6 +74,8 @@ interface EnvironmentQueryAtomOptions<Input, A, E, R> extends Omit<
74 readonly environmentId: EnvironmentIdType;74 readonly environmentId: EnvironmentIdType;
75 readonly input: Input;75 readonly input: Input;
76 }) => Atom.Atom<unknown> | undefined;76 }) => 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;
77}79}
7880
79interface EnvironmentSubscriptionAtomOptions<Input, A, E, R> {81interface EnvironmentSubscriptionAtomOptions<Input, A, E, R> {
...@@ -485,8 +487,8 @@ function runInEnvironment<A, E, R>(...@@ -485,8 +487,8 @@ function runInEnvironment<A, E, R>(
485 * refreshes on its next read.487 * refreshes on its next read.
486 */488 */
487const refreshOnSignalWhileRead =489const refreshOnSignalWhileRead =
488 (signal: Atom.Atom<unknown>) =>490 <V>(signal: Atom.Atom<unknown>, isFinal: (value: V) => boolean) =>
489 <A>(self: Atom.Atom<A>): Atom.Atom<A> => {491 <A extends V>(self: Atom.Atom<A>): Atom.Atom<A> => {
490 // The signal value each registry's data was read under. Held outside the node because the492 // The signal value each registry's data was read under. Held outside the node because the
491 // registry can sweep this node while `self` keeps the data, and the rebuilt node must still493 // registry can sweep this node while `self` keeps the data, and the rebuilt node must still
492 // see a change it missed.494 // see a change it missed.
...@@ -499,13 +501,31 @@ const refreshOnSignalWhileRead =...@@ -499,13 +501,31 @@ const refreshOnSignalWhileRead =
499 const changed =501 const changed =
500 readUnder.has(get.registry) && !Object.is(readUnder.get(get.registry), current);502 readUnder.has(get.registry) && !Object.is(readUnder.get(get.registry), current);
501 readUnder.set(get.registry, current);503 readUnder.set(get.registry, current);
502 if (changed) get.refresh(self);504 if (changed && !isFinal(get.once(self))) get.refresh(self);
503 return get.once(self);505 return get.once(self);
504 },506 },
505 { initialValueTarget: self },507 { initialValueTarget: self },
506 );508 );
507 };509 };
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
509export function createEnvironmentQueryAtomFamily<R, ER, Input, A, E>(529export function createEnvironmentQueryAtomFamily<R, ER, Input, A, E>(
510 runtime: Atom.AtomRuntime<EnvironmentRegistry.EnvironmentRegistry | R, ER>,530 runtime: Atom.AtomRuntime<EnvironmentRegistry.EnvironmentRegistry | R, ER>,
511 options: EnvironmentQueryAtomOptions<531 options: EnvironmentQueryAtomOptions<
...@@ -626,15 +646,17 @@ export function createEnvironmentQueryAtomFamily<R, ER, Input, A, E>(...@@ -626,15 +646,17 @@ export function createEnvironmentQueryAtomFamily<R, ER, Input, A, E>(
626 }),646 }),
627 Atom.setIdleTTL(idleTtlMs),647 Atom.setIdleTTL(idleTtlMs),
628 );648 );
649 const isFinal = (result: AsyncResult.AsyncResult<A, unknown>) =>
650 options.isFinal !== undefined && Option.exists(AsyncResult.value(result), options.isFinal);
629 const intervalQuery =651 const intervalQuery =
630 options.refreshIntervalMs === undefined652 options.refreshIntervalMs === undefined
631 ? queryAtom653 ? queryAtom
632 : queryAtom.pipe(Atom.withRefresh(options.refreshIntervalMs));654 : queryAtom.pipe(refreshEveryUntilFinal(options.refreshIntervalMs, isFinal));
633 const refreshTrigger = options.refreshTrigger?.(target);655 const refreshTrigger = options.refreshTrigger?.(target);
634 return (656 return (
635 refreshTrigger === undefined657 refreshTrigger === undefined
636 ? intervalQuery658 ? intervalQuery
637 : intervalQuery.pipe(refreshOnSignalWhileRead(refreshTrigger))659 : intervalQuery.pipe(refreshOnSignalWhileRead(refreshTrigger, isFinal))
638 ).pipe(Atom.setIdleTTL(idleTtlMs), Atom.withLabel(`${options.label}:${key}`));660 ).pipe(Atom.setIdleTTL(idleTtlMs), Atom.withLabel(`${options.label}:${key}`));
639 });661 });
640 return (target) => family(environmentRpcKey(target));662 return (target) => family(environmentRpcKey(target));
...@@ -716,6 +738,7 @@ export function createEnvironmentRpcQueryAtomFamily<...@@ -716,6 +738,7 @@ export function createEnvironmentRpcQueryAtomFamily<
716 readonly environmentId: EnvironmentIdType;738 readonly environmentId: EnvironmentIdType;
717 readonly input: EnvironmentRpcInput<TTag>;739 readonly input: EnvironmentRpcInput<TTag>;
718 }) => Atom.Atom<unknown> | undefined;740 }) => Atom.Atom<unknown> | undefined;
741 readonly isFinal?: (value: EnvironmentRpcSuccess<TTag>) => boolean;
719 },742 },
720) {743) {
721 return createEnvironmentQueryAtomFamily(runtime, {744 return createEnvironmentQueryAtomFamily(runtime, {
...@@ -727,6 +750,7 @@ export function createEnvironmentRpcQueryAtomFamily<...@@ -727,6 +750,7 @@ export function createEnvironmentRpcQueryAtomFamily<
727 ? {}750 ? {}
728 : { refreshIntervalMs: options.refreshIntervalMs }),751 : { refreshIntervalMs: options.refreshIntervalMs }),
729 ...(options.refreshTrigger === undefined ? {} : { refreshTrigger: options.refreshTrigger }),752 ...(options.refreshTrigger === undefined ? {} : { refreshTrigger: options.refreshTrigger }),
753 ...(options.isFinal === undefined ? {} : { isFinal: options.isFinal }),
730 execute: (754 execute: (
731 input: EnvironmentRpcInput<TTag>,755 input: EnvironmentRpcInput<TTag>,
732 emit: (value: EnvironmentRpcSuccess<TTag>) => Effect.Effect<void>,756 emit: (value: EnvironmentRpcSuccess<TTag>) => Effect.Effect<void>,