diff --git a/package.json b/package.json index c7a373e1..ba85e6a7 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "react": "*" }, "dependencies": { - "evt": "^2.5.7", "minimal-polyfills": "^2.2.3", "react-markdown": "^5.0.3", "tsafe": "^1.6.6" @@ -121,6 +120,7 @@ "vite": "^5.2.11", "vitest": "^0.29.8", "yauzl": "^2.10.0", - "zod": "^3.17.10" + "zod": "^3.17.10", + "evt": "^2.5.7" } } diff --git a/src/login/lib/useDownloadTerms.ts b/src/login/lib/useDownloadTerms.ts index f1c3b02f..a90c46b1 100644 --- a/src/login/lib/useDownloadTerms.ts +++ b/src/login/lib/useDownloadTerms.ts @@ -4,11 +4,13 @@ import { fallbackLanguageTag } from "keycloakify/login/i18n/i18n"; import { useConst } from "keycloakify/tools/useConst"; import { useConstCallback } from "keycloakify/tools/useConstCallback"; import { assert } from "tsafe/assert"; -import { Evt } from "evt"; -import { useRerenderOnStateChange } from "evt/hooks/useRerenderOnStateChange"; +import { + createStatefulObservable, + useRerenderOnChange +} from "keycloakify/tools/StatefulObservable"; import { KcContext } from "../kcContext"; -const evtTermsMarkdown = Evt.create(undefined); +const obsTermsMarkdown = createStatefulObservable(() => undefined); export type KcContextLike = { pageId: string; @@ -45,15 +47,15 @@ export function useDownloadTerms(params: { if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) { downloadTermMarkdownMemoized( kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag - ).then(thermMarkdown => (evtTermsMarkdown.state = thermMarkdown)); + ).then(thermMarkdown => (obsTermsMarkdown.current = thermMarkdown)); } }, []); } export function useTermsMarkdown() { - useRerenderOnStateChange(evtTermsMarkdown); + useRerenderOnChange(obsTermsMarkdown); - const termsMarkdown = evtTermsMarkdown.state; + const termsMarkdown = obsTermsMarkdown.current; return { termsMarkdown }; } diff --git a/src/tools/StatefulObservable/README.md b/src/tools/StatefulObservable/README.md new file mode 100644 index 00000000..7a2ad9df --- /dev/null +++ b/src/tools/StatefulObservable/README.md @@ -0,0 +1,16 @@ +`StatefulObservable` is a construct that allow to avoid having to depend on [EVT](https://evt.land). + +A `StatefulObservable` can be converted to an evt with: + +```ts +import { statefulObservableToStatefulEvt } from "powerhooks/tools/StatefulObservable/statefulObservableToStatefulEvt"; + +const evtXyz = statefulObservableToStatefulEvt({ + statefulObservable: $xyz + //Optionally you can pass a Ctx +}); +``` + +WARNING: Unlike `StatefulEvt`, `StatefulObservable` do not post when we first attach. +If the current value was not yet evaluated `next()` is called on the initial value returned by the function that +returns it. diff --git a/src/tools/StatefulObservable/StatefulObservable.ts b/src/tools/StatefulObservable/StatefulObservable.ts new file mode 100644 index 00000000..50fcefbf --- /dev/null +++ b/src/tools/StatefulObservable/StatefulObservable.ts @@ -0,0 +1,58 @@ +import { assert } from "tsafe/assert"; +import { is } from "tsafe/is"; + +export type StatefulObservable = { + current: T; + subscribe: (next: (data: T) => void) => Subscription; +}; + +export type Subscription = { + unsubscribe(): void; +}; + +export function createStatefulObservable( + getInitialValue: () => T +): StatefulObservable { + const nextFunctions: ((data: T) => void)[] = []; + + const { get, set } = (() => { + let wrappedState: [T] | undefined = undefined; + + function set(data: T) { + wrappedState = [data]; + + nextFunctions.forEach(next => next(data)); + } + + return { + get: () => { + if (wrappedState === undefined) { + set(getInitialValue()); + assert(!is(wrappedState)); + } + return wrappedState[0]; + }, + set + }; + })(); + + return Object.defineProperty( + { + current: null as any as T, + subscribe: (next: (data: T) => void) => { + nextFunctions.push(next); + + return { + unsubscribe: () => + nextFunctions.splice(nextFunctions.indexOf(next), 1) + }; + } + }, + "current", + { + enumerable: true, + get, + set + } + ); +} diff --git a/src/tools/StatefulObservable/hooks/index.ts b/src/tools/StatefulObservable/hooks/index.ts new file mode 100644 index 00000000..6eab3926 --- /dev/null +++ b/src/tools/StatefulObservable/hooks/index.ts @@ -0,0 +1,2 @@ +export * from "./useObservable"; +export * from "./useRerenderOnChange"; diff --git a/src/tools/StatefulObservable/hooks/useObservable.ts b/src/tools/StatefulObservable/hooks/useObservable.ts new file mode 100644 index 00000000..dac88448 --- /dev/null +++ b/src/tools/StatefulObservable/hooks/useObservable.ts @@ -0,0 +1,25 @@ +import { useEffect } from "react"; +import type { Subscription } from "../StatefulObservable"; + +/** + * Equivalent of https://docs.evt.land/api/react-hooks + */ +export function useObservable( + effect: (params: { + registerSubscription: (subscription: Subscription) => void; + }) => void, + deps: React.DependencyList +): void { + useEffect(() => { + const subscriptions: Subscription[] = []; + + effect({ + registerSubscription: subscription => subscriptions.push(subscription) + }); + + return () => { + subscriptions.forEach(subscription => subscription.unsubscribe()); + subscriptions.length = 0; + }; + }, deps); +} diff --git a/src/tools/StatefulObservable/hooks/useRerenderOnChange.ts b/src/tools/StatefulObservable/hooks/useRerenderOnChange.ts new file mode 100644 index 00000000..c6045fc4 --- /dev/null +++ b/src/tools/StatefulObservable/hooks/useRerenderOnChange.ts @@ -0,0 +1,19 @@ +import { useObservable } from "./useObservable"; +import { useState } from "react"; +import type { StatefulObservable } from "../StatefulObservable"; + +/** + * Equivalent of https://docs.evt.land/api/react-hooks + * */ +export function useRerenderOnChange($: StatefulObservable): void { + //NOTE: We use function in case the state is a function + const [, setCurrent] = useState(() => $.current); + + useObservable( + ({ registerSubscription }) => { + const subscription = $.subscribe(current => setCurrent(() => current)); + registerSubscription(subscription); + }, + [$] + ); +} diff --git a/src/tools/StatefulObservable/index.ts b/src/tools/StatefulObservable/index.ts new file mode 100644 index 00000000..f41068ac --- /dev/null +++ b/src/tools/StatefulObservable/index.ts @@ -0,0 +1,2 @@ +export * from "./StatefulObservable"; +export * from "./hooks";