Add explicit lifecycle pipelines and scheduled query refreshes #68

Merged
Thilawyn merged 4 commits from next into master 2026-07-30 15:36:50 +02:00
10 changed files with 22 additions and 22 deletions
Showing only changes of commit f8ac04101a - Show all commits
+4 -4
View File
@@ -82,7 +82,7 @@ const CreateProfileView = Component.make("CreateProfile")(function* () {
Effect.log( Effect.log(
`Creating ${profile.displayName}, age ${profile.age}`, `Creating ${profile.displayName}, age ${profile.age}`,
), ),
}).pipe(MutationForm.run), }).pipe(MutationForm.thenRun),
) )
const [canCommit, isCommitting] = yield* View.useAll([ const [canCommit, isCommitting] = yield* View.useAll([
@@ -108,7 +108,7 @@ mutation receives the schema's decoded profile, so `profile.age` is a number.
Schema transformations happen before the mutation and schema issues prevent an Schema transformations happen before the mutation and schema issues prevent an
invalid draft from being submitted. invalid draft from being submitted.
`MutationForm.run` starts initial validation. In the example above, `MutationForm.thenRun` starts initial validation. In the example above,
`Component.useOnMount` keeps that validation and the form's mutation work tied `Component.useOnMount` keeps that validation and the form's mutation work tied
to the component that owns them. to the component that owns them.
@@ -137,7 +137,7 @@ const EditProfileView = Component.make("EditProfile")(function* () {
const form = yield* LensForm.make({ const form = yield* LensForm.make({
schema: ProfileSchema, schema: ProfileSchema,
target: profile, target: profile,
}).pipe(LensForm.run) }).pipe(LensForm.thenRun)
return [form, profile] as const return [form, profile] as const
}), }),
@@ -432,7 +432,7 @@ const AppointmentView = Component.make("Appointment")(function* () {
Effect.log( Effect.log(
`Saving ${DateTime.formatIso(appointment.startsAt)}`, `Saving ${DateTime.formatIso(appointment.startsAt)}`,
), ),
}).pipe(MutationForm.run) }).pipe(MutationForm.thenRun)
return [form, Form.focusObjectOn(form, "startsAt")] as const return [form, Form.focusObjectOn(form, "startsAt")] as const
}), }),
+6 -6
View File
@@ -60,7 +60,7 @@ A query is driven by a `View` rather than by a value read during one React
render. Whenever that key changes, a running Query checks the cache and starts render. Whenever that key changes, a running Query checks the cache and starts
the query effect when necessary. the query effect when necessary.
`Query.make` constructs the Query and `Query.run` starts it in the current `Query.make` constructs the Query and `Query.thenRun` starts it in the current
scope. Create each query instance once and keep it stable. In a component, the usual place is scope. Create each query instance once and keep it stable. In a component, the usual place is
`Component.useOnMount`; a query shared by multiple components can instead be `Component.useOnMount`; a query shared by multiple components can instead be
owned by an Effect service. Change the existing query's reactive key rather owned by an Effect service. Change the existing query's reactive key rather
@@ -95,7 +95,7 @@ const PostView = Component.make("Post")(function* () {
Effect.andThen((response) => response.json), Effect.andThen((response) => response.json),
Effect.andThen(Schema.decodeUnknownEffect(Post)), Effect.andThen(Schema.decodeUnknownEffect(Post)),
), ),
}).pipe(Query.run) }).pipe(Query.thenRun)
return [Lens.focusTupleAt(key, 1), query] as const return [Lens.focusTupleAt(key, 1), query] as const
}), }),
@@ -124,7 +124,7 @@ Effect equality by default, so structurally equal Effect data types work well
as query keys. Supply `keyEquivalence` when the key needs different equality as query keys. Supply `keyEquivalence` when the key needs different equality
semantics. semantics.
`Query.run` starts watching its key in the current scope. The `Query.thenRun` starts watching its key in the current scope. The
`Component.useOnMount` call above keeps both the query instance and its query `Component.useOnMount` call above keeps both the query instance and its query
function identity stable for the component's lifetime. function identity stable for the component's lifetime.
@@ -256,7 +256,7 @@ Refresh every five minutes, starting after five minutes:
import { Schedule } from "effect" import { Schedule } from "effect"
const query = yield* Query.make(options).pipe( const query = yield* Query.make(options).pipe(
Query.run, Query.thenRun,
Query.withScheduledRefresh(Schedule.spaced("5 minutes")), Query.withScheduledRefresh(Schedule.spaced("5 minutes")),
) )
``` ```
@@ -265,7 +265,7 @@ Limit the number of refreshes:
```ts ```ts
const query = yield* Query.make(options).pipe( const query = yield* Query.make(options).pipe(
Query.run, Query.thenRun,
Query.withScheduledRefresh( Query.withScheduledRefresh(
Schedule.spaced("5 minutes").pipe( Schedule.spaced("5 minutes").pipe(
Schedule.upTo({ times: 3 }), Schedule.upTo({ times: 3 }),
@@ -296,7 +296,7 @@ const query = yield* Query.make({
f: loadPost, f: loadPost,
staleTime: "10 seconds", staleTime: "10 seconds",
refreshOnWindowFocus: false, refreshOnWindowFocus: false,
}).pipe(Query.run) }).pipe(Query.thenRun)
``` ```
Window-focus refresh depends on the optional `@effect/platform-browser` Window-focus refresh depends on the optional `@effect/platform-browser`
+1 -1
View File
@@ -189,7 +189,7 @@ export const make = Effect.fnUntraced(function* <A, I = A, RD = never, RE = neve
) )
}) })
export const run = <A, I = A, RD = never, RE = never, TER = never, TEW = never, TRR = never, TRW = never, E = never, R = never>( export const thenRun = <A, I = A, RD = never, RE = never, TER = never, TEW = never, TRR = never, TRW = never, E = never, R = never>(
self: Effect.Effect<LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>, E, R>, self: Effect.Effect<LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>, E, R>,
): Effect.Effect< ): Effect.Effect<
LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>, LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>,
+1 -1
View File
@@ -196,7 +196,7 @@ export const make = Effect.fnUntraced(function* <A, I = A, RD = never, RE = neve
) )
}) })
export const run = <A, I = A, RD = never, RE = never, MA = void, ME = never, MR = never, E = never, R = never>( export const thenRun = <A, I = A, RD = never, RE = never, MA = void, ME = never, MR = never, E = never, R = never>(
self: Effect.Effect<MutationForm<A, I, RD, RE, MA, ME, MR>, E, R>, self: Effect.Effect<MutationForm<A, I, RD, RE, MA, ME, MR>, E, R>,
): Effect.Effect< ): Effect.Effect<
MutationForm<A, I, RD, RE, MA, ME, MR>, MutationForm<A, I, RD, RE, MA, ME, MR>,
+2 -2
View File
@@ -93,7 +93,7 @@ describe("Query", () => {
}), }),
staleTime: "0 millis", staleTime: "0 millis",
}).pipe( }).pipe(
Query.run, Query.thenRun,
Query.withScheduledRefresh( Query.withScheduledRefresh(
Schedule.spaced("1 second").pipe( Schedule.spaced("1 second").pipe(
Schedule.upTo({ times: 1 }), Schedule.upTo({ times: 1 }),
@@ -187,7 +187,7 @@ describe("Query", () => {
return `value:${id}:${calls}` return `value:${id}:${calls}`
}), }),
staleTime: "1 minute", staleTime: "1 minute",
}).pipe(Query.run) }).pipe(Query.thenRun)
const latestFinalState = yield* Effect.sleep("1 millis").pipe( const latestFinalState = yield* Effect.sleep("1 millis").pipe(
Effect.andThen(View.get(query.latestFinalState)), Effect.andThen(View.get(query.latestFinalState)),
+3 -3
View File
@@ -410,7 +410,7 @@ export const make = Effect.fnUntraced(function* <K, A, E = never, R = never>(
) )
}) })
export const run = <K, A, E = never, R = never, E2 = never, R2 = never>( export const thenRun = <K, A, E = never, R = never, E2 = never, R2 = never>(
self: Effect.Effect<Query<K, A, E, R>, E2, R2>, self: Effect.Effect<Query<K, A, E, R>, E2, R2>,
): Effect.Effect<Query<K, A, E, R>, E2, Scope.Scope | R2> => Effect.tap( ): Effect.Effect<Query<K, A, E, R>, E2, Scope.Scope | R2> => Effect.tap(
self, self,
@@ -423,7 +423,7 @@ export const run = <K, A, E = never, R = never, E2 = never, R2 = never>(
* @example Refresh every five minutes, starting after five minutes * @example Refresh every five minutes, starting after five minutes
* ```ts * ```ts
* yield* Query.make(options).pipe( * yield* Query.make(options).pipe(
* Query.run, * Query.thenRun,
* Query.withScheduledRefresh(Schedule.spaced("5 minutes")), * Query.withScheduledRefresh(Schedule.spaced("5 minutes")),
* ) * )
* ``` * ```
@@ -431,7 +431,7 @@ export const run = <K, A, E = never, R = never, E2 = never, R2 = never>(
* @example Refresh at most three times * @example Refresh at most three times
* ```ts * ```ts
* yield* Query.make(options).pipe( * yield* Query.make(options).pipe(
* Query.run, * Query.thenRun,
* Query.withScheduledRefresh( * Query.withScheduledRefresh(
* Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 })), * Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 })),
* ), * ),
+2 -2
View File
@@ -115,14 +115,14 @@ export const make = Effect.fnUntraced(function* (
) )
}) })
export const run = <E = never, R = never>( export const thenRun = <E = never, R = never>(
self: Effect.Effect<QueryClientService, E, R>, self: Effect.Effect<QueryClientService, E, R>,
): Effect.Effect<QueryClientService, E, Scope.Scope | R> => Effect.tap( ): Effect.Effect<QueryClientService, E, Scope.Scope | R> => Effect.tap(
self, self,
client => Effect.forkScoped(client.run), client => Effect.forkScoped(client.run),
) )
export const layer = (options?: make.Options) => Layer.effect(QueryClient, run(make(options))) export const layer = (options?: make.Options) => Layer.effect(QueryClient, thenRun(make(options)))
export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-view/QueryClient/QueryClientCacheKey") export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-view/QueryClient/QueryClientCacheKey")
+1 -1
View File
@@ -31,7 +31,7 @@ const RegisterRouteComponent = Component.make("RegisterRouteView")(function*() {
initialEncodedValue: { email: "", password: "" }, initialEncodedValue: { email: "", password: "" },
f: ([value]) => Effect.log(`Registered ${value.email}`), f: ([value]) => Effect.log(`Registered ${value.email}`),
}).pipe( }).pipe(
MutationForm.run, MutationForm.thenRun,
) )
const emailField = Form.focusObjectOn(form, "email") const emailField = Form.focusObjectOn(form, "email")
+1 -1
View File
@@ -73,7 +73,7 @@ const UserProfileEditorView = Component.make("UserProfileEditorView")(function*(
password: "", password: "",
}, },
}).pipe( }).pipe(
LensForm.run, LensForm.thenRun,
) )
const emailField = Form.focusObjectOn(form, "email") const emailField = Form.focusObjectOn(form, "email")
+1 -1
View File
@@ -46,7 +46,7 @@ const QueryRouteComponent = Component.make("QueryRouteView")(function*() {
), ),
staleTime: "10 seconds", staleTime: "10 seconds",
}).pipe( }).pipe(
Query.run, Query.thenRun,
) )
const mutation = yield* Mutation.make({ const mutation = yield* Mutation.make({