Skip to content

Commit

Permalink
refactor: hydrate
Browse files Browse the repository at this point in the history
  • Loading branch information
gronxb committed Jun 30, 2024
1 parent 41f5693 commit 669fe53
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 88 deletions.
2 changes: 2 additions & 0 deletions packages/web/esbuild.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ await Promise.all([
target: "es2015",
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
outfile: "dist/commonjs/index.cjs",
platform: "node",
format: "cjs",
Expand All @@ -13,6 +14,7 @@ await Promise.all([
target: "es2015",
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
outfile: "dist/module/index.mjs",
platform: "browser",
format: "esm",
Expand Down
139 changes: 53 additions & 86 deletions packages/web/src/internal/bridgeInstance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
Bridge,
BridgeStore,
ExcludePrimitive,
ExtractStore,
KeyOfOrString,
Parser,
Expand All @@ -15,33 +14,51 @@ import {
timeout,
} from "@webview-bridge/util";

import { MethodNotFoundError, NativeMethodError } from "../error";
import { NativeMethodError } from "../error";
import { LinkBridgeOptions } from "../linkBridge";
import { LinkBridge } from "../types";
import { createPromiseProxy } from "./createPromiseProxy";
import { linkBridgeStore } from "./linkBridgeStore";

export class BridgeInstance<
T extends BridgeStore<T extends Bridge ? T : any>,
V extends ParserSchema<any> = ParserSchema<any>,
> {
private defaultTimeoutMs = 2000;
private $proxy: LinkBridge<
ExcludePrimitive<ExtractStore<T>>,
Omit<T, "setState">,
V
>;

constructor(
private options: LinkBridgeOptions<T, V>,

public emitter: DefaultEmitter,
public bridgeMethods: string[],
public nativeInitialState: PrimitiveObject,
private _emitter: DefaultEmitter,
private _bridgeMethods: string[],
public _nativeInitialState: PrimitiveObject,
) {
this.$proxy = this.hydrate(bridgeMethods, nativeInitialState);
this.hydrate(_bridgeMethods, _nativeInitialState);
}

private postMessage(type: string, body?: unknown) {
private defaultTimeoutMs = 2000;

public store: Omit<T, "setState"> = {} as Omit<T, "setState">;

public isWebViewBridgeAvailable = Boolean(window.ReactNativeWebView);

public isNativeMethodAvailable(methodName: string) {
return (
typeof methodName === "string" &&
Boolean(window.ReactNativeWebView) &&
this._bridgeMethods.includes(methodName)
);
}

public addEventListener<EventName extends KeyOfOrString<V>>(
eventName: EventName,
listener: (args: Parser<V, EventName>) => void,
) {
return this._emitter.on(`postMessage/${String(eventName)}`, listener);
}

public loose: LinkBridge<ExtractStore<T>, Omit<T, "setState">, V> =
createPromiseProxy() as LinkBridge<ExtractStore<T>, Omit<T, "setState">, V>;

private _postMessage(type: string, body?: unknown) {
window.ReactNativeWebView?.postMessage(
JSON.stringify(
body
Expand All @@ -56,7 +73,7 @@ export class BridgeInstance<
);
}

private createNativeMethod(
private _createNativeMethod(
methodName: string,
throwOnError: boolean,
timeoutMs: number,
Expand All @@ -68,11 +85,11 @@ export class BridgeInstance<
return Promise.race(
[
createResolver({
emitter: this.emitter,
emitter: this._emitter,
methodName,
eventId,
evaluate: () => {
this.postMessage("bridge", {
this._postMessage("bridge", {
method: methodName,
eventId,
args,
Expand All @@ -89,15 +106,15 @@ export class BridgeInstance<
};
}

private willMethodThrowOnError(methodName: string) {
private _willMethodThrowOnError(methodName: string) {
const { throwOnError } = this.options;
return (
throwOnError === true ||
(Array.isArray(throwOnError) && throwOnError.includes(methodName))
);
}

private createLoose(
private _createLoose(
initialState: LinkBridge<ExtractStore<T>, Omit<T, "setState">, V>,
) {
const { timeout: timeoutMs = this.defaultTimeoutMs, onFallback } =
Expand All @@ -113,9 +130,9 @@ export class BridgeInstance<
) {
return target[methodName];
}
return this.createNativeMethod(
return this._createNativeMethod(
methodName,
this.willMethodThrowOnError(methodName),
this._willMethodThrowOnError(methodName),
timeoutMs,
onFallback,
);
Expand All @@ -127,19 +144,19 @@ export class BridgeInstance<
bridgeMethods: string[],
nativeInitialState: PrimitiveObject = {},
) {
const {
timeout: timeoutMs = this.defaultTimeoutMs,
onFallback,
onReady,
} = this.options;
this._bridgeMethods = bridgeMethods;
this._nativeInitialState = nativeInitialState;

const { timeout: timeoutMs = this.defaultTimeoutMs, onFallback } =
this.options;

const initialState = bridgeMethods.reduce(
(acc, methodName) => {
return {
...acc,
[methodName]: this.createNativeMethod(
[methodName]: this._createNativeMethod(
methodName,
this.willMethodThrowOnError(methodName),
this._willMethodThrowOnError(methodName),
timeoutMs,
onFallback,
),
Expand All @@ -148,76 +165,26 @@ export class BridgeInstance<
{} as LinkBridge<ExtractStore<T>, Omit<T, "setState">, V>,
);

const loose = this.createLoose(initialState);
this.loose = this._createLoose(initialState);

const store = linkBridgeStore<T>(
this.emitter,
this.store = linkBridgeStore<T>(
this._emitter,
initialState,
nativeInitialState as ExtractStore<T>,
);

Object.assign(initialState, {
loose,
store,
isWebViewBridgeAvailable:
Boolean(window.ReactNativeWebView) && bridgeMethods.length > 0,
isNativeMethodAvailable(methodName: string) {
return (
typeof methodName === "string" &&
Boolean(window.ReactNativeWebView) &&
bridgeMethods.includes(methodName)
);
},
addEventListener: <EventName extends KeyOfOrString<V>>(
eventName: EventName,
listener: (args: Parser<V, EventName>) => void,
) => {
return this.emitter.on(`postMessage/${String(eventName)}`, listener);
},
});
this.isWebViewBridgeAvailable =
Boolean(window.ReactNativeWebView) && bridgeMethods.length > 0;

document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
this.postMessage("getBridgeState");
this._postMessage("getBridgeState");
}
});
this.postMessage("getBridgeState");

const proxy = new Proxy(initialState, {
get: (target, methodName: string) => {
if (methodName in target) {
return target[methodName];
}

this.postMessage("fallback", {
method: methodName,
});

if (this.willMethodThrowOnError(methodName)) {
return (...args: unknown[]) => {
onFallback?.(methodName, args);
return Promise.reject(new MethodNotFoundError(methodName));
};
} else {
console.warn(
`[WebViewBridge] ${methodName} is not defined, using fallback.`,
);
}
return () => Promise.resolve();
},
});
this._postMessage("getBridgeState");

for (const [eventName, ...args] of window.nativeBatchedEvents ?? []) {
this.emitter.emit(eventName, ...args);
this._emitter.emit(eventName, ...args);
}
window.nativeBatchedEvents = [];

onReady?.(proxy);
this.$proxy = proxy;
return proxy;
}

get proxy() {
return this.$proxy;
}
}
10 changes: 10 additions & 0 deletions packages/web/src/internal/createPromiseProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const createPromiseProxy = () => {
return new Proxy(
{},
{
get: () => {
return () => Promise.resolve();
},
},
);
};
32 changes: 30 additions & 2 deletions packages/web/src/linkBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "@webview-bridge/types";
import { createEvents, noop } from "@webview-bridge/util";

import { MethodNotFoundError } from "./error";
import { BridgeInstance } from "./internal/bridgeInstance";
import { LinkBridge } from "./types";

Expand Down Expand Up @@ -73,11 +74,38 @@ export const linkBridge = <
const unsubscribe = emitter.on(
"hydrate",
({ bridgeMethods, nativeInitialState }: HydrateEventPayload) => {
alert("hydrating");
instance.hydrate(bridgeMethods, nativeInitialState);
unsubscribe();
},
);
}
return instance.proxy; // to only expose instance;

const { onFallback, onReady } = options;

const proxy = new Proxy(instance, {
get: (target: any, methodName: string, proxy) => {
if (methodName in target) {
return target[methodName];
}

proxy._postMessage("fallback", {
method: methodName,
});

if (proxy._willMethodThrowOnError(methodName)) {
return (...args: unknown[]) => {
onFallback?.(methodName, args);
return Promise.reject(new MethodNotFoundError(methodName));
};
} else {
console.warn(
`[WebViewBridge] ${methodName} is not defined, using fallback.`,
);
}
return () => Promise.resolve();
},
}) as LinkBridge<ExcludePrimitive<ExtractStore<T>>, Omit<T, "setState">, V>;

onReady?.(proxy);
return proxy;
};

0 comments on commit 669fe53

Please sign in to comment.