Compare commits
83 Commits
16a7dec3fd
...
reffuse-ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a78232ec7 | ||
|
|
19194d6677 | ||
|
|
40871b793d | ||
|
|
f079b90f28 | ||
|
|
28b6e9276e | ||
|
|
8025ec4a22 | ||
|
|
02ee2c10cc | ||
|
|
bb1a71f63b | ||
|
|
a9448f55cf | ||
|
|
c0f3073d20 | ||
|
|
8cfe186574 | ||
|
|
625cecda27 | ||
|
|
7cc0a68170 | ||
|
|
8be1295e2f | ||
|
|
a781be8f24 | ||
|
|
4913f5cc35 | ||
|
|
2a37f843ca | ||
|
|
78a3735038 | ||
|
|
37d9400ada | ||
|
|
2ef47bed70 | ||
|
|
2b78d4dc49 | ||
|
|
6fa73ee33f | ||
|
|
3ea4c81872 | ||
|
|
782629d5b3 | ||
|
|
8b2abbbd19 | ||
|
|
152657d97b | ||
|
|
faf1d4963c | ||
|
|
9ba36ebc04 | ||
|
|
f327728b3a | ||
|
|
8920674b26 | ||
|
|
b440503e50 | ||
|
|
4088d86652 | ||
|
|
79cf1e5eb7 | ||
|
|
8007c2693a | ||
|
|
1769c4074d | ||
|
|
8c5613aa62 | ||
|
|
7d220cb61a | ||
|
|
d81a9fcd91 | ||
|
|
45ce747ff0 | ||
|
|
e089bf9fee | ||
|
|
c2a1a7b212 | ||
|
|
4dc336fbf4 | ||
|
|
1fe2fec325 | ||
|
|
d8b40088cb | ||
|
|
38bf3f99ea | ||
|
|
30b72b5b52 | ||
|
|
3a8a1ed0c3 | ||
|
|
7013bed037 | ||
|
|
0b7a2dbe92 | ||
|
|
0d3e09354e | ||
|
|
dc46d03aab | ||
|
|
37ffc161d3 | ||
|
|
e8a267f4cb | ||
|
|
6dc0a548cd | ||
|
|
53c06e3dae | ||
|
|
82d154ac54 | ||
|
|
ed788af128 | ||
|
|
f4e380ddcb | ||
|
|
e58bd7ab5a | ||
|
|
21d011dd12 | ||
|
|
5b64d0d783 | ||
|
|
2a29f19ece | ||
|
|
919dad97ef | ||
|
|
2fb1a2b897 | ||
|
|
c9263c3d8a | ||
|
|
8444061de3 | ||
|
|
2f870e56cd | ||
|
|
f95c2596a3 | ||
|
|
5d85449fef | ||
|
|
3548ed5718 | ||
|
|
82f0f67ee6 | ||
|
|
8d52443b55 | ||
|
|
27f50db664 | ||
|
|
165b7bbeee | ||
|
|
58b2da373e | ||
|
|
f25fefb0f3 | ||
|
|
6edac19fa6 | ||
|
|
1c99d5c161 | ||
|
|
5044887d90 | ||
|
|
810e4bb9fd | ||
|
|
619dbe32ae | ||
|
|
28efea18f1 | ||
|
|
7e9a8a5fee |
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
20
bun.lock
20
bun.lock
@@ -10,6 +10,18 @@
|
|||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/effect-components": {
|
||||||
|
"name": "effect-components",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@effect/language-service": "^0.23.3",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"effect": "^3.15.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/example": {
|
"packages/example": {
|
||||||
"name": "@reffuse/example",
|
"name": "@reffuse/example",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -62,7 +74,7 @@
|
|||||||
},
|
},
|
||||||
"packages/extension-query": {
|
"packages/extension-query": {
|
||||||
"name": "@reffuse/extension-query",
|
"name": "@reffuse/extension-query",
|
||||||
"version": "0.1.3",
|
"version": "0.1.5",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"reffuse": "workspace:*",
|
"reffuse": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -78,7 +90,7 @@
|
|||||||
},
|
},
|
||||||
"packages/reffuse": {
|
"packages/reffuse": {
|
||||||
"name": "reffuse",
|
"name": "reffuse",
|
||||||
"version": "0.1.11",
|
"version": "0.1.13",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"effect": "^3.15.0",
|
"effect": "^3.15.0",
|
||||||
@@ -129,6 +141,8 @@
|
|||||||
|
|
||||||
"@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
"@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
||||||
|
|
||||||
|
"@effect/language-service": ["@effect/language-service@0.23.3", "", {}, "sha512-yurF+FHd1HwM/3Mh7kQCea+z4wvbs2/NLPyk6FiEWA2sppRKv4Kp4luwfUqWqyd9/uyScWJcHX7WK1caLpx4Pw=="],
|
||||||
|
|
||||||
"@effect/platform": ["@effect/platform@0.82.1", "", { "dependencies": { "find-my-way-ts": "^0.1.5", "msgpackr": "^1.11.2", "multipasta": "^0.2.5" }, "peerDependencies": { "effect": "^3.15.1" } }, "sha512-fX5Lu//VkLXPegouxT1AdSyuRkxF55k70YaLV0vIzjgK97/u3Mow0ux8fYglm2dWDXWTLBkNprlhheGm/5/bvQ=="],
|
"@effect/platform": ["@effect/platform@0.82.1", "", { "dependencies": { "find-my-way-ts": "^0.1.5", "msgpackr": "^1.11.2", "multipasta": "^0.2.5" }, "peerDependencies": { "effect": "^3.15.1" } }, "sha512-fX5Lu//VkLXPegouxT1AdSyuRkxF55k70YaLV0vIzjgK97/u3Mow0ux8fYglm2dWDXWTLBkNprlhheGm/5/bvQ=="],
|
||||||
|
|
||||||
"@effect/platform-browser": ["@effect/platform-browser@0.62.1", "", { "dependencies": { "multipasta": "^0.2.5" }, "peerDependencies": { "@effect/platform": "^0.82.1", "effect": "^3.15.1" } }, "sha512-+aioMY5OsD9SQc7S88yv6tlWpkKhbA5Dv3lDs4CXQbRL5TWuHjzzDGpFNRhCBdv5ouAjoBAzu2Zi4+HIaWYqHQ=="],
|
"@effect/platform-browser": ["@effect/platform-browser@0.62.1", "", { "dependencies": { "multipasta": "^0.2.5" }, "peerDependencies": { "@effect/platform": "^0.82.1", "effect": "^3.15.1" } }, "sha512-+aioMY5OsD9SQc7S88yv6tlWpkKhbA5Dv3lDs4CXQbRL5TWuHjzzDGpFNRhCBdv5ouAjoBAzu2Zi4+HIaWYqHQ=="],
|
||||||
@@ -573,6 +587,8 @@
|
|||||||
|
|
||||||
"effect": ["effect@3.15.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-n3bDF6K3R+FSVuH+dSVU3ya2pI4Wt/tnKzum3DC/3b5e0E9HfhrhbkonOkYU3AVJJOzCA6zZE2/y6EUgQNAY4g=="],
|
"effect": ["effect@3.15.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-n3bDF6K3R+FSVuH+dSVU3ya2pI4Wt/tnKzum3DC/3b5e0E9HfhrhbkonOkYU3AVJJOzCA6zZE2/y6EUgQNAY4g=="],
|
||||||
|
|
||||||
|
"effect-components": ["effect-components@workspace:packages/effect-components"],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.152", "", {}, "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.152", "", {}, "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg=="],
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|||||||
11
packages/effect-components/README.md
Normal file
11
packages/effect-components/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Reffuse
|
||||||
|
|
||||||
|
[Effect-TS](https://effect.website/) integration for React 19+ with the aim of integrating the Effect context system within React's component hierarchy, while avoiding touching React's internals.
|
||||||
|
|
||||||
|
This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome.
|
||||||
|
|
||||||
|
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
||||||
|
|
||||||
|
## Peer dependencies
|
||||||
|
- `effect` 3.13+
|
||||||
|
- `react` & `@types/react` 19+
|
||||||
44
packages/effect-components/package.json
Normal file
44
packages/effect-components/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "effect-components",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"./README.md",
|
||||||
|
"./dist"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"url": "git+https://github.com/Thiladev/reffuse.git"
|
||||||
|
},
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./types": {
|
||||||
|
"types": "./dist/types/index.d.ts",
|
||||||
|
"default": "./dist/types/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./dist/*.d.ts",
|
||||||
|
"default": "./dist/*.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"lint:tsc": "tsc --noEmit",
|
||||||
|
"pack": "npm pack",
|
||||||
|
"clean:cache": "rm -f tsconfig.tsbuildinfo",
|
||||||
|
"clean:dist": "rm -rf dist",
|
||||||
|
"clean:node": "rm -rf node_modules"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"effect": "^3.15.0",
|
||||||
|
"react": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@effect/language-service": "^0.23.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
72
packages/effect-components/src/ReactComponent.ts
Normal file
72
packages/effect-components/src/ReactComponent.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Context, Effect, Function, Runtime, Scope, Tracer } from "effect"
|
||||||
|
import type { Mutable } from "effect/Types"
|
||||||
|
import * as React from "react"
|
||||||
|
import * as ReactHook from "./ReactHook.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface ReactComponent<E, R, P> {
|
||||||
|
(props: P): Effect.Effect<React.ReactNode, E, R>
|
||||||
|
readonly displayName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nonReactiveTags = [Tracer.ParentSpan] as const
|
||||||
|
|
||||||
|
export const withDisplayName: {
|
||||||
|
<C extends ReactComponent<any, any, any>>(displayName: string): (self: C) => C
|
||||||
|
<C extends ReactComponent<any, any, any>>(self: C, displayName: string): C
|
||||||
|
} = Function.dual(2, <C extends ReactComponent<any, any, any>>(
|
||||||
|
self: C,
|
||||||
|
displayName: string,
|
||||||
|
): C => {
|
||||||
|
(self as Mutable<C>).displayName = displayName
|
||||||
|
return self
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useFC: {
|
||||||
|
<E, R, P extends {} = {}>(
|
||||||
|
self: ReactComponent<E, R, P>,
|
||||||
|
options?: ReactHook.ScopeOptions,
|
||||||
|
): Effect.Effect<React.FC<P>, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R, P extends {}>(
|
||||||
|
self: ReactComponent<E, R, P>,
|
||||||
|
options?: ReactHook.ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
return React.useMemo(() => function ScopeProvider(props: P) {
|
||||||
|
const scope = Runtime.runSync(runtime)(ReactHook.useScope(options))
|
||||||
|
|
||||||
|
const FC = React.useMemo(() => {
|
||||||
|
const f = (props: P) => Runtime.runSync(runtime)(
|
||||||
|
Effect.provideService(self(props), Scope.Scope, scope)
|
||||||
|
)
|
||||||
|
if (self.displayName) f.displayName = self.displayName
|
||||||
|
return f
|
||||||
|
}, [scope])
|
||||||
|
|
||||||
|
return React.createElement(FC, props)
|
||||||
|
}, Array.from(
|
||||||
|
Context.omit(...nonReactiveTags)(runtime.context).unsafeMap.values()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const use: {
|
||||||
|
<E, R, P extends {} = {}>(
|
||||||
|
self: ReactComponent<E, R, P>,
|
||||||
|
fn: (Component: React.FC<P>) => React.ReactNode,
|
||||||
|
options?: ReactHook.ScopeOptions,
|
||||||
|
): Effect.Effect<React.ReactNode, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function*(self, fn, options) {
|
||||||
|
return fn(yield* useFC(self, options))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const withRuntime: {
|
||||||
|
<E, R, P extends {} = {}>(context: React.Context<Runtime.Runtime<R>>): (self: ReactComponent<E, R, P>) => React.FC<P>
|
||||||
|
<E, R, P extends {} = {}>(self: ReactComponent<E, R, P>, context: React.Context<Runtime.Runtime<R>>): React.FC<P>
|
||||||
|
} = Function.dual(2, <E, R, P extends {}>(
|
||||||
|
self: ReactComponent<E, R, P>,
|
||||||
|
context: React.Context<Runtime.Runtime<R>>,
|
||||||
|
): React.FC<P> => function WithRuntime(props) {
|
||||||
|
const runtime = React.useContext(context)
|
||||||
|
return React.createElement(Runtime.runSync(runtime)(useFC(self)), props)
|
||||||
|
})
|
||||||
317
packages/effect-components/src/ReactHook.ts
Normal file
317
packages/effect-components/src/ReactHook.ts
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { type Context, Effect, ExecutionStrategy, Exit, type Layer, Option, pipe, PubSub, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
import { SetStateAction } from "./types/index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface ScopeOptions {
|
||||||
|
readonly finalizerExecutionStrategy?: ExecutionStrategy.ExecutionStrategy
|
||||||
|
readonly finalizerExecutionMode?: "sync" | "fork"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useScope: {
|
||||||
|
(options?: ScopeOptions): Effect.Effect<Scope.Scope>
|
||||||
|
} = Effect.fnUntraced(function* (options?: ScopeOptions) {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
|
||||||
|
const [isInitialRun, initialScope] = React.useMemo(() => Runtime.runSync(runtime)(
|
||||||
|
Effect.all([Ref.make(true), makeScope(options)])
|
||||||
|
), [])
|
||||||
|
const [scope, setScope] = React.useState(initialScope)
|
||||||
|
|
||||||
|
React.useEffect(() => Runtime.runSync(runtime)(
|
||||||
|
Effect.if(isInitialRun, {
|
||||||
|
onTrue: () => Effect.as(
|
||||||
|
Ref.set(isInitialRun, false),
|
||||||
|
() => closeScope(scope, runtime, options),
|
||||||
|
),
|
||||||
|
|
||||||
|
onFalse: () => makeScope(options).pipe(
|
||||||
|
Effect.tap(scope => Effect.sync(() => setScope(scope))),
|
||||||
|
Effect.map(scope => () => closeScope(scope, runtime, options)),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
), [])
|
||||||
|
|
||||||
|
return scope
|
||||||
|
})
|
||||||
|
|
||||||
|
const makeScope = (options?: ScopeOptions) => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
const closeScope = (
|
||||||
|
scope: Scope.CloseableScope,
|
||||||
|
runtime: Runtime.Runtime<never>,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useMemo: {
|
||||||
|
<A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<A, E, R>
|
||||||
|
} = Effect.fnUntraced(function* <A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
return yield* React.useMemo(() => Runtime.runSync(runtime)(Effect.cached(factory())), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useOnce: {
|
||||||
|
<A, E, R>(factory: () => Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
|
||||||
|
} = Effect.fnUntraced(function* <A, E, R>(
|
||||||
|
factory: () => Effect.Effect<A, E, R>
|
||||||
|
) {
|
||||||
|
return yield* useMemo(factory, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useMemoLayer: {
|
||||||
|
<ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>
|
||||||
|
): Effect.Effect<Context.Context<ROut>, E, RIn>
|
||||||
|
} = Effect.fnUntraced(function* <ROut, E, RIn>(
|
||||||
|
layer: Layer.Layer<ROut, E, RIn>
|
||||||
|
) {
|
||||||
|
return yield* useMemo(() => Effect.provide(Effect.context<ROut>(), layer), [layer])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useCallbackSync: {
|
||||||
|
<Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<(...args: Args) => A, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<R>()
|
||||||
|
return React.useCallback((...args: Args) => Runtime.runSync(runtime)(callback(...args)), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useCallbackPromise: {
|
||||||
|
<Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
): Effect.Effect<(...args: Args) => Promise<A>, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <Args extends unknown[], A, E, R>(
|
||||||
|
callback: (...args: Args) => Effect.Effect<A, E, R>,
|
||||||
|
deps: React.DependencyList,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<R>()
|
||||||
|
return React.useCallback((...args: Args) => Runtime.runPromise(runtime)(callback(...args)), deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useEffect: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const { scope, exit } = Effect.Do.pipe(
|
||||||
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||||
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, exit))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, exit))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useLayoutEffect: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const { scope, exit } = Effect.Do.pipe(
|
||||||
|
Effect.bind("scope", () => Scope.make(options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)),
|
||||||
|
Effect.bind("exit", ({ scope }) => Effect.exit(Effect.provideService(effect(), Scope.Scope, scope))),
|
||||||
|
Runtime.runSync(runtime),
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "sync") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, exit))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, exit))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useFork: {
|
||||||
|
<E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||||
|
): Effect.Effect<void, never, Exclude<R, Scope.Scope>>
|
||||||
|
} = Effect.fnUntraced(function* <E, R>(
|
||||||
|
effect: () => Effect.Effect<void, E, R>,
|
||||||
|
deps?: React.DependencyList,
|
||||||
|
options?: Runtime.RunForkOptions & ScopeOptions,
|
||||||
|
) {
|
||||||
|
const runtime = yield* Effect.runtime<Exclude<R, Scope.Scope>>()
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const scope = Runtime.runSync(runtime)(options?.scope
|
||||||
|
? Scope.fork(options.scope, options?.finalizerExecutionStrategy ?? ExecutionStrategy.sequential)
|
||||||
|
: Scope.make(options?.finalizerExecutionStrategy)
|
||||||
|
)
|
||||||
|
Runtime.runFork(runtime)(Effect.provideService(effect(), Scope.Scope, scope), { ...options, scope })
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
switch (options?.finalizerExecutionMode ?? "fork") {
|
||||||
|
case "sync":
|
||||||
|
Runtime.runSync(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
case "fork":
|
||||||
|
Runtime.runFork(runtime)(Scope.close(scope, Exit.void))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, deps)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useRefFromReactiveValue: {
|
||||||
|
<A>(value: A): Effect.Effect<SubscriptionRef.SubscriptionRef<A>>
|
||||||
|
} = Effect.fnUntraced(function*(value) {
|
||||||
|
const ref = yield* useOnce(() => SubscriptionRef.make(value))
|
||||||
|
yield* useEffect(() => Ref.set(ref, value), [value])
|
||||||
|
return ref
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useSubscribeRefs: {
|
||||||
|
<const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||||
|
...refs: Refs
|
||||||
|
): Effect.Effect<{ [K in keyof Refs]: Effect.Effect.Success<Refs[K]> }>
|
||||||
|
} = Effect.fnUntraced(function* <const Refs extends readonly SubscriptionRef.SubscriptionRef<any>[]>(
|
||||||
|
...refs: Refs
|
||||||
|
) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() =>
|
||||||
|
Effect.all(refs as readonly SubscriptionRef.SubscriptionRef<any>[])
|
||||||
|
))
|
||||||
|
|
||||||
|
yield* useFork(() => pipe(
|
||||||
|
refs.map(ref => Stream.changesWith(ref.changes, (x, y) => x === y)),
|
||||||
|
streams => Stream.zipLatestAll(...streams),
|
||||||
|
Stream.runForEach(v =>
|
||||||
|
Effect.sync(() => setReactStateValue(v))
|
||||||
|
),
|
||||||
|
), refs)
|
||||||
|
|
||||||
|
return reactStateValue as any
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useRefState: {
|
||||||
|
<A>(
|
||||||
|
ref: SubscriptionRef.SubscriptionRef<A>
|
||||||
|
): Effect.Effect<readonly [A, React.Dispatch<React.SetStateAction<A>>]>
|
||||||
|
} = Effect.fnUntraced(function* <A>(ref: SubscriptionRef.SubscriptionRef<A>) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(yield* useOnce(() => ref))
|
||||||
|
|
||||||
|
yield* useFork(() => Stream.runForEach(
|
||||||
|
Stream.changesWith(ref.changes, (x, y) => x === y),
|
||||||
|
v => Effect.sync(() => setReactStateValue(v)),
|
||||||
|
), [ref])
|
||||||
|
|
||||||
|
const setValue = yield* useCallbackSync((setStateAction: React.SetStateAction<A>) =>
|
||||||
|
Ref.update(ref, prevState =>
|
||||||
|
SetStateAction.value(setStateAction, prevState)
|
||||||
|
),
|
||||||
|
[ref])
|
||||||
|
|
||||||
|
return [reactStateValue, setValue]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const useStreamFromReactiveValues: {
|
||||||
|
<const A extends React.DependencyList>(
|
||||||
|
values: A
|
||||||
|
): Effect.Effect<Stream.Stream<A>, never, Scope.Scope>
|
||||||
|
} = Effect.fnUntraced(function* <const A extends React.DependencyList>(values: A) {
|
||||||
|
const { latest, pubsub, stream } = yield* useOnce(() => Effect.Do.pipe(
|
||||||
|
Effect.bind("latest", () => Ref.make(values)),
|
||||||
|
Effect.bind("pubsub", () => Effect.acquireRelease(PubSub.unbounded<A>(), PubSub.shutdown)),
|
||||||
|
Effect.let("stream", ({ latest, pubsub }) => latest.pipe(
|
||||||
|
Effect.flatMap(a => Effect.map(
|
||||||
|
Stream.fromPubSub(pubsub, { scoped: true }),
|
||||||
|
s => Stream.concat(Stream.make(a), s),
|
||||||
|
)),
|
||||||
|
Stream.unwrapScoped,
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
|
||||||
|
yield* useEffect(() => Ref.set(latest, values).pipe(
|
||||||
|
Effect.andThen(PubSub.publish(pubsub, values)),
|
||||||
|
Effect.unlessEffect(PubSub.isShutdown(pubsub)),
|
||||||
|
), values)
|
||||||
|
|
||||||
|
return stream
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useSubscribeStream: {
|
||||||
|
<A, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>
|
||||||
|
): Effect.Effect<Option.Option<A>, never, R>
|
||||||
|
<A extends NonNullable<unknown>, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue: A,
|
||||||
|
): Effect.Effect<Option.Some<A>, never, R>
|
||||||
|
} = Effect.fnUntraced(function* <A extends NonNullable<unknown>, E, R>(
|
||||||
|
stream: Stream.Stream<A, E, R>,
|
||||||
|
initialValue?: A,
|
||||||
|
) {
|
||||||
|
const [reactStateValue, setReactStateValue] = React.useState(
|
||||||
|
React.useMemo(() => initialValue
|
||||||
|
? Option.some(initialValue)
|
||||||
|
: Option.none(),
|
||||||
|
[])
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* useFork(() => Stream.runForEach(
|
||||||
|
Stream.changesWith(stream, (x, y) => x === y),
|
||||||
|
v => Effect.sync(() => setReactStateValue(Option.some(v))),
|
||||||
|
), [stream])
|
||||||
|
|
||||||
|
return reactStateValue as Option.Some<A>
|
||||||
|
})
|
||||||
47
packages/effect-components/src/ReactManagedRuntime.ts
Normal file
47
packages/effect-components/src/ReactManagedRuntime.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Effect, type Layer, ManagedRuntime, type Runtime } from "effect"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export interface ReactManagedRuntime<R, ER> {
|
||||||
|
readonly runtime: ManagedRuntime.ManagedRuntime<R, ER>
|
||||||
|
readonly context: React.Context<Runtime.Runtime<R>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <R, ER>(
|
||||||
|
layer: Layer.Layer<R, ER>,
|
||||||
|
memoMap?: Layer.MemoMap,
|
||||||
|
): ReactManagedRuntime<R, ER> => ({
|
||||||
|
runtime: ManagedRuntime.make(layer, memoMap),
|
||||||
|
context: React.createContext<Runtime.Runtime<R>>(null!),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export interface AsyncProviderProps<R, ER> extends React.SuspenseProps {
|
||||||
|
readonly runtime: ReactManagedRuntime<R, ER>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsyncProvider<R, ER>(
|
||||||
|
{ runtime, children, ...suspenseProps }: AsyncProviderProps<R, ER>
|
||||||
|
): React.ReactNode {
|
||||||
|
const promise = React.useMemo(() => Effect.runPromise(runtime.runtime.runtimeEffect), [runtime])
|
||||||
|
|
||||||
|
return React.createElement(
|
||||||
|
React.Suspense,
|
||||||
|
suspenseProps,
|
||||||
|
React.createElement(AsyncProviderInner<R, ER>, { runtime, promise, children }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AsyncProviderInnerProps<R, ER> {
|
||||||
|
readonly runtime: ReactManagedRuntime<R, ER>
|
||||||
|
readonly promise: Promise<Runtime.Runtime<R>>
|
||||||
|
readonly children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function AsyncProviderInner<R, ER>(
|
||||||
|
{ runtime, promise, children }: AsyncProviderInnerProps<R, ER>
|
||||||
|
): React.ReactNode {
|
||||||
|
const value = React.use(promise)
|
||||||
|
return React.createElement(runtime.context, { value }, children)
|
||||||
|
}
|
||||||
3
packages/effect-components/src/index.ts
Normal file
3
packages/effect-components/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * as ReactComponent from "./ReactComponent.js"
|
||||||
|
export * as ReactHook from "./ReactHook.js"
|
||||||
|
export * as ReactManagedRuntime from "./ReactManagedRuntime.js"
|
||||||
99
packages/effect-components/src/types/PropertyPath.ts
Normal file
99
packages/effect-components/src/types/PropertyPath.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { Array, Function, Option, Predicate } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
|
||||||
|
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
||||||
|
D extends never ? [] :
|
||||||
|
T extends Seen ? [] :
|
||||||
|
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
||||||
|
T extends object ? ObjectPaths<T, D, Seen | T> :
|
||||||
|
never
|
||||||
|
)
|
||||||
|
|
||||||
|
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
||||||
|
[K in keyof T as K extends number ? K : never]:
|
||||||
|
| [K]
|
||||||
|
| [K, ...Paths<T[K], Prev[D], Seen>]
|
||||||
|
} extends infer O
|
||||||
|
? O[keyof O]
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
||||||
|
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
||||||
|
NonNullable<T[K]> extends infer V
|
||||||
|
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
||||||
|
: never
|
||||||
|
} extends infer O
|
||||||
|
? O[keyof O]
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
||||||
|
? Head extends keyof T
|
||||||
|
? ValueFromPath<T[Head], Tail>
|
||||||
|
: T extends readonly any[]
|
||||||
|
? Head extends number
|
||||||
|
? ValueFromPath<T[number], Tail>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: T
|
||||||
|
|
||||||
|
export type AnyKey = string | number | symbol
|
||||||
|
export type AnyPath = readonly AnyKey[]
|
||||||
|
|
||||||
|
|
||||||
|
export const unsafeGet: {
|
||||||
|
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P>
|
||||||
|
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): ValueFromPath<T, P> =>
|
||||||
|
path.reduce((acc: any, key: any) => acc?.[key], self)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const get: {
|
||||||
|
<T, const P extends Paths<T>>(path: P): (self: T) => Option.Option<ValueFromPath<T, P>>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>>
|
||||||
|
} = Function.dual(2, <T, const P extends Paths<T>>(self: T, path: P): Option.Option<ValueFromPath<T, P>> =>
|
||||||
|
path.reduce(
|
||||||
|
(acc: Option.Option<any>, key: any): Option.Option<any> => Option.isSome(acc)
|
||||||
|
? Predicate.hasProperty(acc.value, key)
|
||||||
|
? Option.some(acc.value[key])
|
||||||
|
: Option.none()
|
||||||
|
: acc,
|
||||||
|
|
||||||
|
Option.some(self),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
export const immutableSet: {
|
||||||
|
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
||||||
|
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
||||||
|
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
||||||
|
const key = Array.head(path as AnyPath)
|
||||||
|
if (Option.isNone(key))
|
||||||
|
return Option.some(value as T)
|
||||||
|
if (!Predicate.hasProperty(self, key.value))
|
||||||
|
return Option.none()
|
||||||
|
|
||||||
|
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
||||||
|
if (Option.isNone(child))
|
||||||
|
return child
|
||||||
|
|
||||||
|
if (Array.isArray(self))
|
||||||
|
return typeof key.value === "number"
|
||||||
|
? Option.some([
|
||||||
|
...self.slice(0, key.value),
|
||||||
|
child.value,
|
||||||
|
...self.slice(key.value + 1),
|
||||||
|
] as T)
|
||||||
|
: Option.none()
|
||||||
|
|
||||||
|
if (typeof self === "object")
|
||||||
|
return Option.some(
|
||||||
|
Object.assign(
|
||||||
|
Object.create(Object.getPrototypeOf(self)),
|
||||||
|
{ ...self, [key.value]: child.value },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Option.none()
|
||||||
|
})
|
||||||
12
packages/effect-components/src/types/SetStateAction.ts
Normal file
12
packages/effect-components/src/types/SetStateAction.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Function } from "effect"
|
||||||
|
import type * as React from "react"
|
||||||
|
|
||||||
|
|
||||||
|
export const value: {
|
||||||
|
<S>(prevState: S): (self: React.SetStateAction<S>) => S
|
||||||
|
<S>(self: React.SetStateAction<S>, prevState: S): S
|
||||||
|
} = Function.dual(2, <S>(self: React.SetStateAction<S>, prevState: S): S =>
|
||||||
|
typeof self === "function"
|
||||||
|
? (self as (prevState: S) => S)(prevState)
|
||||||
|
: self
|
||||||
|
)
|
||||||
100
packages/effect-components/src/types/SubscriptionSubRef.ts
Normal file
100
packages/effect-components/src/types/SubscriptionSubRef.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Effect, Effectable, Option, Readable, Ref, Stream, Subscribable, SubscriptionRef, SynchronizedRef, type Types, type Unify } from "effect"
|
||||||
|
import * as PropertyPath from "./PropertyPath.js"
|
||||||
|
|
||||||
|
|
||||||
|
export const SubscriptionSubRefTypeId: unique symbol = Symbol.for("reffuse/types/SubscriptionSubRef")
|
||||||
|
export type SubscriptionSubRefTypeId = typeof SubscriptionSubRefTypeId
|
||||||
|
|
||||||
|
export interface SubscriptionSubRef<in out A, in out B> extends SubscriptionSubRef.Variance<A, B>, SubscriptionRef.SubscriptionRef<A> {
|
||||||
|
readonly parent: SubscriptionRef.SubscriptionRef<B>
|
||||||
|
|
||||||
|
readonly [Unify.typeSymbol]?: unknown
|
||||||
|
readonly [Unify.unifySymbol]?: SubscriptionSubRefUnify<this>
|
||||||
|
readonly [Unify.ignoreSymbol]?: SubscriptionSubRefUnifyIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare namespace SubscriptionSubRef {
|
||||||
|
export interface Variance<in out A, in out B> {
|
||||||
|
readonly [SubscriptionSubRefTypeId]: {
|
||||||
|
readonly _A: Types.Invariant<A>
|
||||||
|
readonly _B: Types.Invariant<B>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionSubRefUnify<A extends { [Unify.typeSymbol]?: any }> extends SubscriptionRef.SubscriptionRefUnify<A> {
|
||||||
|
SubscriptionSubRef?: () => Extract<A[Unify.typeSymbol], SubscriptionSubRef<any, any>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionSubRefUnifyIgnore extends SubscriptionRef.SubscriptionRefUnifyIgnore {
|
||||||
|
SubscriptionRef?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const refVariance = { _A: (_: any) => _ }
|
||||||
|
const synchronizedRefVariance = { _A: (_: any) => _ }
|
||||||
|
const subscriptionRefVariance = { _A: (_: any) => _ }
|
||||||
|
const subscriptionSubRefVariance = { _A: (_: any) => _, _B: (_: any) => _ }
|
||||||
|
|
||||||
|
class SubscriptionSubRefImpl<in out A, in out B> extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||||
|
readonly [Readable.TypeId]: Readable.TypeId = Readable.TypeId
|
||||||
|
readonly [Subscribable.TypeId]: Subscribable.TypeId = Subscribable.TypeId
|
||||||
|
readonly [Ref.RefTypeId] = refVariance
|
||||||
|
readonly [SynchronizedRef.SynchronizedRefTypeId] = synchronizedRefVariance
|
||||||
|
readonly [SubscriptionRef.SubscriptionRefTypeId] = subscriptionRefVariance
|
||||||
|
readonly [SubscriptionSubRefTypeId] = subscriptionSubRefVariance
|
||||||
|
|
||||||
|
readonly get: Effect.Effect<A>
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
readonly getter: (parentValue: B) => A,
|
||||||
|
readonly setter: (parentValue: B, value: A) => B,
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.get = Effect.map(Ref.get(this.parent), this.getter)
|
||||||
|
}
|
||||||
|
|
||||||
|
commit() {
|
||||||
|
return this.get
|
||||||
|
}
|
||||||
|
|
||||||
|
get changes(): Stream.Stream<A> {
|
||||||
|
return this.get.pipe(
|
||||||
|
Effect.map(a => this.parent.changes.pipe(
|
||||||
|
Stream.map(this.getter),
|
||||||
|
s => Stream.concat(Stream.make(a), s),
|
||||||
|
)),
|
||||||
|
Stream.unwrap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
modify<C>(f: (a: A) => readonly [C, A]): Effect.Effect<C> {
|
||||||
|
return this.modifyEffect(a => Effect.succeed(f(a)))
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyEffect<C, E, R>(f: (a: A) => Effect.Effect<readonly [C, A], E, R>): Effect.Effect<C, E, R> {
|
||||||
|
return Effect.Do.pipe(
|
||||||
|
Effect.bind("b", () => Ref.get(this.parent)),
|
||||||
|
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
||||||
|
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
||||||
|
Effect.map(({ ca: [c] }) => c),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const makeFromGetSet = <A, B>(
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
getter: (parentValue: B) => A,
|
||||||
|
setter: (parentValue: B, value: A) => B,
|
||||||
|
): SubscriptionSubRef<A, B> => new SubscriptionSubRefImpl(parent, getter, setter)
|
||||||
|
|
||||||
|
export const makeFromPath = <B, const P extends PropertyPath.Paths<B>>(
|
||||||
|
parent: SubscriptionRef.SubscriptionRef<B>,
|
||||||
|
path: P,
|
||||||
|
): SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> => new SubscriptionSubRefImpl(
|
||||||
|
parent,
|
||||||
|
parentValue => Option.getOrThrow(PropertyPath.get(parentValue, path)),
|
||||||
|
(parentValue, value) => Option.getOrThrow(PropertyPath.immutableSet(parentValue, path, value)),
|
||||||
|
)
|
||||||
3
packages/effect-components/src/types/index.ts
Normal file
3
packages/effect-components/src/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * as PropertyPath from "./PropertyPath.js"
|
||||||
|
export * as SetStateAction from "./SetStateAction.js"
|
||||||
|
export * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
||||||
33
packages/effect-components/tsconfig.json
Normal file
33
packages/effect-components/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Enable latest features
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
// "allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
// "allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
// "noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
// Build
|
||||||
|
"outDir": "./dist",
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"include": ["./src"]
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
import { AlertDialog, Button, Flex, Text } from "@radix-ui/themes"
|
||||||
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
import { Cause, Console, Effect, Either, flow, Match, Option, Stream } from "effect"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { AppQueryClient } from "./query"
|
|
||||||
import { R } from "./reffuse"
|
import { R } from "./reffuse"
|
||||||
|
import { AppQueryErrorHandler } from "./services"
|
||||||
|
|
||||||
|
|
||||||
export function VQueryErrorHandler() {
|
export function VQueryErrorHandler() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const error = R.useSubscribeStream(
|
const error = R.useSubscribeStream(
|
||||||
R.useMemo(() => AppQueryClient.pipe(
|
R.useMemo(() => AppQueryErrorHandler.AppQueryErrorHandler.pipe(
|
||||||
Effect.map(client => client.errorHandler.errors.pipe(
|
Effect.map(handler => handler.errors.pipe(
|
||||||
Stream.changes,
|
Stream.changes,
|
||||||
Stream.tap(Console.error),
|
Stream.tap(Console.error),
|
||||||
Stream.tap(() => Effect.sync(() => setOpen(true))),
|
Stream.tap(() => Effect.sync(() => setOpen(true))),
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { Layer } from "effect"
|
|||||||
import { StrictMode } from "react"
|
import { StrictMode } from "react"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { ReffuseRuntime } from "reffuse"
|
import { ReffuseRuntime } from "reffuse"
|
||||||
import { AppQueryClient, AppQueryErrorHandler } from "./query"
|
|
||||||
import { RootContext } from "./reffuse"
|
import { RootContext } from "./reffuse"
|
||||||
import { routeTree } from "./routeTree.gen"
|
import { routeTree } from "./routeTree.gen"
|
||||||
|
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
||||||
|
|
||||||
|
|
||||||
const layer = Layer.empty.pipe(
|
const layer = Layer.empty.pipe(
|
||||||
Layer.provideMerge(AppQueryClient.Live),
|
Layer.provideMerge(AppQueryClient.AppQueryClient.Default),
|
||||||
Layer.provideMerge(AppQueryErrorHandler.Live),
|
Layer.provideMerge(AppQueryErrorHandler.AppQueryErrorHandler.Default),
|
||||||
Layer.provideMerge(Clipboard.layer),
|
Layer.provideMerge(Clipboard.layer),
|
||||||
Layer.provideMerge(Geolocation.layer),
|
Layer.provideMerge(Geolocation.layer),
|
||||||
Layer.provideMerge(Permissions.layer),
|
Layer.provideMerge(Permissions.layer),
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { HttpClientError } from "@effect/platform"
|
|
||||||
import { QueryClient, QueryErrorHandler } from "@reffuse/extension-query"
|
|
||||||
import { Effect } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export class AppQueryErrorHandler extends QueryErrorHandler.Service<AppQueryErrorHandler,
|
|
||||||
HttpClientError.HttpClientError
|
|
||||||
>()(
|
|
||||||
"AppQueryErrorHandler",
|
|
||||||
|
|
||||||
(self, failure, defect) => self.pipe(
|
|
||||||
Effect.catchTag("RequestError", "ResponseError", failure),
|
|
||||||
Effect.catchAllDefect(defect),
|
|
||||||
),
|
|
||||||
) {}
|
|
||||||
|
|
||||||
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({ ErrorHandler: AppQueryErrorHandler }) {}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { QueryService } from "@reffuse/extension-query"
|
import { QueryRunner } from "@reffuse/extension-query"
|
||||||
import { ParseResult, Schema } from "effect"
|
import { ParseResult, Schema } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export const Result = Schema.Array(Schema.String)
|
export const Result = Schema.Array(Schema.String)
|
||||||
|
|
||||||
export class Uuid4Query extends QueryService.Tag("Uuid4Query")<Uuid4Query,
|
export class Uuid4Query extends QueryRunner.Tag("Uuid4Query")<Uuid4Query,
|
||||||
readonly ["uuid4", number],
|
readonly ["uuid4", number],
|
||||||
typeof Result.Type,
|
typeof Result.Type,
|
||||||
ParseResult.ParseError
|
ParseResult.ParseError
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function Uuid4QueryService() {
|
|||||||
const runFork = R.useRunFork()
|
const runFork = R.useRunFork()
|
||||||
|
|
||||||
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
const query = R.useMemo(() => Uuid4Query.Uuid4Query, [])
|
||||||
const [state] = R.useRefState(query.state)
|
const [state] = R.useSubscribeRefs(query.stateRef)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
|
|||||||
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
import { LazyRefExtension } from "@reffuse/extension-lazyref"
|
||||||
import { QueryExtension } from "@reffuse/extension-query"
|
import { QueryExtension } from "@reffuse/extension-query"
|
||||||
import { Reffuse, ReffuseContext } from "reffuse"
|
import { Reffuse, ReffuseContext } from "reffuse"
|
||||||
import { AppQueryClient } from "./query"
|
import { AppQueryClient, AppQueryErrorHandler } from "./services"
|
||||||
|
|
||||||
|
|
||||||
export const RootContext = ReffuseContext.make<
|
export const RootContext = ReffuseContext.make<
|
||||||
| AppQueryClient
|
| AppQueryClient.AppQueryClient
|
||||||
|
| AppQueryErrorHandler.AppQueryErrorHandler
|
||||||
| Clipboard.Clipboard
|
| Clipboard.Clipboard
|
||||||
| Geolocation.Geolocation
|
| Geolocation.Geolocation
|
||||||
| Permissions.Permissions
|
| Permissions.Permissions
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Route as TimeImport } from './routes/time'
|
|||||||
import { Route as TestsImport } from './routes/tests'
|
import { Route as TestsImport } from './routes/tests'
|
||||||
import { Route as PromiseImport } from './routes/promise'
|
import { Route as PromiseImport } from './routes/promise'
|
||||||
import { Route as LazyrefImport } from './routes/lazyref'
|
import { Route as LazyrefImport } from './routes/lazyref'
|
||||||
|
import { Route as EffectComponentTestsImport } from './routes/effect-component-tests'
|
||||||
import { Route as CountImport } from './routes/count'
|
import { Route as CountImport } from './routes/count'
|
||||||
import { Route as BlankImport } from './routes/blank'
|
import { Route as BlankImport } from './routes/blank'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
@@ -56,6 +57,12 @@ const LazyrefRoute = LazyrefImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const EffectComponentTestsRoute = EffectComponentTestsImport.update({
|
||||||
|
id: '/effect-component-tests',
|
||||||
|
path: '/effect-component-tests',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const CountRoute = CountImport.update({
|
const CountRoute = CountImport.update({
|
||||||
id: '/count',
|
id: '/count',
|
||||||
path: '/count',
|
path: '/count',
|
||||||
@@ -123,6 +130,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof CountImport
|
preLoaderRoute: typeof CountImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/effect-component-tests': {
|
||||||
|
id: '/effect-component-tests'
|
||||||
|
path: '/effect-component-tests'
|
||||||
|
fullPath: '/effect-component-tests'
|
||||||
|
preLoaderRoute: typeof EffectComponentTestsImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/lazyref': {
|
'/lazyref': {
|
||||||
id: '/lazyref'
|
id: '/lazyref'
|
||||||
path: '/lazyref'
|
path: '/lazyref'
|
||||||
@@ -195,6 +209,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
|
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
@@ -210,6 +225,7 @@ export interface FileRoutesByTo {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
|
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
@@ -226,6 +242,7 @@ export interface FileRoutesById {
|
|||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/count': typeof CountRoute
|
'/count': typeof CountRoute
|
||||||
|
'/effect-component-tests': typeof EffectComponentTestsRoute
|
||||||
'/lazyref': typeof LazyrefRoute
|
'/lazyref': typeof LazyrefRoute
|
||||||
'/promise': typeof PromiseRoute
|
'/promise': typeof PromiseRoute
|
||||||
'/tests': typeof TestsRoute
|
'/tests': typeof TestsRoute
|
||||||
@@ -243,6 +260,7 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/count'
|
||||||
|
| '/effect-component-tests'
|
||||||
| '/lazyref'
|
| '/lazyref'
|
||||||
| '/promise'
|
| '/promise'
|
||||||
| '/tests'
|
| '/tests'
|
||||||
@@ -257,6 +275,7 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/count'
|
||||||
|
| '/effect-component-tests'
|
||||||
| '/lazyref'
|
| '/lazyref'
|
||||||
| '/promise'
|
| '/promise'
|
||||||
| '/tests'
|
| '/tests'
|
||||||
@@ -271,6 +290,7 @@ export interface FileRouteTypes {
|
|||||||
| '/'
|
| '/'
|
||||||
| '/blank'
|
| '/blank'
|
||||||
| '/count'
|
| '/count'
|
||||||
|
| '/effect-component-tests'
|
||||||
| '/lazyref'
|
| '/lazyref'
|
||||||
| '/promise'
|
| '/promise'
|
||||||
| '/tests'
|
| '/tests'
|
||||||
@@ -287,6 +307,7 @@ export interface RootRouteChildren {
|
|||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BlankRoute: typeof BlankRoute
|
BlankRoute: typeof BlankRoute
|
||||||
CountRoute: typeof CountRoute
|
CountRoute: typeof CountRoute
|
||||||
|
EffectComponentTestsRoute: typeof EffectComponentTestsRoute
|
||||||
LazyrefRoute: typeof LazyrefRoute
|
LazyrefRoute: typeof LazyrefRoute
|
||||||
PromiseRoute: typeof PromiseRoute
|
PromiseRoute: typeof PromiseRoute
|
||||||
TestsRoute: typeof TestsRoute
|
TestsRoute: typeof TestsRoute
|
||||||
@@ -302,6 +323,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BlankRoute: BlankRoute,
|
BlankRoute: BlankRoute,
|
||||||
CountRoute: CountRoute,
|
CountRoute: CountRoute,
|
||||||
|
EffectComponentTestsRoute: EffectComponentTestsRoute,
|
||||||
LazyrefRoute: LazyrefRoute,
|
LazyrefRoute: LazyrefRoute,
|
||||||
PromiseRoute: PromiseRoute,
|
PromiseRoute: PromiseRoute,
|
||||||
TestsRoute: TestsRoute,
|
TestsRoute: TestsRoute,
|
||||||
@@ -326,6 +348,7 @@ export const routeTree = rootRoute
|
|||||||
"/",
|
"/",
|
||||||
"/blank",
|
"/blank",
|
||||||
"/count",
|
"/count",
|
||||||
|
"/effect-component-tests",
|
||||||
"/lazyref",
|
"/lazyref",
|
||||||
"/promise",
|
"/promise",
|
||||||
"/tests",
|
"/tests",
|
||||||
@@ -346,6 +369,9 @@ export const routeTree = rootRoute
|
|||||||
"/count": {
|
"/count": {
|
||||||
"filePath": "count.tsx"
|
"filePath": "count.tsx"
|
||||||
},
|
},
|
||||||
|
"/effect-component-tests": {
|
||||||
|
"filePath": "effect-component-tests.tsx"
|
||||||
|
},
|
||||||
"/lazyref": {
|
"/lazyref": {
|
||||||
"filePath": "lazyref.tsx"
|
"filePath": "lazyref.tsx"
|
||||||
},
|
},
|
||||||
|
|||||||
105
packages/example/src/routes/effect-component-tests.tsx
Normal file
105
packages/example/src/routes/effect-component-tests.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { Box, TextField } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Array, Console, Effect, Layer, pipe, Ref, Runtime, SubscriptionRef } from "effect"
|
||||||
|
import { ReactComponent, ReactHook, ReactManagedRuntime } from "effect-components"
|
||||||
|
|
||||||
|
|
||||||
|
const LogLive = Layer.scopedDiscard(Effect.acquireRelease(
|
||||||
|
Console.log("Runtime built."),
|
||||||
|
() => Console.log("Runtime destroyed."),
|
||||||
|
))
|
||||||
|
|
||||||
|
class TestService extends Effect.Service<TestService>()("TestService", {
|
||||||
|
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("value")),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
class SubService extends Effect.Service<SubService>()("SubService", {
|
||||||
|
effect: Effect.bind(Effect.Do, "ref", () => SubscriptionRef.make("subvalue")),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
const runtime = ReactManagedRuntime.make(Layer.empty.pipe(
|
||||||
|
Layer.provideMerge(LogLive),
|
||||||
|
Layer.provideMerge(TestService.Default),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/effect-component-tests")({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<ReactManagedRuntime.AsyncProvider runtime={runtime}>
|
||||||
|
<MyRoute />
|
||||||
|
</ReactManagedRuntime.AsyncProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MyRoute = pipe(
|
||||||
|
Effect.fn(function*() {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
|
||||||
|
const service = yield* TestService
|
||||||
|
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
||||||
|
|
||||||
|
// const MyTestComponentFC = yield* Effect.provide(
|
||||||
|
// ReactComponent.useFC(MyTestComponent),
|
||||||
|
// yield* ReactHook.useMemoLayer(SubService.Default),
|
||||||
|
// )
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Box>
|
||||||
|
<TextField.Root
|
||||||
|
value={value}
|
||||||
|
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* {yield* ReactComponent.use(MyTestComponent, C => <C />).pipe(
|
||||||
|
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default))
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{/* {Array.range(0, 3).map(k =>
|
||||||
|
<MyTestComponentFC key={k} />
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{yield* pipe(
|
||||||
|
Array.range(0, 3),
|
||||||
|
Array.map(k => ReactComponent.use(MyTestComponent, FC =>
|
||||||
|
<FC key={k} />
|
||||||
|
)),
|
||||||
|
Effect.all,
|
||||||
|
Effect.provide(yield* ReactHook.useMemoLayer(SubService.Default)),
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}),
|
||||||
|
|
||||||
|
ReactComponent.withDisplayName("MyRoute"),
|
||||||
|
ReactComponent.withRuntime(runtime.context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const MyTestComponent = pipe(
|
||||||
|
Effect.fn(function*() {
|
||||||
|
const runtime = yield* Effect.runtime()
|
||||||
|
|
||||||
|
const service = yield* SubService
|
||||||
|
const [value] = yield* ReactHook.useSubscribeRefs(service.ref)
|
||||||
|
|
||||||
|
// yield* ReactHook.useMemo(() => Effect.andThen(
|
||||||
|
// Effect.addFinalizer(() => Console.log("MyTestComponent umounted")),
|
||||||
|
// Console.log("MyTestComponent mounted"),
|
||||||
|
// ), [])
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Box>
|
||||||
|
<TextField.Root
|
||||||
|
value={value}
|
||||||
|
onChange={e => Runtime.runSync(runtime)(Ref.set(service.ref, e.target.value))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
}),
|
||||||
|
|
||||||
|
ReactComponent.withDisplayName("MyTestComponent"),
|
||||||
|
)
|
||||||
@@ -4,7 +4,7 @@ import { Uuid4QueryService } from "@/query/views/Uuid4QueryService"
|
|||||||
import { R } from "@/reffuse"
|
import { R } from "@/reffuse"
|
||||||
import { HttpClient } from "@effect/platform"
|
import { HttpClient } from "@effect/platform"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Console, Effect, Schema } from "effect"
|
import { Console, Effect, Layer, Schema } from "effect"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
|
|
||||||
|
|
||||||
@@ -17,15 +17,18 @@ function RouteComponent() {
|
|||||||
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
|
key: R.useStreamFromReactiveValues(["uuid4", 10 as number]),
|
||||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
Effect.andThen(Effect.sleep("500 millis")),
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
Effect.andThen(Effect.map(
|
||||||
|
HttpClient.HttpClient,
|
||||||
HttpClient.withTracerPropagation(false),
|
HttpClient.withTracerPropagation(false),
|
||||||
|
)),
|
||||||
|
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
Effect.flatMap(res => res.json),
|
Effect.flatMap(res => res.json),
|
||||||
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
Effect.flatMap(Schema.decodeUnknown(Uuid4Query.Result)),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const layer = useMemo(() => query.layer(Uuid4Query.Uuid4Query), [query])
|
const layer = useMemo(() => Layer.succeed(Uuid4Query.Uuid4Query, query), [query])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryContext.Provider layer={layer}>
|
<QueryContext.Provider layer={layer}>
|
||||||
|
|||||||
@@ -29,15 +29,18 @@ function RouteComponent() {
|
|||||||
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
Effect.tap(() => QueryProgress.QueryProgress.update(() =>
|
||||||
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
AsyncData.Progress.make({ loaded: 50, total: Option.some(100) })
|
||||||
)),
|
)),
|
||||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
Effect.andThen(Effect.map(
|
||||||
|
HttpClient.HttpClient,
|
||||||
HttpClient.withTracerPropagation(false),
|
HttpClient.withTracerPropagation(false),
|
||||||
|
)),
|
||||||
|
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
Effect.flatMap(res => res.json),
|
Effect.flatMap(res => res.json),
|
||||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [state] = R.useSubscribeRefs(mutation.state)
|
const [state] = R.useSubscribeRefs(mutation.stateRef)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -23,15 +23,18 @@ function RouteComponent() {
|
|||||||
key: R.useStreamFromReactiveValues(["uuid4", count]),
|
key: R.useStreamFromReactiveValues(["uuid4", count]),
|
||||||
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
query: ([, count]) => Console.log(`Querying ${ count } IDs...`).pipe(
|
||||||
Effect.andThen(Effect.sleep("500 millis")),
|
Effect.andThen(Effect.sleep("500 millis")),
|
||||||
Effect.andThen(HttpClient.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
Effect.andThen(Effect.map(
|
||||||
|
HttpClient.HttpClient,
|
||||||
HttpClient.withTracerPropagation(false),
|
HttpClient.withTracerPropagation(false),
|
||||||
|
)),
|
||||||
|
Effect.flatMap(client => client.get(`https://www.uuidtools.com/api/generate/v4/count/${ count }`)),
|
||||||
Effect.flatMap(res => res.json),
|
Effect.flatMap(res => res.json),
|
||||||
Effect.flatMap(Schema.decodeUnknown(Result)),
|
Effect.flatMap(Schema.decodeUnknown(Result)),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const [state] = R.useSubscribeRefs(query.state)
|
const [state] = R.useSubscribeRefs(query.stateRef)
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ import { Console, Effect, Option } from "effect"
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
value: string
|
||||||
|
left?: Leaf
|
||||||
|
right?: Leaf
|
||||||
|
}
|
||||||
|
interface Leaf {
|
||||||
|
node: Node
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
|
const makeUuid = Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)
|
||||||
|
|
||||||
|
|
||||||
@@ -32,6 +42,11 @@ function RouteComponent() {
|
|||||||
runSync,
|
runSync,
|
||||||
), [scopeLayer, runSync])
|
), [scopeLayer, runSync])
|
||||||
|
|
||||||
|
|
||||||
|
const nodeRef = R.useRef(() => Effect.succeed<Node>({ value: "prout" }))
|
||||||
|
const nodeValueRef = R.useSubRefFromPath(nodeRef, ["value"])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" justify="center" align="center" gap="2">
|
<Flex direction="column" justify="center" align="center" gap="2">
|
||||||
<Text>{uuid}</Text>
|
<Text>{uuid}</Text>
|
||||||
|
|||||||
7
packages/example/src/services/AppQueryClient.ts
Normal file
7
packages/example/src/services/AppQueryClient.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { QueryClient } from "@reffuse/extension-query"
|
||||||
|
import * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
||||||
|
|
||||||
|
|
||||||
|
export class AppQueryClient extends QueryClient.Service<AppQueryClient>()({
|
||||||
|
errorHandler: AppQueryErrorHandler.AppQueryErrorHandler
|
||||||
|
}) {}
|
||||||
13
packages/example/src/services/AppQueryErrorHandler.ts
Normal file
13
packages/example/src/services/AppQueryErrorHandler.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { HttpClientError } from "@effect/platform"
|
||||||
|
import { QueryErrorHandler } from "@reffuse/extension-query"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
|
||||||
|
|
||||||
|
export class AppQueryErrorHandler extends Effect.Service<AppQueryErrorHandler>()("AppQueryErrorHandler", {
|
||||||
|
effect: QueryErrorHandler.make<HttpClientError.HttpClientError>()(
|
||||||
|
(self, failure, defect) => self.pipe(
|
||||||
|
Effect.catchTag("RequestError", "ResponseError", failure),
|
||||||
|
Effect.catchAllDefect(defect),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}) {}
|
||||||
@@ -1 +1,2 @@
|
|||||||
export {}
|
export * as AppQueryClient from "./AppQueryClient"
|
||||||
|
export * as AppQueryErrorHandler from "./AppQueryErrorHandler"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@reffuse/extension-query",
|
"name": "@reffuse/extension-query",
|
||||||
"version": "0.1.3",
|
"version": "0.1.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
@@ -39,6 +39,6 @@
|
|||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"effect": "^3.15.0",
|
"effect": "^3.15.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"reffuse": "^0.1.6"
|
"reffuse": "^0.1.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import * as AsyncData from "@typed/async-data"
|
import * as AsyncData from "@typed/async-data"
|
||||||
import { type Context, Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
import { Effect, type Fiber, Queue, Ref, Stream, SubscriptionRef } from "effect"
|
||||||
import type * as QueryClient from "../QueryClient.js"
|
import type * as QueryClient from "./QueryClient.js"
|
||||||
import * as QueryProgress from "../QueryProgress.js"
|
import * as QueryProgress from "./QueryProgress.js"
|
||||||
import * as QueryState from "./QueryState.js"
|
import { QueryState } from "./internal/index.js"
|
||||||
|
|
||||||
|
|
||||||
export interface MutationRunner<K extends readonly unknown[], A, E, R> {
|
export interface MutationRunner<K extends readonly unknown[], A, E> {
|
||||||
readonly context: Context.Context<R>
|
|
||||||
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
|
||||||
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
||||||
@@ -17,6 +16,11 @@ export interface MutationRunner<K extends readonly unknown[], A, E, R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
|
Self, K extends readonly unknown[], A, E = never,
|
||||||
|
>() => Effect.Tag(id)<Self, MutationRunner<K, A, E>>()
|
||||||
|
|
||||||
|
|
||||||
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
||||||
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
||||||
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
@@ -28,7 +32,7 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
|||||||
mutation,
|
mutation,
|
||||||
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
||||||
): Effect.Effect<
|
): Effect.Effect<
|
||||||
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
|
MutationRunner<K, A | FallbackA, Exclude<E, HandledE>>,
|
||||||
never,
|
never,
|
||||||
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
||||||
> => Effect.gen(function*() {
|
> => Effect.gen(function*() {
|
||||||
@@ -37,25 +41,18 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
|||||||
|
|
||||||
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
||||||
|
|
||||||
const run = (key: K) => Effect.Do.pipe(
|
const run = (key: K) => Effect.all([QueryClient, queryStateTag]).pipe(
|
||||||
Effect.bind("state", () => queryStateTag),
|
Effect.flatMap(([client, state]) => state.set(AsyncData.loading()).pipe(
|
||||||
Effect.bind("client", () => QueryClient),
|
|
||||||
|
|
||||||
Effect.flatMap(({ state, client }) => state.set(AsyncData.loading()).pipe(
|
|
||||||
Effect.andThen(mutation(key)),
|
Effect.andThen(mutation(key)),
|
||||||
client.errorHandler.handle,
|
client.errorHandler.handle,
|
||||||
Effect.matchCauseEffect({
|
Effect.matchCauseEffect({
|
||||||
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
|
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
|
||||||
Effect.tap(state.set)
|
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
|
||||||
),
|
|
||||||
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
|
|
||||||
Effect.tap(state.set)
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
Effect.provide(context),
|
Effect.provide(context),
|
||||||
Effect.provide(QueryProgress.QueryProgress.Live),
|
Effect.provide(QueryProgress.QueryProgress.Default),
|
||||||
)
|
)
|
||||||
|
|
||||||
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
|
const mutate = (...key: K) => Effect.provide(run(key), QueryState.layer(
|
||||||
@@ -64,11 +61,11 @@ export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>
|
|||||||
value => Ref.set(globalStateRef, value),
|
value => Ref.set(globalStateRef, value),
|
||||||
))
|
))
|
||||||
|
|
||||||
const forkMutate = (...key: K) => Effect.Do.pipe(
|
const forkMutate = (...key: K) => Effect.all([
|
||||||
Effect.bind("stateRef", () => Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())),
|
Ref.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>()),
|
||||||
Effect.bind("stateQueue", () => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>()),
|
Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>(),
|
||||||
|
]).pipe(
|
||||||
Effect.flatMap(({ stateRef, stateQueue }) =>
|
Effect.flatMap(([stateRef, stateQueue]) =>
|
||||||
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
|
Effect.addFinalizer(() => Queue.shutdown(stateQueue)).pipe(
|
||||||
Effect.andThen(run(key)),
|
Effect.andThen(run(key)),
|
||||||
Effect.scoped,
|
Effect.scoped,
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type * as AsyncData from "@typed/async-data"
|
|
||||||
import { Effect, type Fiber, type Stream, type SubscriptionRef } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export interface MutationService<K extends readonly unknown[], A, E> {
|
|
||||||
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
|
||||||
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
|
||||||
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Tag = <const Id extends string>(id: Id) => <
|
|
||||||
Self, K extends readonly unknown[], A, E = never,
|
|
||||||
>() => Effect.Tag(id)<Self, MutationService<K, A, E>>()
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Context, Effect, Layer } from "effect"
|
import { Context, Effect, identity, Layer } from "effect"
|
||||||
import type { Mutable } from "effect/Types"
|
import type { Mutable } from "effect/Types"
|
||||||
import * as QueryErrorHandler from "./QueryErrorHandler.js"
|
import * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||||
|
|
||||||
@@ -8,6 +8,17 @@ export interface QueryClient<FallbackA, HandledE> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface MakeProps<FallbackA, HandledE> {
|
||||||
|
readonly errorHandler: QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <FallbackA, HandledE>(
|
||||||
|
{ errorHandler }: MakeProps<FallbackA, HandledE>
|
||||||
|
): Effect.Effect<QueryClient<FallbackA, HandledE>> => Effect.Do.pipe(
|
||||||
|
Effect.let("errorHandler", () => errorHandler)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
const id = "@reffuse/extension-query/QueryClient"
|
const id = "@reffuse/extension-query/QueryClient"
|
||||||
|
|
||||||
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
|
export type TagClassShape<FallbackA, HandledE> = Context.TagClassShape<typeof id, QueryClient<FallbackA, HandledE>>
|
||||||
@@ -19,46 +30,28 @@ export type GenericTagClass<FallbackA, HandledE> = Context.TagClass<
|
|||||||
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
|
export const makeGenericTagClass = <FallbackA = never, HandledE = never>(): GenericTagClass<FallbackA, HandledE> => Context.Tag(id)()
|
||||||
|
|
||||||
|
|
||||||
export interface ServiceProps<EH, FallbackA, HandledE> {
|
export interface ServiceProps<FallbackA = never, HandledE = never, E = never, R = never> {
|
||||||
readonly ErrorHandler?: Context.Tag<EH, QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>>
|
readonly errorHandler?: Effect.Effect<QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>, E, R>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceResult<Self, EH, FallbackA, HandledE> extends Context.TagClass<
|
export interface ServiceResult<Self, FallbackA, HandledE, E, R> extends Context.TagClass<
|
||||||
Self,
|
Self,
|
||||||
typeof id,
|
typeof id,
|
||||||
QueryClient<FallbackA, HandledE>
|
QueryClient<FallbackA, HandledE>
|
||||||
> {
|
> {
|
||||||
readonly Live: Layer.Layer<
|
readonly Default: Layer.Layer<Self, E, R>
|
||||||
Self | (EH extends QueryErrorHandler.DefaultQueryErrorHandler ? EH : never),
|
|
||||||
never,
|
|
||||||
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Service = <Self>() => (
|
export const Service = <Self>() => (
|
||||||
<
|
<FallbackA = never, HandledE = never, E = never, R = never>(
|
||||||
EH = QueryErrorHandler.DefaultQueryErrorHandler,
|
props?: ServiceProps<FallbackA, HandledE, E, R>
|
||||||
FallbackA = QueryErrorHandler.Fallback<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>,
|
): ServiceResult<Self, FallbackA, HandledE, E, R> => {
|
||||||
HandledE = QueryErrorHandler.Error<Context.Tag.Service<QueryErrorHandler.DefaultQueryErrorHandler>>,
|
const TagClass = Context.Tag(id)() as ServiceResult<Self, FallbackA, HandledE, E, R>
|
||||||
>(
|
|
||||||
props?: ServiceProps<EH, FallbackA, HandledE>
|
|
||||||
): ServiceResult<Self, EH, FallbackA, HandledE> => {
|
|
||||||
const TagClass = Context.Tag(id)() as ServiceResult<Self, EH, FallbackA, HandledE>
|
|
||||||
|
|
||||||
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.Do.pipe(
|
(TagClass as Mutable<typeof TagClass>).Default = Layer.effect(TagClass, Effect.flatMap(
|
||||||
Effect.bind("errorHandler", () =>
|
props?.errorHandler ?? QueryErrorHandler.make<never>()(identity),
|
||||||
(props?.ErrorHandler ?? QueryErrorHandler.DefaultQueryErrorHandler) as Effect.Effect<
|
errorHandler => make({ errorHandler }),
|
||||||
QueryErrorHandler.QueryErrorHandler<FallbackA, HandledE>,
|
))
|
||||||
never,
|
|
||||||
EH extends QueryErrorHandler.DefaultQueryErrorHandler ? never : EH
|
|
||||||
>
|
|
||||||
)
|
|
||||||
)).pipe(
|
|
||||||
Layer.provideMerge((props?.ErrorHandler
|
|
||||||
? Layer.empty
|
|
||||||
: QueryErrorHandler.DefaultQueryErrorHandler.Live
|
|
||||||
) as Layer.Layer<EH>)
|
|
||||||
)
|
|
||||||
|
|
||||||
return TagClass
|
return TagClass
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Cause, Context, Effect, identity, Layer, PubSub, Stream } from "effect"
|
import { Cause, Effect, PubSub, Stream } from "effect"
|
||||||
import type { Mutable } from "effect/Types"
|
|
||||||
|
|
||||||
|
|
||||||
export interface QueryErrorHandler<FallbackA, HandledE> {
|
export interface QueryErrorHandler<FallbackA, HandledE> {
|
||||||
@@ -11,31 +10,14 @@ export type Fallback<T> = T extends QueryErrorHandler<infer A, any> ? A : never
|
|||||||
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
|
export type Error<T> = T extends QueryErrorHandler<any, infer E> ? E : never
|
||||||
|
|
||||||
|
|
||||||
export interface ServiceResult<
|
export const make = <HandledE = never>() => (
|
||||||
Self,
|
<FallbackA>(
|
||||||
Id extends string,
|
|
||||||
FallbackA,
|
|
||||||
HandledE,
|
|
||||||
> extends Context.TagClass<
|
|
||||||
Self,
|
|
||||||
Id,
|
|
||||||
QueryErrorHandler<FallbackA, HandledE>
|
|
||||||
> {
|
|
||||||
readonly Live: Layer.Layer<Self>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Service = <Self, HandledE = never>() => (
|
|
||||||
<const Id extends string, FallbackA>(
|
|
||||||
id: Id,
|
|
||||||
f: (
|
f: (
|
||||||
self: Effect.Effect<never, HandledE>,
|
self: Effect.Effect<never, HandledE>,
|
||||||
failure: (failure: HandledE) => Effect.Effect<never>,
|
failure: (failure: HandledE) => Effect.Effect<never>,
|
||||||
defect: (defect: unknown) => Effect.Effect<never>,
|
defect: (defect: unknown) => Effect.Effect<never>,
|
||||||
) => Effect.Effect<FallbackA>,
|
) => Effect.Effect<FallbackA>
|
||||||
): ServiceResult<Self, Id, FallbackA, HandledE> => {
|
): Effect.Effect<QueryErrorHandler<FallbackA, HandledE>> => Effect.gen(function*() {
|
||||||
const TagClass = Context.Tag(id)() as ServiceResult<Self, Id, FallbackA, HandledE>
|
|
||||||
|
|
||||||
(TagClass as Mutable<typeof TagClass>).Live = Layer.effect(TagClass, Effect.gen(function*() {
|
|
||||||
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
|
const pubsub = yield* PubSub.unbounded<Cause.Cause<HandledE>>()
|
||||||
const errors = Stream.fromPubSub(pubsub)
|
const errors = Stream.fromPubSub(pubsub)
|
||||||
|
|
||||||
@@ -43,23 +25,16 @@ export const Service = <Self, HandledE = never>() => (
|
|||||||
self: Effect.Effect<A, E, R>
|
self: Effect.Effect<A, E, R>
|
||||||
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
): Effect.Effect<A | FallbackA, Exclude<E, HandledE>, R> => f(
|
||||||
self as unknown as Effect.Effect<never, HandledE, never>,
|
self as unknown as Effect.Effect<never, HandledE, never>,
|
||||||
(failure: HandledE) => PubSub.publish(pubsub, Cause.fail(failure)).pipe(
|
(failure: HandledE) => Effect.andThen(
|
||||||
Effect.andThen(Effect.failCause(Cause.empty))
|
PubSub.publish(pubsub, Cause.fail(failure)),
|
||||||
|
Effect.failCause(Cause.empty),
|
||||||
),
|
),
|
||||||
(defect: unknown) => PubSub.publish(pubsub, Cause.die(defect)).pipe(
|
(defect: unknown) => Effect.andThen(
|
||||||
Effect.andThen(Effect.failCause(Cause.empty))
|
PubSub.publish(pubsub, Cause.die(defect)),
|
||||||
|
Effect.failCause(Cause.empty),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return { errors, handle }
|
return { errors, handle }
|
||||||
}))
|
})
|
||||||
|
|
||||||
return TagClass
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
export class DefaultQueryErrorHandler extends Service<DefaultQueryErrorHandler>()(
|
|
||||||
"@reffuse/extension-query/DefaultQueryErrorHandler",
|
|
||||||
identity,
|
|
||||||
) {}
|
|
||||||
|
|||||||
@@ -1,53 +1,21 @@
|
|||||||
import type * as AsyncData from "@typed/async-data"
|
import type { Effect, Stream } from "effect"
|
||||||
import { type Cause, type Context, Effect, type Fiber, Layer, type Option, type Stream, type SubscriptionRef } from "effect"
|
|
||||||
import * as React from "react"
|
|
||||||
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
import { ReffuseExtension, type ReffuseNamespace } from "reffuse"
|
||||||
import type * as MutationService from "./MutationService.js"
|
import * as MutationRunner from "./MutationRunner.js"
|
||||||
import * as QueryClient from "./QueryClient.js"
|
import * as QueryClient from "./QueryClient.js"
|
||||||
import type * as QueryProgress from "./QueryProgress.js"
|
import type * as QueryProgress from "./QueryProgress.js"
|
||||||
import type * as QueryService from "./QueryService.js"
|
import * as QueryRunner from "./QueryRunner.js"
|
||||||
import { MutationRunner, QueryRunner } from "./internal/index.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
export interface UseQueryProps<K extends readonly unknown[], A, E, R> {
|
||||||
readonly key: Stream.Stream<K>
|
readonly key: Stream.Stream<K>
|
||||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
readonly refreshOnWindowFocus?: boolean
|
readonly options?: QueryRunner.RunOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseQueryResult<K extends readonly unknown[], A, E> {
|
|
||||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
|
||||||
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
|
||||||
|
|
||||||
readonly forkRefresh: Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
|
|
||||||
readonly layer: <Self, Id extends string>(
|
|
||||||
tag: Context.TagClass<Self, Id, QueryService.QueryService<K, A, E>>
|
|
||||||
) => Layer.Layer<Self>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
|
export interface UseMutationProps<K extends readonly unknown[], A, E, R> {
|
||||||
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
readonly mutation: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseMutationResult<K extends readonly unknown[], A, E> {
|
|
||||||
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
|
||||||
|
|
||||||
readonly mutate: (...key: K) => Effect.Effect<AsyncData.Success<A> | AsyncData.Failure<E>>
|
|
||||||
readonly forkMutate: (...key: K) => Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
|
|
||||||
readonly layer: <Self, Id extends string>(
|
|
||||||
tag: Context.TagClass<Self, Id, MutationService.MutationService<K, A, E>>
|
|
||||||
) => Layer.Layer<Self>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const QueryExtension = ReffuseExtension.make(() => ({
|
export const QueryExtension = ReffuseExtension.make(() => ({
|
||||||
useQuery<
|
useQuery<
|
||||||
@@ -61,32 +29,16 @@ export const QueryExtension = ReffuseExtension.make(() => ({
|
|||||||
>(
|
>(
|
||||||
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
||||||
props: UseQueryProps<QK, QA, QE, QR>,
|
props: UseQueryProps<QK, QA, QE, QR>,
|
||||||
): UseQueryResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
): QueryRunner.QueryRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||||
const runner = this.useMemo(() => QueryRunner.make({
|
const runner = this.useMemo(() => QueryRunner.make({
|
||||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||||
key: props.key,
|
key: props.key,
|
||||||
query: props.query,
|
query: props.query,
|
||||||
}), [props.key])
|
}), [props.key])
|
||||||
|
|
||||||
this.useFork(() => runner.fetchOnKeyChange, [runner])
|
this.useFork(() => QueryRunner.run(runner, props.options), [runner])
|
||||||
|
|
||||||
this.useFork(() => (props.refreshOnWindowFocus ?? true)
|
return runner
|
||||||
? runner.refreshOnWindowFocus
|
|
||||||
: Effect.void,
|
|
||||||
[props.refreshOnWindowFocus, runner])
|
|
||||||
|
|
||||||
return React.useMemo(() => ({
|
|
||||||
latestKey: runner.latestKeyRef,
|
|
||||||
state: runner.stateRef,
|
|
||||||
|
|
||||||
forkRefresh: runner.forkRefresh,
|
|
||||||
|
|
||||||
layer: tag => Layer.succeed(tag, {
|
|
||||||
latestKey: runner.latestKeyRef,
|
|
||||||
state: runner.stateRef,
|
|
||||||
forkRefresh: runner.forkRefresh,
|
|
||||||
}),
|
|
||||||
}), [runner])
|
|
||||||
},
|
},
|
||||||
|
|
||||||
useMutation<
|
useMutation<
|
||||||
@@ -100,23 +52,10 @@ export const QueryExtension = ReffuseExtension.make(() => ({
|
|||||||
>(
|
>(
|
||||||
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
this: ReffuseNamespace.ReffuseNamespace<R | QueryClient.TagClassShape<FallbackA, HandledE>>,
|
||||||
props: UseMutationProps<QK, QA, QE, QR>,
|
props: UseMutationProps<QK, QA, QE, QR>,
|
||||||
): UseMutationResult<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
): MutationRunner.MutationRunner<QK, QA | FallbackA, Exclude<QE, HandledE>> {
|
||||||
const runner = this.useMemo(() => MutationRunner.make({
|
return this.useMemo(() => MutationRunner.make({
|
||||||
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
QueryClient: QueryClient.makeGenericTagClass<FallbackA, HandledE>(),
|
||||||
mutation: props.mutation,
|
mutation: props.mutation,
|
||||||
}), [])
|
}), [])
|
||||||
|
|
||||||
return React.useMemo(() => ({
|
|
||||||
state: runner.stateRef,
|
|
||||||
|
|
||||||
mutate: runner.mutate,
|
|
||||||
forkMutate: runner.forkMutate,
|
|
||||||
|
|
||||||
layer: tag => Layer.succeed(tag, {
|
|
||||||
state: runner.stateRef,
|
|
||||||
mutate: runner.mutate,
|
|
||||||
forkMutate: runner.forkMutate,
|
|
||||||
}),
|
|
||||||
}), [runner])
|
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class QueryProgress extends Effect.Tag("@reffuse/extension-query/QueryPro
|
|||||||
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
|
f: (previous: Option.Option<AsyncData.Progress>) => AsyncData.Progress
|
||||||
) => Effect.Effect<void>
|
) => Effect.Effect<void>
|
||||||
}>() {
|
}>() {
|
||||||
static readonly Live: Layer.Layer<
|
static readonly Default: Layer.Layer<
|
||||||
QueryProgress,
|
QueryProgress,
|
||||||
never,
|
never,
|
||||||
QueryState.QueryState<any, any>
|
QueryState.QueryState<any, any>
|
||||||
|
|||||||
193
packages/extension-query/src/QueryRunner.ts
Normal file
193
packages/extension-query/src/QueryRunner.ts
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { BrowserStream } from "@effect/platform-browser"
|
||||||
|
import * as AsyncData from "@typed/async-data"
|
||||||
|
import { type Cause, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
||||||
|
import type * as QueryClient from "./QueryClient.js"
|
||||||
|
import * as QueryProgress from "./QueryProgress.js"
|
||||||
|
import { QueryState } from "./internal/index.js"
|
||||||
|
|
||||||
|
|
||||||
|
export interface QueryRunner<K extends readonly unknown[], A, E> {
|
||||||
|
readonly queryKey: Stream.Stream<K>
|
||||||
|
readonly latestKeyValueRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
||||||
|
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
||||||
|
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
|
||||||
|
AsyncData.Success<A> | AsyncData.Failure<E>,
|
||||||
|
Cause.NoSuchElementException
|
||||||
|
>>>
|
||||||
|
|
||||||
|
readonly interrupt: Effect.Effect<void>
|
||||||
|
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void>>
|
||||||
|
readonly forkFetch: (keyValue: K) => Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
readonly forkRefresh: Effect.Effect<readonly [
|
||||||
|
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
||||||
|
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
||||||
|
]>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const Tag = <const Id extends string>(id: Id) => <
|
||||||
|
Self, K extends readonly unknown[], A, E = never
|
||||||
|
>() => Effect.Tag(id)<Self, QueryRunner<K, A, E>>()
|
||||||
|
|
||||||
|
|
||||||
|
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
||||||
|
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
||||||
|
readonly key: Stream.Stream<K>
|
||||||
|
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
|
||||||
|
{
|
||||||
|
QueryClient,
|
||||||
|
key,
|
||||||
|
query,
|
||||||
|
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
||||||
|
): Effect.Effect<
|
||||||
|
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>>,
|
||||||
|
never,
|
||||||
|
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
||||||
|
> => Effect.gen(function*() {
|
||||||
|
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
|
||||||
|
|
||||||
|
const latestKeyValueRef = yield* SubscriptionRef.make(Option.none<K>())
|
||||||
|
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
|
||||||
|
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
|
||||||
|
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
|
||||||
|
Cause.NoSuchElementException
|
||||||
|
>>())
|
||||||
|
|
||||||
|
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
||||||
|
|
||||||
|
const interrupt = Effect.flatMap(fiberRef, Option.match({
|
||||||
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Fiber.interrupt(fiber))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.void,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const forkInterrupt = Effect.flatMap(fiberRef, Option.match({
|
||||||
|
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
||||||
|
Effect.andThen(Fiber.interrupt(fiber).pipe(
|
||||||
|
Effect.asVoid,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
))
|
||||||
|
),
|
||||||
|
onNone: () => Effect.forkDaemon(Effect.void),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const run = (keyValue: K) => Effect.all([QueryClient, queryStateTag]).pipe(
|
||||||
|
Effect.flatMap(([client, state]) => Ref.set(latestKeyValueRef, Option.some(keyValue)).pipe(
|
||||||
|
Effect.andThen(query(keyValue)),
|
||||||
|
client.errorHandler.handle,
|
||||||
|
Effect.matchCauseEffect({
|
||||||
|
onSuccess: v => Effect.tap(Effect.succeed(AsyncData.success(v)), state.set),
|
||||||
|
onFailure: c => Effect.tap(Effect.succeed(AsyncData.failure(c)), state.set),
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(context),
|
||||||
|
Effect.provide(QueryProgress.QueryProgress.Default),
|
||||||
|
)
|
||||||
|
|
||||||
|
const forkFetch = (keyValue: K) => Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
||||||
|
Effect.flatMap(stateQueue => queryStateTag.pipe(
|
||||||
|
Effect.flatMap(state => interrupt.pipe(
|
||||||
|
Effect.andThen(
|
||||||
|
Effect.addFinalizer(() => Effect.andThen(
|
||||||
|
Ref.set(fiberRef, Option.none()),
|
||||||
|
Queue.shutdown(stateQueue),
|
||||||
|
)).pipe(
|
||||||
|
Effect.andThen(state.set(AsyncData.loading())),
|
||||||
|
Effect.andThen(run(keyValue)),
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
||||||
|
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||||
|
)),
|
||||||
|
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
stateRef,
|
||||||
|
value => Effect.andThen(
|
||||||
|
Queue.offer(stateQueue, value),
|
||||||
|
Ref.set(stateRef, value),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
const setInitialRefreshState = Effect.flatMap(queryStateTag, state => state.update(previous => {
|
||||||
|
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
||||||
|
return AsyncData.refreshing(previous)
|
||||||
|
if (AsyncData.isRefreshing(previous))
|
||||||
|
return AsyncData.refreshing(previous.previous)
|
||||||
|
return AsyncData.loading()
|
||||||
|
}))
|
||||||
|
|
||||||
|
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
||||||
|
Effect.flatMap(stateQueue => interrupt.pipe(
|
||||||
|
Effect.andThen(
|
||||||
|
Effect.addFinalizer(() => Effect.andThen(
|
||||||
|
Ref.set(fiberRef, Option.none()),
|
||||||
|
Queue.shutdown(stateQueue),
|
||||||
|
)).pipe(
|
||||||
|
Effect.andThen(setInitialRefreshState),
|
||||||
|
Effect.andThen(latestKeyValueRef.pipe(
|
||||||
|
Effect.flatMap(identity),
|
||||||
|
Effect.flatMap(run),
|
||||||
|
)),
|
||||||
|
Effect.scoped,
|
||||||
|
Effect.forkDaemon,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
||||||
|
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
||||||
|
|
||||||
|
Effect.provide(QueryState.layer(
|
||||||
|
queryStateTag,
|
||||||
|
stateRef,
|
||||||
|
value => Effect.andThen(
|
||||||
|
Queue.offer(stateQueue, value),
|
||||||
|
Ref.set(stateRef, value),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryKey: key,
|
||||||
|
latestKeyValueRef,
|
||||||
|
stateRef,
|
||||||
|
fiberRef,
|
||||||
|
|
||||||
|
interrupt,
|
||||||
|
forkInterrupt,
|
||||||
|
forkFetch,
|
||||||
|
forkRefresh,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export interface RunOptions {
|
||||||
|
readonly refreshOnWindowFocus?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const run = <K extends readonly unknown[], A, E>(
|
||||||
|
self: QueryRunner<K, A, E>,
|
||||||
|
options?: RunOptions,
|
||||||
|
): Effect.Effect<void, never, Scope.Scope> => Effect.gen(function*() {
|
||||||
|
if (typeof window !== "undefined" && (options?.refreshOnWindowFocus ?? true))
|
||||||
|
yield* Effect.forkScoped(
|
||||||
|
Stream.runForEach(BrowserStream.fromEventListenerWindow("focus"), () => self.forkRefresh)
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Effect.addFinalizer(() => self.interrupt)
|
||||||
|
yield* Stream.runForEach(Stream.changes(self.queryKey), latestKey => self.forkFetch(latestKey))
|
||||||
|
})
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type * as AsyncData from "@typed/async-data"
|
|
||||||
import { type Cause, Effect, type Fiber, type Option, type Stream, type SubscriptionRef } from "effect"
|
|
||||||
|
|
||||||
|
|
||||||
export interface QueryService<K extends readonly unknown[], A, E> {
|
|
||||||
readonly latestKey: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
|
||||||
readonly state: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
|
||||||
readonly forkRefresh: Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Tag = <const Id extends string>(id: Id) => <
|
|
||||||
Self, K extends readonly unknown[], A, E = never,
|
|
||||||
>() => Effect.Tag(id)<Self, QueryService<K, A, E>>()
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export * as MutationService from "./MutationService.js"
|
export * as MutationRunner from "./MutationRunner.js"
|
||||||
export * as QueryClient from "./QueryClient.js"
|
export * as QueryClient from "./QueryClient.js"
|
||||||
export * as QueryErrorHandler from "./QueryErrorHandler.js"
|
export * as QueryErrorHandler from "./QueryErrorHandler.js"
|
||||||
export * from "./QueryExtension.js"
|
export * from "./QueryExtension.js"
|
||||||
export * as QueryProgress from "./QueryProgress.js"
|
export * as QueryProgress from "./QueryProgress.js"
|
||||||
export * as QueryService from "./QueryService.js"
|
export * as QueryRunner from "./QueryRunner.js"
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import { BrowserStream } from "@effect/platform-browser"
|
|
||||||
import * as AsyncData from "@typed/async-data"
|
|
||||||
import { type Cause, type Context, Effect, Fiber, identity, Option, Queue, Ref, type Scope, Stream, SubscriptionRef } from "effect"
|
|
||||||
import type * as QueryClient from "../QueryClient.js"
|
|
||||||
import * as QueryProgress from "../QueryProgress.js"
|
|
||||||
import * as QueryState from "./QueryState.js"
|
|
||||||
|
|
||||||
|
|
||||||
export interface QueryRunner<K extends readonly unknown[], A, E, R> {
|
|
||||||
readonly context: Context.Context<R>
|
|
||||||
|
|
||||||
readonly latestKeyRef: SubscriptionRef.SubscriptionRef<Option.Option<K>>
|
|
||||||
readonly stateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<A, E>>
|
|
||||||
readonly fiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.RuntimeFiber<
|
|
||||||
AsyncData.Success<A> | AsyncData.Failure<E>,
|
|
||||||
Cause.NoSuchElementException
|
|
||||||
>>>
|
|
||||||
|
|
||||||
readonly forkInterrupt: Effect.Effect<Fiber.RuntimeFiber<void, Cause.NoSuchElementException>>
|
|
||||||
readonly forkFetch: Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
readonly forkRefresh: Effect.Effect<readonly [
|
|
||||||
fiber: Fiber.RuntimeFiber<AsyncData.Success<A> | AsyncData.Failure<E>, Cause.NoSuchElementException>,
|
|
||||||
state: Stream.Stream<AsyncData.AsyncData<A, E>>,
|
|
||||||
]>
|
|
||||||
|
|
||||||
readonly fetchOnKeyChange: Effect.Effect<void, Cause.NoSuchElementException, Scope.Scope>
|
|
||||||
readonly refreshOnWindowFocus: Effect.Effect<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface MakeProps<K extends readonly unknown[], A, FallbackA, E, HandledE, R> {
|
|
||||||
readonly QueryClient: QueryClient.GenericTagClass<FallbackA, HandledE>
|
|
||||||
readonly key: Stream.Stream<K>
|
|
||||||
readonly query: (key: K) => Effect.Effect<A, E, R | QueryProgress.QueryProgress>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const make = <K extends readonly unknown[], A, FallbackA, E, HandledE, R>(
|
|
||||||
{
|
|
||||||
QueryClient,
|
|
||||||
key,
|
|
||||||
query,
|
|
||||||
}: MakeProps<K, A, FallbackA, E, HandledE, R>
|
|
||||||
): Effect.Effect<
|
|
||||||
QueryRunner<K, A | FallbackA, Exclude<E, HandledE>, R>,
|
|
||||||
never,
|
|
||||||
R | QueryClient.TagClassShape<FallbackA, HandledE>
|
|
||||||
> => Effect.gen(function*() {
|
|
||||||
const context = yield* Effect.context<R | QueryClient.TagClassShape<FallbackA, HandledE>>()
|
|
||||||
|
|
||||||
const latestKeyRef = yield* SubscriptionRef.make(Option.none<K>())
|
|
||||||
const stateRef = yield* SubscriptionRef.make(AsyncData.noData<A | FallbackA, Exclude<E, HandledE>>())
|
|
||||||
const fiberRef = yield* SubscriptionRef.make(Option.none<Fiber.RuntimeFiber<
|
|
||||||
AsyncData.Success<A | FallbackA> | AsyncData.Failure<Exclude<E, HandledE>>,
|
|
||||||
Cause.NoSuchElementException
|
|
||||||
>>())
|
|
||||||
|
|
||||||
const queryStateTag = QueryState.makeTag<A | FallbackA, Exclude<E, HandledE>>()
|
|
||||||
|
|
||||||
const interrupt = fiberRef.pipe(
|
|
||||||
Effect.flatMap(Option.match({
|
|
||||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
Effect.andThen(Fiber.interrupt(fiber))
|
|
||||||
),
|
|
||||||
onNone: () => Effect.void,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const forkInterrupt = fiberRef.pipe(
|
|
||||||
Effect.flatMap(Option.match({
|
|
||||||
onSome: fiber => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
Effect.andThen(Fiber.interrupt(fiber).pipe(
|
|
||||||
Effect.asVoid,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
))
|
|
||||||
),
|
|
||||||
onNone: () => Effect.forkDaemon(Effect.void),
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const run = Effect.Do.pipe(
|
|
||||||
Effect.bind("state", () => queryStateTag),
|
|
||||||
Effect.bind("client", () => QueryClient),
|
|
||||||
Effect.bind("latestKey", () => latestKeyRef.pipe(Effect.flatMap(identity))),
|
|
||||||
|
|
||||||
Effect.flatMap(({ state, client, latestKey }) => query(latestKey).pipe(
|
|
||||||
client.errorHandler.handle,
|
|
||||||
Effect.matchCauseEffect({
|
|
||||||
onSuccess: v => Effect.succeed(AsyncData.success(v)).pipe(
|
|
||||||
Effect.tap(state.set)
|
|
||||||
),
|
|
||||||
onFailure: c => Effect.succeed(AsyncData.failure(c)).pipe(
|
|
||||||
Effect.tap(state.set)
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.provide(context),
|
|
||||||
Effect.provide(QueryProgress.QueryProgress.Live),
|
|
||||||
)
|
|
||||||
|
|
||||||
const forkFetch = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
|
||||||
Effect.flatMap(stateQueue => queryStateTag.pipe(
|
|
||||||
Effect.flatMap(state => interrupt.pipe(
|
|
||||||
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
Effect.andThen(Queue.shutdown(stateQueue))
|
|
||||||
)).pipe(
|
|
||||||
Effect.andThen(state.set(AsyncData.loading())),
|
|
||||||
Effect.andThen(run),
|
|
||||||
Effect.scoped,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
|
||||||
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.provide(QueryState.layer(
|
|
||||||
queryStateTag,
|
|
||||||
stateRef,
|
|
||||||
value => Queue.offer(stateQueue, value).pipe(
|
|
||||||
Effect.andThen(Ref.set(stateRef, value))
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
const setInitialRefreshState = queryStateTag.pipe(
|
|
||||||
Effect.flatMap(state => state.update(previous => {
|
|
||||||
if (AsyncData.isSuccess(previous) || AsyncData.isFailure(previous))
|
|
||||||
return AsyncData.refreshing(previous)
|
|
||||||
if (AsyncData.isRefreshing(previous))
|
|
||||||
return AsyncData.refreshing(previous.previous)
|
|
||||||
return AsyncData.loading()
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
const forkRefresh = Queue.unbounded<AsyncData.AsyncData<A | FallbackA, Exclude<E, HandledE>>>().pipe(
|
|
||||||
Effect.flatMap(stateQueue => interrupt.pipe(
|
|
||||||
Effect.andThen(Effect.addFinalizer(() => Ref.set(fiberRef, Option.none()).pipe(
|
|
||||||
Effect.andThen(Queue.shutdown(stateQueue))
|
|
||||||
)).pipe(
|
|
||||||
Effect.andThen(setInitialRefreshState),
|
|
||||||
Effect.andThen(run),
|
|
||||||
Effect.scoped,
|
|
||||||
Effect.forkDaemon,
|
|
||||||
)),
|
|
||||||
|
|
||||||
Effect.tap(fiber => Ref.set(fiberRef, Option.some(fiber))),
|
|
||||||
Effect.map(fiber => [fiber, Stream.fromQueue(stateQueue)] as const),
|
|
||||||
|
|
||||||
Effect.provide(QueryState.layer(
|
|
||||||
queryStateTag,
|
|
||||||
stateRef,
|
|
||||||
value => Queue.offer(stateQueue, value).pipe(
|
|
||||||
Effect.andThen(Ref.set(stateRef, value))
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
const fetchOnKeyChange = Effect.addFinalizer(() => interrupt).pipe(
|
|
||||||
Effect.andThen(Stream.runForEach(Stream.changes(key), latestKey =>
|
|
||||||
Ref.set(latestKeyRef, Option.some(latestKey)).pipe(
|
|
||||||
Effect.andThen(forkFetch)
|
|
||||||
)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
const refreshOnWindowFocus = Stream.runForEach(
|
|
||||||
BrowserStream.fromEventListenerWindow("focus"),
|
|
||||||
() => forkRefresh,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
context,
|
|
||||||
|
|
||||||
latestKeyRef,
|
|
||||||
stateRef,
|
|
||||||
fiberRef,
|
|
||||||
|
|
||||||
forkInterrupt,
|
|
||||||
forkFetch,
|
|
||||||
forkRefresh,
|
|
||||||
|
|
||||||
fetchOnKeyChange,
|
|
||||||
refreshOnWindowFocus,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
export * as MutationRunner from "./MutationRunner.js"
|
|
||||||
export * as QueryRunner from "./QueryRunner.js"
|
|
||||||
export * as QueryState from "./QueryState.js"
|
export * as QueryState from "./QueryState.js"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "reffuse",
|
"name": "reffuse",
|
||||||
"version": "0.1.11",
|
"version": "0.1.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"./README.md",
|
"./README.md",
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ export abstract class ReffuseNamespace<R> {
|
|||||||
): SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> {
|
): SubscriptionSubRef.SubscriptionSubRef<PropertyPath.ValueFromPath<B, P>, B> {
|
||||||
return React.useMemo(
|
return React.useMemo(
|
||||||
() => SubscriptionSubRef.makeFromPath(parent, path),
|
() => SubscriptionSubRef.makeFromPath(parent, path),
|
||||||
[parent],
|
[parent, ...path],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
import { Array, Function, Option, Predicate } from "effect"
|
import { Array, Function, Option, Predicate } from "effect"
|
||||||
|
|
||||||
|
|
||||||
export type Paths<T> = [] | (
|
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
T extends readonly any[] ? ArrayPaths<T> :
|
|
||||||
T extends object ? ObjectPaths<T> :
|
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
||||||
|
D extends never ? [] :
|
||||||
|
T extends Seen ? [] :
|
||||||
|
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
||||||
|
T extends object ? ObjectPaths<T, D, Seen | T> :
|
||||||
never
|
never
|
||||||
)
|
)
|
||||||
|
|
||||||
export type ArrayPaths<T extends readonly any[]> = {
|
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
||||||
[K in keyof T as K extends number ? K : never]:
|
[K in keyof T as K extends number ? K : never]:
|
||||||
| [K]
|
| [K]
|
||||||
| [K, ...Paths<T[K]>]
|
| [K, ...Paths<T[K], Prev[D], Seen>]
|
||||||
} extends infer O
|
} extends infer O
|
||||||
? O[keyof O]
|
? O[keyof O]
|
||||||
: never
|
: never
|
||||||
|
|
||||||
export type ObjectPaths<T extends object> = {
|
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
||||||
[K in keyof T as K extends string | number | symbol ? K : never]:
|
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
||||||
| [K]
|
NonNullable<T[K]> extends infer V
|
||||||
| [K, ...Paths<T[K]>]
|
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
||||||
|
: never
|
||||||
} extends infer O
|
} extends infer O
|
||||||
? O[keyof O]
|
? O[keyof O]
|
||||||
: never
|
: never
|
||||||
|
|||||||
@@ -26,7 +26,13 @@
|
|||||||
|
|
||||||
// Build
|
// Build
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "@effect/language-service"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": ["./src"]
|
"include": ["./src"]
|
||||||
|
|||||||
Reference in New Issue
Block a user