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:
Sasha 2023-02-28 00:57:59 +01:00 committed by GitHub
parent bd3bfb8134
commit e4d0106499
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1540 additions and 2578 deletions

View File

@ -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"],

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
dist

3533
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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": [
{

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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";

View File

@ -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;
};

View File

@ -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]);

View File

@ -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,
});
};

83
src/useFilterMessages.ts Normal file
View File

@ -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,
};
};

View File

@ -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,
};
};

54
src/useLightPush.ts Normal file
View File

@ -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,
};
};

71
src/usePeers.ts Normal file
View File

@ -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;
});
}

103
src/useStoreMessages.ts Normal file
View File

@ -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,
};
};