Compare commits

...

6 Commits

Author SHA1 Message Date
7cf8e30408 Update bun minor+patch updates
Some checks failed
Lint / lint (push) Failing after 8s
Test build / test-build (pull_request) Failing after 7s
2025-11-07 01:19:19 +01:00
Julien Valverdé
6d52c4ee31 Add ErrorObserver handle
All checks were successful
Lint / lint (push) Successful in 41s
2025-11-07 01:13:50 +01:00
Julien Valverdé
c80b25b0e0 Fix ReactRuntime
All checks were successful
Lint / lint (push) Successful in 12s
2025-11-06 15:21:56 +01:00
Julien Valverdé
7e14f27df4 Finish ErrorObserver
All checks were successful
Lint / lint (push) Successful in 40s
2025-11-06 01:17:06 +01:00
Julien Valverdé
5bc1e65a61 ErrorObserver work
Some checks failed
Lint / lint (push) Failing after 10s
2025-11-05 15:15:59 +01:00
Julien Valverdé
53bc4407b3 Add ErrorObserver
Some checks failed
Lint / lint (push) Failing after 36s
2025-11-05 01:12:56 +01:00
8 changed files with 86 additions and 26 deletions

View File

@@ -5,7 +5,7 @@
"name": "@effect-fc/monorepo",
"devDependencies": {
"@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.54.0",
"@effect/language-service": "^0.55.0",
"@types/bun": "^1.2.23",
"npm-check-updates": "^19.0.0",
"npm-sort": "^0.0.4",
@@ -130,7 +130,7 @@
"@effect-fc/example": ["@effect-fc/example@workspace:packages/example"],
"@effect/language-service": ["@effect/language-service@0.54.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-s9z8sbmZH1dIQzL+9imlBTt4XEbo7o+D8lAAxyp6kcjnvwYUYoebdT3KhBYpvqzIBGgiJlCMcXNzhXUTrz25VA=="],
"@effect/language-service": ["@effect/language-service@0.55.1", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-vMKYEzX9yC31a0NYGBQR4BI009NbSn6h9g9QCVnbatkb1VPZAuKble6OVXoyBq+VuYnyCePqwsc81TyiNsqRrw=="],
"@effect/platform": ["@effect/platform@0.92.1", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.18.1" } }, "sha512-XXWCBVwyhaKZISN7aM1fv/3fWDGyxr84ObywnUrL8aHvJLoIeskWFAP/fqw3c5MFCrJ3ZV97RWLbv6JiBQugdg=="],

View File

@@ -16,7 +16,7 @@
},
"devDependencies": {
"@biomejs/biome": "^2.2.5",
"@effect/language-service": "^0.54.0",
"@effect/language-service": "^0.55.0",
"@types/bun": "^1.2.23",
"npm-check-updates": "^19.0.0",
"npm-sort": "^0.0.4",

View File

@@ -2,6 +2,7 @@
import { Effect, Function, Predicate, Runtime, Scope } from "effect"
import * as React from "react"
import * as Component from "./Component.js"
import * as ErrorObserver from "./ErrorObserver.js"
export const TypeId: unique symbol = Symbol.for("@effect-fc/Async/Async")
@@ -31,10 +32,10 @@ const SuspenseProto = Object.freeze({
return ({ fallback, name, ...props }: Async.Props) => {
const promise = Runtime.runPromise(runtimeRef.current)(
Effect.andThen(
ErrorObserver.handle(Effect.andThen(
Component.useScope([], this),
scope => Effect.provideService(this.body(props as P), Scope.Scope, scope),
)
))
)
return React.createElement(

View File

@@ -2,6 +2,7 @@
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { Context, type Duration, Effect, Effectable, Equivalence, ExecutionStrategy, Exit, Fiber, Function, HashMap, Layer, ManagedRuntime, Option, Predicate, Ref, Runtime, Scope, Tracer, type Utils } from "effect"
import * as React from "react"
import * as ErrorObserver from "./ErrorObserver.js"
import { Memoized } from "./index.js"
@@ -75,10 +76,10 @@ const ComponentProto = Object.freeze({
runtimeRef: React.RefObject<Runtime.Runtime<Exclude<R, Scope.Scope>>>,
) {
return (props: P) => Runtime.runSync(runtimeRef.current)(
Effect.andThen(
ErrorObserver.handle(Effect.andThen(
useScope([], this),
scope => Effect.provideService(this.body(props), Scope.Scope, scope),
)
))
)
},
} as const)
@@ -547,14 +548,15 @@ const runReactEffect = <E, R>(
() => {
switch (options?.finalizerExecutionMode ?? "fork") {
case "sync":
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
Runtime.runSync(runtime)(ErrorObserver.handle(Scope.close(scope, Exit.void)))
break
case "fork":
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
Runtime.runFork(runtime)(ErrorObserver.handle(Scope.close(scope, Exit.void)))
break
}
}
),
ErrorObserver.handle,
Runtime.runSync(runtime),
)

View File

@@ -0,0 +1,46 @@
import { type Cause, Context, Effect, Layer, Option, Pipeable, Predicate, PubSub, type Queue, type Scope } from "effect"
export const TypeId: unique symbol = Symbol.for("@effect-fc/ErrorObserver/ErrorObserver")
export type TypeId = typeof TypeId
export interface ErrorObserver<in out E = never> extends Pipeable.Pipeable {
readonly [TypeId]: TypeId
handle<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
}
export const ErrorObserver = <E = never>(): Context.Tag<ErrorObserver, ErrorObserver<E>> => Context.GenericTag("@effect-fc/ErrorObserver/ErrorObserver")
class ErrorObserverImpl<in out E = never>
extends Pipeable.Class() implements ErrorObserver<E> {
readonly [TypeId]: TypeId = TypeId
readonly subscribe: Effect.Effect<Queue.Dequeue<Cause.Cause<E>>, never, Scope.Scope>
constructor(
private readonly pubsub: PubSub.PubSub<Cause.Cause<E>>
) {
super()
this.subscribe = pubsub.subscribe
}
handle<A, EffE, R>(effect: Effect.Effect<A, EffE, R>): Effect.Effect<A, EffE, R> {
return Effect.tapErrorCause(effect, cause => PubSub.publish(this.pubsub, cause as Cause.Cause<E>))
}
}
export const isErrorObserver = (u: unknown): u is ErrorObserver<unknown> => Predicate.hasProperty(u, TypeId)
export const layer: Layer.Layer<ErrorObserver> = Layer.effect(ErrorObserver(), Effect.andThen(
PubSub.unbounded<Cause.Cause<never>>(),
pubsub => new ErrorObserverImpl(pubsub),
))
export const handle = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => Effect.andThen(
Effect.serviceOption(ErrorObserver()),
Option.match({
onSome: observer => observer.handle(effect),
onNone: () => effect,
}),
)

View File

@@ -1,7 +1,8 @@
/** biome-ignore-all lint/complexity/useArrowFunction: necessary for class prototypes */
import { Effect, Layer, ManagedRuntime, Predicate, type Runtime } from "effect"
import { Effect, Layer, ManagedRuntime, Predicate, Runtime, Scope } from "effect"
import * as React from "react"
import * as Component from "./Component.js"
import * as ErrorObserver from "./ErrorObserver.js"
export const TypeId: unique symbol = Symbol.for("@effect-fc/ReactRuntime/ReactRuntime")
@@ -16,16 +17,21 @@ export interface ReactRuntime<R, ER> {
const ReactRuntimeProto = Object.freeze({ [TypeId]: TypeId } as const)
export const Prelude: Layer.Layer<Component.ScopeMap | ErrorObserver.ErrorObserver> = Layer.mergeAll(
Component.ScopeMap.Default,
ErrorObserver.layer,
)
export const isReactRuntime = (u: unknown): u is ReactRuntime<unknown, unknown> => Predicate.hasProperty(u, TypeId)
export const make = <R, ER>(
layer: Layer.Layer<R, ER>,
memoMap?: Layer.MemoMap,
): ReactRuntime<R | Component.ScopeMap, ER> => Object.setPrototypeOf(
): ReactRuntime<Layer.Layer.Success<typeof Prelude> | R, ER> => Object.setPrototypeOf(
Object.assign(function() {}, {
runtime: ManagedRuntime.make(
Layer.merge(layer, Component.ScopeMap.Default),
Layer.merge(layer, Prelude),
memoMap,
),
// biome-ignore lint/style/noNonNullAssertion: context initialization
@@ -54,16 +60,20 @@ export const Provider = <R, ER>(
)
}
interface ProviderInnerProps<R, ER> {
const ProviderInner = <R, ER>(
{ runtime, promise, children }: {
readonly runtime: ReactRuntime<R, ER>
readonly promise: Promise<Runtime.Runtime<R>>
readonly children?: React.ReactNode
}
): React.ReactNode => {
const effectRuntime = React.use(promise)
const scope = Runtime.runSync(effectRuntime)(Component.useScope([effectRuntime]))
Runtime.runSync(effectRuntime)(Effect.provideService(
Component.useOnChange(() => Effect.addFinalizer(() => runtime.runtime.disposeEffect), [scope]),
Scope.Scope,
scope,
))
const ProviderInner = <R, ER>(
{ runtime, promise, children }: ProviderInnerProps<R, ER>
): React.ReactNode => React.createElement(
runtime.context,
{ value: React.use(promise) },
children,
)
return React.createElement(runtime.context, { value: effectRuntime }, children)
}

View File

@@ -1,5 +1,6 @@
export * as Async from "./Async.js"
export * as Component from "./Component.js"
export * as ErrorObserver from "./ErrorObserver.js"
export * as Form from "./Form.js"
export * as Memoized from "./Memoized.js"
export * as PropertyPath from "./PropertyPath.js"

View File

@@ -26,8 +26,8 @@
"vite": "^7.1.8"
},
"dependencies": {
"@effect/platform": "^0.92.1",
"@effect/platform-browser": "^0.72.0",
"@effect/platform": "^0.93.0",
"@effect/platform-browser": "^0.73.0",
"@radix-ui/themes": "^3.2.1",
"@typed/id": "^0.17.2",
"effect": "^3.18.1",