Skip to content

Commit

Permalink
language: add daml-react package to ts libraries (digital-asset#4259)
Browse files Browse the repository at this point in the history
* language: add daml-react package to ts libraries

This adds the library formerly known as `daml-react-hook` into the
monorepo. We renamed it to `@daml/react`.

The tests sadly don't work with bazel right now because the local
imports aren't resolved correctly. Local testing with `yarn run test`
works as usual.

CHANGELOG_BEGIN
CHANGELOG_END

* address moritz comments

* get rid of DAVL mentions

* fix eslint warnings

* Update language-support/ts/daml-react/tsconfig.json

Co-Authored-By: Martin Huschenbett <martin.huschenbett@posteo.me>

Co-authored-by: Martin Huschenbett <martin.huschenbett@posteo.me>
  • Loading branch information
Robin Krom and hurryabit authored Jan 29, 2020
1 parent 34ac1b0 commit 2e9fe6a
Show file tree
Hide file tree
Showing 20 changed files with 1,129 additions and 20 deletions.
5 changes: 5 additions & 0 deletions language-support/ts/daml-react/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
index.d.ts
index.js
index.js.map
lib/
yarn.lock
74 changes: 74 additions & 0 deletions language-support/ts/daml-react/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright (c) 2020 The DAML Authors. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

load("@os_info//:os_info.bzl", "is_windows")
load("@build_bazel_rules_nodejs//:index.bzl", "pkg_npm")
load("@npm_bazel_typescript//:index.bzl", "ts_library")
load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar")
load("//language-support/ts:eslint.bzl", "eslint_test")
load("//language-support/ts:jest.bzl", "jest_test")
load("@sdk_version//:sdk_version.bzl", "sdk_version")

ts_library(
name = "daml-react",
srcs = glob([
"**/*.ts",
"**/*.tsx",
]),
data = [
":LICENSE",
":package.json",
],
module_name = "@daml/react",
node_modules = "@language_support_ts_deps//:node_modules",
tsconfig = ":tsconfig.json",
visibility = ["//visibility:public"],
deps = [
"//language-support/ts/daml-ledger",
"//language-support/ts/daml-types",
"@language_support_ts_deps//:node_modules",
],
) if not is_windows else None

# We can't reference any files outside of the directory, hence this rule.
genrule(
name = "license",
srcs = ["//:LICENSE"],
outs = ["LICENSE"],
cmd = """
cp $(location //:LICENSE) $@
""",
)

eslint_test(
name = "lint",
srcs = glob([
"**/*.ts",
"**/*.tsx",
]),
)

pkg_npm(
name = "npm_package",
srcs = [
":package.json",
":tsconfig.json",
],
substitutions = {"0.0.0-SDKVERSION": sdk_version},
visibility = ["//visibility:public"],
deps = [
"daml-react",
":license",
],
) if not is_windows else None

# This doesn't work currently because imports of local packages aren't resolved correctly.
# jest_test(
# name = "test",
# srcs = glob(["**/*.ts", "**/*.tsx"]),
# jest_config = ":jest.config.js",
# deps = [
# "//language-support/ts/daml-types",
# "//language-support/ts/daml-ledger",
# ],
# )
31 changes: 31 additions & 0 deletions language-support/ts/daml-react/DamlLedger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useReducer, useMemo } from 'react';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { DamlLedgerContext } from './context';
import Credentials from './credentials';
import * as LedgerStore from './ledgerStore';
import Ledger from '@daml/ledger';
import { reducer } from './reducer';

type Props = {
credentials: Credentials;
}

const DamlLedger: React.FC<Props> = (props) => {
const [store, dispatch] = useReducer(reducer, LedgerStore.empty());
const state = useMemo(() => ({
store,
dispatch,
party: props.credentials.party,
ledger: new Ledger(props.credentials.token),
}), [props.credentials, store, dispatch])
return (
<DamlLedgerContext.Provider value={state}>
{props.children}
</DamlLedgerContext.Provider>
);
}

export default DamlLedger;
21 changes: 21 additions & 0 deletions language-support/ts/daml-react/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//Copyright (c) 2020 The DAML Authors. All rights reserved.
//SPDX-License-Identifier: Apache-2.0

import Ledger from '@daml/ledger';
import * as LedgerStore from './ledgerStore';
import React from "react";
import { Action } from "./reducer";
import { Party } from '@daml/types';

export type DamlLedgerState = {
store: LedgerStore.Store;
dispatch: React.Dispatch<Action>;
party: Party;
ledger: Ledger;
}

export const DamlLedgerContext = React.createContext(null as DamlLedgerState | null);

27 changes: 27 additions & 0 deletions language-support/ts/daml-react/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { decode } from 'jwt-simple';

export type Credentials = {
party: string;
token: string;
ledgerId: string;
}

/**
* Check that the party in the token matches the party of the credentials and
* that the ledger ID in the token matches the given ledger id.
*/
export const preCheckCredentials = ({party, token, ledgerId}: Credentials): string | null => {
const decoded = decode(token, '', true);
if (!decoded.ledgerId || decoded.ledgerId !== ledgerId) {
return 'The password is not valid for the given ledger id.';
}
if (!decoded.party || decoded.party !== party) {
return 'The password is not valid for this user.';
}
return null;
}

export default Credentials;
136 changes: 136 additions & 0 deletions language-support/ts/daml-react/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Template, Choice, ContractId } from "@daml/types";
import { Query, CreateEvent } from '@daml/ledger';
import { useEffect, useMemo, useState, useContext } from "react";
import * as LedgerStore from './ledgerStore';
import * as TemplateStore from './templateStore';
import { setQueryLoading, setQueryResult, setFetchByKeyLoading, setFetchByKeyResult, addEvents } from "./reducer";
import { DamlLedgerState, DamlLedgerContext } from './context';

export const useDamlState = (): DamlLedgerState => {
const state = useContext(DamlLedgerContext);
if (!state) {
throw Error("Trying to use DamlLedgerContext before initializing.")
}
return state;
}

export const useParty = () => {
const state = useDamlState();
return state.party;
}

const loadQuery = async <T extends object>(state: DamlLedgerState, template: Template<T>, query: Query<T>) => {
state.dispatch(setQueryLoading(template, query));
const contracts = await state.ledger.query(template, query);
state.dispatch(setQueryResult(template, query, contracts));
}

const emptyQueryFactory = <T extends object>(): Query<T> => ({} as Query<T>);

export type QueryResult<T extends object, K> = {
contracts: CreateEvent<T, K>[];
loading: boolean;
}

/// React Hook for a query against the `/contracts/search` endpoint of the JSON API.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useQuery = <T extends object, K>(template: Template<T, K>, queryFactory: () => Query<T> = emptyQueryFactory, queryDeps?: readonly any[]): QueryResult<T, K> => {
const state = useDamlState();
const query = useMemo(queryFactory, queryDeps);
const contracts = LedgerStore.getQueryResult(state.store, template, query);
useEffect(() => {
if (contracts === undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
loadQuery(state, template, query);
}
}, [state, template, query, contracts]);
return contracts ?? TemplateStore.emptyQueryResult();
}

const loadFetchByKey = async <T extends object, K>(state: DamlLedgerState, template: Template<T, K>, key: K) => {
state.dispatch(setFetchByKeyLoading(template, key));
let contract;
if (key === undefined) {
console.error(`Calling useFetchByKey on template without a contract key: ${template}`);
contract = null;
} else {
contract = await state.ledger.lookupByKey(template, key as K extends undefined ? never : K);
}
state.dispatch(setFetchByKeyResult(template, key, contract));
}

export type FetchResult<T extends object, K> = {
contract: CreateEvent<T, K> | null;
loading: boolean;
}

/// React Hook for a lookup by key against the `/contracts/lookup` endpoint of the JSON API.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const useFetchByKey = <T extends object, K>(template: Template<T, K>, keyFactory: () => K, keyDeps?: readonly any[]): FetchResult<T, K> => {
const state = useDamlState();
const key = useMemo(keyFactory, keyDeps);
const contract = LedgerStore.getFetchByKeyResult(state.store, template, key);
useEffect(() => {
if (contract === undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
loadFetchByKey(state, template, key);
}
}, [state, template, key, contract]);
return contract ?? TemplateStore.emptyFetchResult();
}

const reloadTemplate = async <T extends object, K>(state: DamlLedgerState, template: Template<T, K>) => {
const templateStore = state.store.templateStores.get(template) as TemplateStore.Store<T, K> | undefined;
if (templateStore) {
const queries: Query<T>[] = Array.from(templateStore.queryResults.keys());
const keys: K[] = Array.from(templateStore.fetchByKeyResults.keys());
await Promise.all([
Promise.all(queries.map(async (query) => await loadQuery(state, template, query))),
Promise.all(keys.map(async (key) => await loadFetchByKey(state, template, key))),
]);
}
}

/// React Hook that returns a function to exercise a choice and a boolean
/// indicator whether the exercise is currently running.
export const useExercise = <T extends object, C, R>(choice: Choice<T, C, R>): [(cid: ContractId<T>, argument: C) => Promise<R>, boolean] => {
const [loading, setLoading] = useState(false);
const state = useDamlState();

const exercise = async (cid: ContractId<T>, argument: C) => {
setLoading(true);
const [result, events] = await state.ledger.exercise(choice, cid, argument);
state.dispatch(addEvents(events));
setLoading(false);
return result;
}
return [exercise, loading];
}

/// React Hook that returns a function to exercise a choice and a boolean
/// indicator whether the exercise is currently running.
export const usePseudoExerciseByKey = <T extends object, C, R>(choice: Choice<T, C, R>): [(key: Query<T>, argument: C) => Promise<R>, boolean] => {
const [loading, setLoading] = useState(false);
const state = useDamlState();

const exercise = async (key: Query<T>, argument: C) => {
setLoading(true);
const [result, events] = await state.ledger.exerciseByKey(choice, key, argument);
state.dispatch(addEvents(events));
setLoading(false);
return result;
}
return [exercise, loading];
}

/// React Hook to reload all queries currently present in the store.
export const useReload = (): () => Promise<void> => {
const state = useDamlState();
return async () => {
const templates = Array.from(state.store.templateStores.keys());
await Promise.all(templates.map((template) => reloadTemplate(state, template)));
}
}
8 changes: 8 additions & 0 deletions language-support/ts/daml-react/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import DamlLedger from './DamlLedger';

export default DamlLedger;

export * from './hooks';
14 changes: 14 additions & 0 deletions language-support/ts/daml-react/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

module.exports = {
testEnvironment: "node",
testMatch: [
"**/__tests__/**/*.+(ts|tsx|js)",
"**/?(*.)+(spec|test).+(ts|tsx|js)"
],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest"
}
}

Loading

0 comments on commit 2e9fe6a

Please sign in to comment.