Skip to content

Commit

Permalink
Properly handle errors during connection process (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
robknight authored Oct 24, 2024
1 parent f34687b commit c15ede3
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 29 deletions.
10 changes: 7 additions & 3 deletions apps/client-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ function App() {
}, [dispatch]);

useEffect(() => {
if (state.advice && state.connectionState === ConnectionState.CONNECTING) {
if (
state.advice &&
(state.connectionState === ConnectionState.CONNECTED ||
state.connectionState === ConnectionState.CONNECTING)
) {
state.advice.showClient();
}
}, [state.advice, state.connectionState]);
Expand Down Expand Up @@ -64,7 +68,7 @@ function App() {
{state.connectionState === ConnectionState.CONNECTING && (
<button
className="border-2 font-semibold cursor-pointer border-white py-1 px-2 uppercase active:translate-x-[2px] active:translate-y-[2px]"
onClick={() => dispatch({ type: "login", loggedIn: true })}
onClick={() => dispatch({ type: "connect" })}
>
Connect
</button>
Expand Down Expand Up @@ -395,7 +399,7 @@ function Authorize({
</button>
<button
className="border-2 font-semibold cursor-pointer border-white py-1 px-2 uppercase active:translate-x-[2px] active:translate-y-[2px]"
onClick={() => dispatch({ type: "logout" })}
onClick={() => dispatch({ type: "cancel-connection" })}
>
Cancel
</button>
Expand Down
4 changes: 2 additions & 2 deletions apps/client-web/src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ export function loadPODsFromStorage(): Record<string, PODCollection> {
try {
const serializedCollections = JSON.parse(storedSerializedPODs) as unknown;
const parsed = v.parse(
v.record(v.string(), v.array(v.string())),
v.record(v.string(), v.array(v.any())),
serializedCollections
);
for (const [collectionId, serializedPODs] of Object.entries(parsed)) {
result[collectionId] = new PODCollection(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
serializedPODs.map((str) => POD.fromJSON(JSON.parse(str)))
serializedPODs.map((obj) => POD.fromJSON(obj))
);
}
} catch (e) {
Expand Down
19 changes: 9 additions & 10 deletions apps/client-web/src/state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import { PODCollectionManager } from "./client/pod_collection_manager";
import { getIdentity } from "./client/utils";

export enum ConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
AUTHORIZED
DISCONNECTED = "DISCONNECTED",
CONNECTING = "CONNECTING",
CONNECTED = "CONNECTED",
AUTHORIZED = "AUTHORIZED"
}

export const StateContext = createContext<
Expand Down Expand Up @@ -93,8 +93,7 @@ export type ClientState = {

export type ClientAction =
| {
type: "login";
loggedIn: boolean;
type: "connect";
}
| {
type: "authorize";
Expand Down Expand Up @@ -122,13 +121,13 @@ export type ClientAction =
type: "clear-proof-in-progress";
}
| {
type: "logout";
type: "cancel-connection";
};

export function clientReducer(state: ClientState, action: ClientAction) {
switch (action.type) {
case "login":
return { ...state, loggedIn: action.loggedIn };
case "connect":
return { ...state, connectionState: ConnectionState.CONNECTED };
case "authorize":
if (!state.zapp || !state.zappOrigin) {
throw new Error("No zapp or zapp origin");
Expand Down Expand Up @@ -227,7 +226,7 @@ export function clientReducer(state: ClientState, action: ClientAction) {
...state,
proofInProgress: undefined
};
case "logout":
case "cancel-connection":
return initializeState();
}
}
21 changes: 19 additions & 2 deletions packages/app-connector-react/src/context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { type ParcnetAPI, type Zapp, connect } from "@parcnet-js/app-connector";
import type { ReactNode } from "react";
import { createContext, useCallback, useRef, useState } from "react";
import {
createContext,
useCallback,
useContext,
useRef,
useState
} from "react";

export enum ClientConnectionState {
DISCONNECTED = "DISCONNECTED",
Expand All @@ -13,6 +19,16 @@ export const ParcnetClientContext = createContext<
ParcnetClientContextType | undefined
>(undefined);

export const useParcnetClientContext = (): ParcnetClientContextType => {
const context = useContext(ParcnetClientContext);
if (!context) {
throw new Error(
"useParcnetClientContext must be used within a ParcnetClientProvider"
);
}
return context;
};

type ParcnetContextBase = {
zapp: Zapp;
connect: () => Promise<void>;
Expand Down Expand Up @@ -79,7 +95,8 @@ export function ParcnetIframeProvider({
}
if (
url !== connectUrl ||
connectionState === ClientConnectionState.DISCONNECTED
connectionState === ClientConnectionState.DISCONNECTED ||
connectionState === ClientConnectionState.ERROR
) {
setConnectionState(ClientConnectionState.CONNECTING);
try {
Expand Down
14 changes: 10 additions & 4 deletions packages/app-connector/src/adapters/hosted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Zapp } from "@parcnet-js/client-rpc";
import { InitializationMessageType } from "@parcnet-js/client-rpc";
import { createNanoEvents } from "nanoevents";
import { ParcnetAPI } from "../api_wrapper.js";
import { UserCancelledConnectionError } from "../errors.js";
import { ParcnetRPCConnector } from "../rpc_client.js";
import type { DialogController, ModalEvents } from "./iframe.js";
import { postWindowMessage } from "./iframe.js";
Expand All @@ -28,7 +29,7 @@ export function connectToHost(zapp: Zapp): Promise<ParcnetAPI> {

const emitter = createNanoEvents<ModalEvents>();

return new Promise<ParcnetAPI>((resolve) => {
return new Promise<ParcnetAPI>((resolve, reject) => {
// Create a new MessageChannel to communicate with the parent window
const chan = new MessageChannel();

Expand All @@ -42,9 +43,14 @@ export function connectToHost(zapp: Zapp): Promise<ParcnetAPI> {
// promise and return the API wrapper to the caller.
// See below for how the other port of the message channel is sent to
// the client.
client.start(() => {
resolve(new ParcnetAPI(client, emitter));
});
client.start(
() => {
resolve(new ParcnetAPI(client, emitter));
},
() => {
reject(new UserCancelledConnectionError());
}
);

// Send the other port of the message channel to the client
postWindowMessage(
Expand Down
23 changes: 18 additions & 5 deletions packages/app-connector/src/adapters/iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
import { InitializationMessageType } from "@parcnet-js/client-rpc";
import { createNanoEvents } from "nanoevents";
import { ParcnetAPI } from "../api_wrapper.js";
import { UserCancelledConnectionError } from "../errors.js";
import { ParcnetRPCConnector } from "../rpc_client.js";

export type ModalEvents = {
Expand Down Expand Up @@ -118,7 +119,10 @@ export function connect(
iframe.style.height = "100%";
iframe.src = normalizedUrl.toString();

return new Promise<ParcnetAPI>((resolve) => {
return new Promise<ParcnetAPI>((resolve, reject) => {
const unsubscribeCloseEvent = emitter.on("close", () => {
reject(new UserCancelledConnectionError());
});
iframe.addEventListener(
"load",
() => {
Expand All @@ -132,12 +136,21 @@ export function connect(
);
// Tell the RPC client to start. It will call the function we pass in
// when the connection is ready, at which point we can resolve the
// promise and return the API wrapper to the caller.
// promise and return the API wrapper to the caller. Alternatively,
// the user can cancel the connection, in which case we reject the
// promise.
// See below for how the other port of the message channel is sent to
// the client.
client.start(() => {
resolve(new ParcnetAPI(client, emitter));
});
client.start(
() => {
unsubscribeCloseEvent();
resolve(new ParcnetAPI(client, emitter));
},
() => {
unsubscribeCloseEvent();
reject(new UserCancelledConnectionError());
}
);

if (iframe.contentWindow) {
const contentWindow = iframe.contentWindow;
Expand Down
6 changes: 6 additions & 0 deletions packages/app-connector/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ export class ClientDisconnectedError extends Error {
super("Client disconnected");
}
}

export class UserCancelledConnectionError extends Error {
constructor() {
super("User cancelled connection");
}
}
12 changes: 9 additions & 3 deletions packages/app-connector/src/rpc_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ export class ParcnetRPCConnector implements ParcnetRPC, ParcnetEvents {
*
* @param onConnect - Callback to call when the client is ready.
*/
public start(onConnect: () => void): void {
const eventLoop = this.main(onConnect);
public start(onConnect: () => void, onCancel: () => void): void {
const eventLoop = this.main(onConnect, onCancel);
eventLoop.next();

// Set up a listener for messages from the client
Expand Down Expand Up @@ -304,7 +304,10 @@ export class ParcnetRPCConnector implements ParcnetRPC, ParcnetEvents {
*
* @param onConnect - Callback to call when the client is ready.
*/
private *main(onConnect: () => void): Generator<undefined, void, RPCMessage> {
private *main(
onConnect: () => void,
onCancel: () => void
): Generator<undefined, void, RPCMessage> {
// Loop indefinitely until we get a PARCNET_CLIENT_READY message
// In the meantime, we will handle PARCNET_CLIENT_SHOW and
// PARCNET_CLIENT_HIDE, as these may be necessary for the client to allow
Expand All @@ -321,6 +324,9 @@ export class ParcnetRPCConnector implements ParcnetRPC, ParcnetEvents {
this.#dialogController.show();
} else if (event.type === RPCMessageType.PARCNET_CLIENT_HIDE) {
this.#dialogController.close();
} else if (event.type === RPCMessageType.PARCNET_CLIENT_CANCEL) {
onCancel();
return;
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/client-helpers/src/connection/advice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export interface ConnectorAdvice {
result: SubscriptionUpdateResult,
subscriptionSerial: number
): void;
cancel(): void;
}
6 changes: 6 additions & 0 deletions packages/client-helpers/src/connection/iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export class AdviceChannel implements ConnectorAdvice {
});
}

public cancel(): void {
this.port.postMessage({
type: RPCMessageType.PARCNET_CLIENT_CANCEL
});
}

public subscriptionUpdate(
{ update, subscriptionId }: SubscriptionUpdateResult,
subscriptionSerial: number
Expand Down
4 changes: 4 additions & 0 deletions packages/client-rpc/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum RPCMessageType {
PARCNET_CLIENT_INVOKE_RESULT = "zupass-client-invoke-result",
PARCNET_CLIENT_INVOKE_ERROR = "zupass-client-invoke-error",
PARCNET_CLIENT_READY = "zupass-client-ready",
PARCNET_CLIENT_CANCEL = "zupass-client-cancel",
PARCNET_CLIENT_SHOW = "zupass-client-show",
PARCNET_CLIENT_HIDE = "zupass-client-hide",
PARCNET_CLIENT_SUBSCRIPTION_UPDATE = "zupass-client-subscription-update"
Expand Down Expand Up @@ -57,6 +58,9 @@ export const RPCMessageSchema = v.variant("type", [
v.object({
type: v.literal(RPCMessageType.PARCNET_CLIENT_READY)
}),
v.object({
type: v.literal(RPCMessageType.PARCNET_CLIENT_CANCEL)
}),
v.object({
type: v.literal(RPCMessageType.PARCNET_CLIENT_SHOW)
}),
Expand Down

0 comments on commit c15ede3

Please sign in to comment.