mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
Group context and hook creation so that it is exportable. (#6451)
CHANGELOG_BEGIN Export Ledger context and hook creation to enable nested interaction, with different parties or different ledgers, within one React app. CHANGELOG_END * Group context and hook creation so that it is exportable. * Use undefined as default state to avoid cast * Word choice * Document new functions * Revert commit to build script * Test nesting of contexts * Document extra feature in README * Reorganize code to preserve individual function documentation. * Correct names to starting with lowercase in build. * Single quote imports and spacing style * Add copyright notice * Spacing around useReload * Use a good variable name * Do not export by default Co-authored-by: Martin Huschenbett <martin.huschenbett@posteo.me>
This commit is contained in:
parent
6bab178195
commit
ea344518e9
@ -13,9 +13,8 @@ load("@build_environment//:configuration.bzl", "sdk_version")
|
||||
sources = [
|
||||
"index.ts",
|
||||
"index.test.ts",
|
||||
"context.ts",
|
||||
"hooks.ts",
|
||||
"DamlLedger.ts",
|
||||
"createLedgerContext.ts",
|
||||
"defaultLedgerContext.ts",
|
||||
]
|
||||
|
||||
da_ts_library(
|
||||
|
@ -1,28 +0,0 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { DamlLedgerContext, DamlLedgerState } from './context';
|
||||
import Ledger from '@daml/ledger';
|
||||
import { Party } from '@daml/types';
|
||||
|
||||
type Props = {
|
||||
token: string;
|
||||
httpBaseUrl?: string;
|
||||
wsBaseUrl?: string;
|
||||
party: Party;
|
||||
}
|
||||
|
||||
const DamlLedger: React.FC<Props> = ({token, httpBaseUrl, wsBaseUrl, party, children}) => {
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
const ledger = useMemo(() => new Ledger({token, httpBaseUrl, wsBaseUrl}), [token, httpBaseUrl, wsBaseUrl]);
|
||||
const state: DamlLedgerState = useMemo(() => ({
|
||||
reloadToken,
|
||||
triggerReload: (): void => setReloadToken(x => x + 1),
|
||||
party,
|
||||
ledger,
|
||||
}), [party, ledger, reloadToken]);
|
||||
return React.createElement(DamlLedgerContext.Provider, {value: state}, children);
|
||||
}
|
||||
|
||||
export default DamlLedger;
|
@ -117,6 +117,15 @@ the result.
|
||||
const {contract, loading} = useStreamFetchByKey(ContractTemplate, () => key, [dependency1, dependency2, ...]);
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
In order to interact as multiple parties or to connect to several ledgers, one needs to create an extra
|
||||
`DamlLedger` [contexts](https://reactjs.org/docs/context.html) specific to your requirement.
|
||||
|
||||
`createLedgerContext`
|
||||
---------------------
|
||||
`createLedgerContext` returns another `DamlLedger` context and associated hooks (`useParty`, `useLedger` ... etc)
|
||||
that will look up their connection within that returned context.
|
||||
|
||||
## Source
|
||||
https://github.com/digital-asset/daml.
|
||||
|
@ -1,15 +0,0 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
import Ledger from '@daml/ledger';
|
||||
import { Party } from '@daml/types';
|
||||
|
||||
export type DamlLedgerState = {
|
||||
reloadToken: unknown;
|
||||
triggerReload: () => void;
|
||||
party: Party;
|
||||
ledger: Ledger;
|
||||
}
|
||||
|
||||
export const DamlLedgerContext = React.createContext(null as DamlLedgerState | null);
|
203
language-support/ts/daml-react/createLedgerContext.ts
Normal file
203
language-support/ts/daml-react/createLedgerContext.ts
Normal file
@ -0,0 +1,203 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, {useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Party, Template } from '@daml/types';
|
||||
import Ledger, { CreateEvent, Query } from '@daml/ledger';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
type DamlLedgerState = {
|
||||
reloadToken: unknown;
|
||||
triggerReload: () => void;
|
||||
party: Party;
|
||||
ledger: Ledger;
|
||||
}
|
||||
|
||||
/**
|
||||
* React props to initiate a connect to a DAML ledger.
|
||||
*/
|
||||
export type LedgerProps = {
|
||||
token: string;
|
||||
httpBaseUrl?: string;
|
||||
wsBaseUrl?: string;
|
||||
party: Party;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a ``query`` against the ledger.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*/
|
||||
export type QueryResult<T extends object, K, I extends string> = {
|
||||
/** Contracts matching the query. */
|
||||
contracts: readonly CreateEvent<T, K, I>[];
|
||||
/** Indicator for whether the query is executing. */
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a ``fetch`` against the ledger.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*/
|
||||
export type FetchResult<T extends object, K, I extends string> = {
|
||||
/** Contracts of the given contract template and key. */
|
||||
contract: CreateEvent<T, K, I> | null;
|
||||
/** Indicator for whether the fetch is executing. */
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A LedgerContext is a React context that stores information about a DAML Ledger
|
||||
* and hooks necessary to use it.
|
||||
*/
|
||||
export type LedgerContext = {
|
||||
DamlLedger: React.FC<LedgerProps>;
|
||||
useParty: () => Party;
|
||||
useLedger: () => Ledger;
|
||||
useQuery: <T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]) => QueryResult<T, K, I>;
|
||||
useFetchByKey: <T extends object, K, I extends string>(template: Template<T, K, I>, keyFactory: () => K, keyDeps: readonly unknown[]) => FetchResult<T, K, I>;
|
||||
useStreamQuery: <T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]) => QueryResult<T, K, I>;
|
||||
useStreamFetchByKey: <T extends object, K, I extends string>(template: Template<T, K, I>, keyFactory: () => K, keyDeps: readonly unknown[]) => FetchResult<T, K, I>;
|
||||
useReload: () => () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [[LedgerContext]]. One should use this function, instead of the default [[DamlLedger]],
|
||||
* where one needs to be able to nest ledger interactions, by different parties or connections, within
|
||||
* one React application.
|
||||
*
|
||||
* @param contextName Used to refer to a context in case of errors.
|
||||
*/
|
||||
export function createLedgerContext(contextName="DamlLedgerContext"): LedgerContext {
|
||||
|
||||
// NOTE(MH, useEffect dependencies): There are various places in this file
|
||||
// where we need to maintain the dependencies of the `useEffect` hook manually
|
||||
// and there's no tool to help us enfore they are correct. Thus, we need to be
|
||||
// extra careful in these locations. If we add too many dependencies, we will
|
||||
// make unnecessary network requests. If we forget adding some dependencies, we
|
||||
// not make a new network request although they are required to refresh data.
|
||||
|
||||
const ledgerContext = React.createContext<DamlLedgerState | undefined>(undefined);
|
||||
const DamlLedger: React.FC<LedgerProps> = ({token, httpBaseUrl, wsBaseUrl, party, children}) => {
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
const ledger = useMemo(() => new Ledger({token, httpBaseUrl, wsBaseUrl}), [token, httpBaseUrl, wsBaseUrl]);
|
||||
const state: DamlLedgerState = useMemo(() => ({
|
||||
reloadToken,
|
||||
triggerReload: (): void => setReloadToken(x => x + 1),
|
||||
party,
|
||||
ledger,
|
||||
}), [party, ledger, reloadToken]);
|
||||
return React.createElement(ledgerContext.Provider, {value: state}, children);
|
||||
}
|
||||
|
||||
const useDamlState = (): DamlLedgerState => {
|
||||
const state = useContext(ledgerContext);
|
||||
if (!state) {
|
||||
throw Error(`Trying to use ${contextName} before initializing.`);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const useParty = (): Party => {
|
||||
const state = useDamlState();
|
||||
return state.party;
|
||||
}
|
||||
|
||||
const useLedger = (): Ledger => {
|
||||
return useDamlState().ledger;
|
||||
}
|
||||
|
||||
function useQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]): QueryResult<T, K, I> {
|
||||
const state = useDamlState();
|
||||
const [result, setResult] = useState<QueryResult<T, K, I>>({contracts: [], loading: true});
|
||||
useEffect(() => {
|
||||
setResult({contracts: [], loading: true});
|
||||
const query = queryFactory ? queryFactory() : undefined;
|
||||
const load = async (): Promise<void> => {
|
||||
const contracts = await state.ledger.query(template, query);
|
||||
setResult({contracts, loading: false});
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
load();
|
||||
// NOTE(MH): See note at the top of the file regarding "useEffect dependencies".
|
||||
}, [state.ledger, state.reloadToken, template, ...(queryDeps ?? [])]);
|
||||
return result;
|
||||
}
|
||||
|
||||
function useFetchByKey<T extends object, K, I extends string>(template: Template<T, K, I>, keyFactory: () => K, keyDeps: readonly unknown[]): FetchResult<T, K, I> {
|
||||
const state = useDamlState();
|
||||
const [result, setResult] = useState<FetchResult<T, K, I>>({contract: null, loading: true});
|
||||
useEffect(() => {
|
||||
const key = keyFactory();
|
||||
setResult({contract: null, loading: true});
|
||||
const load = async (): Promise<void> => {
|
||||
const contract = await state.ledger.fetchByKey(template, key);
|
||||
setResult({contract, loading: false});
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
load();
|
||||
// NOTE(MH): See note at the top of the file regarding "useEffect dependencies".
|
||||
}, [state.ledger, state.reloadToken, template, ...(keyDeps ?? [])]);
|
||||
return result;
|
||||
}
|
||||
|
||||
function useStreamQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]): QueryResult<T, K, I> {
|
||||
const [result, setResult] = useState<QueryResult<T, K, I>>({contracts: [], loading: true});
|
||||
const state = useDamlState();
|
||||
useEffect(() => {
|
||||
setResult({contracts: [], loading: true});
|
||||
const query = queryFactory ? queryFactory() : undefined;
|
||||
console.debug(`mount useStreamQuery(${template.templateId}, ...)`, query);
|
||||
const stream = state.ledger.streamQuery(template, query);
|
||||
stream.on('live', () => setResult(result => ({...result, loading: false})));
|
||||
stream.on('change', contracts => setResult(result => ({...result, contracts})));
|
||||
stream.on('close', closeEvent => {
|
||||
console.error('useStreamQuery: web socket closed', closeEvent);
|
||||
setResult(result => ({...result, loading: true}));
|
||||
});
|
||||
return (): void => {
|
||||
console.debug(`unmount useStreamQuery(${template.templateId}, ...)`, query);
|
||||
stream.close();
|
||||
};
|
||||
// NOTE(MH): See note at the top of the file regarding "useEffect dependencies".
|
||||
}, [state.ledger, template, ...(queryDeps ?? [])]);
|
||||
return result;
|
||||
}
|
||||
|
||||
function useStreamFetchByKey<T extends object, K, I extends string>(template: Template<T, K, I>, keyFactory: () => K, keyDeps: readonly unknown[]): FetchResult<T, K, I> {
|
||||
const [result, setResult] = useState<FetchResult<T, K, I>>({contract: null, loading: true});
|
||||
const state = useDamlState();
|
||||
useEffect(() => {
|
||||
setResult({contract: null, loading: true});
|
||||
const key = keyFactory();
|
||||
console.debug(`mount useStreamFetchByKey(${template.templateId}, ...)`, key);
|
||||
const stream = state.ledger.streamFetchByKey(template, key);
|
||||
stream.on('change', contract => setResult(result => ({...result, contract})));
|
||||
stream.on('close', closeEvent => {
|
||||
console.error('useStreamFetchByKey: web socket closed', closeEvent);
|
||||
setResult(result => ({...result, loading: true}));
|
||||
});
|
||||
setResult(result => ({...result, loading: false}));
|
||||
return (): void => {
|
||||
console.debug(`unmount useStreamFetchByKey(${template.templateId}, ...)`, key);
|
||||
stream.close();
|
||||
};
|
||||
// NOTE(MH): See note at the top of the file regarding "useEffect dependencies".
|
||||
}, [state.ledger, template, ...keyDeps]);
|
||||
return result;
|
||||
}
|
||||
|
||||
const useReload = (): () => void => {
|
||||
const state = useDamlState();
|
||||
return (): void => state.triggerReload();
|
||||
}
|
||||
|
||||
return { DamlLedger, useParty, useLedger, useQuery, useFetchByKey, useStreamQuery, useStreamFetchByKey, useReload };
|
||||
}
|
109
language-support/ts/daml-react/defaultLedgerContext.ts
Normal file
109
language-support/ts/daml-react/defaultLedgerContext.ts
Normal file
@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { createLedgerContext, FetchResult, QueryResult, LedgerProps } from "./createLedgerContext";
|
||||
import { Party, Template } from '@daml/types';
|
||||
import Ledger, { Query } from '@daml/ledger';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const ledgerContext = createLedgerContext();
|
||||
|
||||
/**
|
||||
* Within a `DamlLedger` one can use the hooks provided here.
|
||||
*
|
||||
* @param props React props and children for this element.
|
||||
*/
|
||||
export function DamlLedger(props: React.PropsWithChildren<LedgerProps>): React.ReactElement|null {
|
||||
return ledgerContext.DamlLedger(props);
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to get the party currently connected to the ledger.
|
||||
*/
|
||||
export function useParty(): Party { return ledgerContext.useParty(); }
|
||||
|
||||
/**
|
||||
* React Hook that returns the Ledger instance to interact with the connected DAML ledger.
|
||||
*/
|
||||
export function useLedger(): Ledger { return ledgerContext.useLedger(); }
|
||||
|
||||
/**
|
||||
* React Hook for a ``query`` against the ledger.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*
|
||||
* @param template The contract template to filter for.
|
||||
* @param queryFactory A function returning a query. If the query is omitted, all visible contracts of the given template are returned.
|
||||
* @param queryDeps The dependencies of the query (which trigger a reload when changed).
|
||||
*
|
||||
* @return The result of the query.
|
||||
*/
|
||||
export function useQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory: () => Query<T>, queryDeps: readonly unknown[]): QueryResult<T, K, I>
|
||||
export function useQuery<T extends object, K, I extends string>(template: Template<T, K, I>): QueryResult<T, K, I>
|
||||
export function useQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]): QueryResult<T, K, I> {
|
||||
return ledgerContext.useQuery(template, queryFactory, queryDeps);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook for a lookup by key against the `/v1/fetch` endpoint of the JSON API.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*
|
||||
* @param template The template of the contracts to fetch.
|
||||
* @param keyFactory A function returning the contract key of the contracts to fetch.
|
||||
* @param keyDeps Dependencies of this hook (for which the fetch is reexecuted on change).
|
||||
*
|
||||
* @return The fetched contract.
|
||||
*/
|
||||
export function useFetchByKey<T extends object, K, I extends string>(template: Template<T, K, I>, keyFactory: () => K, keyDeps: readonly unknown[]): FetchResult<T, K, I> {
|
||||
return ledgerContext.useFetchByKey(template, keyFactory, keyDeps);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook to query the ledger, the returned result is updated as the ledger state changes.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*
|
||||
* @param template The template of the contracts to match.
|
||||
* @param queryFactory A function returning a query. If the query is omitted, all visible contracts of the given template are returned.
|
||||
* @param queryDeps The dependencies of the query (for which a change triggers an update of the result)
|
||||
*
|
||||
* @return The matching contracts.
|
||||
*/
|
||||
export function useStreamQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory: () => Query<T>, queryDeps: readonly unknown[]): QueryResult<T, K, I>
|
||||
export function useStreamQuery<T extends object, K, I extends string>(template: Template<T, K, I>): QueryResult<T, K, I>
|
||||
export function useStreamQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]): QueryResult<T, K, I> {
|
||||
return ledgerContext.useStreamQuery(template, queryFactory, queryDeps);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook to query the ledger. Same as useStreamQuery, but query by contract key instead.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*
|
||||
* @param template The template of the contracts to match.
|
||||
* @param queryFactory A function returning a contract key.
|
||||
* @param queryDeps The dependencies of the query (for which a change triggers an update of the result)
|
||||
*
|
||||
* @return The matching (unique) contract.
|
||||
*/
|
||||
export function useStreamFetchByKey<T extends object, K, I extends string>(template: Template<T, K, I>, keyFactory: () => K, keyDeps: readonly unknown[]): FetchResult<T, K, I> {
|
||||
return ledgerContext.useStreamFetchByKey(template, keyFactory, keyDeps);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook to reload all active queries.
|
||||
*/
|
||||
export function useReload(): () => void {
|
||||
return ledgerContext.useReload();
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Template, Party } from "@daml/types";
|
||||
import Ledger, { CreateEvent, Query } from '@daml/ledger';
|
||||
import { useState, useContext, useEffect } from "react";
|
||||
import { DamlLedgerState, DamlLedgerContext } from './context'
|
||||
|
||||
// NOTE(MH, useEffect dependencies): There are various places in this file
|
||||
// where we need to maintain the dependencies of the `useEffect` hook manually
|
||||
// and there's no tool to help us enfore they are correct. Thus, we need to be
|
||||
// extra careful in these locations. If we add too many dependencies, we will
|
||||
// make unnecessary network requests. If we forget adding some dependencies, we
|
||||
// not make a new network request although they are required to refresh data.
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
const useDamlState = (): DamlLedgerState => {
|
||||
const state = useContext(DamlLedgerContext);
|
||||
if (!state) {
|
||||
throw Error("Trying to use DamlLedgerContext before initializing.")
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to get the party currently connected to the ledger.
|
||||
*/
|
||||
export const useParty = (): Party => {
|
||||
const state = useDamlState();
|
||||
return state.party;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook that returns the Ledger instance to interact with the connected DAML ledger.
|
||||
*/
|
||||
export const useLedger = (): Ledger => {
|
||||
return useDamlState().ledger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a query against the ledger.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*/
|
||||
export type QueryResult<T extends object, K, I extends string> = {
|
||||
/** Contracts matching the query. */
|
||||
contracts: readonly CreateEvent<T, K, I>[];
|
||||
/** Indicator for whether the query is executing. */
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook for a ``query`` against the ledger.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*
|
||||
* @param template The contract template to filter for.
|
||||
* @param queryFactory A function returning a query. If the query is omitted, all visible contracts of the given template are returned.
|
||||
* @param queryDeps The dependencies of the query (which trigger a reload when changed).
|
||||
*
|
||||
* @return The result of the query.
|
||||
*/
|
||||
export function useQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory: () => Query<T>, queryDeps: readonly unknown[]): QueryResult<T, K, I>
|
||||
export function useQuery<T extends object, K, I extends string>(template: Template<T, K, I>): QueryResult<T, K, I>
|
||||
export function useQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]): QueryResult<T, K, I> {
|
||||
const state = useDamlState();
|
||||
const [result, setResult] = useState<QueryResult<T, K, I>>({contracts: [], loading: true});
|
||||
useEffect(() => {
|
||||
setResult({contracts: [], loading: true});
|
||||
const query = queryFactory ? queryFactory() : undefined;
|
||||
const load = async (): Promise<void> => {
|
||||
const contracts = await state.ledger.query(template, query);
|
||||
setResult({contracts, loading: false});
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
load();
|
||||
// NOTE(MH): See note at the top of the file regarding "useEffect dependencies".
|
||||
}, [state.ledger, state.reloadToken, template, ...(queryDeps ?? [])]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a ``fetch`` against the ledger.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*/
|
||||
export type FetchResult<T extends object, K, I extends string> = {
|
||||
/** Contracts of the given contract template and key. */
|
||||
contract: CreateEvent<T, K, I> | null;
|
||||
/** Indicator for whether the fetch is executing. */
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook for a lookup by key against the `/v1/fetch` endpoint of the JSON API.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*
|
||||
* @param template The template of the contracts to fetch.
|
||||
* @param keyFactory A function returning the contract key of the contracts to fetch.
|
||||
* @param keyDeps Dependencies of this hook (for which the fetch is reexecuted on change).
|
||||
*
|
||||
* @return The fetched contract.
|
||||
*/
|
||||
export function useFetchByKey<T extends object, K, I extends string>(template: Template<T, K, I>, keyFactory: () => K, keyDeps: readonly unknown[]): FetchResult<T, K, I> {
|
||||
const state = useDamlState();
|
||||
const [result, setResult] = useState<FetchResult<T, K, I>>({contract: null, loading: true});
|
||||
useEffect(() => {
|
||||
const key = keyFactory();
|
||||
setResult({contract: null, loading: true});
|
||||
const load = async (): Promise<void> => {
|
||||
const contract = await state.ledger.fetchByKey(template, key);
|
||||
setResult({contract, loading: false});
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
load();
|
||||
// NOTE(MH): See note at the top of the file regarding "useEffect dependencies".
|
||||
}, [state.ledger, state.reloadToken, template, ...(keyDeps ?? [])]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook to query the ledger, the returned result is updated as the ledger state changes.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*
|
||||
* @param template The template of the contracts to match.
|
||||
* @param queryFactory A function returning a query. If the query is omitted, all visible contracts of the given template are returned.
|
||||
* @param queryDeps The dependencies of the query (for which a change triggers an update of the result)
|
||||
*
|
||||
* @return The matching contracts.
|
||||
*
|
||||
*/
|
||||
export function useStreamQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory: () => Query<T>, queryDeps: readonly unknown[]): QueryResult<T, K, I>
|
||||
export function useStreamQuery<T extends object, K, I extends string>(template: Template<T, K, I>): QueryResult<T, K, I>
|
||||
export function useStreamQuery<T extends object, K, I extends string>(template: Template<T, K, I>, queryFactory?: () => Query<T>, queryDeps?: readonly unknown[]): QueryResult<T, K, I> {
|
||||
const [result, setResult] = useState<QueryResult<T, K, I>>({contracts: [], loading: true});
|
||||
const state = useDamlState();
|
||||
useEffect(() => {
|
||||
setResult({contracts: [], loading: true});
|
||||
const query = queryFactory ? queryFactory() : undefined;
|
||||
console.debug(`mount useStreamQuery(${template.templateId}, ...)`, query);
|
||||
const stream = state.ledger.streamQuery(template, query);
|
||||
stream.on('live', () => setResult(result => ({...result, loading: false})));
|
||||
stream.on('change', contracts => setResult(result => ({...result, contracts})));
|
||||
stream.on('close', closeEvent => {
|
||||
console.error('useStreamQuery: web socket closed', closeEvent);
|
||||
setResult(result => ({...result, loading: true}));
|
||||
});
|
||||
return (): void => {
|
||||
console.debug(`unmount useStreamQuery(${template.templateId}, ...)`, query);
|
||||
stream.close();
|
||||
};
|
||||
// NOTE(MH): See note at the top of the file regarding "useEffect dependencies".
|
||||
}, [state.ledger, template, ...(queryDeps ?? [])]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook to query the ledger. Same as useStreamQuery, but query by contract key instead.
|
||||
*
|
||||
* @typeparam T The contract template type of the query.
|
||||
* @typeparam K The contract key type of the query.
|
||||
* @typeparam I The template id type.
|
||||
*
|
||||
* @param template The template of the contracts to match.
|
||||
* @param queryFactory A function returning a contract key.
|
||||
* @param queryDeps The dependencies of the query (for which a change triggers an update of the result)
|
||||
*
|
||||
* @return The matching (unique) contract.
|
||||
*/
|
||||
export function useStreamFetchByKey<T extends object, K, I extends string>(template: Template<T, K, I>, keyFactory: () => K, keyDeps: readonly unknown[]): FetchResult<T, K, I> {
|
||||
const [result, setResult] = useState<FetchResult<T, K, I>>({contract: null, loading: true});
|
||||
const state = useDamlState();
|
||||
useEffect(() => {
|
||||
setResult({contract: null, loading: true});
|
||||
const key = keyFactory();
|
||||
console.debug(`mount useStreamFetchByKey(${template.templateId}, ...)`, key);
|
||||
const stream = state.ledger.streamFetchByKey(template, key);
|
||||
stream.on('change', contract => setResult(result => ({...result, contract})));
|
||||
stream.on('close', closeEvent => {
|
||||
console.error('useStreamFetchByKey: web socket closed', closeEvent);
|
||||
setResult(result => ({...result, loading: true}));
|
||||
});
|
||||
setResult(result => ({...result, loading: false}));
|
||||
return (): void => {
|
||||
console.debug(`unmount useStreamFetchByKey(${template.templateId}, ...)`, key);
|
||||
stream.close();
|
||||
};
|
||||
// NOTE(MH): See note at the top of the file regarding "useEffect dependencies".
|
||||
}, [state.ledger, template, ...keyDeps]);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook to reload all active queries.
|
||||
*/
|
||||
export const useReload = (): () => void => {
|
||||
const state = useDamlState();
|
||||
return (): void => state.triggerReload();
|
||||
}
|
@ -2,11 +2,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// NOTE(MH): Unfortunately the `act` function triggers this warning by looking
|
||||
// like a promis without being one.
|
||||
// like a promise without being one.
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import React, { ComponentType, useState } from 'react';
|
||||
import { renderHook, RenderHookResult, act } from '@testing-library/react-hooks';
|
||||
import DamlLedger, { useParty, useQuery, useFetchByKey, useStreamQuery, useStreamFetchByKey, useReload } from './index';
|
||||
import DamlLedger, { useParty, useQuery, useFetchByKey, useStreamQuery, useStreamFetchByKey, useReload, createLedgerContext } from './index';
|
||||
import { Template } from '@daml/types';
|
||||
import { Stream } from '@daml/ledger';
|
||||
import {EventEmitter} from 'events';
|
||||
@ -466,5 +466,30 @@ describe('useStreamFetchByKey', () => {
|
||||
act(() => result.current.setKey(key2));
|
||||
expect(mockStreamFetchByKey).toHaveBeenCalledTimes(1);
|
||||
expect(mockStreamFetchByKey).toHaveBeenLastCalledWith(Foo, key2);
|
||||
});
|
||||
|
||||
describe('createLedgerContext', () => {
|
||||
test('contexts can nest', () => {
|
||||
const innerLedger = createLedgerContext();
|
||||
const innerTOKEN = "inner_TOKEN";
|
||||
const innerPARTY = "inner_PARTY";
|
||||
const outerLedger = createLedgerContext('Outer');
|
||||
const outerTOKEN = "outer_TOKEN";
|
||||
const outerPARTY = "outer_PARTY";
|
||||
|
||||
const innerWrapper: ComponentType = ({children}) => React.createElement(innerLedger.DamlLedger, {token:innerTOKEN, party:innerPARTY}, children);
|
||||
const r1 = renderHook(() => innerLedger.useParty(), {wrapper:innerWrapper});
|
||||
|
||||
expect( r1.result.current).toBe(innerPARTY);
|
||||
|
||||
const outerWrapper: ComponentType = ({children}) => React.createElement(outerLedger.DamlLedger, {token:outerTOKEN, party:outerPARTY}, innerWrapper({children}) );
|
||||
const r2 = renderHook(() => outerLedger.useParty(), {wrapper:outerWrapper});
|
||||
expect( r2.result.current).toBe(outerPARTY);
|
||||
|
||||
const r3 = renderHook(() => innerLedger.useParty(), {wrapper:outerWrapper});
|
||||
expect( r3.result.current).toBe(innerPARTY);
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
});
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import DamlLedger from './DamlLedger';
|
||||
export { createLedgerContext, FetchResult, LedgerContext, QueryResult } from './createLedgerContext';
|
||||
|
||||
import { DamlLedger, useParty, useLedger, useQuery, useFetchByKey, useStreamQuery, useStreamFetchByKey, useReload } from "./defaultLedgerContext";
|
||||
export { useParty, useLedger, useQuery, useFetchByKey, useStreamQuery, useStreamFetchByKey, useReload };
|
||||
export default DamlLedger;
|
||||
|
||||
export * from './hooks';
|
||||
|
Loading…
Reference in New Issue
Block a user