Compare commits

..

1 Commits

Author SHA1 Message Date
a274fb77f6 Update bun minor+patch updates
Some checks failed
Lint / lint (push) Failing after 37s
Test build / test-build (pull_request) Failing after 7s
2026-01-04 12:01:48 +00:00
5 changed files with 55 additions and 99 deletions

View File

@@ -6,7 +6,7 @@
"name": "@effect-fc/monorepo", "name": "@effect-fc/monorepo",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.8", "@biomejs/biome": "^2.3.8",
"@effect/language-service": "^0.65.0", "@effect/language-service": "^0.64.0",
"@types/bun": "^1.3.3", "@types/bun": "^1.3.3",
"npm-check-updates": "^19.1.2", "npm-check-updates": "^19.1.2",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",
@@ -131,7 +131,7 @@
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"], "@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
"@effect/language-service": ["@effect/language-service@0.65.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-eHcpLNCZa1XEDRrXLZqTdky6jAQojL6zQEW53Ba6vJL35j77tJTnV9BFkk34G3rxKoplNo39U0Mum3RfuH9rsg=="], "@effect/language-service": ["@effect/language-service@0.64.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-Ch6s0aAcrdaOWKmsXw0ys/MvYzz2DD5wf6qALnuyb8Tq1LO8livCUf2ax2cIWzPuEitRyvmIFb0byxKAYi1kcw=="],
"@effect/platform": ["@effect/platform@0.93.6", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.8" } }, "sha512-I5lBGQWzWXP4zlIdPs7z7WHmEFVBQhn+74emr/h16GZX96EEJ6I1rjGaKyZF7mtukbMuo9wEckDPssM8vskZ/w=="], "@effect/platform": ["@effect/platform@0.93.6", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.19.8" } }, "sha512-I5lBGQWzWXP4zlIdPs7z7WHmEFVBQhn+74emr/h16GZX96EEJ6I1rjGaKyZF7mtukbMuo9wEckDPssM8vskZ/w=="],

View File

@@ -16,7 +16,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.3.8", "@biomejs/biome": "^2.3.8",
"@effect/language-service": "^0.65.0", "@effect/language-service": "^0.64.0",
"@types/bun": "^1.3.3", "@types/bun": "^1.3.3",
"npm-check-updates": "^19.1.2", "npm-check-updates": "^19.1.2",
"npm-sort": "^0.0.4", "npm-sort": "^0.0.4",

View File

@@ -1,4 +1,4 @@
import { type Cause, type Context, DateTime, type Duration, Effect, Exit, Fiber, HashMap, identity, Option, Pipeable, Predicate, type Scope, Stream, Subscribable, SubscriptionRef } from "effect" import { type Cause, type Context, DateTime, type Duration, Effect, Fiber, HashMap, identity, Option, Pipeable, Predicate, type Scope, Stream, type 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"
@@ -21,7 +21,6 @@ extends Pipeable.Pipeable {
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 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 refetch: Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException>
@@ -55,16 +54,6 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
super() super()
} }
get run(): Effect.Effect<void> {
return Stream.runForEach(this.key, key => this.interrupt.pipe(
Effect.andThen(SubscriptionRef.set(this.latestKey, Option.some(key))),
Effect.andThen(this.startCached(key, Result.initial(), false)),
Effect.andThen(sub => Effect.forkScoped(this.watch(sub))),
Effect.provide(this.context),
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,
@@ -75,21 +64,21 @@ 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.startCached(key, Result.initial(), false), this.context)), Effect.andThen(Effect.provide(this.start(key), this.context)),
Effect.andThen(sub => this.watch(sub)), Effect.andThen(sub => this.watch(sub)),
) )
} }
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.startCached(key, Result.initial(), false), this.context)), Effect.andThen(Effect.provide(this.start(key), this.context)),
) )
} }
get refetch(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> { get refetch(): Effect.Effect<Result.Final<A, E, P>, Cause.NoSuchElementException> {
return this.interrupt.pipe( return this.interrupt.pipe(
Effect.andThen(this.latestKey), Effect.andThen(this.latestKey),
Effect.andThen(identity), Effect.andThen(identity),
Effect.andThen(key => Effect.provide(this.start(key, Result.initial(), false), this.context)), Effect.andThen(key => Effect.provide(this.start(key), this.context)),
Effect.andThen(sub => this.watch(sub)), Effect.andThen(sub => this.watch(sub)),
) )
} }
@@ -97,14 +86,14 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
return this.interrupt.pipe( return this.interrupt.pipe(
Effect.andThen(this.latestKey), Effect.andThen(this.latestKey),
Effect.andThen(identity), Effect.andThen(identity),
Effect.andThen(key => Effect.provide(this.start(key, Result.initial(), false), this.context)), 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(this.latestKey),
Effect.andThen(identity), Effect.andThen(identity),
Effect.andThen(key => Effect.provide(this.start(key, Result.initial(), true), this.context)), Effect.andThen(key => Effect.provide(this.start(key, true), this.context)),
Effect.andThen(sub => this.watch(sub)), Effect.andThen(sub => this.watch(sub)),
) )
} }
@@ -112,53 +101,26 @@ extends Pipeable.Class() implements Query<K, A, E, R, P> {
return this.interrupt.pipe( return this.interrupt.pipe(
Effect.andThen(this.latestKey), Effect.andThen(this.latestKey),
Effect.andThen(identity), Effect.andThen(identity),
Effect.andThen(key => Effect.provide(this.start(key, Result.initial(), true), this.context)), Effect.andThen(key => Effect.provide(this.start(key, true), this.context)),
) )
} }
startCached(
key: K,
initial: Result.Result<A, E, P>,
refresh: boolean,
): 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 => QueryClient.isQueryClientCacheEntryStale(entry, this.staleTime)
? Effect.map(
SubscriptionRef.set(this.result, entry.result as Result.Result<A, E, P>),
() => Subscribable.make({
get: Effect.succeed(entry.result as Result.Result<A, E, P>),
get changes() { return Stream.empty },
}),
)
: this.start(key, Result.optimistic(entry.result) as Result.Result<A, E, P>, false),
onNone: () => this.start(key, initial, refresh),
}))
}
start( start(
key: K, key: K,
initial: Result.Result<A, E, P>, refresh?: boolean,
refresh: boolean,
): Effect.Effect< ): Effect.Effect<
Subscribable.Subscribable<Result.Result<A, E, P>>, Subscribable.Subscribable<Result.Result<A, E, P>>,
never, never,
Scope.Scope | QueryClient.QueryClient | R Scope.Scope | R
> { > {
return this.result.pipe( return this.result.pipe(
Effect.map(previous => Result.isFinal(previous) ? previous : undefined), Effect.map(previous => Result.isFinal(previous)
? previous
: undefined
),
Effect.andThen(previous => Result.unsafeForkEffect( Effect.andThen(previous => Result.unsafeForkEffect(
Effect.onExit(this.f(key), exit => Effect.andThen( Effect.onExit(this.f(key), () => SubscriptionRef.set(this.fiber, Option.none())),
Exit.isSuccess(exit)
? this.updateCacheEntry(key, Result.succeed(exit.value))
: Effect.void,
SubscriptionRef.set(this.fiber, Option.none()),
)),
{ {
initial,
initialProgress: this.initialProgress, initialProgress: this.initialProgress,
refresh: refresh && previous, refresh: refresh && previous,
previous, previous,
@@ -252,5 +214,18 @@ 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(query.run), query => Effect.forkScoped(run(query)),
) )
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),
))
}

View File

@@ -1,4 +1,4 @@
import { DateTime, Duration, Effect, Equal, Equivalence, Hash, HashMap, Pipeable, Predicate, type Scope, SubscriptionRef } from "effect" import { type DateTime, type 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"
@@ -105,11 +105,3 @@ 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.lessThan(DateTime.distanceDuration(self.createdAt, now), staleTime),
)

View File

@@ -5,16 +5,18 @@ export const ResultTypeId: unique symbol = Symbol.for("@effect-fc/Result/Result"
export type ResultTypeId = typeof ResultTypeId export type ResultTypeId = typeof ResultTypeId
export type Result<A, E = never, P = never> = ( export type Result<A, E = never, P = never> = (
// biome-ignore lint/complexity/noBannedTypes: "{}" is relevant here | Initial
| (Initial & ({} | WillFetch))
| 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> = ( export type Final<A, E = never, P = never> = (
& (Success<A> | Failure<A, E>) & (Success<A> | Failure<A, E>)
// biome-ignore lint/complexity/noBannedTypes: "{}" is relevant here // biome-ignore lint/complexity/noBannedTypes: relevant here
& ({} | WillFetch | WillRefresh | Refreshing<P>) & ({} | Refreshing<P>)
// biome-ignore lint/complexity/noBannedTypes: relevant here
& ({} | Optimistic)
) )
export namespace Result { export namespace Result {
@@ -47,19 +49,15 @@ export interface Failure<A, E = never> extends Result.Prototype {
readonly previousSuccess: Option.Option<Success<A>> readonly previousSuccess: Option.Option<Success<A>>
} }
export interface WillFetch extends Result.Prototype { export interface Refreshing<P = never> {
readonly willFetch: true
}
export interface WillRefresh extends Result.Prototype {
readonly willRefresh: true
}
export interface Refreshing<P = never> extends Result.Prototype {
readonly refreshing: true readonly refreshing: true
readonly progress: P readonly progress: P
} }
export interface Optimistic {
readonly optimistic: true
}
const ResultPrototype = Object.freeze({ const ResultPrototype = Object.freeze({
...Pipeable.Prototype, ...Pipeable.Prototype,
@@ -113,9 +111,8 @@ export const isInitial = (u: unknown): u is Initial => isResult(u) && u._tag ===
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, unknown> => isResult(u) && u._tag === "Failure"
export const isWillFetch = (u: unknown): u is WillFetch => isResult(u) && Predicate.hasProperty(u, "willFetch") export const isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing") && u.refreshing
export const isWillRefresh = (u: unknown): u is WillRefresh => isResult(u) && Predicate.hasProperty(u, "willRefresh") export const isOptimistic = (u: unknown): u is Optimistic => isResult(u) && Predicate.hasProperty(u, "optimistic") && u.optimistic
export const isRefreshing = (u: unknown): u is Refreshing<unknown> => isResult(u) && Predicate.hasProperty(u, "refreshing")
export const initial: { export const initial: {
(): Initial (): Initial
@@ -133,21 +130,7 @@ export const fail = <E, A = never>(
previousSuccess: Option.fromNullable(previousSuccess), previousSuccess: Option.fromNullable(previousSuccess),
}, ResultPrototype) }, ResultPrototype)
export const willFetch = <R extends Initial | Final<any, any, any>>( export const refreshing = <R extends Success<any> | Failure<any, any>, P = never>(
result: R
): Omit<R, keyof WillFetch> & WillFetch => Object.setPrototypeOf(
Object.assign({}, result, { willFetch: true }),
Object.getPrototypeOf(result),
)
export const willRefresh = <R extends Final<any, any, any>>(
result: R
): Omit<R, keyof WillRefresh> & WillRefresh => Object.setPrototypeOf(
Object.assign({}, result, { willRefresh: true }),
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 Refreshing<Result.Progress<R>>> & Refreshing<P> => Object.setPrototypeOf(
@@ -155,6 +138,13 @@ export const refreshing = <R extends Final<any, any, any>, P = never>(
Object.getPrototypeOf(result), Object.getPrototypeOf(result),
) )
export const optimistic = <R extends Success<any> | Failure<any, any>>(
result: R
): Omit<R, keyof Optimistic> & Optimistic => Object.setPrototypeOf(
Object.assign({}, result, { optimistic: true }),
Object.getPrototypeOf(result),
)
export const fromExit = <A, E>( export const fromExit = <A, E>(
exit: Exit.Exit<A, E>, exit: Exit.Exit<A, E>,
previousSuccess?: Success<NoInfer<A>>, previousSuccess?: Success<NoInfer<A>>,
@@ -224,7 +214,6 @@ 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 type Options<A, E, P> = {
readonly initial?: Result<A, E, P>
readonly initialProgress?: P readonly initialProgress?: P
readonly previous?: Final<A, E, P> readonly previous?: Final<A, E, P>
} & ( } & (
@@ -246,7 +235,7 @@ 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(options?.initial ?? initial<A, E, P>())), Effect.bind("ref", () => Ref.make(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(options?.refresh