import {IJob} from "./IJob";
import {IBridge} from "./IBridge";
import {Readily} from "./Readily";
import {global} from "./global";
/**
* The middleware that native(Android, iOS, Desktop etc) and JS talk to each other.
* The JS side doesn't need to know which native platform it is running on. Calls to
* the instance will be bridged to the bond bridge.
* @example
* import {Drmer} from "@drmer/core";
* const drmer = new Drmer();
* // 1. bind the bridge
* drmer.bindBridge(bridge);
* // or you can expose a global variable to `window` as name `androidBridge`, `browserBridge`, `desktopBridge`,
* // `macBridge`, `iOSBridge`, `drmer` will automatically bind it.
*
* // 2. call native functions
* drmer.run("AppService@test");
* drmer.callJson("ProjectService@get", {
* "id": 1,
* }).then((project) => {
* // process project
* console.log(project);
* });
* // or
* (async () => {
* const project = await drmer.callJson("ProjectService@get", {
* "id": 1,
* });
* console.log(project);
* })();
*
* // 3. native returns the results
* drmer.dequeue("li123", {
* volume: 10,
* });
*
* //
* // When you needs native to call JS proactively, you can register an listener to
* // an event on JS, then emit that event from native.
*
* // On JS
* drmer.on("app.pause", () => {
* // do things when app pause
* });
*
* // On native
* drmer.emit("app.pause");
* @memberof core
*/
class Drmer extends Readily {
private _bridge: IBridge | undefined;
private jobs: Map<string, IJob> = new Map();
private backCallbacks: Function[] = [];
private timeoutId: NodeJS.Timeout | undefined;
/**
* The current bond bridge
*/
public get bridge(): IBridge | undefined {
return this._bridge;
}
/**
* Register for callback when this instance turns into
* the ready state, will be called directly if it was ready.
* This will also start waiting for the bridge.
* @param fcn
*/
public onReady(fcn: () => void): void {
super.onReady(fcn);
this.waitForBridge();
}
private waitForBridge() {
if (this.bridge) {
return;
}
console.log("waiting for active bridge");
const bridges = [
"androidBridge",
"browserBridge",
"desktopBridge",
"macBridge",
"iOSBridge",
];
for (let i = 0; i < bridges.length; i++) {
const bridge = global[bridges[i]];
if (bridge) {
console.log(`found bridge: ${bridges[i]}`);
this.bindBridge(bridge);
return;
}
}
this.timeoutId = setTimeout(() => {
this.waitForBridge();
}, 50);
}
/**
* Unbind the bond bridge
*/
public unbindBridge(): void {
this._bridge = undefined;
this.ready = false;
}
/**
* Bind bridge manually, you need to unbind the bridge first if
* other bridge is bond first. After the bridge is bond, this instance
* will turn into the `ready` state.
* @param bridge - Bridge that will handle the calls.
*/
public bindBridge(bridge: IBridge): void {
if (this._bridge) {
console.error("bridge bond, unbind first if you need bind another bridge");
return;
}
this._bridge = bridge;
this.ready = true;
clearTimeout(this.timeoutId);
}
/**
* Bind the given bridge after some time.
* This gives us a chance that we can bind other bridges before the given time.
* For example, we can lazy bind the browser bridge when running on iOS, when the native
* bridge on iOS is ready before the given time, we will use it. And our programs can also run
* on browser with the given browser bridge.
* ```js
* import {bridge} from "@drmer/browser";
* drmer.lazyBindBridge(bridge);
* drmer.lazyBindBridge(bridge, 1000);
* ```
* @param bridge - bridge to be bound
* @param timeout - milliseconds to wait to bind given bridge
*/
public lazyBindBridge(bridge: IBridge, timeout: number = 500): void {
setTimeout(() => {
this.bindBridge(bridge);
}, timeout);
}
public close(): void {
const backCallbacks = this.backCallbacks;
do {
const callbackFn = backCallbacks.pop();
callbackFn && callbackFn();
} while (backCallbacks.length > 0);
}
/**
* Generate a uniq id that will be used by callbacks
* @private
*/
private getId() {
let id: string;
do {
id = "li" + (new Date()).getTime() + Math.floor(Math.random() * 1000);
if (!this.jobs.has(id)) {
return id
}
// eslint-disable-next-line no-constant-condition
} while (true);
}
/**
* Send commands to native
* Use this if you need the results.
* Use `run` instead if you don't needs the results.
* @param method - Method signature
* @param params - Arguments
*/
public call(method: string, params?: Object): Promise<any> {
return this.remoteCall(method, false, params);
}
/**
* Send commands to native
* Use this if you need to deserialize the result by JSON.
* Use `run` instead if you don't needs the results.
* @param method - Method signature
* @param params - Arguments
*/
public callJson(method: string, params?: Object): Promise<any> {
return this.remoteCall(method, true, params);
}
private remoteCall(method: string, needsJson: boolean, params?: Object) {
if (!this.bridge) {
console.error("No bridge bound, please bind a bridge first");
return null;
}
return new Promise((resolve: any) => {
const id = this.getId();
this.jobs.set(id, {
needsJson: needsJson,
callback: resolve,
})
this.bridge.postMessage(JSON.stringify({
id: id,
method: method,
params: params,
}));
});
}
/**
* Call native and get live results
* @param method - Method signature
* @param params - payload
* @param cbFn - callback listener
* @return job id
*/
public live(method: string, params?: Object, cbFn?: Function): string | null {
if (!this.bridge) {
console.error("No bridge bound, please bind a bridge first");
return null;
}
if (typeof params == "function") {
cbFn = params;
params = undefined;
}
const id = this.getId();
this.jobs.set(id, {
listen: true,
callback: cbFn,
})
this.bridge.postMessage(JSON.stringify({
id: id,
method: method,
params: params,
}));
return id;
}
/**
* Unregister for native live jobs
* @param id - job id
*/
public die(id: string): void {
this.jobs.delete(id)
}
/**
* Send commands to native
* Use this if you don't need the results.
* Use `call` or `callJson` if you need to get the result.
* @param method - Method signature id
* @param params - Arguments
*/
public run(method: string, params?: Object): void {
if (!this.bridge) {
return;
}
this.bridge.postMessage(JSON.stringify({
method: method,
params: params,
}));
}
/**
* This is called by the native parts to return results
* for remote calls.
* @param id - Job id
* @param res - Results
*/
public dequeue(id: string, res: Object): void {
if (!id || !this.jobs.has(id)) {
return;
}
const job = this.jobs.get(id);
if (typeof job?.callback === "function") {
if (job.needsJson) {
if (typeof res == "string") {
res = JSON.parse(res);
}
} else {
if (typeof res == "object") {
res = JSON.stringify(res);
}
}
job.callback(res);
}
if (job?.listen) {
return;
}
this.jobs.delete(id)
}
public destroy(): void {
super.destroy();
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this._bridge = undefined;
this.jobs.clear();
}
}
export {
Drmer,
}