Skip to content

Commit

Permalink
react
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime committed Aug 13, 2024
1 parent 49263c6 commit 704552b
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .syncpackrc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
}
],
"versionGroups": [
{
"label": "Ignore @tanstack/react-query",
"dependencies": [
"@tanstack/react-query"
],
"isIgnored": true
},
{
"label": "Use workspace protocol for local packages",
"dependencies": [
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const isLegacyProvider = (
provider: PenumbraProvider,
): provider is PenumbraProvider & { request: () => Promise<void> } => 'request' in provider;

interface PenumbraClientOptions {
export interface PenumbraClientOptions {
/** Custom options for this client's `Transport`. */
transportOptions: Omit<ChannelTransportOptions, 'getPort'>;
}
Expand Down
57 changes: 57 additions & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@penumbra-zone/react",
"version": "1.2.0",
"license": "(MIT OR Apache-2.0)",
"description": "React package for connecting to any Penumbra extension, including Prax.",
"type": "module",
"scripts": {
"build": "tsc --build --verbose",
"clean": "rm -rfv dist *.tsbuildinfo package penumbra-zone-*.tgz",
"dev:pack": "tsc-watch --onSuccess \"$npm_execpath pack\"",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint:strict": "tsc --noEmit && eslint src --max-warnings 0"
},
"files": [
"dist"
],
"exports": {
".": "./src/index.ts",
"./components/*": "./src/components/*.tsx",
"./hooks/*": "./src/hooks/*.ts"
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./components/*": {
"types": "./dist/components/*.d.ts",
"default": "./dist/components/*.js"
},
"./hooks/*": {
"types": "./dist/hooks/*.d.ts",
"default": "./dist/hooks/*.js"
}
}
},
"dependencies": {
"@penumbra-zone/client": "workspace:*"
},
"devDependencies": {
"@bufbuild/protobuf": "^1.10.0",
"@connectrpc/connect": "^1.4.0",
"@penumbra-zone/transport-dom": "workspace:*",
"@tanstack/react-query": "5.51.23",
"@types/react": "^18.3.2",
"react": "^18.3.1"
},
"peerDependencies": {
"@bufbuild/protobuf": "^1.10.0",
"@connectrpc/connect": "^1.4.0",
"@penumbra-zone/protobuf": "workspace:*",
"@penumbra-zone/transport-dom": "workspace:*",
"react": "^18.3.1"
}
}
45 changes: 45 additions & 0 deletions packages/react/src/components/penumbra-context-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { penumbraContext } from '../context/penumbra-context.js';
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import {
PenumbraContextInput,
resolvePenumbraContextInput,
} from '../context/penumbra-context-input.js';
import { PenumbraEventDetail } from '@penumbra-zone/client';

export const PenumbraContextProvider = ({
client,
provider,
children,
}: PenumbraContextInput & { children?: ReactNode }) => {
const penumbra = useMemo(
() => resolvePenumbraContextInput({ client, provider }),
[client, provider],
);

const [providerState, setProviderState] = useState(penumbra.state);
const [providerConnected, setProviderConnected] = useState(penumbra.connected);

useEffect(() => {
console.log('setting providerConnected', providerState, penumbra.connected);
setProviderConnected(penumbra.connected);
}, [penumbra, providerState]);

const listener = useCallback((update: PenumbraEventDetail<'penumbrastate'>) => {
console.log('listener', update);
setProviderState(update.state);
}, []);

useEffect(() => {
const ac = new AbortController();
penumbra.onConnectionStateChange(listener, ac.signal);
return () => ac.abort();
}, [penumbra, listener]);

console.log('providerConnected', providerConnected);

return (
<penumbraContext.Provider value={providerConnected ? penumbra : penumbra}>
{children}
</penumbraContext.Provider>
);
};
25 changes: 25 additions & 0 deletions packages/react/src/context/penumbra-context-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { PenumbraClient, PenumbraProvider } from '@penumbra-zone/client';
import { PenumbraClientOptions } from '@penumbra-zone/client/client';

export interface PenumbraContextInput {
client?: PenumbraClient | PenumbraClientOptions;
provider?: PenumbraProvider | string;
}

export const resolvePenumbraContextInput = (input: PenumbraContextInput): PenumbraClient => {
let client = input.client instanceof PenumbraClient ? input.client : undefined;
const options = input.client instanceof PenumbraClient ? undefined : input.client;

const providerOrigin =
typeof input.provider === 'string'
? input.provider
: input.provider && new URL(input.provider.manifest).origin;

client ??= new PenumbraClient(providerOrigin, options);

if (providerOrigin) {
void client.attach(providerOrigin);
}

return client;
};
4 changes: 4 additions & 0 deletions packages/react/src/context/penumbra-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PenumbraClient } from '@penumbra-zone/client/client';
import { createContext } from 'react';

export const penumbraContext = createContext(new PenumbraClient());
60 changes: 60 additions & 0 deletions packages/react/src/hooks/use-penumbra-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ServiceType } from '@bufbuild/protobuf';
import { usePenumbra } from './use-penumbra.js';
import { QueryOptions, useQuery, UseQueryResult } from '@tanstack/react-query';
import { PromiseClient } from '@connectrpc/connect';

export const usePenumbraQuery = <S extends ServiceType>(
serviceType: S,
): PenumbraQuerier<S> | undefined => {
const penumbra = usePenumbra();

if (!penumbra.transport) {
return;
}

const wrappedMethods = Object.keys(serviceType.methods).map(
<N extends keyof S['methods']>(methodName: N) => {
const serviceClient = penumbra.service(serviceType);

const queryFn: QueryOptions['queryFn'] = ({ meta }) => {
const {
params: [input, options],
} = meta as { params: Parameters<PromiseClient<S>[N]> };
const response = serviceClient[methodName](input as never, options);

if (Symbol.asyncIterator in response) {
return Array.fromAsync(response);
} else {
return response;
}
};

const useMethodQuery: PenumbraQuerierMethod<S, N> = (queryOptions, ...params) =>
useQuery({
...queryOptions,
queryKey: [serviceType.typeName, methodName, params],
queryFn,
meta: { params },
});

return [methodName, useMethodQuery] as const;
},
);

return Object.fromEntries(wrappedMethods) as PenumbraQuerier<S>;
};

type PromiseClientMethod<S extends ServiceType, M extends keyof S['methods']> = PromiseClient<S>[M];

type PenumbraQuerierMethod<S extends ServiceType, M extends keyof S['methods']> = (
queryOptions: Omit<QueryOptions, 'queryFn' | 'queryKey' | 'meta'>,
...args: Parameters<PromiseClientMethod<S, M>>
) => UsePenumbraQueryResult<S, M>;

type UsePenumbraQueryResult<S extends ServiceType, M extends keyof S['methods']> = UseQueryResult<
ReturnType<PromiseClientMethod<S, M>>
>;

type PenumbraQuerier<S extends ServiceType> = {
[localName in keyof S['methods']]: PenumbraQuerierMethod<S, localName>;
};
15 changes: 15 additions & 0 deletions packages/react/src/hooks/use-penumbra-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PromiseClient } from '@connectrpc/connect';
import { useMemo } from 'react';
import { usePenumbra } from './use-penumbra.js';
import { ServiceType } from '@bufbuild/protobuf';

export const usePenumbraService = <S extends ServiceType>(
serviceType: S,
): PromiseClient<S> | undefined => {
const penumbra = usePenumbra();
const connected = penumbra.connected;
return useMemo(
() => (connected ? penumbra.service(serviceType) : undefined),
[connected, penumbra, serviceType],
);
};
4 changes: 4 additions & 0 deletions packages/react/src/hooks/use-penumbra.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { penumbraContext } from '../context/penumbra-context.js';

export const usePenumbra = () => useContext(penumbraContext);
5 changes: 5 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { usePenumbra } from './hooks/use-penumbra.js';
export { usePenumbraQuery } from './hooks/use-penumbra-query.js';
export { usePenumbraService } from './hooks/use-penumbra-service.js';
export { penumbraContext } from './context/penumbra-context.js';
export { PenumbraContextProvider } from './components/penumbra-context-provider.js';
14 changes: 14 additions & 0 deletions packages/react/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"exactOptionalPropertyTypes": false,
"composite": true,
"jsx": "react-jsx",
"module": "Node16",
"outDir": "dist",
"preserveWatchOutput": true,
"rootDir": "src",
"target": "ESNext"
},
"extends": "@tsconfig/strictest/tsconfig.json",
"include": ["src"]
}
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 704552b

Please sign in to comment.