From 2b1c6aeda988cf9b7b215e075963c78bb7fce36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 30 Jul 2026 15:36:49 +0200 Subject: [PATCH] Add explicit lifecycle pipelines and scheduled query refreshes (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Replace auto-running `service` constructors with explicit `make(...).pipe(thenRun)` lifecycle pipelines. - Add scoped, schedule-driven Query refreshes with `Query.withScheduledRefresh`. - Upgrade the Effect v4 stack to beta 102. - Add `@effect-view/vite-plugin` to the publish workflow. - Update examples and documentation for the new APIs. ## Breaking changes The following constructors have been replaced: - `Query.service(options)` → `Query.make(options).pipe(Query.thenRun)` - `QueryClient.service(options)` → `QueryClient.make(options).pipe(QueryClient.thenRun)` - `MutationForm.service(options)` → `MutationForm.make(options).pipe(MutationForm.thenRun)` - `LensForm.service(options)` → `LensForm.make(options).pipe(LensForm.thenRun)` This separates object construction from starting its scoped background behavior. ## Scheduled Query refreshes Queries can now be refreshed with any Effect `Schedule`: ```ts const query = yield* Query.make(options).pipe( Query.thenRun, Query.withScheduledRefresh(Schedule.spaced("5 minutes")), ) --------- Co-authored-by: Julien Valverdé Reviewed-on: https://git.valverde.cloud/Thilawyn/effect-view/pulls/68 --- .gitea/workflows/publish.yaml | 7 ++++ bun.lock | 22 +++++----- packages/docs/docs/forms.md | 30 +++++++------- packages/docs/docs/query.md | 45 ++++++++++++++++---- packages/effect-view/package.json | 8 ++-- packages/effect-view/src/LensForm.ts | 15 +++---- packages/effect-view/src/MutationForm.ts | 15 +++---- packages/effect-view/src/Query.test.ts | 50 ++++++++++++++++++++-- packages/effect-view/src/Query.ts | 53 ++++++++++++++++++++---- packages/effect-view/src/QueryClient.ts | 14 +++---- packages/example/package.json | 6 +-- packages/example/src/routes/form.tsx | 6 ++- packages/example/src/routes/lensform.tsx | 6 ++- packages/example/src/routes/query.tsx | 14 ++++--- 14 files changed, 198 insertions(+), 93 deletions(-) diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index f553df5..3625031 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -40,6 +40,13 @@ jobs: access: public token: ${{ secrets.NPM_TOKEN }} registry: https://registry.npmjs.org + - name: Publish @effect-view/vite-plugin + uses: JS-DevTools/npm-publish@v4 + with: + package: packages/vite-plugin + access: public + token: ${{ secrets.NPM_TOKEN }} + registry: https://registry.npmjs.org - name: Publish effect-fc uses: JS-DevTools/npm-publish@v4 with: diff --git a/bun.lock b/bun.lock index 5239904..79a0670 100644 --- a/bun.lock +++ b/bun.lock @@ -85,18 +85,18 @@ "version": "0.1.0", "dependencies": { "@standard-schema/spec": "^1.1.0", - "effect-lens": "^2.0.1-beta.101", + "effect-lens": "^2.0.1-beta.102", }, "devDependencies": { - "@effect/platform-browser": "4.0.0-beta.101", + "@effect/platform-browser": "4.0.0-beta.102", "@testing-library/react": "^16.3.0", - "effect": "4.0.0-beta.101", + "effect": "4.0.0-beta.102", "jsdom": "^26.1.0", "vitest": "^3.2.4", }, "peerDependencies": { "@types/react": "^19.2.0", - "effect": "4.0.0-beta.101", + "effect": "4.0.0-beta.102", "react": "^19.2.0", }, }, @@ -104,9 +104,9 @@ "name": "@effect-view/example", "version": "0.0.0", "dependencies": { - "@effect/platform-browser": "4.0.0-beta.101", + "@effect/platform-browser": "4.0.0-beta.102", "@radix-ui/themes": "^3.3.0", - "effect": "4.0.0-beta.101", + "effect": "4.0.0-beta.102", "effect-view": "workspace:*", "react-icons": "^5.6.0", }, @@ -3052,9 +3052,9 @@ "@docusaurus/utils/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], - "@effect-view/example/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.101", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-05//60oEzMyQyNjk8Ll1mML6gU2RvT9TTaomq5cGmNQu6Gzb6jlWfzn2/ZGzFtehhB9iOpqtjy0QkT5tDh9ElA=="], + "@effect-view/example/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.102", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.102" } }, "sha512-DXXVt+oOpukqI3lEaGvvXQxSobzIy3lAsIQvlpVK+MuYBm1o4FHZrHKbrOFItDUvSR1t0kiTncFPRi01YaJUmA=="], - "@effect-view/example/effect": ["effect@4.0.0-beta.101", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="], + "@effect-view/example/effect": ["effect@4.0.0-beta.102", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-z8Y+Q76Hh/kjLFZrXu8tGn6e+tDsg45R+UHhxd190pXxD53OGwf/G/zDxXTkse4HJ5mobNZfitLfUCp4fMvu6w=="], "@effect-view/vite-plugin/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], @@ -3122,11 +3122,11 @@ "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], - "effect-view/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.101", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.101" } }, "sha512-05//60oEzMyQyNjk8Ll1mML6gU2RvT9TTaomq5cGmNQu6Gzb6jlWfzn2/ZGzFtehhB9iOpqtjy0QkT5tDh9ElA=="], + "effect-view/@effect/platform-browser": ["@effect/platform-browser@4.0.0-beta.102", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.102" } }, "sha512-DXXVt+oOpukqI3lEaGvvXQxSobzIy3lAsIQvlpVK+MuYBm1o4FHZrHKbrOFItDUvSR1t0kiTncFPRi01YaJUmA=="], - "effect-view/effect": ["effect@4.0.0-beta.101", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-HjowumlIo+orthn4jMlEJPuzIYPBV+uq/XiciHWhiedLsXQpWHdNJHO5d59BVDP5s1LPuvERcktwFqRXnJqnhA=="], + "effect-view/effect": ["effect@4.0.0-beta.102", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-z8Y+Q76Hh/kjLFZrXu8tGn6e+tDsg45R+UHhxd190pXxD53OGwf/G/zDxXTkse4HJ5mobNZfitLfUCp4fMvu6w=="], - "effect-view/effect-lens": ["effect-lens@2.0.1-beta.101", "", { "peerDependencies": { "effect": "4.0.0-beta.101" } }, "sha512-ME5JruBkF3AG34rhb9MlHb1uwEbW70hr6lFITf8tcHBTgVF5QXFCe8nWATraYZpxBHGqM2bOnRvoadmwh2yyvQ=="], + "effect-view/effect-lens": ["effect-lens@2.0.1-beta.102", "", { "peerDependencies": { "effect": "4.0.0-beta.102" } }, "sha512-YQSPI/r42Do/24X+RurTd4BLV2EdxG4LcgySpR4Pt7W2Z+wCmoBlSAV+B7rs2lTpqOny3kKTLnn9tbpfQVzSAA=="], "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], diff --git a/packages/docs/docs/forms.md b/packages/docs/docs/forms.md index 7f0cb1e..f2b9062 100644 --- a/packages/docs/docs/forms.md +++ b/packages/docs/docs/forms.md @@ -54,11 +54,9 @@ valid decoded value should go: ## Create forms once -`MutationForm.service` and `LensForm.service` are Effect constructors, not -hooks. Create each root form once and keep it stable. For a component-owned -form, call the constructor inside `Component.useOnMount`; for a form shared by -multiple components, create it in an Effect service. Update the existing form's -Lenses rather than reconstructing the form during render. +`MutationForm.make` and `LensForm.make` construct forms. Pipe them through the +module's `run` operator to start validation or synchronization in the current +scope. Create each root form once and keep it stable. ## MutationForm: validate, then submit @@ -73,7 +71,7 @@ import { Component, MutationForm, View } from "effect-view" const CreateProfileView = Component.make("CreateProfile")(function* () { const form = yield* Component.useOnMount(() => - MutationForm.service({ + MutationForm.make({ schema: ProfileSchema, initialEncodedValue: { displayName: "", @@ -84,7 +82,7 @@ const CreateProfileView = Component.make("CreateProfile")(function* () { Effect.log( `Creating ${profile.displayName}, age ${profile.age}`, ), - }), + }).pipe(MutationForm.thenRun), ) const [canCommit, isCommitting] = yield* View.useAll([ @@ -110,9 +108,9 @@ mutation receives the schema's decoded profile, so `profile.age` is a number. Schema transformations happen before the mutation and schema issues prevent an invalid draft from being submitted. -`MutationForm.service` also starts initial validation in the current scope. In -the example above, `Component.useOnMount` keeps that validation and the form's -mutation work tied to the component that owns them. +`MutationForm.thenRun` starts initial validation. In the example above, +`Component.useOnMount` keeps that validation and the form's mutation work tied +to the component that owns them. ## LensForm: validate, then synchronize @@ -136,10 +134,10 @@ const EditProfileView = Component.make("EditProfile")(function* () { }), ) - const form = yield* LensForm.service({ + const form = yield* LensForm.make({ schema: ProfileSchema, target: profile, - }) + }).pipe(LensForm.thenRun) return [form, profile] as const }), @@ -168,8 +166,8 @@ reaches the target. If another part of the application updates the target, `LensForm` encodes that value back into the draft. Pass `initialEncodedValue` only when the first draft should differ from the -encoded target. Otherwise `LensForm.service` obtains the initial draft by -encoding the target through the schema. +encoded target. Otherwise `LensForm.make` obtains the initial draft by encoding +the target through the schema. ## Focus into subforms @@ -427,14 +425,14 @@ const AppointmentSchema = Schema.Struct({ const AppointmentView = Component.make("Appointment")(function* () { const [form, startsAtField] = yield* Component.useOnMount(() => Effect.gen(function* () { - const form = yield* MutationForm.service({ + const form = yield* MutationForm.make({ schema: AppointmentSchema, initialEncodedValue: { startsAt: "" }, f: ([appointment]) => Effect.log( `Saving ${DateTime.formatIso(appointment.startsAt)}`, ), - }) + }).pipe(MutationForm.thenRun) return [form, Form.focusObjectOn(form, "startsAt")] as const }), diff --git a/packages/docs/docs/query.md b/packages/docs/docs/query.md index ccb4efc..039df45 100644 --- a/packages/docs/docs/query.md +++ b/packages/docs/docs/query.md @@ -57,11 +57,11 @@ client defaults. Window-focus refresh also requires the optional ## Create a reactive query A query is driven by a `View` rather than by a value read during one React -render. Whenever that key changes, `Query.service` checks the cache and starts +render. Whenever that key changes, a running Query checks the cache and starts the query effect when necessary. -`Query.service` is an Effect constructor, not a hook. Create each query instance -once and keep it stable. In a component, the usual place is +`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 `Component.useOnMount`; a query shared by multiple components can instead be owned by an Effect service. Change the existing query's reactive key rather than reconstructing the query during render. @@ -84,7 +84,7 @@ const PostView = Component.make("Post")(function* () { yield* SubscriptionRef.make(["post", 1 as number] as const), ) - const query = yield* Query.service({ + const query = yield* Query.make({ key, staleTime: "1 minute", f: ([, id]) => @@ -95,7 +95,7 @@ const PostView = Component.make("Post")(function* () { Effect.andThen((response) => response.json), Effect.andThen(Schema.decodeUnknownEffect(Post)), ), - }) + }).pipe(Query.thenRun) 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 semantics. -`Query.service` 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 function identity stable for the component's lifetime. @@ -248,6 +248,35 @@ workflows that need to wait for the final success or failure state. Invalidating does not itself refetch. Follow it with `refreshView`, change the key, or allow a later fetch to repopulate the cache. +### Refresh on an interval + +Refresh every five minutes, starting after five minutes: + +```ts +import { Schedule } from "effect" + +const query = yield* Query.make(options).pipe( + Query.thenRun, + Query.withScheduledRefresh(Schedule.spaced("5 minutes")), +) +``` + +Limit the number of refreshes: + +```ts +const query = yield* Query.make(options).pipe( + Query.thenRun, + Query.withScheduledRefresh( + Schedule.spaced("5 minutes").pipe( + Schedule.upTo({ times: 3 }), + ), + ), +) +``` + +The refresh fiber stops with the surrounding scope. The Effect returns the +original query. Cache and `staleTime` rules still apply. + ## Staleness and cache lifetime `staleTime` controls how long a successful result can satisfy a fetch without @@ -262,12 +291,12 @@ By default, the client enables refresh on browser window focus. Set it globally or override it for one query: ```tsx -const query = yield* Query.service({ +const query = yield* Query.make({ key, f: loadPost, staleTime: "10 seconds", refreshOnWindowFocus: false, -}) +}).pipe(Query.thenRun) ``` Window-focus refresh depends on the optional `@effect/platform-browser` diff --git a/packages/effect-view/package.json b/packages/effect-view/package.json index 6be9216..677772f 100644 --- a/packages/effect-view/package.json +++ b/packages/effect-view/package.json @@ -97,19 +97,19 @@ "clean:modules": "rm -rf node_modules" }, "devDependencies": { - "@effect/platform-browser": "4.0.0-beta.101", + "@effect/platform-browser": "4.0.0-beta.102", "@testing-library/react": "^16.3.0", - "effect": "4.0.0-beta.101", + "effect": "4.0.0-beta.102", "jsdom": "^26.1.0", "vitest": "^3.2.4" }, "peerDependencies": { "@types/react": "^19.2.0", - "effect": "4.0.0-beta.101", + "effect": "4.0.0-beta.102", "react": "^19.2.0" }, "dependencies": { "@standard-schema/spec": "^1.1.0", - "effect-lens": "^2.0.1-beta.101" + "effect-lens": "^2.0.1-beta.102" } } diff --git a/packages/effect-view/src/LensForm.ts b/packages/effect-view/src/LensForm.ts index 11debd1..3426d84 100644 --- a/packages/effect-view/src/LensForm.ts +++ b/packages/effect-view/src/LensForm.ts @@ -189,18 +189,13 @@ export const make = Effect.fnUntraced(function* - extends make.Options {} -} - -export const service = ( - options: service.Options +export const thenRun = ( + self: Effect.Effect, E, R>, ): Effect.Effect< LensForm, - Schema.SchemaError | TER, - Scope.Scope | RD | RE | TRR | TRW + E, + Scope.Scope | R > => Effect.tap( - make(options), + self, form => Effect.forkScoped(form.run), ) diff --git a/packages/effect-view/src/MutationForm.ts b/packages/effect-view/src/MutationForm.ts index 160bbdb..27debad 100644 --- a/packages/effect-view/src/MutationForm.ts +++ b/packages/effect-view/src/MutationForm.ts @@ -196,18 +196,13 @@ export const make = Effect.fnUntraced(function* - extends make.Options {} -} - -export const service = ( - options: service.Options +export const thenRun = ( + self: Effect.Effect, E, R>, ): Effect.Effect< MutationForm, - never, - Scope.Scope | RD | RE | MR + E, + Scope.Scope | R > => Effect.tap( - make(options), + self, form => Effect.forkScoped(form.run), ) diff --git a/packages/effect-view/src/Query.test.ts b/packages/effect-view/src/Query.test.ts index b518c0d..af30f89 100644 --- a/packages/effect-view/src/Query.test.ts +++ b/packages/effect-view/src/Query.test.ts @@ -1,4 +1,5 @@ -import { Effect, type Scope, Stream } from "effect" +import { Effect, Schedule, type Scope, Stream } from "effect" +import { TestClock } from "effect/testing" import { AsyncResult } from "effect/unstable/reactivity" import { describe, expect, it } from "vitest" import * as Query from "./Query.js" @@ -79,6 +80,49 @@ describe("Query", () => { expect(expectSuccessValue(result[1])).toBe("value:1:2") }) + it("withScheduledRefresh lets the Schedule control the first refresh", async () => { + let calls = 0 + const key = staticKey([1]) + + const result = await runQueryTest(Effect.gen(function*() { + const query = yield* Query.make({ + key, + f: () => Effect.sync(() => { + calls += 1 + return calls + }), + staleTime: "0 millis", + }).pipe( + Query.thenRun, + Query.withScheduledRefresh( + Schedule.spaced("1 second").pipe( + Schedule.upTo({ times: 1 }), + ), + ), + ) + + yield* TestClock.adjust("999 millis") + const beforeInterval = calls + + yield* TestClock.adjust("1 millis") + const afterFirstInterval = calls + + return { + isQuery: Query.isQuery(query), + beforeInterval, + afterFirstInterval, + } + }).pipe( + Effect.provide(TestClock.layer()), + )) + + expect(result).toEqual({ + isQuery: true, + beforeInterval: 1, + afterFirstInterval: 2, + }) + }) + it("invalidateCacheEntry forces the next fetch for that key to rerun", async () => { let calls = 0 const key = staticKey([1]) @@ -136,14 +180,14 @@ describe("Query", () => { const key = staticKey([1]) const effect = Effect.gen(function*() { - const query = yield* Query.service({ + const query = yield* Query.make({ key, f: ([id]: readonly [number]) => Effect.sync(() => { calls += 1 return `value:${id}:${calls}` }), staleTime: "1 minute", - }) + }).pipe(Query.thenRun) const latestFinalState = yield* Effect.sleep("1 millis").pipe( Effect.andThen(View.get(query.latestFinalState)), diff --git a/packages/effect-view/src/Query.ts b/packages/effect-view/src/Query.ts index e03bce7..8c21126 100644 --- a/packages/effect-view/src/Query.ts +++ b/packages/effect-view/src/Query.ts @@ -1,4 +1,4 @@ -import { Cause, type Context, Duration, Effect, Equal, type Equivalence, Exit, Fiber, Option, Pipeable, Predicate, PubSub, Ref, type Scope, Semaphore, Stream, SubscriptionRef } from "effect" +import { Cause, type Context, Duration, Effect, Equal, type Equivalence, Exit, Fiber, Function, Option, Pipeable, Predicate, PubSub, Ref, type Schedule, type Scope, Semaphore, Stream, SubscriptionRef } from "effect" import { AsyncResult } from "effect/unstable/reactivity" import * as Lens from "./Lens.js" import * as QueryClient from "./QueryClient.js" @@ -410,17 +410,52 @@ export const make = Effect.fnUntraced(function* ( ) }) -export const service = ( - options: make.Options -): Effect.Effect< - Query, - Cause.NoSuchElementError, - Scope.Scope | QueryClient.QueryClient | R -> => Effect.tap( - make(options), +export const thenRun = ( + self: Effect.Effect, E2, R2>, +): Effect.Effect, E2, Scope.Scope | R2> => Effect.tap( + self, query => Effect.forkScoped(query.run), ) +/** + * Refreshes the Query on a schedule and returns it. + * + * @example Refresh every five minutes, starting after five minutes + * ```ts + * yield* Query.make(options).pipe( + * Query.thenRun, + * Query.withScheduledRefresh(Schedule.spaced("5 minutes")), + * ) + * ``` + * + * @example Refresh at most three times + * ```ts + * yield* Query.make(options).pipe( + * Query.thenRun, + * Query.withScheduledRefresh( + * Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 })), + * ), + * ) + * ``` + */ +export const withScheduledRefresh: { + ( + schedule: Schedule.Schedule, + ): ( + self: Effect.Effect, E2, R2>, + ) => Effect.Effect, E2, Scope.Scope | Env | R2> + ( + self: Effect.Effect, E2, R2>, + schedule: Schedule.Schedule, + ): Effect.Effect, E2, Scope.Scope | Env | R2> +} = Function.dual(2, ( + self: Effect.Effect, E2, R2>, + schedule: Schedule.Schedule, +) => Effect.tap( + self, + query => Effect.forkScoped(Effect.schedule(query.refresh, schedule)), +)) + export class QueryStateLens extends Lens.LensImpl, never, never, never, never> { diff --git a/packages/effect-view/src/QueryClient.ts b/packages/effect-view/src/QueryClient.ts index 6868fca..4c1aa34 100644 --- a/packages/effect-view/src/QueryClient.ts +++ b/packages/effect-view/src/QueryClient.ts @@ -115,18 +115,14 @@ export const make = Effect.fnUntraced(function* ( ) }) -export declare namespace service { - export interface Options extends make.Options {} -} - -export const service = ( - options?: service.Options -): Effect.Effect => Effect.tap( - make(options), +export const thenRun = ( + self: Effect.Effect, +): Effect.Effect => Effect.tap( + self, client => Effect.forkScoped(client.run), ) -export const layer = (options?: service.Options) => Layer.effect(QueryClient, service(options)) +export const layer = (options?: make.Options) => Layer.effect(QueryClient, thenRun(make(options))) export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-view/QueryClient/QueryClientCacheKey") diff --git a/packages/example/package.json b/packages/example/package.json index e297535..7bfd7da 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -27,15 +27,15 @@ "vite": "^8.0.16" }, "dependencies": { - "@effect/platform-browser": "4.0.0-beta.101", + "@effect/platform-browser": "4.0.0-beta.102", "@radix-ui/themes": "^3.3.0", - "effect": "4.0.0-beta.101", + "effect": "4.0.0-beta.102", "effect-view": "workspace:*", "react-icons": "^5.6.0" }, "overrides": { "@types/react": "^19.2.15", - "effect": "4.0.0-beta.101", + "effect": "4.0.0-beta.102", "react": "^19.2.6" } } diff --git a/packages/example/src/routes/form.tsx b/packages/example/src/routes/form.tsx index a6e0cbc..d61ffb6 100644 --- a/packages/example/src/routes/form.tsx +++ b/packages/example/src/routes/form.tsx @@ -26,11 +26,13 @@ const RegisterRouteComponent = Component.make("RegisterRouteView")(function*() { })) const [form, emailField, passwordField] = yield* Component.useOnMount(() => Effect.gen(function*() { - const form = yield* MutationForm.service({ + const form = yield* MutationForm.make({ schema: RegisterSchema, initialEncodedValue: { email: "", password: "" }, f: ([value]) => Effect.log(`Registered ${value.email}`), - }) + }).pipe( + MutationForm.thenRun, + ) const emailField = Form.focusObjectOn(form, "email") const passwordField = Form.focusObjectOn(form, "password") diff --git a/packages/example/src/routes/lensform.tsx b/packages/example/src/routes/lensform.tsx index 95f8030..1a0c4a3 100644 --- a/packages/example/src/routes/lensform.tsx +++ b/packages/example/src/routes/lensform.tsx @@ -65,14 +65,16 @@ const UserProfileEditorView = Component.make("UserProfileEditorView")(function*( Stream.runForEach(Lens.changes(appState.lens), Console.log) ) - const form = yield* LensForm.service({ + const form = yield* LensForm.make({ schema: UserProfileSchema, target: Lens.focusObjectOn(appState.lens, "currentUser"), initialEncodedValue: { email: "", password: "", }, - }) + }).pipe( + LensForm.thenRun, + ) const emailField = Form.focusObjectOn(form, "email") const passwordField = Form.focusObjectOn(form, "password") diff --git a/packages/example/src/routes/query.tsx b/packages/example/src/routes/query.tsx index 22b5a39..95edabd 100644 --- a/packages/example/src/routes/query.tsx +++ b/packages/example/src/routes/query.tsx @@ -36,16 +36,18 @@ const QueryRouteComponent = Component.make("QueryRouteView")(function*() { const keyLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(["post", 1 as number] as const)) const idLens = Lens.focusTupleAt(keyLens, 1) - const query = yield* Query.service({ + const query = yield* Query.make({ key: keyLens, f: ([, id]) => HttpClient.HttpClient.pipe( - Effect.tap(Effect.sleep("1 second")), - Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)), - Effect.andThen(response => response.json), - Effect.andThen(Schema.decodeUnknownEffect(Post)), + Effect.tap(Effect.sleep("500 millis")), + Effect.flatMap(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)), + Effect.flatMap(response => response.json), + Effect.flatMap(Schema.decodeUnknownEffect(Post)), ), staleTime: "10 seconds", - }) + }).pipe( + Query.thenRun, + ) const mutation = yield* Mutation.make({ f: ([id]: [id: number]) => HttpClient.HttpClient.pipe(