From 72dd3958cc447ad25a6f292cd2f710b028051315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=9E=AC=EC=9B=90=20=28Jaewon=20Ahn=29?= <89064304+haenah-riiid@users.noreply.github.com> Date: Fri, 14 Oct 2022 22:48:55 +0900 Subject: [PATCH] change a lot (#17) Co-authored-by: JongChan Choi --- src/glue/child-window.ts | 12 ++--- src/glue/iframe.ts | 2 +- src/glue/parent-window.ts | 10 ++-- src/glue/parent.ts | 46 +++++++++++++++++ src/jotai/iframe.ts | 19 ++++++- src/jotai/index.ts | 39 ++++---------- src/jotai/parent.ts | 34 ++++++------ src/react/index.ts | 2 - src/react/useOnceEffect.ts | 16 ++++++ src/react/useWrpIframeSocket.ts | 18 ++----- src/react/useWrpParentSocket.ts | 20 ++++--- src/react/useWrpServer.ts | 92 --------------------------------- src/tee.ts | 36 +++++++++++++ 13 files changed, 165 insertions(+), 181 deletions(-) create mode 100644 src/glue/parent.ts create mode 100644 src/react/useOnceEffect.ts delete mode 100644 src/react/useWrpServer.ts create mode 100644 src/tee.ts diff --git a/src/glue/child-window.ts b/src/glue/child-window.ts index 867398f..f8df9fe 100644 --- a/src/glue/child-window.ts +++ b/src/glue/child-window.ts @@ -9,14 +9,14 @@ import { export interface CreateChildWindowSocketConfig { child: Window; - childOrigin: string; + childWindowOrigin: string; onClosed?: () => void; } export async function createChildWindowSocket( config: CreateChildWindowSocketConfig, ): Promise { - const { child, childOrigin, onClosed } = config; - await handshake(child, childOrigin); + const { child, childWindowOrigin, onClosed } = config; + await handshake(child, childWindowOrigin); const healthcheckId = setInterval(healthcheck, 100); const glue = createGlue(); return { @@ -25,7 +25,7 @@ export async function createChildWindowSocket( const length = data.byteLength; const success = postGlueMessage({ target: child, - targetOrigin: childOrigin, + targetOrigin: childWindowOrigin, payload: data, }); if (!success) close(); @@ -44,7 +44,7 @@ export async function createChildWindowSocket( } // wait syn -> send syn-ack -> wait ack -async function handshake(child: Window, childOrigin: string) { +async function handshake(child: Window, childWindowOrigin: string) { let synAcked = false; const wait = defer(); const healthcheckId = setInterval(healthcheck, 100); @@ -84,7 +84,7 @@ async function handshake(child: Window, childOrigin: string) { synAcked = true; const success = postGlueHandshakeMessage({ target: child, - targetOrigin: childOrigin, + targetOrigin: childWindowOrigin, payload: "syn", }); if (!success) abort(new Error("Failed to send syn-ack.")); diff --git a/src/glue/iframe.ts b/src/glue/iframe.ts index 177112b..b4915cc 100644 --- a/src/glue/iframe.ts +++ b/src/glue/iframe.ts @@ -16,7 +16,7 @@ export async function createIframeSocket( ); const childWindowSocket = await createChildWindowSocket({ child: iframeWindow, - childOrigin: iframeOrigin, + childWindowOrigin: iframeOrigin, onClosed, }); onceIframeReloaded(iframeElement, childWindowSocket.close); diff --git a/src/glue/parent-window.ts b/src/glue/parent-window.ts index c4beeaa..f074200 100644 --- a/src/glue/parent-window.ts +++ b/src/glue/parent-window.ts @@ -12,13 +12,13 @@ import { export interface CreateParentWindowSocketConfig { parent?: Window | null; - parentOrigin: string; + parentWindowOrigin: string; onClosed?: () => void; } export async function createParentWindowSocket( config: CreateParentWindowSocketConfig, ): Promise { - const { parent = globalThis.parent, parentOrigin, onClosed } = config; + const { parent = globalThis.parent, parentWindowOrigin, onClosed } = config; if (!parent?.postMessage) throw new Error("There is no parent window."); if (parent === globalThis.self) throw new Error("Invalid parent window."); let handshakeIsDone = false; @@ -33,7 +33,7 @@ export async function createParentWindowSocket( const length = data.byteLength; const success = postGlueMessage({ target: parent, - targetOrigin: parentOrigin, + targetOrigin: parentWindowOrigin, payload: data, }); if (!success) close(); @@ -51,7 +51,7 @@ export async function createParentWindowSocket( function syn() { const success = postGlueHandshakeMessage({ target: parent!, - targetOrigin: parentOrigin, + targetOrigin: parentWindowOrigin, payload: "syn", }); if (!success) close(); @@ -60,7 +60,7 @@ export async function createParentWindowSocket( handshakeIsDone = true; const success = postGlueHandshakeMessage({ target: parent!, - targetOrigin: parentOrigin, + targetOrigin: parentWindowOrigin, payload: "ack", }); if (!success) close(); diff --git a/src/glue/parent.ts b/src/glue/parent.ts new file mode 100644 index 0000000..b6602a9 --- /dev/null +++ b/src/glue/parent.ts @@ -0,0 +1,46 @@ +import { defer } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/observer.ts"; +import { Socket } from "../socket.ts"; +import { createIosSocket } from "../glue/ios.ts"; +import { createAndroidSocket } from "../glue/android.ts"; +import { createParentWindowSocket } from "../glue/parent-window.ts"; + +export type UnsubscribeFn = () => void; +export type SetSocketFn = (socket?: Socket, error?: Error) => void; +export function subscribeParentSocket(set: SetSocketFn): UnsubscribeFn { + let run = true; + const unsubscribe = () => (run = false); + const parent = globalThis.opener || globalThis.parent; + if (parent && parent !== globalThis.self) { + (async () => { + while (run) { + const closed = defer(); + try { + const socket = await createParentWindowSocket({ + parent, + parentWindowOrigin: "*", + onClosed: () => closed.resolve(), + }); + run && set(socket, undefined); + } catch (error) { + run && set(undefined, error); + } + await closed; + run && set(undefined, undefined); + } + })(); + } else { + getAndroidOrIosSocket().then( + (socket) => run && set(socket), + ).catch( + (error) => run && set(undefined, error), + ); + } + return unsubscribe; +} + +async function getAndroidOrIosSocket(): Promise { + try { + return await Promise.any([createAndroidSocket(), createIosSocket()]); + } catch {} + return; +} diff --git a/src/jotai/iframe.ts b/src/jotai/iframe.ts index fea387f..37ac624 100644 --- a/src/jotai/iframe.ts +++ b/src/jotai/iframe.ts @@ -1,6 +1,10 @@ import { useEffect } from "react"; import { atom, PrimitiveAtom, useSetAtom } from "jotai"; -import { createWrpAtomSetFromSourceChannelAtom, WrpAtomSet } from "./index.ts"; +import { + createWrpAtomSetFromSourceChannelAtom, + PrimitiveSocketAtom, + WrpAtomSet, +} from "./index.ts"; import { Socket } from "../socket.ts"; import { createWrpChannel as createWrpChannelFn, @@ -11,6 +15,19 @@ import { UseWrpIframeSocketResult, } from "../react/useWrpIframeSocket.ts"; +export function useIframeWrpSocketAtomUpdateEffect( + socketAtom: PrimitiveSocketAtom, +): UseWrpIframeSocketResult { + const setSocket = useSetAtom(socketAtom); + const useWrpIframeSocketResult = useWrpIframeSocket(); + const { socket } = useWrpIframeSocketResult; + useEffect(() => { + if (!socket) return; + setSocket(socket); + }, [socket]); + return useWrpIframeSocketResult; +} + export function useIframeWrpAtomSetUpdateEffect( primitiveWrpAtomSetAtom: PrimitiveAtom, createWrpChannel: (socket: Socket) => WrpChannel = createWrpChannelFn, diff --git a/src/jotai/index.ts b/src/jotai/index.ts index c69d775..d37f3a7 100644 --- a/src/jotai/index.ts +++ b/src/jotai/index.ts @@ -1,16 +1,20 @@ -import { Atom, atom } from "jotai"; +import { Atom, atom, PrimitiveAtom } from "jotai"; import { selectAtom } from "jotai/utils"; import type { RpcClientImpl } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; -import { Type as WrpMessage } from "../generated/messages/pbkit/wrp/WrpMessage.ts"; import { Socket } from "../socket.ts"; import { createWrpChannel, WrpChannel } from "../channel.ts"; import { createWrpClientImpl } from "../rpc/client.ts"; -import { createWrpGuest, WrpGuest } from "../guest.ts"; +import { WrpGuest } from "../guest.ts"; +import tee from "../tee.ts"; -export type SocketAtom = Atom>; +export type SocketAtom = PrimitiveSocketAtom | AsyncSocketAtom; export type ChannelAtom = Atom; export type GuestAtom = Atom | undefined>; export type ClientImplAtom = Atom; + +export type PrimitiveSocketAtom = PrimitiveAtom; +export type AsyncSocketAtom = Atom>; + export interface WrpAtomSet { channelAtom: ChannelAtom; guestAtom: GuestAtom; @@ -36,32 +40,7 @@ export function createWrpAtomSetFromSourceChannelAtom( async (get) => { const sourceChannel = get(sourceChannelAtom); if (!sourceChannel) return; - const listeners: ((message?: WrpMessage) => void)[] = []; - const guest = createWrpGuest({ - channel: { - ...sourceChannel, - async *listen() { - for await (const message of sourceChannel.listen()) { - yield message; - for (const listener of listeners) listener(message); - listeners.length = 0; - } - }, - }, - }); - const channel: WrpChannel = { - ...sourceChannel, - async *listen() { - while (true) { - const message = await new Promise( - (resolve) => listeners.push(resolve), - ); - if (!message) break; - yield message; - } - }, - }; - return { channel, guest }; + return tee(sourceChannel); }, ); const channelAtom = selectAtom( diff --git a/src/jotai/parent.ts b/src/jotai/parent.ts index 6e2cfa7..0ffe017 100644 --- a/src/jotai/parent.ts +++ b/src/jotai/parent.ts @@ -1,32 +1,30 @@ -import { atom, useAtomValue } from "jotai"; +import { atom, useAtomValue, useSetAtom } from "jotai"; import type { RpcClientImpl } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; -import { createIosSocket } from "../glue/ios.ts"; -import { createAndroidSocket } from "../glue/android.ts"; -import { createParentWindowSocket } from "../glue/parent-window.ts"; +import { Socket } from "../socket.ts"; import { WrpChannel } from "../channel.ts"; import { WrpGuest } from "../guest.ts"; +import useOnceEffect from "../react/useOnceEffect.ts"; +import { subscribeParentSocket } from "../glue/parent.ts"; import { ChannelAtom, ClientImplAtom, createWrpAtomSet, GuestAtom, - SocketAtom, + PrimitiveSocketAtom, WrpAtomSet, } from "./index.ts"; -export const socketAtom: SocketAtom = atom(async () => { - return await Promise.any([ - createAndroidSocket(), - createIosSocket(), - createParentWindowSocket({ - parentWindowOrigin: "*", - }), - createParentWindowSocket({ - parent: globalThis.opener, - parentWindowOrigin: "*", - }), - ]).catch(() => undefined); -}); +/** + * Use it on root of your react application + */ +export function useInitParentSocketEffect() { + const setSocket = useSetAtom(socketAtom); + useOnceEffect(() => subscribeParentSocket(setSocket)); +} + +export const socketAtom: PrimitiveSocketAtom = atom( + undefined, +); const wrpAtomSet: WrpAtomSet = createWrpAtomSet(socketAtom); diff --git a/src/react/index.ts b/src/react/index.ts index 1fea265..2795f54 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -4,6 +4,4 @@ export * from "./useWrpIframeSocket.ts"; export { default as useWrpIframeSocket } from "./useWrpIframeSocket.ts"; export * from "./useWrpParentSocket.ts"; export { default as useWrpParentSocket } from "./useWrpParentSocket.ts"; -export * from "./useWrpServer.ts"; -export { default as useWrpServer } from "./useWrpServer.ts"; export * as server from "./server.ts"; diff --git a/src/react/useOnceEffect.ts b/src/react/useOnceEffect.ts new file mode 100644 index 0000000..cb71eea --- /dev/null +++ b/src/react/useOnceEffect.ts @@ -0,0 +1,16 @@ +import { useEffect, useRef } from "react"; + +/** + * Guaranteed to be called only once in a component's lifecycle. + * It called only once, even in strict mode. + */ +const useOnceEffect: typeof useEffect = (effect) => { + const effectHasFiredRef = useRef(); + useEffect(() => { + if (effectHasFiredRef.current) return; + else effectHasFiredRef.current = true; + return effect(); + }, []); +}; + +export default useOnceEffect; diff --git a/src/react/useWrpIframeSocket.ts b/src/react/useWrpIframeSocket.ts index 500d683..3635f17 100644 --- a/src/react/useWrpIframeSocket.ts +++ b/src/react/useWrpIframeSocket.ts @@ -1,7 +1,8 @@ import { defer } from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/observer.ts"; -import { Ref, useEffect, useRef, useState } from "react"; +import { Ref, useRef, useState } from "react"; import { Closer, Socket } from "../socket.ts"; import { createIframeSocket } from "../glue/iframe.ts"; +import useOnceEffect from "./useOnceEffect.ts"; export interface UseWrpIframeSocketResult { iframeRef: Ref; @@ -15,7 +16,6 @@ export default function useWrpIframeSocket(): UseWrpIframeSocketResult { let unmounted = false; let waitForReconnect = defer(); const iframeElement = iframeRef.current!; - iframeElement.addEventListener("load", tryReconnect); (async () => { // reconnection loop while (true) { if (unmounted) return; @@ -23,6 +23,7 @@ export default function useWrpIframeSocket(): UseWrpIframeSocketResult { socket = await createIframeSocket({ iframeElement, iframeOrigin: "*", + onClosed: tryReconnect, }); setSocket(socket); await waitForReconnect; @@ -32,24 +33,11 @@ export default function useWrpIframeSocket(): UseWrpIframeSocketResult { return () => { unmounted = true; tryReconnect(); - void iframeElement.removeEventListener("load", tryReconnect); }; function tryReconnect() { - if (socket) socket.close(); waitForReconnect.resolve(); waitForReconnect = defer(); } }); return { iframeRef, socket }; } - -// Guaranteed to be called only once in a component's lifecycle. -// It called only once, even in strict mode. -const useOnceEffect: typeof useEffect = (effect) => { - const effectHasFiredRef = useRef(); - useEffect(() => { - if (effectHasFiredRef.current) return; - else effectHasFiredRef.current = true; - return effect(); - }, []); -}; diff --git a/src/react/useWrpParentSocket.ts b/src/react/useWrpParentSocket.ts index e4a94e1..12df202 100644 --- a/src/react/useWrpParentSocket.ts +++ b/src/react/useWrpParentSocket.ts @@ -1,8 +1,7 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Socket } from "../socket.ts"; -import { createAndroidSocket } from "../glue/android.ts"; -import { createIosSocket } from "../glue/ios.ts"; -import { createParentWindowSocket } from "../glue/parent-window.ts"; +import useOnceEffect from "../react/useOnceEffect.ts"; +import { subscribeParentSocket } from "../glue/parent.ts"; export interface UseWrpParentSocketResult { socket: Socket | undefined; @@ -14,12 +13,11 @@ export interface UseWrpParentSocketResult { export default function useWrpParentSocket(): UseWrpParentSocketResult { const [socket, setSocket] = useState(undefined); const [error, setError] = useState(undefined); - useEffect(() => { - Promise.any([ - createAndroidSocket(), - createIosSocket(), - createParentWindowSocket({ parentWindowOrigin: "*" }), - ]).then(setSocket).catch(setError); - }, []); + useOnceEffect(() => + subscribeParentSocket((socket, error) => { + setSocket(socket); + setError(error); + }) + ); return { socket, error }; } diff --git a/src/react/useWrpServer.ts b/src/react/useWrpServer.ts deleted file mode 100644 index aed9092..0000000 --- a/src/react/useWrpServer.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - MethodDescriptor, - MethodImplHandler, -} from "https://deno.land/x/pbkit@v0.0.45/core/runtime/rpc.ts"; -import { - createEventEmitter, - EventEmitter, -} from "https://deno.land/x/pbkit@v0.0.45/core/runtime/async/event-emitter.ts"; -import { useEffect, useRef } from "react"; -import { WrpChannel } from "../channel.ts"; -import { createWrpHost, WrpRequest } from "../host.ts"; -import { Metadata } from "../metadata.ts"; -import { createWrpServer, createWrpServerImplBuilder } from "../rpc/server.ts"; - -export type GetStateFn = () => TState; -export type MethodImpl, TReq, TRes> = [ - MethodDescriptor, - ( - params: { - req: Parameters< - MethodImplHandler - >[0]; - res: Parameters< - MethodImplHandler - >[1]; - getState: GetStateFn; - stateChanges: EventEmitter; - }, - ) => void, -]; - -/** - * @deprecated use `server.useWrpServer, server.rpc` instead - */ -export default function useWrpServer< - TState extends Record, - TMethodImpls extends MethodImpl[], ->( - channel: WrpChannel | undefined, - state: TState, - methodImpls: TMethodImpls, -) { - const ref = useRef | undefined>(undefined); - useEffect(() => { - if (!channel) return; - if (!ref.current) ref.current = createRef(); - const getState = () => ref.current?.state!; - const stateChanges = ref.current.stateChanges; - (async () => { - const host = await createWrpHost({ - channel, - availableMethods: new Set( - methodImpls.map(([{ service, methodName }]) => - `${service.serviceName}/${methodName}` - ), - ), - }); - const builder = createWrpServerImplBuilder(); - for (const [methodDescriptor, methodImpl] of methodImpls) { - builder.register( - methodDescriptor, - (req, res) => methodImpl({ req, res, getState, stateChanges }), - ); - } - builder.finish(); - const methods = builder.drain(); - const server = await createWrpServer({ host, methods }); - server.listen(); - })(); - }, [channel]); - useEffect(() => { - if (!ref.current) ref.current = createRef(); - const prev = { ...ref.current?.state }; - ref.current.state = state; - for (const key of new Set([...Object.keys(prev), ...Object.keys(state)])) { - if (prev[key] !== state[key]) { - ref.current.stateChanges.emit(key, state[key]); - } - } - }, [state]); -} - -interface Ref { - state: TState; - stateChanges: EventEmitter; -} -function createRef(): Ref { - return { - state: undefined as unknown as TState, - stateChanges: createEventEmitter(), - }; -} diff --git a/src/tee.ts b/src/tee.ts new file mode 100644 index 0000000..636ede8 --- /dev/null +++ b/src/tee.ts @@ -0,0 +1,36 @@ +import { Type as WrpMessage } from "./generated/messages/pbkit/wrp/WrpMessage.ts"; +import { WrpChannel } from "./channel.ts"; +import { createWrpGuest, WrpGuest } from "./guest.ts"; + +export interface ChannelAndGuest { + channel: WrpChannel; + guest: Promise; +} +export default function tee(sourceChannel: WrpChannel): ChannelAndGuest { + const listeners: ((message?: WrpMessage) => void)[] = []; + const guest = createWrpGuest({ + channel: { + ...sourceChannel, + async *listen() { + for await (const message of sourceChannel.listen()) { + yield message; + for (const listener of listeners) listener(message); + listeners.length = 0; + } + }, + }, + }); + const channel: WrpChannel = { + ...sourceChannel, + async *listen() { + while (true) { + const message = await new Promise( + (resolve) => listeners.push(resolve), + ); + if (!message) break; + yield message; + } + }, + }; + return { channel, guest }; +}