Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27bb78dc14 |
@@ -40,13 +40,6 @@ jobs:
|
|||||||
access: public
|
access: public
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
token: ${{ secrets.NPM_TOKEN }}
|
||||||
registry: https://registry.npmjs.org
|
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
|
- name: Publish effect-fc
|
||||||
uses: JS-DevTools/npm-publish@v4
|
uses: JS-DevTools/npm-publish@v4
|
||||||
with:
|
with:
|
||||||
@@ -74,7 +67,7 @@ jobs:
|
|||||||
flavor: |
|
flavor: |
|
||||||
latest=true
|
latest=true
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v2
|
uses: oven-sh/setup-bun@v2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
flavor: |
|
flavor: |
|
||||||
latest=true
|
latest=true
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
@@ -85,18 +85,18 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.1.0",
|
"@standard-schema/spec": "^1.1.0",
|
||||||
"effect-lens": "^2.0.1-beta.102",
|
"effect-lens": "^2.0.1-beta.101",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/platform-browser": "4.0.0-beta.102",
|
"@effect/platform-browser": "4.0.0-beta.101",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"effect": "4.0.0-beta.102",
|
"effect": "4.0.0-beta.101",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "4.0.0-beta.102",
|
"effect": "4.0.0-beta.101",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
"name": "@effect-view/example",
|
"name": "@effect-view/example",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-browser": "4.0.0-beta.102",
|
"@effect/platform-browser": "4.0.0-beta.101",
|
||||||
"@radix-ui/themes": "^3.3.0",
|
"@radix-ui/themes": "^3.3.0",
|
||||||
"effect": "4.0.0-beta.102",
|
"effect": "4.0.0-beta.101",
|
||||||
"effect-view": "workspace:*",
|
"effect-view": "workspace:*",
|
||||||
"react-icons": "^5.6.0",
|
"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=="],
|
"@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.102", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.102" } }, "sha512-DXXVt+oOpukqI3lEaGvvXQxSobzIy3lAsIQvlpVK+MuYBm1o4FHZrHKbrOFItDUvSR1t0kiTncFPRi01YaJUmA=="],
|
"@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": ["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/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/vite-plugin/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
"@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=="],
|
"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.102", "", { "dependencies": { "multipasta": "^0.2.8" }, "peerDependencies": { "effect": "^4.0.0-beta.102" } }, "sha512-DXXVt+oOpukqI3lEaGvvXQxSobzIy3lAsIQvlpVK+MuYBm1o4FHZrHKbrOFItDUvSR1t0kiTncFPRi01YaJUmA=="],
|
"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": ["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": ["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-lens": ["effect-lens@2.0.1-beta.102", "", { "peerDependencies": { "effect": "4.0.0-beta.102" } }, "sha512-YQSPI/r42Do/24X+RurTd4BLV2EdxG4LcgySpR4Pt7W2Z+wCmoBlSAV+B7rs2lTpqOny3kKTLnn9tbpfQVzSAA=="],
|
"effect-view/effect-lens": ["effect-lens@2.0.1-beta.101", "", { "peerDependencies": { "effect": "4.0.0-beta.101" } }, "sha512-ME5JruBkF3AG34rhb9MlHb1uwEbW70hr6lFITf8tcHBTgVF5QXFCe8nWATraYZpxBHGqM2bOnRvoadmwh2yyvQ=="],
|
||||||
|
|
||||||
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
|
|||||||
+16
-14
@@ -54,9 +54,11 @@ valid decoded value should go:
|
|||||||
|
|
||||||
## Create forms once
|
## Create forms once
|
||||||
|
|
||||||
`MutationForm.make` and `LensForm.make` construct forms. Pipe them through the
|
`MutationForm.service` and `LensForm.service` are Effect constructors, not
|
||||||
module's `run` operator to start validation or synchronization in the current
|
hooks. Create each root form once and keep it stable. For a component-owned
|
||||||
scope. Create each root form once and keep it stable.
|
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: validate, then submit
|
## MutationForm: validate, then submit
|
||||||
|
|
||||||
@@ -71,7 +73,7 @@ import { Component, MutationForm, View } from "effect-view"
|
|||||||
|
|
||||||
const CreateProfileView = Component.make("CreateProfile")(function* () {
|
const CreateProfileView = Component.make("CreateProfile")(function* () {
|
||||||
const form = yield* Component.useOnMount(() =>
|
const form = yield* Component.useOnMount(() =>
|
||||||
MutationForm.make({
|
MutationForm.service({
|
||||||
schema: ProfileSchema,
|
schema: ProfileSchema,
|
||||||
initialEncodedValue: {
|
initialEncodedValue: {
|
||||||
displayName: "",
|
displayName: "",
|
||||||
@@ -82,7 +84,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.thenRun),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [canCommit, isCommitting] = yield* View.useAll([
|
const [canCommit, isCommitting] = yield* View.useAll([
|
||||||
@@ -108,9 +110,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
|
Schema transformations happen before the mutation and schema issues prevent an
|
||||||
invalid draft from being submitted.
|
invalid draft from being submitted.
|
||||||
|
|
||||||
`MutationForm.thenRun` starts initial validation. In the example above,
|
`MutationForm.service` also starts initial validation in the current scope. In
|
||||||
`Component.useOnMount` keeps that validation and the form's mutation work tied
|
the example above, `Component.useOnMount` keeps that validation and the form's
|
||||||
to the component that owns them.
|
mutation work tied to the component that owns them.
|
||||||
|
|
||||||
## LensForm: validate, then synchronize
|
## LensForm: validate, then synchronize
|
||||||
|
|
||||||
@@ -134,10 +136,10 @@ const EditProfileView = Component.make("EditProfile")(function* () {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const form = yield* LensForm.make({
|
const form = yield* LensForm.service({
|
||||||
schema: ProfileSchema,
|
schema: ProfileSchema,
|
||||||
target: profile,
|
target: profile,
|
||||||
}).pipe(LensForm.thenRun)
|
})
|
||||||
|
|
||||||
return [form, profile] as const
|
return [form, profile] as const
|
||||||
}),
|
}),
|
||||||
@@ -166,8 +168,8 @@ reaches the target. If another part of the application updates the target,
|
|||||||
`LensForm` encodes that value back into the draft.
|
`LensForm` encodes that value back into the draft.
|
||||||
|
|
||||||
Pass `initialEncodedValue` only when the first draft should differ from the
|
Pass `initialEncodedValue` only when the first draft should differ from the
|
||||||
encoded target. Otherwise `LensForm.make` obtains the initial draft by encoding
|
encoded target. Otherwise `LensForm.service` obtains the initial draft by
|
||||||
the target through the schema.
|
encoding the target through the schema.
|
||||||
|
|
||||||
## Focus into subforms
|
## Focus into subforms
|
||||||
|
|
||||||
@@ -425,14 +427,14 @@ const AppointmentSchema = Schema.Struct({
|
|||||||
const AppointmentView = Component.make("Appointment")(function* () {
|
const AppointmentView = Component.make("Appointment")(function* () {
|
||||||
const [form, startsAtField] = yield* Component.useOnMount(() =>
|
const [form, startsAtField] = yield* Component.useOnMount(() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const form = yield* MutationForm.make({
|
const form = yield* MutationForm.service({
|
||||||
schema: AppointmentSchema,
|
schema: AppointmentSchema,
|
||||||
initialEncodedValue: { startsAt: "" },
|
initialEncodedValue: { startsAt: "" },
|
||||||
f: ([appointment]) =>
|
f: ([appointment]) =>
|
||||||
Effect.log(
|
Effect.log(
|
||||||
`Saving ${DateTime.formatIso(appointment.startsAt)}`,
|
`Saving ${DateTime.formatIso(appointment.startsAt)}`,
|
||||||
),
|
),
|
||||||
}).pipe(MutationForm.thenRun)
|
})
|
||||||
|
|
||||||
return [form, Form.focusObjectOn(form, "startsAt")] as const
|
return [form, Form.focusObjectOn(form, "startsAt")] as const
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ client defaults. Window-focus refresh also requires the optional
|
|||||||
## Create a reactive query
|
## Create a reactive query
|
||||||
|
|
||||||
A query is driven by a `View` rather than by a value read during one React
|
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, `Query.service` checks the cache and starts
|
||||||
the query effect when necessary.
|
the query effect when necessary.
|
||||||
|
|
||||||
`Query.make` constructs the Query and `Query.thenRun` starts it in the current
|
`Query.service` is an Effect constructor, not a hook. Create each query instance
|
||||||
scope. Create each query instance once and keep it stable. In a component, the usual place is
|
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
|
||||||
than reconstructing the query during render.
|
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),
|
yield* SubscriptionRef.make(["post", 1 as number] as const),
|
||||||
)
|
)
|
||||||
|
|
||||||
const query = yield* Query.make({
|
const query = yield* Query.service({
|
||||||
key,
|
key,
|
||||||
staleTime: "1 minute",
|
staleTime: "1 minute",
|
||||||
f: ([, id]) =>
|
f: ([, id]) =>
|
||||||
@@ -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.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.thenRun` starts watching its key in the current scope. The
|
`Query.service` 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.
|
||||||
|
|
||||||
@@ -248,35 +248,6 @@ workflows that need to wait for the final success or failure state.
|
|||||||
Invalidating does not itself refetch. Follow it with `refreshView`, change the
|
Invalidating does not itself refetch. Follow it with `refreshView`, change the
|
||||||
key, or allow a later fetch to repopulate the cache.
|
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
|
## Staleness and cache lifetime
|
||||||
|
|
||||||
`staleTime` controls how long a successful result can satisfy a fetch without
|
`staleTime` controls how long a successful result can satisfy a fetch without
|
||||||
@@ -291,12 +262,12 @@ By default, the client enables refresh on browser window focus. Set it globally
|
|||||||
or override it for one query:
|
or override it for one query:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
const query = yield* Query.make({
|
const query = yield* Query.service({
|
||||||
key,
|
key,
|
||||||
f: loadPost,
|
f: loadPost,
|
||||||
staleTime: "10 seconds",
|
staleTime: "10 seconds",
|
||||||
refreshOnWindowFocus: false,
|
refreshOnWindowFocus: false,
|
||||||
}).pipe(Query.thenRun)
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Window-focus refresh depends on the optional `@effect/platform-browser`
|
Window-focus refresh depends on the optional `@effect/platform-browser`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-view",
|
"name": "effect-view",
|
||||||
"description": "Write React function components with Effect",
|
"description": "Write React function components with Effect",
|
||||||
"version": "0.1.1",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -97,19 +97,19 @@
|
|||||||
"clean:modules": "rm -rf node_modules"
|
"clean:modules": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@effect/platform-browser": "4.0.0-beta.102",
|
"@effect/platform-browser": "4.0.0-beta.101",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"effect": "4.0.0-beta.102",
|
"effect": "4.0.0-beta.101",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "4.0.0-beta.102",
|
"effect": "4.0.0-beta.101",
|
||||||
"react": "^19.2.0"
|
"react": "^19.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.1.0",
|
"@standard-schema/spec": "^1.1.0",
|
||||||
"effect-lens": "^2.0.1-beta.102"
|
"effect-lens": "^2.0.1-beta.101"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,13 +189,18 @@ export const make = Effect.fnUntraced(function* <A, I = A, RD = never, RE = neve
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const thenRun = <A, I = A, RD = never, RE = never, TER = never, TEW = never, TRR = never, TRW = never, E = never, R = never>(
|
export declare namespace service {
|
||||||
self: Effect.Effect<LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>, E, R>,
|
export interface Options<in out A, out I = A, out RD = never, out RE = never, out TER = never, out TEW = never, out TRR = never, out TRW = never>
|
||||||
|
extends make.Options<A, I, RD, RE, TER, TEW, TRR, TRW> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const service = <A, I = A, RD = never, RE = never, TER = never, TEW = never, TRR = never, TRW = never>(
|
||||||
|
options: service.Options<A, I, RD, RE, TER, TEW, TRR, TRW>
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>,
|
LensForm<A, I, RD, RE, TER, TEW, TRR, TRW>,
|
||||||
E,
|
Schema.SchemaError | TER,
|
||||||
Scope.Scope | R
|
Scope.Scope | RD | RE | TRR | TRW
|
||||||
> => Effect.tap(
|
> => Effect.tap(
|
||||||
self,
|
make(options),
|
||||||
form => Effect.forkScoped(form.run),
|
form => Effect.forkScoped(form.run),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -196,13 +196,18 @@ export const make = Effect.fnUntraced(function* <A, I = A, RD = never, RE = neve
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const thenRun = <A, I = A, RD = never, RE = never, MA = void, ME = never, MR = never, E = never, R = never>(
|
export declare namespace service {
|
||||||
self: Effect.Effect<MutationForm<A, I, RD, RE, MA, ME, MR>, E, R>,
|
export interface Options<in out A, in out I = A, in out RD = never, in out RE = never, out MA = void, out ME = never, out MR = never>
|
||||||
|
extends make.Options<A, I, RD, RE, MA, ME, MR> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const service = <A, I = A, RD = never, RE = never, MA = void, ME = never, MR = never>(
|
||||||
|
options: service.Options<A, I, RD, RE, MA, ME, MR>
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
MutationForm<A, I, RD, RE, MA, ME, MR>,
|
MutationForm<A, I, RD, RE, MA, ME, MR>,
|
||||||
E,
|
never,
|
||||||
Scope.Scope | R
|
Scope.Scope | RD | RE | MR
|
||||||
> => Effect.tap(
|
> => Effect.tap(
|
||||||
self,
|
make(options),
|
||||||
form => Effect.forkScoped(form.run),
|
form => Effect.forkScoped(form.run),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Effect, Schedule, type Scope, Stream } from "effect"
|
import { Effect, type Scope, Stream } from "effect"
|
||||||
import { TestClock } from "effect/testing"
|
|
||||||
import { AsyncResult } from "effect/unstable/reactivity"
|
import { AsyncResult } from "effect/unstable/reactivity"
|
||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
import * as Query from "./Query.js"
|
import * as Query from "./Query.js"
|
||||||
@@ -80,49 +79,6 @@ describe("Query", () => {
|
|||||||
expect(expectSuccessValue(result[1])).toBe("value:1:2")
|
expect(expectSuccessValue(result[1])).toBe("value:1:2")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("withScheduledRefresh lets the Schedule control the first refresh", async () => {
|
|
||||||
let calls = 0
|
|
||||||
const key = staticKey<readonly [number]>([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 () => {
|
it("invalidateCacheEntry forces the next fetch for that key to rerun", async () => {
|
||||||
let calls = 0
|
let calls = 0
|
||||||
const key = staticKey<readonly [number]>([1])
|
const key = staticKey<readonly [number]>([1])
|
||||||
@@ -180,14 +136,14 @@ describe("Query", () => {
|
|||||||
const key = staticKey<readonly [number]>([1])
|
const key = staticKey<readonly [number]>([1])
|
||||||
|
|
||||||
const effect = Effect.gen(function*() {
|
const effect = Effect.gen(function*() {
|
||||||
const query = yield* Query.make({
|
const query = yield* Query.service({
|
||||||
key,
|
key,
|
||||||
f: ([id]: readonly [number]) => Effect.sync(() => {
|
f: ([id]: readonly [number]) => Effect.sync(() => {
|
||||||
calls += 1
|
calls += 1
|
||||||
return `value:${id}:${calls}`
|
return `value:${id}:${calls}`
|
||||||
}),
|
}),
|
||||||
staleTime: "1 minute",
|
staleTime: "1 minute",
|
||||||
}).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)),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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 { Cause, type Context, Duration, Effect, Equal, type Equivalence, Exit, Fiber, Option, Pipeable, Predicate, PubSub, Ref, type Scope, Semaphore, Stream, SubscriptionRef } from "effect"
|
||||||
import { AsyncResult } from "effect/unstable/reactivity"
|
import { AsyncResult } from "effect/unstable/reactivity"
|
||||||
import * as Lens from "./Lens.js"
|
import * as Lens from "./Lens.js"
|
||||||
import * as QueryClient from "./QueryClient.js"
|
import * as QueryClient from "./QueryClient.js"
|
||||||
@@ -410,52 +410,17 @@ export const make = Effect.fnUntraced(function* <K, A, E = never, R = never>(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const thenRun = <K, A, E = never, R = never, E2 = never, R2 = never>(
|
export const service = <K, A, E = never, R = never>(
|
||||||
self: Effect.Effect<Query<K, A, E, R>, E2, R2>,
|
options: make.Options<K, A, E, R>
|
||||||
): Effect.Effect<Query<K, A, E, R>, E2, Scope.Scope | R2> => Effect.tap(
|
): Effect.Effect<
|
||||||
self,
|
Query<K, A, E, R>,
|
||||||
|
Cause.NoSuchElementError,
|
||||||
|
Scope.Scope | QueryClient.QueryClient | R
|
||||||
|
> => Effect.tap(
|
||||||
|
make(options),
|
||||||
query => Effect.forkScoped(query.run),
|
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: {
|
|
||||||
<Output, Error, Env>(
|
|
||||||
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
|
|
||||||
): <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 | Env | R2>
|
|
||||||
<K, A, E, R, E2, R2, Output, Error, Env>(
|
|
||||||
self: Effect.Effect<Query<K, A, E, R>, E2, R2>,
|
|
||||||
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
|
|
||||||
): Effect.Effect<Query<K, A, E, R>, E2, Scope.Scope | Env | R2>
|
|
||||||
} = Function.dual(2, <K, A, E, R, E2, R2, Output, Error, Env>(
|
|
||||||
self: Effect.Effect<Query<K, A, E, R>, E2, R2>,
|
|
||||||
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
|
|
||||||
) => Effect.tap(
|
|
||||||
self,
|
|
||||||
query => Effect.forkScoped(Effect.schedule(query.refresh, schedule)),
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
export class QueryStateLens<in out K, in out A, in out E = never>
|
export class QueryStateLens<in out K, in out A, in out E = never>
|
||||||
extends Lens.LensImpl<QueryState<K, A, E>, never, never, never, never> {
|
extends Lens.LensImpl<QueryState<K, A, E>, never, never, never, never> {
|
||||||
|
|||||||
@@ -115,14 +115,18 @@ export const make = Effect.fnUntraced(function* (
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const thenRun = <E = never, R = never>(
|
export declare namespace service {
|
||||||
self: Effect.Effect<QueryClientService, E, R>,
|
export interface Options extends make.Options {}
|
||||||
): Effect.Effect<QueryClientService, E, Scope.Scope | R> => Effect.tap(
|
}
|
||||||
self,
|
|
||||||
|
export const service = (
|
||||||
|
options?: service.Options
|
||||||
|
): Effect.Effect<QueryClientService, Cause.NoSuchElementError, Scope.Scope> => Effect.tap(
|
||||||
|
make(options),
|
||||||
client => Effect.forkScoped(client.run),
|
client => Effect.forkScoped(client.run),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const layer = (options?: make.Options) => Layer.effect(QueryClient, thenRun(make(options)))
|
export const layer = (options?: service.Options) => Layer.effect(QueryClient, service(options))
|
||||||
|
|
||||||
|
|
||||||
export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-view/QueryClient/QueryClientCacheKey")
|
export const QueryClientCacheKeyTypeId: unique symbol = Symbol.for("@effect-view/QueryClient/QueryClientCacheKey")
|
||||||
|
|||||||
@@ -27,15 +27,15 @@
|
|||||||
"vite": "^8.0.16"
|
"vite": "^8.0.16"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-browser": "4.0.0-beta.102",
|
"@effect/platform-browser": "4.0.0-beta.101",
|
||||||
"@radix-ui/themes": "^3.3.0",
|
"@radix-ui/themes": "^3.3.0",
|
||||||
"effect": "4.0.0-beta.102",
|
"effect": "4.0.0-beta.101",
|
||||||
"effect-view": "workspace:*",
|
"effect-view": "workspace:*",
|
||||||
"react-icons": "^5.6.0"
|
"react-icons": "^5.6.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.15",
|
||||||
"effect": "4.0.0-beta.102",
|
"effect": "4.0.0-beta.101",
|
||||||
"react": "^19.2.6"
|
"react": "^19.2.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,11 @@ const RegisterRouteComponent = Component.make("RegisterRouteView")(function*() {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const [form, emailField, passwordField] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
const [form, emailField, passwordField] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
const form = yield* MutationForm.make({
|
const form = yield* MutationForm.service({
|
||||||
schema: RegisterSchema,
|
schema: RegisterSchema,
|
||||||
initialEncodedValue: { email: "", password: "" },
|
initialEncodedValue: { email: "", password: "" },
|
||||||
f: ([value]) => Effect.log(`Registered ${value.email}`),
|
f: ([value]) => Effect.log(`Registered ${value.email}`),
|
||||||
}).pipe(
|
})
|
||||||
MutationForm.thenRun,
|
|
||||||
)
|
|
||||||
|
|
||||||
const emailField = Form.focusObjectOn(form, "email")
|
const emailField = Form.focusObjectOn(form, "email")
|
||||||
const passwordField = Form.focusObjectOn(form, "password")
|
const passwordField = Form.focusObjectOn(form, "password")
|
||||||
|
|||||||
@@ -65,16 +65,14 @@ const UserProfileEditorView = Component.make("UserProfileEditorView")(function*(
|
|||||||
Stream.runForEach(Lens.changes(appState.lens), Console.log)
|
Stream.runForEach(Lens.changes(appState.lens), Console.log)
|
||||||
)
|
)
|
||||||
|
|
||||||
const form = yield* LensForm.make({
|
const form = yield* LensForm.service({
|
||||||
schema: UserProfileSchema,
|
schema: UserProfileSchema,
|
||||||
target: Lens.focusObjectOn(appState.lens, "currentUser"),
|
target: Lens.focusObjectOn(appState.lens, "currentUser"),
|
||||||
initialEncodedValue: {
|
initialEncodedValue: {
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
}).pipe(
|
})
|
||||||
LensForm.thenRun,
|
|
||||||
)
|
|
||||||
|
|
||||||
const emailField = Form.focusObjectOn(form, "email")
|
const emailField = Form.focusObjectOn(form, "email")
|
||||||
const passwordField = Form.focusObjectOn(form, "password")
|
const passwordField = Form.focusObjectOn(form, "password")
|
||||||
|
|||||||
@@ -36,18 +36,16 @@ const QueryRouteComponent = Component.make("QueryRouteView")(function*() {
|
|||||||
const keyLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(["post", 1 as number] as const))
|
const keyLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(["post", 1 as number] as const))
|
||||||
const idLens = Lens.focusTupleAt(keyLens, 1)
|
const idLens = Lens.focusTupleAt(keyLens, 1)
|
||||||
|
|
||||||
const query = yield* Query.make({
|
const query = yield* Query.service({
|
||||||
key: keyLens,
|
key: keyLens,
|
||||||
f: ([, id]) => HttpClient.HttpClient.pipe(
|
f: ([, id]) => HttpClient.HttpClient.pipe(
|
||||||
Effect.tap(Effect.sleep("500 millis")),
|
Effect.tap(Effect.sleep("1 second")),
|
||||||
Effect.flatMap(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||||
Effect.flatMap(response => response.json),
|
Effect.andThen(response => response.json),
|
||||||
Effect.flatMap(Schema.decodeUnknownEffect(Post)),
|
Effect.andThen(Schema.decodeUnknownEffect(Post)),
|
||||||
),
|
),
|
||||||
staleTime: "10 seconds",
|
staleTime: "10 seconds",
|
||||||
}).pipe(
|
})
|
||||||
Query.thenRun,
|
|
||||||
)
|
|
||||||
|
|
||||||
const mutation = yield* Mutation.make({
|
const mutation = yield* Mutation.make({
|
||||||
f: ([id]: [id: number]) => HttpClient.HttpClient.pipe(
|
f: ([id]: [id: number]) => HttpClient.HttpClient.pipe(
|
||||||
|
|||||||
Reference in New Issue
Block a user