diff --git a/src/core/keat.ts b/src/core/keat.ts index 78d968c..9aaedcb 100644 --- a/src/core/keat.ts +++ b/src/core/keat.ts @@ -13,11 +13,15 @@ import { mutable } from "./utils"; export type ExtractFeatures = K extends KeatApi ? keyof K : never; +export type Listener = () => void; +export type Unsubscribe = () => void; + export function keatCore({ features, display = "swap", plugins = [], }: KeatInit): KeatApi { + let listeners: Listener[] = []; let defaultDisplay = display; let defaultUser: User | undefined = undefined; let configId = 0; @@ -33,6 +37,9 @@ export function keatCore({ configId += 1; config = newConfig; }, + onChange: () => { + listeners.forEach((l) => l()); + }, } ) ) @@ -98,6 +105,12 @@ export function keatCore({ const useLatest = loader.useLatest(display); return evaluate(feature as string, user, useLatest ? configId : 0); }, + onChange: (listener: Listener): Unsubscribe => { + listeners.push(listener); + return () => { + listeners = listeners.filter((l) => l === listener); + }; + }, }; } diff --git a/src/core/plugin.ts b/src/core/plugin.ts index a8cb4cc..3508646 100644 --- a/src/core/plugin.ts +++ b/src/core/plugin.ts @@ -25,6 +25,7 @@ export type OnPluginInitCtx = { export type OnPluginInitApi = { setConfig: (newConfig: Config) => void; + onChange: () => void; }; export type OnEvalHook = ( diff --git a/src/core/types.ts b/src/core/types.ts index 90be177..2260041 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,3 +1,4 @@ +import { Listener, Unsubscribe } from "./keat"; import type { Plugin } from "./plugin"; /** @@ -64,6 +65,7 @@ export type KeatApi = { ? TFeatures[TFeature]["variates"][number] : boolean : boolean; + onChange(listener: Listener): Unsubscribe; }; /* * * * * * * * * * * * * diff --git a/src/plugins/localStorage.ts b/src/plugins/localStorage.ts index c7cd5a4..f6085f3 100644 --- a/src/plugins/localStorage.ts +++ b/src/plugins/localStorage.ts @@ -16,28 +16,41 @@ type Options = { value?: string; /** - * Whether the local storage item should only - * retried at initialisation. + * Whether the local storage is polled for updated. * - * @default false + * The default polling time every 2 seconds. + * You can change it by setting a number (in ms). */ - once?: boolean; + poll?: boolean | number; }; export const localStorage = ( name: string, - { key, value, once = false }: Options = {} + { key, value, poll = false }: Options = {} ): Plugin => { - const item = once ?? window.localStorage.getItem(key ?? name); + const k = key ?? name; + let item = window.localStorage.getItem(k); + return { + onPluginInit(_, { onChange }) { + const pollInterval = + poll === true ? 2000 : typeof poll === "number" && poll > 0 ? poll : 0; + if (hasSetInterval() && pollInterval > 0) { + setInterval(() => { + const newItem = window.localStorage.getItem(k); + const hasChanged = item !== newItem; + item = newItem; + if (hasChanged) onChange(); + }, pollInterval); + } + }, onEval({ variates, rules }, { setResult }) { if (typeof window === "undefined") return; const index = rules.findIndex((rule) => takeStrings(rule).some((r) => { if (r !== name) return false; - const i = once ? item : window.localStorage.getItem(key ?? name); - return value ? i === value : Boolean(i); + return value ? item === value : Boolean(item); }) ); @@ -45,3 +58,7 @@ export const localStorage = ( }, }; }; + +function hasSetInterval() { + return typeof window !== "undefined" && window.setInterval; +} diff --git a/src/react/KeatReact.tsx b/src/react/KeatReact.tsx index 2777dc0..08b0c44 100644 --- a/src/react/KeatReact.tsx +++ b/src/react/KeatReact.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useEffect } from "react"; +import React, { ReactNode, useEffect, useReducer } from "react"; import { AnyFeatures, Display, KeatApi, keatCore, KeatInit } from "../core"; type KeatReactApi = KeatApi & { @@ -48,11 +48,17 @@ export function keatReact( children, }) { const [loading, setLoading] = React.useState(true); + const [_, forceUpdate] = useReducer((x) => x + 1, 0); useEffect(() => { keatInstance.ready(display).then(() => setLoading(false)); }, [setLoading]); + useEffect(() => { + const unsubscribe = keatInstance.onChange(forceUpdate); + return () => unsubscribe(); + }, []); + if (loading) { return <>{invisible}; }