Compare commits
24 Commits
c33d98227e
...
edd5b69d17
| Author | SHA1 | Date | |
|---|---|---|---|
| edd5b69d17 | |||
|
|
e744e614ad | ||
|
|
5451c84d34 | ||
|
|
696fa21fab | ||
|
|
7b2e28451a | ||
|
|
93f65c5016 | ||
|
|
95d0bf70bd | ||
|
|
dad4cd60d1 | ||
|
|
731eed4209 | ||
|
|
8c22206ad7 | ||
|
|
a9ed86c4a8 | ||
|
|
4f9441c89c | ||
|
|
4f9bfaafaa | ||
|
|
b8a3b089b7 | ||
|
|
b29dec7d30 | ||
|
|
d1ef42e9cb | ||
|
|
c029f85401 | ||
|
|
8203063253 | ||
|
|
931511b890 | ||
|
|
7705880afe | ||
|
|
0f79f12632 | ||
|
|
cb788952a4 | ||
|
|
cd18a9d108 | ||
|
|
a6d91a93a5 |
5
bun.lock
5
bun.lock
@@ -17,6 +17,9 @@
|
|||||||
"packages/effect-fc": {
|
"packages/effect-fc": {
|
||||||
"name": "effect-fc",
|
"name": "effect-fc",
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
|
"devDependencies": {
|
||||||
|
"@effect/platform-browser": "^0.74.0",
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "^3.19.0",
|
"effect": "^3.19.0",
|
||||||
@@ -655,6 +658,8 @@
|
|||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"effect-fc/@effect/platform-browser": ["@effect/platform-browser@0.74.0", "", { "dependencies": { "multipasta": "^0.2.7" }, "peerDependencies": { "@effect/platform": "^0.94.0", "effect": "^3.19.13" } }, "sha512-PAgkg5L5cASQpScA0SZTSy543MVA4A9kmpVCjo2fCINLRpTeuCFAOQHgPmw8dKHnYS0yGs2TYn7AlrhhqQ5o3g=="],
|
||||||
|
|
||||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "effect-fc",
|
"name": "effect-fc",
|
||||||
"description": "Write React function components with Effect",
|
"description": "Write React function components with Effect",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -37,6 +37,9 @@
|
|||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:modules": "rm -rf node_modules"
|
"clean:modules": "rm -rf node_modules"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@effect/platform-browser": "^0.74.0"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"effect": "^3.19.0",
|
"effect": "^3.19.0",
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ extends Pipeable.Pipeable {
|
|||||||
field<const P extends PropertyPath.Paths<I>>(
|
field<const P extends PropertyPath.Paths<I>>(
|
||||||
path: P
|
path: P
|
||||||
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>>
|
): Effect.Effect<FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>>>
|
||||||
|
|
||||||
|
readonly run: Effect.Effect<void>
|
||||||
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
|
readonly submit: Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
Option.isSome(value) &&
|
Option.isSome(value) &&
|
||||||
Option.isNone(error) &&
|
Option.isNone(error) &&
|
||||||
Option.isNone(validationFiber) &&
|
Option.isNone(validationFiber) &&
|
||||||
!(Result.isRunning(result) || Result.isRefreshing(result))
|
!(Result.isRunning(result) || Result.hasRefreshingFlag(result))
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -89,7 +91,49 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly canSubmit: Subscribable.Subscribable<boolean, never, never>
|
readonly canSubmit: Subscribable.Subscribable<boolean>
|
||||||
|
|
||||||
|
get run(): Effect.Effect<void> {
|
||||||
|
return this.runSemaphore.withPermits(1)(Stream.runForEach(
|
||||||
|
this.encodedValue.changes.pipe(
|
||||||
|
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
|
||||||
|
),
|
||||||
|
|
||||||
|
encodedValue => this.validationFiber.pipe(
|
||||||
|
Effect.andThen(Option.match({
|
||||||
|
onSome: Fiber.interrupt,
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
})),
|
||||||
|
Effect.andThen(
|
||||||
|
Effect.forkScoped(Effect.onExit(
|
||||||
|
Schema.decode(this.schema, { errors: "all" })(encodedValue),
|
||||||
|
exit => Effect.andThen(
|
||||||
|
Exit.matchEffect(exit, {
|
||||||
|
onSuccess: v => Effect.andThen(
|
||||||
|
Ref.set(this.value, Option.some(v)),
|
||||||
|
Ref.set(this.error, Option.none()),
|
||||||
|
),
|
||||||
|
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
||||||
|
onSome: e => Ref.set(this.error, Option.some(e)),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Ref.set(this.validationFiber, Option.none()),
|
||||||
|
),
|
||||||
|
)).pipe(
|
||||||
|
Effect.tap(fiber => Ref.set(this.validationFiber, Option.some(fiber))),
|
||||||
|
Effect.andThen(Fiber.join),
|
||||||
|
Effect.andThen(value => this.autosubmit
|
||||||
|
? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
|
Effect.forkScoped,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
||||||
return this.value.pipe(
|
return this.value.pipe(
|
||||||
@@ -97,6 +141,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
Effect.andThen(value => this.submitValue(value)),
|
Effect.andThen(value => this.submitValue(value)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
submitValue(value: A): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>> {
|
submitValue(value: A): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>> {
|
||||||
return Effect.whenEffect(
|
return Effect.whenEffect(
|
||||||
Effect.tap(
|
Effect.tap(
|
||||||
@@ -158,51 +203,6 @@ export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void,
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const run = <A, I, R, MA, ME, MR, MP>(
|
|
||||||
self: Form<A, I, R, MA, ME, MR, MP>
|
|
||||||
): Effect.Effect<void> => {
|
|
||||||
const _self = self as FormImpl<A, I, R, MA, ME, MR, MP>
|
|
||||||
return _self.runSemaphore.withPermits(1)(Stream.runForEach(
|
|
||||||
_self.encodedValue.changes.pipe(
|
|
||||||
Option.isSome(_self.debounce) ? Stream.debounce(_self.debounce.value) : identity
|
|
||||||
),
|
|
||||||
|
|
||||||
encodedValue => _self.validationFiber.pipe(
|
|
||||||
Effect.andThen(Option.match({
|
|
||||||
onSome: Fiber.interrupt,
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
})),
|
|
||||||
Effect.andThen(
|
|
||||||
Effect.forkScoped(Effect.onExit(
|
|
||||||
Schema.decode(_self.schema, { errors: "all" })(encodedValue),
|
|
||||||
exit => Effect.andThen(
|
|
||||||
Exit.matchEffect(exit, {
|
|
||||||
onSuccess: v => Effect.andThen(
|
|
||||||
Ref.set(_self.value, Option.some(v)),
|
|
||||||
Ref.set(_self.error, Option.none()),
|
|
||||||
),
|
|
||||||
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
|
||||||
onSome: e => Ref.set(_self.error, Option.some(e)),
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
Ref.set(_self.validationFiber, Option.none()),
|
|
||||||
),
|
|
||||||
)).pipe(
|
|
||||||
Effect.tap(fiber => Ref.set(_self.validationFiber, Option.some(fiber))),
|
|
||||||
Effect.andThen(Fiber.join),
|
|
||||||
Effect.andThen(value => _self.autosubmit
|
|
||||||
? Effect.asVoid(Effect.forkScoped(_self.submitValue(value)))
|
|
||||||
: Effect.void
|
|
||||||
),
|
|
||||||
Effect.forkScoped,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Effect.provide(_self.context),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare namespace service {
|
export declare namespace service {
|
||||||
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
export interface Options<in out A, in out I = A, in out R = never, in out MA = void, in out ME = never, in out MR = never, in out MP = never>
|
||||||
extends make.Options<A, I, R, MA, ME, MR, MP> {}
|
extends make.Options<A, I, R, MA, ME, MR, MP> {}
|
||||||
@@ -216,7 +216,7 @@ export const service = <A, I = A, R = never, MA = void, ME = never, MR = never,
|
|||||||
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
Scope.Scope | R | Result.forkEffect.OutputContext<MA, ME, MR, MP>
|
||||||
> => Effect.tap(
|
> => Effect.tap(
|
||||||
make(options),
|
make(options),
|
||||||
form => Effect.forkScoped(run(form)),
|
form => Effect.forkScoped(form.run),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -271,22 +271,21 @@ export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyP
|
|||||||
self: Form<A, I, R, MA, ME, MR, MP>,
|
self: Form<A, I, R, MA, ME, MR, MP>,
|
||||||
path: P,
|
path: P,
|
||||||
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => {
|
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => {
|
||||||
const _self = self as FormImpl<A, I, R, MA, ME, MR, MP>
|
|
||||||
return new FormFieldImpl(
|
return new FormFieldImpl(
|
||||||
Subscribable.mapEffect(_self.value, Option.match({
|
Subscribable.mapEffect(self.value, Option.match({
|
||||||
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
||||||
onNone: () => Option.some(Option.none()),
|
onNone: () => Option.some(Option.none()),
|
||||||
})),
|
})),
|
||||||
SubscriptionSubRef.makeFromPath(_self.encodedValue, path),
|
SubscriptionSubRef.makeFromPath(self.encodedValue, path),
|
||||||
Subscribable.mapEffect(_self.error, Option.match({
|
Subscribable.mapEffect(self.error, Option.match({
|
||||||
onSome: flow(
|
onSome: flow(
|
||||||
ParseResult.ArrayFormatter.formatError,
|
ParseResult.ArrayFormatter.formatError,
|
||||||
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
|
Effect.map(Array.filter(issue => PropertyPath.equivalence(issue.path, path))),
|
||||||
),
|
),
|
||||||
onNone: () => Effect.succeed([]),
|
onNone: () => Effect.succeed([]),
|
||||||
})),
|
})),
|
||||||
Subscribable.map(_self.validationFiber, Option.isSome),
|
Subscribable.map(self.validationFiber, Option.isSome),
|
||||||
Subscribable.map(_self.mutation.result, result => Result.isRunning(result) || Result.isRefreshing(result)),
|
Subscribable.map(self.mutation.result, result => Result.isRunning(result) || Result.hasRefreshingFlag(result)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ extends Pipeable.Pipeable {
|
|||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
||||||
|
|
||||||
mutate(key: K): Effect.Effect<Result.Final<A, E, P>>
|
mutate(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||||
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||||
@@ -30,27 +31,30 @@ extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
|
|||||||
readonly [MutationTypeId]: MutationTypeId = MutationTypeId
|
readonly [MutationTypeId]: MutationTypeId = MutationTypeId
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly context: Context.Context<Scope.Scope | NoInfer<R>>,
|
readonly context: Context.Context<Scope.Scope | R>,
|
||||||
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
readonly f: (key: K) => Effect.Effect<A, E, R>,
|
||||||
readonly initialProgress: P,
|
readonly initialProgress: P,
|
||||||
|
|
||||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||||
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||||
|
readonly latestFinalResult: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
mutate(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
mutate(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
||||||
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
Effect.andThen(this.start(key)),
|
||||||
Effect.andThen(sub => this.watch(sub)),
|
Effect.andThen(sub => this.watch(sub)),
|
||||||
|
Effect.provide(this.context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
mutateSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
||||||
return Effect.andThen(
|
return SubscriptionRef.set(this.latestKey, Option.some(key)).pipe(
|
||||||
SubscriptionRef.set(this.latestKey, Option.some(key)),
|
Effect.andThen(this.start(key)),
|
||||||
Effect.provide(this.start(key), this.context)
|
Effect.tap(sub => Effect.forkScoped(this.watch(sub))),
|
||||||
|
Effect.provide(this.context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,12 +63,8 @@ extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
|
|||||||
never,
|
never,
|
||||||
Scope.Scope | R
|
Scope.Scope | R
|
||||||
> {
|
> {
|
||||||
return this.result.pipe(
|
return this.latestFinalResult.pipe(
|
||||||
Effect.map(previous => Result.isFinal(previous)
|
Effect.andThen(initial => Result.unsafeForkEffect(
|
||||||
? previous
|
|
||||||
: undefined
|
|
||||||
),
|
|
||||||
Effect.andThen(previous => Result.unsafeForkEffect(
|
|
||||||
Effect.onExit(this.f(key), () => Effect.andThen(
|
Effect.onExit(this.f(key), () => Effect.andThen(
|
||||||
Effect.all([Effect.fiberId, this.fiber]),
|
Effect.all([Effect.fiberId, this.fiber]),
|
||||||
([currentFiberId, fiber]) => Option.match(fiber, {
|
([currentFiberId, fiber]) => Option.match(fiber, {
|
||||||
@@ -72,12 +72,12 @@ extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
|
|||||||
? SubscriptionRef.set(this.fiber, Option.none())
|
? SubscriptionRef.set(this.fiber, Option.none())
|
||||||
: Effect.void,
|
: Effect.void,
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
})
|
}),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
{
|
{
|
||||||
|
initial: Option.isSome(initial) ? Result.willFetch(initial.value) : Result.initial(),
|
||||||
initialProgress: this.initialProgress,
|
initialProgress: this.initialProgress,
|
||||||
previous,
|
|
||||||
} as Result.unsafeForkEffect.Options<A, E, P>,
|
} as Result.unsafeForkEffect.Options<A, E, P>,
|
||||||
)),
|
)),
|
||||||
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
||||||
@@ -88,14 +88,14 @@ extends Pipeable.Class() implements Mutation<K, A, E, R, P> {
|
|||||||
watch(
|
watch(
|
||||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
): Effect.Effect<Result.Final<A, E, P>> {
|
): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
return Effect.andThen(
|
return sub.get.pipe(
|
||||||
sub.get,
|
Effect.andThen(initial => Stream.runFoldEffect(
|
||||||
initial => Stream.runFoldEffect(
|
sub.changes,
|
||||||
Stream.filter(sub.changes, Predicate.not(Result.isInitial)),
|
|
||||||
initial,
|
initial,
|
||||||
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
||||||
),
|
) as Effect.Effect<Result.Final<A, E, P>>),
|
||||||
) as Effect.Effect<Result.Final<A, E, P>>
|
Effect.tap(result => SubscriptionRef.set(this.latestFinalResult, Option.some(result))),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,5 +123,6 @@ export const make = Effect.fnUntraced(function* <const K extends Mutation.AnyKey
|
|||||||
yield* SubscriptionRef.make(Option.none<K>()),
|
yield* SubscriptionRef.make(Option.none<K>()),
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
||||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Result.Final<A, E, P>>()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Cause, type Context, DateTime, type Duration, Effect, Fiber, HashMap, identity, Option, Pipeable, Predicate, type Scope, Stream, type Subscribable, SubscriptionRef } from "effect"
|
import { type Cause, type Context, DateTime, type Duration, Effect, Equal, Equivalence, Fiber, HashMap, identity, Option, Pipeable, Predicate, type Scope, Stream, Subscribable, SubscriptionRef } from "effect"
|
||||||
import * as QueryClient from "./QueryClient.js"
|
import * as QueryClient from "./QueryClient.js"
|
||||||
import * as Result from "./Result.js"
|
import * as Result from "./Result.js"
|
||||||
|
|
||||||
@@ -16,17 +16,21 @@ extends Pipeable.Pipeable {
|
|||||||
readonly initialProgress: P
|
readonly initialProgress: P
|
||||||
|
|
||||||
readonly staleTime: Duration.DurationInput
|
readonly staleTime: Duration.DurationInput
|
||||||
|
readonly refreshOnWindowFocus: boolean
|
||||||
|
|
||||||
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
readonly latestKey: Subscribable.Subscribable<Option.Option<K>>
|
||||||
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
readonly fiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, E>>>
|
||||||
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
readonly result: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
|
readonly latestFinalResult: Subscribable.Subscribable<Option.Option<Result.Final<A, E, P>>>
|
||||||
|
|
||||||
|
readonly run: Effect.Effect<void>
|
||||||
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
|
fetch(key: K): Effect.Effect<Result.Final<A, E, P>>
|
||||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>>
|
||||||
readonly refetch: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
|
||||||
readonly refetchSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
|
||||||
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
readonly refresh: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
|
||||||
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
readonly refreshSubscribable: Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException>
|
||||||
|
|
||||||
|
readonly invalidateCache: Effect.Effect<void>
|
||||||
|
invalidateCacheEntry(key: K): Effect.Effect<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare namespace Query {
|
export declare namespace Query {
|
||||||
@@ -44,16 +48,38 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
|||||||
readonly initialProgress: P,
|
readonly initialProgress: P,
|
||||||
|
|
||||||
readonly staleTime: Duration.DurationInput,
|
readonly staleTime: Duration.DurationInput,
|
||||||
|
readonly refreshOnWindowFocus: boolean,
|
||||||
|
|
||||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>,
|
||||||
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
readonly fiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, E>>>,
|
||||||
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
readonly result: SubscriptionRef.SubscriptionRef<Result.Result<A, E, P>>,
|
||||||
|
readonly latestFinalResult: SubscriptionRef.SubscriptionRef<Option.Option<Result.Final<A, E, P>>>,
|
||||||
|
|
||||||
readonly runSemaphore: Effect.Semaphore,
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get run(): Effect.Effect<void> {
|
||||||
|
return Effect.all([
|
||||||
|
Stream.runForEach(this.key, key => this.fetchSubscribable(key)),
|
||||||
|
|
||||||
|
Effect.promise(() => import("@effect/platform-browser")).pipe(
|
||||||
|
Effect.andThen(({ BrowserStream }) => this.refreshOnWindowFocus
|
||||||
|
? Stream.runForEach(
|
||||||
|
BrowserStream.fromEventListenerWindow("focus"),
|
||||||
|
() => this.refreshSubscribable,
|
||||||
|
)
|
||||||
|
: Effect.void
|
||||||
|
),
|
||||||
|
Effect.catchAllDefect(() => Effect.void),
|
||||||
|
),
|
||||||
|
], { concurrency: "unbounded" }).pipe(
|
||||||
|
Effect.ignore,
|
||||||
|
this.runSemaphore.withPermits(1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
get interrupt(): Effect.Effect<void, never, never> {
|
get interrupt(): Effect.Effect<void, never, never> {
|
||||||
return Effect.andThen(this.fiber, Option.match({
|
return Effect.andThen(this.fiber, Option.match({
|
||||||
onSome: Fiber.interrupt,
|
onSome: Fiber.interrupt,
|
||||||
@@ -64,84 +90,136 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
|||||||
fetch(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
fetch(key: K): Effect.Effect<Result.Final<A, E, P>> {
|
||||||
return this.interrupt.pipe(
|
return this.interrupt.pipe(
|
||||||
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||||
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
Effect.andThen(this.latestFinalResult),
|
||||||
Effect.andThen(sub => this.watch(sub)),
|
Effect.andThen(previous => this.startCached(key, Option.isSome(previous)
|
||||||
|
? Result.willFetch(previous.value) as Result.Final<A, E, P>
|
||||||
|
: Result.initial()
|
||||||
|
)),
|
||||||
|
Effect.andThen(sub => this.watch(key, sub)),
|
||||||
|
Effect.provide(this.context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
fetchSubscribable(key: K): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>> {
|
||||||
return this.interrupt.pipe(
|
return this.interrupt.pipe(
|
||||||
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
|
||||||
Effect.andThen(Effect.provide(this.start(key), this.context)),
|
Effect.andThen(this.latestFinalResult),
|
||||||
)
|
Effect.andThen(previous => this.startCached(key, Option.isSome(previous)
|
||||||
}
|
? Result.willFetch(previous.value) as Result.Final<A, E, P>
|
||||||
get refetch(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
: Result.initial()
|
||||||
return this.interrupt.pipe(
|
)),
|
||||||
Effect.andThen(this.latestKey),
|
Effect.tap(sub => Effect.forkScoped(this.watch(key, sub))),
|
||||||
Effect.andThen(identity),
|
Effect.provide(this.context),
|
||||||
Effect.andThen(key => Effect.provide(this.start(key), this.context)),
|
|
||||||
Effect.andThen(sub => this.watch(sub)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
get refetchSubscribable(): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException> {
|
|
||||||
return this.interrupt.pipe(
|
|
||||||
Effect.andThen(this.latestKey),
|
|
||||||
Effect.andThen(identity),
|
|
||||||
Effect.andThen(key => Effect.provide(this.start(key), this.context)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get refresh(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
get refresh(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
|
||||||
return this.interrupt.pipe(
|
return this.interrupt.pipe(
|
||||||
Effect.andThen(this.latestKey),
|
Effect.andThen(Effect.Do),
|
||||||
Effect.andThen(identity),
|
Effect.bind("latestKey", () => Effect.andThen(this.latestKey, identity)),
|
||||||
Effect.andThen(key => Effect.provide(this.start(key, true), this.context)),
|
Effect.bind("latestFinalResult", () => this.latestFinalResult),
|
||||||
Effect.andThen(sub => this.watch(sub)),
|
Effect.bind("subscribable", ({ latestKey, latestFinalResult }) =>
|
||||||
|
this.startCached(latestKey, Option.isSome(latestFinalResult)
|
||||||
|
? Result.willRefresh(latestFinalResult.value) as Result.Final<A, E, P>
|
||||||
|
: Result.initial()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Effect.andThen(({ latestKey, subscribable }) => this.watch(latestKey, subscribable)),
|
||||||
|
Effect.provide(this.context),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
get refreshSubscribable(): Effect.Effect<Subscribable.Subscribable<Result.Result<A, E, P>>, Cause.NoSuchElementException> {
|
|
||||||
|
get refreshSubscribable(): Effect.Effect<
|
||||||
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
|
Cause.NoSuchElementException
|
||||||
|
> {
|
||||||
return this.interrupt.pipe(
|
return this.interrupt.pipe(
|
||||||
Effect.andThen(this.latestKey),
|
Effect.andThen(Effect.Do),
|
||||||
Effect.andThen(identity),
|
Effect.bind("latestKey", () => Effect.andThen(this.latestKey, identity)),
|
||||||
Effect.andThen(key => Effect.provide(this.start(key, true), this.context)),
|
Effect.bind("latestFinalResult", () => this.latestFinalResult),
|
||||||
|
Effect.bind("subscribable", ({ latestKey, latestFinalResult }) =>
|
||||||
|
this.startCached(latestKey, Option.isSome(latestFinalResult)
|
||||||
|
? Result.willRefresh(latestFinalResult.value) as Result.Final<A, E, P>
|
||||||
|
: Result.initial()
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
Effect.tap(({ latestKey, subscribable }) => Effect.forkScoped(this.watch(latestKey, subscribable))),
|
||||||
|
Effect.map(({ subscribable }) => subscribable),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
startCached(
|
||||||
|
key: K,
|
||||||
|
initial: Result.Initial | Result.Final<A, E, P>,
|
||||||
|
): Effect.Effect<
|
||||||
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
|
never,
|
||||||
|
Scope.Scope | QueryClient.QueryClient | R
|
||||||
|
> {
|
||||||
|
return Effect.andThen(this.getCacheEntry(key), Option.match({
|
||||||
|
onSome: entry => Effect.andThen(
|
||||||
|
QueryClient.isQueryClientCacheEntryStale(entry, this.staleTime),
|
||||||
|
isStale => isStale
|
||||||
|
? this.start(key, Result.willRefresh(entry.result) as Result.Final<A, E, P>)
|
||||||
|
: Effect.succeed(Subscribable.make({
|
||||||
|
get: Effect.succeed(entry.result as Result.Result<A, E, P>),
|
||||||
|
get changes() { return Stream.make(entry.result as Result.Result<A, E, P>) },
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
onNone: () => this.start(key, initial),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
start(
|
start(
|
||||||
key: K,
|
key: K,
|
||||||
refresh?: boolean,
|
initial: Result.Initial | Result.Final<A, E, P>,
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
Subscribable.Subscribable<Result.Result<A, E, P>>,
|
||||||
never,
|
never,
|
||||||
Scope.Scope | R
|
Scope.Scope | R
|
||||||
> {
|
> {
|
||||||
return this.result.pipe(
|
return Result.unsafeForkEffect(
|
||||||
Effect.map(previous => Result.isFinal(previous)
|
Effect.onExit(this.f(key), () => Effect.andThen(
|
||||||
? previous
|
Effect.all([Effect.fiberId, this.fiber]),
|
||||||
: undefined
|
([currentFiberId, fiber]) => Option.match(fiber, {
|
||||||
),
|
onSome: v => Equal.equals(currentFiberId, v.id())
|
||||||
Effect.andThen(previous => Result.unsafeForkEffect(
|
? SubscriptionRef.set(this.fiber, Option.none())
|
||||||
Effect.onExit(this.f(key), () => SubscriptionRef.set(this.fiber, Option.none())),
|
: Effect.void,
|
||||||
{
|
onNone: () => Effect.void,
|
||||||
initialProgress: this.initialProgress,
|
}),
|
||||||
refresh: refresh && previous,
|
|
||||||
previous,
|
|
||||||
} as Result.unsafeForkEffect.Options<A, E, P>,
|
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
{
|
||||||
|
initial,
|
||||||
|
initialProgress: this.initialProgress,
|
||||||
|
} as Result.unsafeForkEffect.Options<A, E, P>,
|
||||||
|
).pipe(
|
||||||
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
Effect.tap(([, fiber]) => SubscriptionRef.set(this.fiber, Option.some(fiber))),
|
||||||
Effect.map(([sub]) => sub),
|
Effect.map(([sub]) => sub),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
key: K,
|
||||||
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
sub: Subscribable.Subscribable<Result.Result<A, E, P>>
|
||||||
): Effect.Effect<Result.Final<A, E, P>> {
|
): Effect.Effect<Result.Final<A, E, P>, never, QueryClient.QueryClient> {
|
||||||
return Effect.andThen(
|
return sub.get.pipe(
|
||||||
sub.get,
|
Effect.andThen(initial => Stream.runFoldEffect(
|
||||||
initial => Stream.runFoldEffect(
|
sub.changes,
|
||||||
Stream.filter(sub.changes, Predicate.not(Result.isInitial)),
|
|
||||||
initial,
|
initial,
|
||||||
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
(_, result) => Effect.as(SubscriptionRef.set(this.result, result), result),
|
||||||
|
) as Effect.Effect<Result.Final<A, E, P>>),
|
||||||
|
Effect.tap(result => SubscriptionRef.set(this.latestFinalResult, Option.some(result))),
|
||||||
|
Effect.tap(result => Result.isSuccess(result)
|
||||||
|
? this.updateCacheEntry(key, result)
|
||||||
|
: Effect.void
|
||||||
),
|
),
|
||||||
) as Effect.Effect<Result.Final<A, E, P>>
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCacheKey(key: K): QueryClient.QueryClientCacheKey {
|
||||||
|
return new QueryClient.QueryClientCacheKey(key, this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCacheEntry(
|
getCacheEntry(
|
||||||
@@ -149,7 +227,7 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
|||||||
): Effect.Effect<Option.Option<QueryClient.QueryClientCacheEntry>, never, QueryClient.QueryClient> {
|
): Effect.Effect<Option.Option<QueryClient.QueryClientCacheEntry>, never, QueryClient.QueryClient> {
|
||||||
return QueryClient.QueryClient.pipe(
|
return QueryClient.QueryClient.pipe(
|
||||||
Effect.andThen(client => client.cache),
|
Effect.andThen(client => client.cache),
|
||||||
Effect.map(HashMap.get(new QueryClient.QueryClientCacheKey(key, this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>))),
|
Effect.map(HashMap.get(this.makeCacheKey(key))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,11 +241,31 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
|
|||||||
Effect.let("entry", ({ now }) => new QueryClient.QueryClientCacheEntry(result, now)),
|
Effect.let("entry", ({ now }) => new QueryClient.QueryClientCacheEntry(result, now)),
|
||||||
Effect.tap(({ client, entry }) => SubscriptionRef.update(
|
Effect.tap(({ client, entry }) => SubscriptionRef.update(
|
||||||
client.cache,
|
client.cache,
|
||||||
HashMap.set(new QueryClient.QueryClientCacheKey(key, this.f as (key: Query.AnyKey) => Effect.Effect<unknown, unknown, unknown>), entry),
|
HashMap.set(this.makeCacheKey(key), entry),
|
||||||
)),
|
)),
|
||||||
Effect.map(({ entry }) => entry),
|
Effect.map(({ entry }) => entry),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get invalidateCache(): Effect.Effect<void> {
|
||||||
|
return QueryClient.QueryClient.pipe(
|
||||||
|
Effect.andThen(client => SubscriptionRef.update(
|
||||||
|
client.cache,
|
||||||
|
HashMap.filter((_, key) => !Equivalence.strict()(key.f, this.f)),
|
||||||
|
)),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidateCacheEntry(key: K): Effect.Effect<void> {
|
||||||
|
return QueryClient.QueryClient.pipe(
|
||||||
|
Effect.andThen(client => SubscriptionRef.update(
|
||||||
|
client.cache,
|
||||||
|
HashMap.remove(this.makeCacheKey(key)),
|
||||||
|
)),
|
||||||
|
Effect.provide(this.context),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
|
export const isQuery = (u: unknown): u is Query<readonly unknown[], unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
|
||||||
@@ -178,6 +276,7 @@ export declare namespace make {
|
|||||||
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
readonly f: (key: NoInfer<K>) => Effect.Effect<A, E, Result.forkEffect.InputContext<R, NoInfer<P>>>
|
||||||
readonly initialProgress?: P
|
readonly initialProgress?: P
|
||||||
readonly staleTime?: Duration.DurationInput
|
readonly staleTime?: Duration.DurationInput
|
||||||
|
readonly refreshOnWindowFocus?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +296,12 @@ export const make = Effect.fnUntraced(function* <K extends Query.AnyKey, A, E =
|
|||||||
options.initialProgress as P,
|
options.initialProgress as P,
|
||||||
|
|
||||||
options.staleTime ?? client.defaultStaleTime,
|
options.staleTime ?? client.defaultStaleTime,
|
||||||
|
options.refreshOnWindowFocus ?? client.defaultRefreshOnWindowFocus,
|
||||||
|
|
||||||
yield* SubscriptionRef.make(Option.none<K>()),
|
yield* SubscriptionRef.make(Option.none<K>()),
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, E>>()),
|
||||||
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
yield* SubscriptionRef.make(Result.initial<A, E, P>()),
|
||||||
|
yield* SubscriptionRef.make(Option.none<Result.Final<A, E, P>>()),
|
||||||
|
|
||||||
yield* Effect.makeSemaphore(1),
|
yield* Effect.makeSemaphore(1),
|
||||||
)
|
)
|
||||||
@@ -214,18 +315,5 @@ export const service = <K extends Query.AnyKey, A, E = never, R = never, P = nev
|
|||||||
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | QueryClient.QueryClient | Result.forkEffect.OutputContext<A, E, R, P>
|
||||||
> => Effect.tap(
|
> => Effect.tap(
|
||||||
make(options),
|
make(options),
|
||||||
query => Effect.forkScoped(run(query)),
|
query => Effect.forkScoped(query.run),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const run = <K extends Query.AnyKey, A, E, R, P>(
|
|
||||||
self: Query<K, A, E, R, P>
|
|
||||||
): Effect.Effect<void> => {
|
|
||||||
const _self = self as QueryImpl<K, A, E, R, P>
|
|
||||||
return Stream.runForEach(_self.key, key => _self.interrupt.pipe(
|
|
||||||
Effect.andThen(SubscriptionRef.set(_self.latestKey, Option.some(key))),
|
|
||||||
Effect.andThen(_self.start(key)),
|
|
||||||
Effect.andThen(sub => Effect.forkScoped(_self.watch(sub))),
|
|
||||||
Effect.provide(_self.context),
|
|
||||||
_self.runSemaphore.withPermits(1),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type DateTime, type Duration, Effect, Equal, Equivalence, Hash, HashMap, Pipeable, Predicate, type Scope, SubscriptionRef } from "effect"
|
import { DateTime, Duration, Effect, Equal, Equivalence, Hash, HashMap, Pipeable, Predicate, type Scope, SubscriptionRef } from "effect"
|
||||||
import type * as Query from "./Query.js"
|
import type * as Query from "./Query.js"
|
||||||
import type * as Result from "./Result.js"
|
import type * as Result from "./Result.js"
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ export interface QueryClientService extends Pipeable.Pipeable {
|
|||||||
readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
|
readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>
|
||||||
readonly gcTime: Duration.DurationInput
|
readonly gcTime: Duration.DurationInput
|
||||||
readonly defaultStaleTime: Duration.DurationInput
|
readonly defaultStaleTime: Duration.DurationInput
|
||||||
|
readonly defaultRefreshOnWindowFocus: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryClient extends Effect.Service<QueryClient>()("@effect-fc/QueryClient/QueryClient", {
|
export class QueryClient extends Effect.Service<QueryClient>()("@effect-fc/QueryClient/QueryClient", {
|
||||||
@@ -26,6 +27,7 @@ implements QueryClientService {
|
|||||||
readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>,
|
readonly cache: SubscriptionRef.SubscriptionRef<HashMap.HashMap<QueryClientCacheKey, QueryClientCacheEntry>>,
|
||||||
readonly gcTime: Duration.DurationInput,
|
readonly gcTime: Duration.DurationInput,
|
||||||
readonly defaultStaleTime: Duration.DurationInput,
|
readonly defaultStaleTime: Duration.DurationInput,
|
||||||
|
readonly defaultRefreshOnWindowFocus: boolean,
|
||||||
readonly runSemaphore: Effect.Semaphore,
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
@@ -38,6 +40,7 @@ export declare namespace make {
|
|||||||
export interface Options {
|
export interface Options {
|
||||||
readonly gcTime?: Duration.DurationInput
|
readonly gcTime?: Duration.DurationInput
|
||||||
readonly defaultStaleTime?: Duration.DurationInput
|
readonly defaultStaleTime?: Duration.DurationInput
|
||||||
|
readonly defaultRefreshOnWindowFocus?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +49,7 @@ export const make = Effect.fnUntraced(function* (options: make.Options = {}): Ef
|
|||||||
yield* SubscriptionRef.make(HashMap.empty<QueryClientCacheKey, QueryClientCacheEntry>()),
|
yield* SubscriptionRef.make(HashMap.empty<QueryClientCacheKey, QueryClientCacheEntry>()),
|
||||||
options.gcTime ?? "5 minutes",
|
options.gcTime ?? "5 minutes",
|
||||||
options.defaultStaleTime ?? "0 minutes",
|
options.defaultStaleTime ?? "0 minutes",
|
||||||
|
options.defaultRefreshOnWindowFocus ?? true,
|
||||||
yield* Effect.makeSemaphore(1),
|
yield* Effect.makeSemaphore(1),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -105,3 +109,11 @@ implements Pipeable.Pipeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isQueryClientCacheEntry = (u: unknown): u is QueryClientCacheEntry => Predicate.hasProperty(u, QueryClientCacheEntryTypeId)
|
export const isQueryClientCacheEntry = (u: unknown): u is QueryClientCacheEntry => Predicate.hasProperty(u, QueryClientCacheEntryTypeId)
|
||||||
|
|
||||||
|
export const isQueryClientCacheEntryStale = (
|
||||||
|
self: QueryClientCacheEntry,
|
||||||
|
staleTime: Duration.DurationInput,
|
||||||
|
): Effect.Effect<boolean> => Effect.andThen(
|
||||||
|
DateTime.now,
|
||||||
|
now => Duration.greaterThanOrEqualTo(DateTime.distanceDuration(self.createdAt, now), staleTime),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Option, Pipeable, Predicate, PubSub, pipe, Ref, type Scope, Stream, Subscribable } from "effect"
|
import { Cause, Context, Data, Effect, Equal, Exit, type Fiber, Hash, Layer, Match, Pipeable, Predicate, PubSub, pipe, Ref, type Scope, Stream, Subscribable } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result")
|
||||||
@@ -8,18 +8,13 @@ export type Result<A, E = never, P = never> = (
|
|||||||
| Initial
|
| Initial
|
||||||
| Running<P>
|
| Running<P>
|
||||||
| Final<A, E, P>
|
| Final<A, E, P>
|
||||||
// biome-ignore lint/complexity/noBannedTypes: relevant here
|
|
||||||
) & ({} | Optimistic)
|
|
||||||
|
|
||||||
export type Final<A, E = never, P = never> = (
|
|
||||||
& (Success<A> | Failure<A, E>)
|
|
||||||
// biome-ignore lint/complexity/noBannedTypes: relevant here
|
|
||||||
& ({} | Refreshing<P>)
|
|
||||||
// biome-ignore lint/complexity/noBannedTypes: relevant here
|
|
||||||
& ({} | Optimistic)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export namespace Result {
|
// biome-ignore lint/complexity/noBannedTypes: "{}" is relevant here
|
||||||
|
export type Final<A, E = never, P = never> = (Success<A> | Failure<E>) & ({} | Flags<P>)
|
||||||
|
export type Flags<P = never> = WillFetch | WillRefresh | Refreshing<P>
|
||||||
|
|
||||||
|
export declare namespace Result {
|
||||||
export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
|
export interface Prototype extends Pipeable.Pipeable, Equal.Equal {
|
||||||
readonly [ResultTypeId]: ResultTypeId
|
readonly [ResultTypeId]: ResultTypeId
|
||||||
}
|
}
|
||||||
@@ -29,6 +24,10 @@ export namespace Result {
|
|||||||
export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never
|
export type Progress<R extends Result<any, any, any>> = [R] extends [Result<infer _A, infer _E, infer P>] ? P : never
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare namespace Flags {
|
||||||
|
export type Keys = keyof WillFetch & WillRefresh & Refreshing<any>
|
||||||
|
}
|
||||||
|
|
||||||
export interface Initial extends Result.Prototype {
|
export interface Initial extends Result.Prototype {
|
||||||
readonly _tag: "Initial"
|
readonly _tag: "Initial"
|
||||||
}
|
}
|
||||||
@@ -43,62 +42,56 @@ export interface Success<A> extends Result.Prototype {
|
|||||||
readonly value: A
|
readonly value: A
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Failure<A, E = never> extends Result.Prototype {
|
export interface Failure<E = never> extends Result.Prototype {
|
||||||
readonly _tag: "Failure"
|
readonly _tag: "Failure"
|
||||||
readonly cause: Cause.Cause<E>
|
readonly cause: Cause.Cause<E>
|
||||||
readonly previousSuccess: Option.Option<Success<A>>
|
}
|
||||||
|
|
||||||
|
export interface WillFetch {
|
||||||
|
readonly _flag: "WillFetch"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WillRefresh {
|
||||||
|
readonly _flag: "WillRefresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Refreshing<P = never> {
|
export interface Refreshing<P = never> {
|
||||||
readonly refreshing: true
|
readonly _flag: "Refreshing"
|
||||||
readonly progress: P
|
readonly progress: P
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Optimistic {
|
|
||||||
readonly optimistic: true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const ResultPrototype = Object.freeze({
|
const ResultPrototype = Object.freeze({
|
||||||
...Pipeable.Prototype,
|
...Pipeable.Prototype,
|
||||||
[ResultTypeId]: ResultTypeId,
|
[ResultTypeId]: ResultTypeId,
|
||||||
|
|
||||||
[Equal.symbol](this: Result<any, any, any>, that: Result<any, any, any>): boolean {
|
[Equal.symbol](this: Result<any, any, any>, that: Result<any, any, any>): boolean {
|
||||||
if (this._tag !== that._tag)
|
if (this._tag !== that._tag || (this as Flags)._flag !== (that as Flags)._flag)
|
||||||
|
return false
|
||||||
|
if (hasRefreshingFlag(this) && !Equal.equals(this.progress, (that as Refreshing<any>).progress))
|
||||||
return false
|
return false
|
||||||
|
|
||||||
return Match.value(this).pipe(
|
return Match.value(this).pipe(
|
||||||
Match.tag("Initial", () => true),
|
Match.tag("Initial", () => true),
|
||||||
Match.tag("Running", self => Equal.equals(self.progress, (that as Running<any>).progress)),
|
Match.tag("Running", self => Equal.equals(self.progress, (that as Running<any>).progress)),
|
||||||
Match.tag("Success", self =>
|
Match.tag("Success", self => Equal.equals(self.value, (that as Success<any>).value)),
|
||||||
Equal.equals(self.value, (that as Success<any>).value) &&
|
Match.tag("Failure", self => Equal.equals(self.cause, (that as Failure<any>).cause)),
|
||||||
(isRefreshing(self) ? self.refreshing : false) === (isRefreshing(that) ? that.refreshing : false) &&
|
|
||||||
Equal.equals(isRefreshing(self) ? self.progress : undefined, isRefreshing(that) ? that.progress : undefined)
|
|
||||||
),
|
|
||||||
Match.tag("Failure", self =>
|
|
||||||
Equal.equals(self.cause, (that as Failure<any, any>).cause) &&
|
|
||||||
(isRefreshing(self) ? self.refreshing : false) === (isRefreshing(that) ? that.refreshing : false) &&
|
|
||||||
Equal.equals(isRefreshing(self) ? self.progress : undefined, isRefreshing(that) ? that.progress : undefined)
|
|
||||||
),
|
|
||||||
Match.exhaustive,
|
Match.exhaustive,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
[Hash.symbol](this: Result<any, any, any>): number {
|
[Hash.symbol](this: Result<any, any, any>): number {
|
||||||
const tagHash = Hash.string(this._tag)
|
return pipe(Hash.string(this._tag),
|
||||||
|
tagHash => Match.value(this).pipe(
|
||||||
return Match.value(this).pipe(
|
|
||||||
Match.tag("Initial", () => tagHash),
|
Match.tag("Initial", () => tagHash),
|
||||||
Match.tag("Running", self => Hash.combine(Hash.hash(self.progress))(tagHash)),
|
Match.tag("Running", self => Hash.combine(Hash.hash(self.progress))(tagHash)),
|
||||||
Match.tag("Success", self => pipe(tagHash,
|
Match.tag("Success", self => Hash.combine(Hash.hash(self.value))(tagHash)),
|
||||||
Hash.combine(Hash.hash(self.value)),
|
Match.tag("Failure", self => Hash.combine(Hash.hash(self.cause))(tagHash)),
|
||||||
Hash.combine(Hash.hash(isRefreshing(self) ? self.progress : undefined)),
|
|
||||||
)),
|
|
||||||
Match.tag("Failure", self => pipe(tagHash,
|
|
||||||
Hash.combine(Hash.hash(self.cause)),
|
|
||||||
Hash.combine(Hash.hash(isRefreshing(self) ? self.progress : undefined)),
|
|
||||||
)),
|
|
||||||
Match.exhaustive,
|
Match.exhaustive,
|
||||||
|
),
|
||||||
|
Hash.combine(Hash.hash((this as Flags)._flag)),
|
||||||
|
hash => hasRefreshingFlag(this)
|
||||||
|
? Hash.combine(Hash.hash(this.progress))(hash)
|
||||||
|
: hash,
|
||||||
Hash.cached(this),
|
Hash.cached(this),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -110,9 +103,11 @@ export const isFinal = (u: unknown): u is Final<unknown, unknown, unknown> => is
|
|||||||
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
|
export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag === "Initial"
|
||||||
export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u._tag === "Running"
|
export const isRunning = (u: unknown): u is Running<unknown> => isResult(u) && u._tag === "Running"
|
||||||
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
|
export const isSuccess = (u: unknown): u is Success<unknown> => isResult(u) && u._tag === "Success"
|
||||||
export const isFailure = (u: unknown): u is Failure<unknown, unknown> => isResult(u) && u._tag === "Failure"
|
export const isFailure = (u: unknown): u is Failure<unknown> => isResult(u) && u._tag === "Failure"
|
||||||
export const isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing") && u.refreshing
|
export const hasFlag = (u: unknown): u is Flags => isResult(u) && Predicate.hasProperty(u, "_flag")
|
||||||
export const isOptimistic = (u: unknown): u is Optimistic => isResult(u) && Predicate.hasProperty(u, "optimistic") && u.optimistic
|
export const hasWillFetchFlag = (u: unknown): u is WillFetch => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "WillFetch"
|
||||||
|
export const hasWillRefreshFlag = (u: unknown): u is WillRefresh => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "WillRefresh"
|
||||||
|
export const hasRefreshingFlag = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "_flag") && u._flag === "Refreshing"
|
||||||
|
|
||||||
export const initial: {
|
export const initial: {
|
||||||
(): Initial
|
(): Initial
|
||||||
@@ -120,41 +115,42 @@ export const initial: {
|
|||||||
} = (): Initial => Object.setPrototypeOf({ _tag: "Initial" }, ResultPrototype)
|
} = (): Initial => Object.setPrototypeOf({ _tag: "Initial" }, ResultPrototype)
|
||||||
export const running = <P = never>(progress?: P): Running<P> => Object.setPrototypeOf({ _tag: "Running", progress }, ResultPrototype)
|
export const running = <P = never>(progress?: P): Running<P> => Object.setPrototypeOf({ _tag: "Running", progress }, ResultPrototype)
|
||||||
export const succeed = <A>(value: A): Success<A> => Object.setPrototypeOf({ _tag: "Success", value }, ResultPrototype)
|
export const succeed = <A>(value: A): Success<A> => Object.setPrototypeOf({ _tag: "Success", value }, ResultPrototype)
|
||||||
|
export const fail = <E>(cause: Cause.Cause<E> ): Failure<E> => Object.setPrototypeOf({ _tag: "Failure", cause }, ResultPrototype)
|
||||||
|
|
||||||
export const fail = <E, A = never>(
|
export const willFetch = <R extends Final<any, any, any>>(
|
||||||
cause: Cause.Cause<E>,
|
result: R
|
||||||
previousSuccess?: Success<NoInfer<A>>,
|
): Omit<R, keyof Flags.Keys> & WillFetch => Object.setPrototypeOf(
|
||||||
): Failure<A, E> => Object.setPrototypeOf({
|
Object.assign({}, result, { _flag: "WillFetch" }),
|
||||||
_tag: "Failure",
|
Object.getPrototypeOf(result),
|
||||||
cause,
|
)
|
||||||
previousSuccess: Option.fromNullable(previousSuccess),
|
|
||||||
}, ResultPrototype)
|
|
||||||
|
|
||||||
export const refreshing = <R extends Success<any> | Failure<any, any>, P = never>(
|
export const willRefresh = <R extends Final<any, any, any>>(
|
||||||
|
result: R
|
||||||
|
): Omit<R, keyof Flags.Keys> & WillRefresh => Object.setPrototypeOf(
|
||||||
|
Object.assign({}, result, { _flag: "WillRefresh" }),
|
||||||
|
Object.getPrototypeOf(result),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const refreshing = <R extends Final<any, any, any>, P = never>(
|
||||||
result: R,
|
result: R,
|
||||||
progress?: P,
|
progress?: P,
|
||||||
): Omit<R, keyof Refreshing<Result.Progress<R>>> & Refreshing<P> => Object.setPrototypeOf(
|
): Omit<R, keyof Flags.Keys> & Refreshing<P> => Object.setPrototypeOf(
|
||||||
Object.assign({}, result, { refreshing: true, progress }),
|
Object.assign({}, result, { _flag: "Refreshing", progress }),
|
||||||
Object.getPrototypeOf(result),
|
Object.getPrototypeOf(result),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const optimistic = <R extends Success<any> | Failure<any, any>>(
|
export const fromExit: {
|
||||||
result: R
|
<A, E>(exit: Exit.Success<A, E>): Success<A>
|
||||||
): Omit<R, keyof Optimistic> & Optimistic => Object.setPrototypeOf(
|
<A, E>(exit: Exit.Failure<A, E>): Failure<E>
|
||||||
Object.assign({}, result, { optimistic: true }),
|
<A, E>(exit: Exit.Exit<A, E>): Success<A> | Failure<E>
|
||||||
Object.getPrototypeOf(result),
|
} = exit => (exit._tag === "Success" ? succeed(exit.value) : fail(exit.cause)) as any
|
||||||
)
|
|
||||||
|
|
||||||
export const fromExit = <A, E>(
|
export const toExit: {
|
||||||
exit: Exit.Exit<A, E>,
|
<A>(self: Success<A>): Exit.Success<A, never>
|
||||||
previousSuccess?: Success<NoInfer<A>>,
|
<E>(self: Failure<E>): Exit.Failure<never, E>
|
||||||
): Success<A> | Failure<A, E> => exit._tag === "Success"
|
<A, E, P>(self: Final<A, E, P>): Exit.Exit<A, E>
|
||||||
? succeed(exit.value)
|
<A, E, P>(self: Result<A, E, P>): Exit.Exit<A, E | Cause.NoSuchElementException>
|
||||||
: fail(exit.cause, previousSuccess)
|
} = <A, E, P>(self: Result<A, E, P>): any => {
|
||||||
|
|
||||||
export const toExit = <A, E, P>(
|
|
||||||
self: Result<A, E, P>
|
|
||||||
): Exit.Exit<A, E | Cause.NoSuchElementException> => {
|
|
||||||
switch (self._tag) {
|
switch (self._tag) {
|
||||||
case "Success":
|
case "Success":
|
||||||
return Exit.succeed(self.value)
|
return Exit.succeed(self.value)
|
||||||
@@ -193,17 +189,17 @@ export const makeProgressLayer = <A, E, P = never>(): Layer.Layer<
|
|||||||
const state = yield* State<A, E, P>()
|
const state = yield* State<A, E, P>()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update: <E, R>(f: (previous: P) => Effect.Effect<P, E, R>) => Effect.Do.pipe(
|
update: <FE, FR>(f: (previous: P) => Effect.Effect<P, FE, FR>) => Effect.Do.pipe(
|
||||||
Effect.bind("previous", () => Effect.andThen(state.get, previous =>
|
Effect.bind("previous", () => Effect.andThen(state.get, previous =>
|
||||||
isRunning(previous) || isRefreshing(previous)
|
(isRunning(previous) || hasRefreshingFlag(previous))
|
||||||
? Effect.succeed(previous)
|
? Effect.succeed(previous)
|
||||||
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })),
|
: Effect.fail(new PreviousResultNotRunningNorRefreshing({ previous })),
|
||||||
)),
|
)),
|
||||||
Effect.bind("progress", ({ previous }) => f(previous.progress)),
|
Effect.bind("progress", ({ previous }) => f(previous.progress)),
|
||||||
Effect.let("next", ({ previous, progress }) => Object.setPrototypeOf(
|
Effect.let("next", ({ previous, progress }) => isRunning(previous)
|
||||||
Object.assign({}, previous, { progress }),
|
? running(progress)
|
||||||
Object.getPrototypeOf(previous),
|
: refreshing(previous, progress) as Final<A, E, P> & Refreshing<P>
|
||||||
)),
|
),
|
||||||
Effect.andThen(({ next }) => state.set(next)),
|
Effect.andThen(({ next }) => state.set(next)),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -213,18 +209,10 @@ export const makeProgressLayer = <A, E, P = never>(): Layer.Layer<
|
|||||||
export namespace unsafeForkEffect {
|
export namespace unsafeForkEffect {
|
||||||
export type OutputContext<A, E, R, P> = Exclude<R, State<A, E, P> | Progress<P> | Progress<never>>
|
export type OutputContext<A, E, R, P> = Exclude<R, State<A, E, P> | Progress<P> | Progress<never>>
|
||||||
|
|
||||||
export type Options<A, E, P> = {
|
export interface Options<A, E, P> {
|
||||||
|
readonly initial?: Initial | Final<A, E, P>
|
||||||
readonly initialProgress?: P
|
readonly initialProgress?: P
|
||||||
readonly previous?: Final<A, E, P>
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
readonly refresh: true
|
|
||||||
readonly previous: Final<A, E, P>
|
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
readonly refresh?: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const unsafeForkEffect = <A, E, R, P = never>(
|
export const unsafeForkEffect = <A, E, R, P = never>(
|
||||||
@@ -235,16 +223,17 @@ export const unsafeForkEffect = <A, E, R, P = never>(
|
|||||||
never,
|
never,
|
||||||
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
|
Scope.Scope | unsafeForkEffect.OutputContext<A, E, R, P>
|
||||||
> => Effect.Do.pipe(
|
> => Effect.Do.pipe(
|
||||||
Effect.bind("ref", () => Ref.make(initial<A, E, P>())),
|
Effect.bind("ref", () => Ref.make(options?.initial ?? initial<A, E, P>())),
|
||||||
Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()),
|
Effect.bind("pubsub", () => PubSub.unbounded<Result<A, E, P>>()),
|
||||||
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
|
Effect.bind("fiber", ({ ref, pubsub }) => Effect.forkScoped(State<A, E, P>().pipe(
|
||||||
Effect.andThen(state => state.set(options?.refresh
|
Effect.andThen(state => state.set(
|
||||||
? refreshing(options.previous, options?.initialProgress) as Result<A, E, P>
|
(isFinal(options?.initial) && hasWillRefreshFlag(options?.initial))
|
||||||
|
? refreshing(options.initial, options?.initialProgress) as Result<A, E, P>
|
||||||
: running(options?.initialProgress)
|
: running(options?.initialProgress)
|
||||||
).pipe(
|
).pipe(
|
||||||
Effect.andThen(effect),
|
Effect.andThen(effect),
|
||||||
Effect.onExit(exit => Effect.andThen(
|
Effect.onExit(exit => Effect.andThen(
|
||||||
state.set(fromExit(exit, (options?.previous && isSuccess(options.previous)) ? options.previous : undefined)),
|
state.set(fromExit(exit)),
|
||||||
Effect.forkScoped(PubSub.shutdown(pubsub)),
|
Effect.forkScoped(PubSub.shutdown(pubsub)),
|
||||||
)),
|
)),
|
||||||
)),
|
)),
|
||||||
@@ -275,7 +264,7 @@ export const unsafeForkEffect = <A, E, R, P = never>(
|
|||||||
export namespace forkEffect {
|
export namespace forkEffect {
|
||||||
export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R
|
export type InputContext<R, P> = R extends Progress<infer X> ? [X] extends [P] ? R : never : R
|
||||||
export type OutputContext<A, E, R, P> = unsafeForkEffect.OutputContext<A, E, R, P>
|
export type OutputContext<A, E, R, P> = unsafeForkEffect.OutputContext<A, E, R, P>
|
||||||
export type Options<A, E, P> = unsafeForkEffect.Options<A, E, P>
|
export interface Options<A, E, P> extends unsafeForkEffect.Options<A, E, P> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const forkEffect: {
|
export const forkEffect: {
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ const ResultView = Component.makeUntraced("Result")(function*() {
|
|||||||
|
|
||||||
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
const idRef = yield* SubscriptionRef.make(1)
|
const idRef = yield* SubscriptionRef.make(1)
|
||||||
const key = Stream.zipLatest(Stream.make("posts" as const), idRef.changes)
|
|
||||||
|
|
||||||
const query = yield* Query.service({
|
const query = yield* Query.service({
|
||||||
key,
|
key: Stream.zipLatest(Stream.make("posts" as const), idRef.changes),
|
||||||
f: ([, id]) => HttpClient.HttpClient.pipe(
|
f: ([, id]) => HttpClient.HttpClient.pipe(
|
||||||
Effect.tap(Effect.sleep("500 millis")),
|
Effect.tap(Effect.sleep("500 millis")),
|
||||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||||
Effect.andThen(response => response.json),
|
Effect.andThen(response => response.json),
|
||||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||||
),
|
),
|
||||||
|
staleTime: "10 seconds",
|
||||||
})
|
})
|
||||||
|
|
||||||
const mutation = yield* Mutation.make({
|
const mutation = yield* Mutation.make({
|
||||||
@@ -74,7 +74,7 @@ const ResultView = Component.makeUntraced("Result")(function*() {
|
|||||||
Match.tag("Success", result => <>
|
Match.tag("Success", result => <>
|
||||||
<Heading>{result.value.title}</Heading>
|
<Heading>{result.value.title}</Heading>
|
||||||
<Text>{result.value.body}</Text>
|
<Text>{result.value.body}</Text>
|
||||||
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
|
{Result.hasRefreshingFlag(result) && <Text>Refreshing...</Text>}
|
||||||
</>),
|
</>),
|
||||||
Match.tag("Failure", result =>
|
Match.tag("Failure", result =>
|
||||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||||
@@ -85,7 +85,7 @@ const ResultView = Component.makeUntraced("Result")(function*() {
|
|||||||
|
|
||||||
<Flex direction="row" justify="center" align="center" gap="1">
|
<Flex direction="row" justify="center" align="center" gap="1">
|
||||||
<Button onClick={() => runPromise(query.refresh)}>Refresh</Button>
|
<Button onClick={() => runPromise(query.refresh)}>Refresh</Button>
|
||||||
<Button onClick={() => runPromise(query.refetch)}>Refetch</Button>
|
<Button onClick={() => runPromise(query.invalidateCache)}>Invalidate cache</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -94,7 +94,7 @@ const ResultView = Component.makeUntraced("Result")(function*() {
|
|||||||
Match.tag("Success", result => <>
|
Match.tag("Success", result => <>
|
||||||
<Heading>{result.value.title}</Heading>
|
<Heading>{result.value.title}</Heading>
|
||||||
<Text>{result.value.body}</Text>
|
<Text>{result.value.body}</Text>
|
||||||
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
|
{Result.hasRefreshingFlag(result) && <Text>Refreshing...</Text>}
|
||||||
</>),
|
</>),
|
||||||
Match.tag("Failure", result =>
|
Match.tag("Failure", result =>
|
||||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user