Skip to content

Commit 6899a95

Browse files
committed
add Reactive experiment
1 parent 8e33546 commit 6899a95

File tree

10 files changed

+2471
-1516
lines changed

10 files changed

+2471
-1516
lines changed

package.json

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,36 @@
1818
"docgen-cp": "node scripts/docs-cp.js"
1919
},
2020
"devDependencies": {
21-
"@babel/cli": "^7.25.7",
22-
"@babel/core": "^7.25.8",
23-
"@babel/plugin-transform-export-namespace-from": "^7.25.8",
24-
"@babel/plugin-transform-modules-commonjs": "^7.25.7",
25-
"@changesets/changelog-github": "^0.5.0",
26-
"@changesets/cli": "^2.27.9",
27-
"@effect/build-utils": "^0.7.8",
28-
"@effect/docgen": "^0.4.5",
21+
"@babel/cli": "^7.27.0",
22+
"@babel/core": "^7.26.10",
23+
"@babel/plugin-transform-export-namespace-from": "^7.25.9",
24+
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
25+
"@changesets/changelog-github": "^0.5.1",
26+
"@changesets/cli": "^2.28.1",
27+
"@effect/build-utils": "^0.7.9",
28+
"@effect/docgen": "^0.5.2",
2929
"@effect/eslint-plugin": "^0.2.0",
30-
"@effect/language-service": "^0.2.0",
31-
"@eslint/compat": "^1.2.1",
32-
"@eslint/eslintrc": "^3.1.0",
33-
"@eslint/js": "^9.13.0",
34-
"@typescript-eslint/eslint-plugin": "^8.10.0",
35-
"@typescript-eslint/parser": "^8.10.0",
36-
"@vitest/coverage-v8": "^2.1.3",
37-
"babel-plugin-annotate-pure-calls": "^0.4.0",
38-
"eslint": "^9.13.0",
39-
"eslint-import-resolver-typescript": "^3.6.3",
40-
"eslint-plugin-codegen": "^0.29.0",
30+
"@effect/language-service": "^0.5.1",
31+
"@eslint/compat": "^1.2.8",
32+
"@eslint/eslintrc": "^3.3.1",
33+
"@eslint/js": "^9.23.0",
34+
"@typescript-eslint/eslint-plugin": "^8.29.0",
35+
"@typescript-eslint/parser": "^8.29.0",
36+
"@vitest/coverage-v8": "^3.1.1",
37+
"babel-plugin-annotate-pure-calls": "^0.5.0",
38+
"eslint": "^9.23.0",
39+
"eslint-import-resolver-typescript": "^4.3.1",
40+
"eslint-plugin-codegen": "^0.30.0",
4141
"eslint-plugin-deprecation": "^3.0.0",
4242
"eslint-plugin-import": "^2.31.0",
4343
"eslint-plugin-simple-import-sort": "^12.1.1",
4444
"eslint-plugin-sort-destructure-keys": "^2.0.0",
45-
"fast-check": "^3.22.0",
46-
"glob": "^11.0.0",
45+
"fast-check": "^4.0.1",
46+
"glob": "^11.0.1",
4747
"madge": "^8.0.0",
48-
"prettier": "^3.3.3",
49-
"tsx": "^4.19.1",
50-
"typescript": "^5.6.3",
51-
"vitest": "^2.1.3"
48+
"prettier": "^3.5.3",
49+
"tsx": "^4.19.3",
50+
"typescript": "^5.8.2",
51+
"vitest": "^3.1.1"
5252
}
5353
}

packages/rx-react/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@
2525
"devDependencies": {
2626
"@types/react": "^18.3.11",
2727
"@types/scheduler": "^0.23.0",
28-
"effect": "^3.9.2",
28+
"effect": "^3.14.0",
2929
"react": "^18.3.1",
3030
"scheduler": "^0.23"
3131
},
3232
"peerDependencies": {
33-
"effect": "^3.8.0",
33+
"effect": "^3.14.0",
3434
"react": ">=18 <20",
3535
"scheduler": "*"
3636
},

packages/rx-react/src/index.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
/* eslint-disable @typescript-eslint/no-empty-object-type */
12
/**
23
* @since 1.0.0
34
*/
5+
import * as Reactive from "@effect-rx/rx/Reactive"
46
import * as Registry from "@effect-rx/rx/Registry"
57
import * as Result from "@effect-rx/rx/Result"
68
import * as Rx from "@effect-rx/rx/Rx"
79
import type * as RxRef from "@effect-rx/rx/RxRef"
810
import * as Cause from "effect/Cause"
11+
import type * as Context from "effect/Context"
12+
import * as Effect from "effect/Effect"
913
import type * as Exit from "effect/Exit"
14+
import * as FiberSet from "effect/FiberSet"
15+
import { constNull } from "effect/Function"
1016
import { globalValue } from "effect/GlobalValue"
17+
import * as Layer from "effect/Layer"
18+
import type { Scope } from "effect/Scope"
1119
import * as React from "react"
1220
import * as Scheduler from "scheduler"
1321

@@ -31,6 +39,16 @@ export * as Rx from "@effect-rx/rx/Rx"
3139
* @category modules
3240
*/
3341
export * as RxRef from "@effect-rx/rx/RxRef"
42+
/**
43+
* @since 1.0.0
44+
* @category modules
45+
*/
46+
export * as Reactive from "@effect-rx/rx/Reactive"
47+
/**
48+
* @since 1.0.0
49+
* @category modules
50+
*/
51+
export * as ReactiveRef from "@effect-rx/rx/ReactiveRef"
3452

3553
/**
3654
* @since 1.0.0
@@ -349,3 +367,133 @@ export const useRxRefProp = <A, K extends keyof A>(ref: RxRef.RxRef<A>, prop: K)
349367
*/
350368
export const useRxRefPropValue = <A, K extends keyof A>(ref: RxRef.RxRef<A>, prop: K): A[K] =>
351369
useRxRef(useRxRefProp(ref, prop))
370+
371+
/**
372+
* @since 1.0.0
373+
* @category Reactive
374+
*/
375+
export interface ReactiveComponent<Props extends Record<string, any>, R = never> {
376+
(
377+
props: Props & [R] extends [never] ? {
378+
readonly context?: Context.Context<never> | undefined
379+
} :
380+
{ readonly context: Context.Context<R> }
381+
): React.ReactNode
382+
383+
provide<AL, EL, RL>(layer: Layer.Layer<AL, EL, RL>): ReactiveComponent<Props, Exclude<R, AL> | RL>
384+
385+
render: Effect.Effect<(props: Props) => React.ReactNode, never, R>
386+
}
387+
388+
const makeReactiveComponent = <Props extends Record<string, any>, E, R>(options: {
389+
readonly name: string
390+
readonly build: (
391+
props: Props,
392+
emit: (_: React.ReactNode) => Effect.Effect<void, never, Reactive.Reactive>
393+
) => Effect.Effect<React.ReactNode, E, R>
394+
readonly layer: Layer.Layer<never>
395+
readonly onInitial: (props: Props) => React.ReactNode
396+
readonly deps: (props: Props) => ReadonlyArray<any>
397+
}): ReactiveComponent<Props, R> => {
398+
function ReactiveComponent(props: Props & { readonly context?: Context.Context<R> }) {
399+
const subscribable = React.useMemo(
400+
() => {
401+
const layer = props.context
402+
? Layer.provideMerge(options.layer, Layer.succeedContext(props.context))
403+
: options.layer
404+
return Reactive.toSubscribable(layer)(
405+
options.build(props, Reactive.emit) as Effect.Effect<React.ReactNode, E, Reactive.Reactive>
406+
)
407+
},
408+
options.deps(props)
409+
)
410+
const store = makeSubscribableStore(subscribable)
411+
const result = React.useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot)
412+
if (result._tag === "Initial") {
413+
return options.onInitial(props)
414+
} else if (result._tag === "Failure") {
415+
throw Cause.squash(result.cause)
416+
}
417+
return result.value
418+
}
419+
ReactiveComponent.displayName = options.name
420+
ReactiveComponent.provide = function provide(layer: Layer.Layer<any, any, any>) {
421+
return makeReactiveComponent({
422+
...options,
423+
layer: options.layer === Layer.empty ? layer : Layer.provideMerge(options.layer, layer) as any
424+
})
425+
}
426+
ReactiveComponent.render = Effect.contextWith((context: Context.Context<any>) => (props: Props) =>
427+
ReactiveComponent({
428+
...props,
429+
context
430+
})
431+
)
432+
return ReactiveComponent as any
433+
}
434+
435+
const subscribableStores = globalValue(
436+
"@effect-rx/rx-react/subscribableStores",
437+
() => new WeakMap<Reactive.Subscribable<any, any>, RxStore<any>>()
438+
)
439+
const makeSubscribableStore = <A, E>(subscribable: Reactive.Subscribable<A, E>): RxStore<Result.Result<A, E>> => {
440+
let store = subscribableStores.get(subscribable)
441+
if (store !== undefined) {
442+
return store
443+
}
444+
445+
let result: Result.Result<A, E> = Result.initial(true)
446+
447+
store = {
448+
subscribe(f) {
449+
return subscribable.subscribe((result_) => {
450+
result = result_
451+
f()
452+
})
453+
},
454+
snapshot() {
455+
return result
456+
}
457+
}
458+
459+
subscribableStores.set(subscribable, store)
460+
461+
return store
462+
}
463+
464+
const defaultDeps = (props: Record<string, any>) => Object.values(props)
465+
466+
/**
467+
* @since 1.0.0
468+
* @category Reactive
469+
*/
470+
export const component = <E, R, Props extends Record<string, any> = {}>(
471+
name: string,
472+
build: (
473+
props: Props,
474+
emit: (_: React.ReactNode) => Effect.Effect<void, never, Reactive.Reactive>
475+
) => Effect.Effect<React.ReactNode, E, R>,
476+
options?: {
477+
readonly onInitial?: (props: Props) => React.ReactNode
478+
readonly deps?: (props: Props) => ReadonlyArray<any>
479+
}
480+
): ReactiveComponent<Props, Exclude<R, Reactive.Reactive | Scope>> =>
481+
makeReactiveComponent({
482+
name,
483+
build,
484+
layer: Layer.empty,
485+
onInitial: options?.onInitial ?? constNull,
486+
deps: options?.deps ?? defaultDeps
487+
}) as any
488+
489+
/**
490+
* @since 1.0.0
491+
* @category Reactive
492+
*/
493+
export const action = <Args extends ReadonlyArray<any>, A, E, R>(
494+
f: (...args: Args) => Effect.Effect<A, E, R>
495+
): Effect.Effect<(...args: Args) => Promise<A>, never, R | Scope> =>
496+
Effect.map(
497+
FiberSet.makeRuntimePromise<R, A, E>(),
498+
(runPromise) => (...args) => runPromise(f(...args))
499+
)

packages/rx/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
"license": "MIT",
2525
"sideEffects": [],
2626
"devDependencies": {
27-
"effect": "^3.9.2"
27+
"effect": "^3.14.0"
2828
},
2929
"peerDependencies": {
30-
"effect": "^3.8.0"
30+
"effect": "^3.14.0"
3131
}
3232
}

0 commit comments

Comments
 (0)