feat!: add new hooks and improvements (#3)
* use React global var * use undefined instead on null * use react as peer dependency * make ephemeral default to false * set default ephemeral to false, use node instead of waku naming * implement useStoreMessages * fix types * export useStoreMessages * make content pair set initially * remove deps for useEffect for createWaku to prevent re rendering * prevent setting of empty messages * accept undefined node, handle empty message case * accept undefined node, handle empty message case * add TODOs * rename to useCreateContentPair * remove export of WakuContext, create ContetnPair provider * remove export of WakuContext, create ContetnPair provider * fix lint * fix typo * remove export * fix prettier * make decoded optional * add jsdocs * add useLightPush hook * update types, add usePeers, add prettierignore * remove full node hook, provider * remove export * remove FullNode stuff
This commit is contained in:
parent
bd3bfb8134
commit
e4d0106499
|
@ -29,7 +29,7 @@
|
|||
],
|
||||
"cypress/no-unnecessary-waiting": "off",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react/display-name": "warn",
|
||||
"react/prop-types": "off",
|
||||
"no-console": ["error"],
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
dist
|
File diff suppressed because it is too large
Load Diff
|
@ -60,8 +60,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@waku/core": "^0.0.10",
|
||||
"@waku/create": "^0.0.6",
|
||||
"react": "^18.2.0"
|
||||
"@waku/create": "^0.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
|
@ -91,6 +90,9 @@
|
|||
"rollup-plugin-typescript2": "^0.34.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
},
|
||||
"bundlewatch": {
|
||||
"files": [
|
||||
{
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import React from "react";
|
||||
|
||||
import type { ContentPair, ReactChildrenProps } from "./types";
|
||||
import { useCreateContentPair } from "./useCreatContentPair";
|
||||
|
||||
type ContentPairContextType = Partial<ContentPair>;
|
||||
|
||||
const ContentPairContext = React.createContext<ContentPairContextType>({
|
||||
decoder: undefined,
|
||||
encoder: undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* Hook to retrieve Encoder/Decoder pair from Context.
|
||||
* @example
|
||||
* const { encoder, decoder } = useContentPair();
|
||||
* @returns {Object} { encoder, decoder }
|
||||
*/
|
||||
export const useContentPair = (): ContentPairContextType =>
|
||||
React.useContext(ContentPairContext);
|
||||
|
||||
type ContentPairProviderProps = ReactChildrenProps & {
|
||||
contentTopic: string;
|
||||
ephemeral?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider for creating Encoder/Decoder pair based on contentTopic
|
||||
* @example
|
||||
* const App = (props) => (
|
||||
* <ContentPairProvider contentTopic="/toy-chat/2/huilong/proto">
|
||||
* <Component />
|
||||
* </ContentPairProvider>
|
||||
* );
|
||||
* const Component = (props) => {
|
||||
* const { encoder, decoder } = useContentPair();
|
||||
* ...
|
||||
* };
|
||||
* @param {string} contentTopic - content topic for configuring the pair
|
||||
* @param {boolean} ephemeral - flag to set messages ephemeral according to RFC https://rfc.vac.dev/spec/14/
|
||||
* @returns React ContentPair Provider component
|
||||
*/
|
||||
export const ContentPairProvider: React.FunctionComponent<
|
||||
ContentPairProviderProps
|
||||
> = (props) => {
|
||||
const result = useCreateContentPair(props.contentTopic, props.ephemeral);
|
||||
|
||||
return (
|
||||
<ContentPairContext.Provider value={result}>
|
||||
{props.children}
|
||||
</ContentPairContext.Provider>
|
||||
);
|
||||
};
|
|
@ -3,23 +3,19 @@ import type { Waku } from "@waku/interfaces";
|
|||
|
||||
import type {
|
||||
BootstrapNodeOptions,
|
||||
CrateWakuHook,
|
||||
FullNodeOptions,
|
||||
CrateNodeResult,
|
||||
LightNodeOptions,
|
||||
ReactChildrenProps,
|
||||
RelayNodeOptions,
|
||||
} from "./types";
|
||||
import {
|
||||
useCreateFullNode,
|
||||
useCreateLightNode,
|
||||
useCreateRelayNode,
|
||||
} from "./useCreateWaku";
|
||||
import { useCreateLightNode, useCreateRelayNode } from "./useCreateWaku";
|
||||
|
||||
type WakuContextType<T extends Waku> = CrateWakuHook<T>;
|
||||
type WakuContextType<T extends Waku> = CrateNodeResult<T>;
|
||||
|
||||
export const WakuContext = React.createContext<WakuContextType<Waku>>({
|
||||
node: null,
|
||||
const WakuContext = React.createContext<WakuContextType<Waku>>({
|
||||
node: undefined,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -37,10 +33,6 @@ export const WakuContext = React.createContext<WakuContextType<Waku>>({
|
|||
export const useWaku = <T extends Waku>(): WakuContextType<T> =>
|
||||
React.useContext(WakuContext) as WakuContextType<T>;
|
||||
|
||||
type ReactChildrenProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
type ProviderProps<T> = ReactChildrenProps & BootstrapNodeOptions<T>;
|
||||
|
||||
/**
|
||||
|
@ -102,33 +94,3 @@ export const RelayNodeProvider: React.FunctionComponent<
|
|||
<WakuContext.Provider value={result}>{props.children}</WakuContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider for creating Full Node based on options passed.
|
||||
* @example
|
||||
* const App = (props) => (
|
||||
* <FullNodeProvider options={{...}}>
|
||||
* <Component />
|
||||
* </FullNodeProvider>
|
||||
* );
|
||||
* const Component = (props) => {
|
||||
* const { node, isLoading, error } = useWaku<FullNode>();
|
||||
* ...
|
||||
* };
|
||||
* @param {Object} props - options to create a node and other React props
|
||||
* @param {FullNodeOptions} props.options - optional options for creating Full Node
|
||||
* @param {Protocols} props.protocols - optional protocols list to initiate node with
|
||||
* @returns React Full Node provider component
|
||||
*/
|
||||
export const FullNodeProvider: React.FunctionComponent<
|
||||
ProviderProps<FullNodeOptions>
|
||||
> = (props) => {
|
||||
const result = useCreateFullNode({
|
||||
options: props.options,
|
||||
protocols: props.protocols,
|
||||
});
|
||||
|
||||
return (
|
||||
<WakuContext.Provider value={result}>{props.children}</WakuContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
24
src/index.ts
24
src/index.ts
|
@ -1,15 +1,9 @@
|
|||
export { FullNodeOptions, LightNodeOptions, RelayNodeOptions } from "./types";
|
||||
export { useContentPair } from "./useContentPair";
|
||||
export {
|
||||
useCreateFullNode,
|
||||
useCreateLightNode,
|
||||
useCreateRelayNode,
|
||||
} from "./useCreateWaku";
|
||||
export { useFilterSubscribe } from "./useFilterSubscribe";
|
||||
export {
|
||||
FullNodeProvider,
|
||||
LightNodeProvider,
|
||||
RelayNodeProvider,
|
||||
useWaku,
|
||||
WakuContext,
|
||||
} from "./WakuProvider";
|
||||
export { ContentPairProvider, useContentPair } from "./ContentPairProvider";
|
||||
export { LightNodeOptions, RelayNodeOptions } from "./types";
|
||||
export { useCreateContentPair } from "./useCreatContentPair";
|
||||
export { useCreateLightNode, useCreateRelayNode } from "./useCreateWaku";
|
||||
export { useFilterMessages } from "./useFilterMessages";
|
||||
export { useLightPush } from "./useLightPush";
|
||||
export { usePeers } from "./usePeers";
|
||||
export { useStoreMessages } from "./useStoreMessages";
|
||||
export { LightNodeProvider, RelayNodeProvider, useWaku } from "./WakuProvider";
|
||||
|
|
19
src/types.ts
19
src/types.ts
|
@ -1,14 +1,15 @@
|
|||
import { RelayCreateOptions, WakuOptions } from "@waku/core";
|
||||
import type { Decoder, Encoder } from "@waku/core/dist/lib/message/version_0";
|
||||
import type { CreateOptions } from "@waku/create";
|
||||
import type { Protocols, Waku } from "@waku/interfaces";
|
||||
|
||||
export type HookState = {
|
||||
isLoading: boolean;
|
||||
error: null | string;
|
||||
error: undefined | string;
|
||||
};
|
||||
|
||||
export type CrateWakuHook<T extends Waku> = HookState & {
|
||||
node: null | T;
|
||||
export type CrateNodeResult<T extends Waku> = HookState & {
|
||||
node: undefined | T;
|
||||
};
|
||||
|
||||
export type BootstrapNodeOptions<T = {}> = {
|
||||
|
@ -20,6 +21,12 @@ export type LightNodeOptions = CreateOptions & WakuOptions;
|
|||
export type RelayNodeOptions = CreateOptions &
|
||||
WakuOptions &
|
||||
Partial<RelayCreateOptions>;
|
||||
export type FullNodeOptions = CreateOptions &
|
||||
WakuOptions &
|
||||
Partial<RelayCreateOptions>;
|
||||
|
||||
export type ContentPair = {
|
||||
encoder: Encoder;
|
||||
decoder: Decoder;
|
||||
};
|
||||
|
||||
export type ReactChildrenProps = {
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { createDecoder, createEncoder } from "@waku/core";
|
||||
import type { Decoder, Encoder } from "@waku/core/dist/lib/message/version_0";
|
||||
|
||||
type ContentPair = {
|
||||
encoder: null | Encoder;
|
||||
decoder: null | Decoder;
|
||||
};
|
||||
import type { ContentPair } from "./types";
|
||||
|
||||
/**
|
||||
* Creates Encoder / Decoder pair for a given contentTopic.
|
||||
* @param {string} contentTopic - topic to orient to
|
||||
* @param {boolean} ephemeral - optional, makes messages ephemeral
|
||||
* @param {boolean} ephemeral - makes messages ephemeral, default to false
|
||||
* @returns {Object} Encoder / Decoder pair
|
||||
*/
|
||||
export const useContentPair = (
|
||||
export const useCreateContentPair = (
|
||||
contentTopic: string,
|
||||
ephemeral?: boolean,
|
||||
ephemeral = false,
|
||||
): ContentPair => {
|
||||
const [encoder, setEncoder] = useState<null | Encoder>(null);
|
||||
const [decoder, setDecoder] = useState<null | Decoder>(null);
|
||||
const [encoder, setEncoder] = React.useState<Encoder>(
|
||||
createEncoder(contentTopic, ephemeral),
|
||||
);
|
||||
const [decoder, setDecoder] = React.useState<Decoder>(
|
||||
createDecoder(contentTopic),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
setEncoder(createEncoder(contentTopic, ephemeral));
|
||||
setDecoder(createDecoder(contentTopic));
|
||||
}, [contentTopic, ephemeral]);
|
|
@ -1,12 +1,11 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { waitForRemotePeer } from "@waku/core";
|
||||
import { createFullNode, createLightNode, createRelayNode } from "@waku/create";
|
||||
import type { FullNode, LightNode, RelayNode, Waku } from "@waku/interfaces";
|
||||
import { createLightNode, createRelayNode } from "@waku/create";
|
||||
import type { LightNode, RelayNode, Waku } from "@waku/interfaces";
|
||||
|
||||
import type {
|
||||
BootstrapNodeOptions,
|
||||
CrateWakuHook,
|
||||
FullNodeOptions,
|
||||
CrateNodeResult,
|
||||
LightNodeOptions,
|
||||
RelayNodeOptions,
|
||||
} from "./types";
|
||||
|
@ -19,14 +18,14 @@ type CreateNodeParams<N extends Waku, T = {}> = BootstrapNodeOptions<T> & {
|
|||
|
||||
const useCreateNode = <N extends Waku, T = {}>(
|
||||
params: CreateNodeParams<N, T>,
|
||||
): CrateWakuHook<N> => {
|
||||
): CrateNodeResult<N> => {
|
||||
const { factory, options, protocols = [] } = params;
|
||||
|
||||
const [node, setNode] = useState<N | null>(null);
|
||||
const [isLoading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
const [node, setNode] = React.useState<N | undefined>(undefined);
|
||||
const [isLoading, setLoading] = React.useState<boolean>(true);
|
||||
const [error, setError] = React.useState<undefined | string>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
|
@ -50,7 +49,8 @@ const useCreateNode = <N extends Waku, T = {}>(
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [factory, options, protocols, setNode, setLoading, setError]);
|
||||
// TODO: missing any dependencies, it will prevent consecutive update if options change
|
||||
}, []);
|
||||
|
||||
return {
|
||||
node,
|
||||
|
@ -86,17 +86,3 @@ export const useCreateRelayNode = (
|
|||
factory: createRelayNode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Full Node helper hook.
|
||||
* @param {Object} params - optional params to configure & bootstrap node
|
||||
* @returns {CrateWakuHook} node, loading state and error
|
||||
*/
|
||||
export const useCreateFullNode = (
|
||||
params?: BootstrapNodeOptions<FullNodeOptions>,
|
||||
) => {
|
||||
return useCreateNode<FullNode, FullNodeOptions>({
|
||||
...params,
|
||||
factory: createFullNode,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
import React from "react";
|
||||
import type {
|
||||
IDecodedMessage,
|
||||
IDecoder,
|
||||
IFilter,
|
||||
Waku,
|
||||
} from "@waku/interfaces";
|
||||
|
||||
import type { HookState } from "./types";
|
||||
|
||||
type AbstractFilterNode = Waku & {
|
||||
filter: IFilter;
|
||||
};
|
||||
|
||||
type UseFilterMessagesParams = {
|
||||
node: undefined | AbstractFilterNode;
|
||||
decoder: undefined | IDecoder<IDecodedMessage>;
|
||||
};
|
||||
|
||||
type UseFilterMessagesResult = HookState & {
|
||||
messages: IDecodedMessage[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns messages from Filter subscription and keeps them up to date
|
||||
* @example
|
||||
* const { isLoading, error, message } = useFilterMessages({node, decoder});
|
||||
* @param {Object} node - node that implements Filter, hook does nothing if undefined
|
||||
* @param {Object} decoder - decoder to use for subscribing, hook does nothing if undefined
|
||||
* @returns {Object} hook state (isLoading, error) and messages array
|
||||
*/
|
||||
export const useFilterMessages = (
|
||||
params: UseFilterMessagesParams,
|
||||
): UseFilterMessagesResult => {
|
||||
const { node, decoder } = params;
|
||||
|
||||
const [error, setError] = React.useState<undefined | string>(undefined);
|
||||
const [isLoading, setLoading] = React.useState<boolean>(false);
|
||||
const [messages, setMessage] = React.useState<IDecodedMessage[]>([]);
|
||||
|
||||
const pushMessage = React.useCallback(
|
||||
(message: IDecodedMessage): void => {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage((prev) => [...prev, message]);
|
||||
},
|
||||
[setMessage],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!node || !decoder) {
|
||||
return;
|
||||
}
|
||||
|
||||
let unsubscribe: null | (() => Promise<void>) = null;
|
||||
setLoading(true);
|
||||
|
||||
node.filter
|
||||
.subscribe([decoder], pushMessage)
|
||||
.then((unsubscribeFn) => {
|
||||
setLoading(false);
|
||||
unsubscribe = unsubscribeFn;
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setError(
|
||||
`Failed to subscribe to filer: ${err?.message || "no message"}`,
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [node, decoder, pushMessage, setError, setLoading]);
|
||||
|
||||
return {
|
||||
error,
|
||||
messages,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -1,58 +0,0 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { IDecodedMessage, IDecoder, Waku } from "@waku/interfaces";
|
||||
|
||||
import type { HookState } from "./types";
|
||||
|
||||
type UseFilterSubscribeParams = {
|
||||
waku: Waku;
|
||||
decoder: IDecoder<IDecodedMessage>;
|
||||
};
|
||||
|
||||
type UseFilterSubscribeResult = HookState & {
|
||||
messages: IDecodedMessage[];
|
||||
};
|
||||
|
||||
export const useFilterSubscribe = (
|
||||
params: UseFilterSubscribeParams,
|
||||
): UseFilterSubscribeResult => {
|
||||
const { waku, decoder } = params;
|
||||
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [messages, setMessage] = useState<IDecodedMessage[]>([]);
|
||||
|
||||
const pushMessage = useCallback(
|
||||
(message: IDecodedMessage): void => {
|
||||
setMessage((prev) => [...prev, message]);
|
||||
},
|
||||
[setMessage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: null | (() => Promise<void>) = null;
|
||||
setLoading(true);
|
||||
|
||||
waku?.filter
|
||||
?.subscribe([decoder], pushMessage)
|
||||
.then((unsubscribeFn) => {
|
||||
setLoading(false);
|
||||
unsubscribe = unsubscribeFn;
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setError(
|
||||
`Failed to subscribe to filer: ${err?.message || "no message"}`,
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [waku, decoder, pushMessage, setError, setLoading]);
|
||||
|
||||
return {
|
||||
error,
|
||||
messages,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
import type {
|
||||
IEncoder,
|
||||
ILightPush,
|
||||
IMessage,
|
||||
ProtocolOptions,
|
||||
SendResult,
|
||||
Waku,
|
||||
} from "@waku/interfaces";
|
||||
|
||||
type AbstractLightPushNode = Waku & {
|
||||
lightPush: ILightPush;
|
||||
};
|
||||
|
||||
type UseLightPushParams = {
|
||||
encoder: undefined | IEncoder;
|
||||
node: undefined | AbstractLightPushNode;
|
||||
};
|
||||
|
||||
type PushFn = (
|
||||
message: IMessage,
|
||||
opts?: ProtocolOptions,
|
||||
) => Promise<SendResult>;
|
||||
|
||||
type UseLightPushResult = {
|
||||
push?: undefined | PushFn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns light push methods bound to node and encoder
|
||||
* @param {Object} params.node - node that implements ILightPush, hook does nothing if empty
|
||||
* @param {Object} params.encoder - encoder for processing messages, hook does nothing if empty
|
||||
* @returns {Object} methods of ILightPush such as push
|
||||
*/
|
||||
export const useLightPush = (
|
||||
params: UseLightPushParams,
|
||||
): UseLightPushResult => {
|
||||
const { node, encoder } = params;
|
||||
|
||||
const push = React.useCallback<PushFn>(
|
||||
(message, opts = undefined) => {
|
||||
return node!.lightPush.push(encoder as IEncoder, message, opts);
|
||||
},
|
||||
[node, encoder],
|
||||
);
|
||||
|
||||
if (!node && !encoder) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
push,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,71 @@
|
|||
import React from "react";
|
||||
import type {
|
||||
Peer,
|
||||
PeerProtocolsChangeData,
|
||||
} from "@libp2p/interface-peer-store";
|
||||
import type { Waku } from "@waku/interfaces";
|
||||
|
||||
type UsePeersParams = {
|
||||
node: undefined | Waku;
|
||||
};
|
||||
|
||||
type UsePeersResults = {
|
||||
storePeers?: undefined | Peer[];
|
||||
filterPeers?: undefined | Peer[];
|
||||
lightPushPeers?: undefined | Peer[];
|
||||
peerExchangePeers?: undefined | Peer[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook returns map of peers for different protocols.
|
||||
* If protocol is not implemented on the node peers are undefined.
|
||||
* @example
|
||||
* const { storePeers } = usePeers({ node });
|
||||
* @param {Waku} params.node - Waku node, if not set then no peers will be returned
|
||||
* @returns {Object} map of peers, if some of the protocols is not implemented then undefined
|
||||
*/
|
||||
export const usePeers = (params: UsePeersParams): UsePeersResults => {
|
||||
const { node } = params;
|
||||
const [peers, setPeers] = React.useState<UsePeersResults>({});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listener = async (_event: CustomEvent<PeerProtocolsChangeData>) => {
|
||||
const peers = await Promise.all([
|
||||
handleCatch(node?.store?.peers()),
|
||||
handleCatch(node?.filter?.peers()),
|
||||
handleCatch(node?.lightPush?.peers()),
|
||||
handleCatch(node?.peerExchange?.peers()),
|
||||
]);
|
||||
|
||||
setPeers({
|
||||
storePeers: peers[0],
|
||||
filterPeers: peers[1],
|
||||
lightPushPeers: peers[2],
|
||||
peerExchangePeers: peers[3],
|
||||
});
|
||||
};
|
||||
|
||||
node.libp2p.peerStore.addEventListener("change:protocols", listener);
|
||||
|
||||
return () => {
|
||||
node.libp2p.peerStore.removeEventListener("change:protocols", listener);
|
||||
};
|
||||
}, [node, setPeers]);
|
||||
|
||||
return peers;
|
||||
};
|
||||
|
||||
// TODO: handle error in case fetching of peers failed
|
||||
function handleCatch(promise?: Promise<Peer[]>): Promise<Peer[] | undefined> {
|
||||
if (!promise) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return promise.catch((_) => {
|
||||
return undefined;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import React from "react";
|
||||
import type {
|
||||
IDecodedMessage,
|
||||
IDecoder,
|
||||
IStore,
|
||||
StoreQueryOptions,
|
||||
Waku,
|
||||
} from "@waku/interfaces";
|
||||
|
||||
import type { HookState } from "./types";
|
||||
|
||||
type AbstractStoreNode = Waku & {
|
||||
store: IStore;
|
||||
};
|
||||
|
||||
type UseStoreMessagesParams = {
|
||||
node: undefined | AbstractStoreNode;
|
||||
decoder: undefined | IDecoder<IDecodedMessage>;
|
||||
options: StoreQueryOptions;
|
||||
};
|
||||
|
||||
type UseStoreMessagesResult = HookState & {
|
||||
messages: IDecodedMessage[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for retrieving messages from Store protocol based on options
|
||||
* @example
|
||||
* const { isLoading, error, messages } = useStoreMessages({node, decoder, options});
|
||||
* @param {Object} node - node that implement Store, hook does nothing if undefined
|
||||
* @param {Object} decoder - decoder to use for getting messages, hook does nothing if undefined
|
||||
* @param {StoreQueryOptions} options - options to initiate query to get messages
|
||||
* @returns {Object} hook state (isLoading, error) and messages array
|
||||
*/
|
||||
export const useStoreMessages = (
|
||||
params: UseStoreMessagesParams,
|
||||
): UseStoreMessagesResult => {
|
||||
const { node, decoder, options } = params;
|
||||
|
||||
const [error, setError] = React.useState<undefined | string>(undefined);
|
||||
const [isLoading, setLoading] = React.useState<boolean>(false);
|
||||
const [messages, setMessage] = React.useState<IDecodedMessage[]>([]);
|
||||
|
||||
const pushMessage = React.useCallback(
|
||||
(newMessages: IDecodedMessage[]): void => {
|
||||
if (!newMessages || !newMessages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMessage((prev) => [...prev, ...newMessages]);
|
||||
},
|
||||
[setMessage],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!node || !decoder) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
for await (const promises of node.store.queryGenerator(
|
||||
[decoder],
|
||||
options,
|
||||
)) {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messagesRaw = await Promise.all(promises);
|
||||
const filteredMessages = messagesRaw.filter(
|
||||
(v): v is IDecodedMessage => !!v,
|
||||
);
|
||||
|
||||
pushMessage(filteredMessages);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false);
|
||||
setError(
|
||||
`Failed to query messages from store: ${
|
||||
err?.message || "no message"
|
||||
}`,
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// TODO: missing dependency on options, it will prevent consecutive update if options change
|
||||
}, [node, decoder, pushMessage, setError, setLoading]);
|
||||
|
||||
return {
|
||||
error,
|
||||
isLoading,
|
||||
messages,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue