Compare commits
108 Commits
v0.0.1
...
v0.0.2-dev
Author | SHA1 | Date |
---|---|---|
semantic-release-bot | 03a8db88d8 | |
Derrick Hammer | 2c931edb85 | |
Derrick Hammer | abd3d1d136 | |
Derrick Hammer | 1ad6c1d692 | |
Derrick Hammer | 8797249ae5 | |
Derrick Hammer | 85cc802977 | |
Derrick Hammer | eb65447adb | |
Derrick Hammer | c2031b4c7f | |
Derrick Hammer | ec937e786b | |
Derrick Hammer | f3e5cebe45 | |
Derrick Hammer | 9cb6d0b6e4 | |
Derrick Hammer | e3e6757e47 | |
Derrick Hammer | 3a1ea39a79 | |
Derrick Hammer | e19e4c1744 | |
Derrick Hammer | 3a41474b26 | |
Derrick Hammer | 339dbba08c | |
Derrick Hammer | c4c1fd8f6b | |
Derrick Hammer | 623601df27 | |
Derrick Hammer | cfd5f69cfb | |
Derrick Hammer | bc85076e7f | |
Derrick Hammer | 85ff38d871 | |
Derrick Hammer | 39499397ea | |
Derrick Hammer | 1882752839 | |
Derrick Hammer | 9791e7c4a1 | |
Derrick Hammer | d30528caa0 | |
Derrick Hammer | b3d6fd4668 | |
Derrick Hammer | b04db8668b | |
Derrick Hammer | 35bf05c25e | |
Derrick Hammer | f842cd84c4 | |
Derrick Hammer | c3c8e6fb3b | |
Derrick Hammer | 96dd1ad46e | |
Derrick Hammer | 10a7b4ebc6 | |
Derrick Hammer | 1c9a430d5e | |
Derrick Hammer | 8b678e81e8 | |
Derrick Hammer | 13ff64002d | |
Derrick Hammer | 40e139df0d | |
Derrick Hammer | 518bdca8ad | |
Derrick Hammer | 84bc6ce1cb | |
Derrick Hammer | 605f760fe3 | |
Derrick Hammer | a8419313b5 | |
Derrick Hammer | f11e3fed78 | |
Derrick Hammer | 95d866c69e | |
Derrick Hammer | 5fa5385249 | |
Derrick Hammer | 774e84996e | |
Derrick Hammer | 4b1b828c69 | |
Derrick Hammer | 8ea1ad3006 | |
Derrick Hammer | ddf6b2c9d8 | |
Derrick Hammer | 422c11b9b8 | |
Derrick Hammer | b2b041c3a1 | |
Derrick Hammer | d63fa22d00 | |
Derrick Hammer | f8b8633287 | |
Derrick Hammer | c01d866d8b | |
Derrick Hammer | 22023baedc | |
Derrick Hammer | 94342e6929 | |
Derrick Hammer | 6099a6c4f6 | |
Derrick Hammer | 5c666d38c0 | |
Derrick Hammer | 94e817f045 | |
Derrick Hammer | 92245a6c1c | |
Derrick Hammer | 8e0edc4bcd | |
Derrick Hammer | c1d495a54b | |
Derrick Hammer | 27261fedd2 | |
Derrick Hammer | 2e639eb92a | |
Derrick Hammer | 619fe7913c | |
Derrick Hammer | 7bbfe577c3 | |
Derrick Hammer | 41984ae588 | |
Derrick Hammer | 6c6f6c4954 | |
Derrick Hammer | b14509d6f1 | |
Derrick Hammer | b3a7c0b4e1 | |
Derrick Hammer | baf0ac31e6 | |
Derrick Hammer | ae2ee61bec | |
Derrick Hammer | b9611cecd7 | |
Derrick Hammer | e5f4f59477 | |
Derrick Hammer | c9cfb64c80 | |
Derrick Hammer | 4220037402 | |
Derrick Hammer | 1d4cb917c5 | |
Derrick Hammer | 08b4bbc893 | |
Derrick Hammer | 3b7be7e892 | |
Derrick Hammer | f89e2716f5 | |
Derrick Hammer | 84f2f96e12 | |
Derrick Hammer | b1ad8399d3 | |
Derrick Hammer | 7980aa29c7 | |
Derrick Hammer | d94eec7c7e | |
Derrick Hammer | 68cd8008a6 | |
Derrick Hammer | 6b42c2400f | |
Derrick Hammer | bdacd01849 | |
Derrick Hammer | 603d7c4e14 | |
Derrick Hammer | 4d75c11299 | |
Derrick Hammer | 29c6a909b8 | |
Derrick Hammer | 68c12f22be | |
Derrick Hammer | 11550659d0 | |
Derrick Hammer | c90e022795 | |
Derrick Hammer | 39471ad9c2 | |
Derrick Hammer | 8bae1c11dd | |
Derrick Hammer | ab98103503 | |
Derrick Hammer | 8734d8180c | |
Derrick Hammer | 9a73a2625a | |
Derrick Hammer | 0a35d7f062 | |
Derrick Hammer | 5e41c10ab6 | |
Derrick Hammer | 10d3c1fde8 | |
Derrick Hammer | 108e1b3dd8 | |
Derrick Hammer | 3ce2313c5a | |
Derrick Hammer | 47c93b1f1a | |
Derrick Hammer | 035be11686 | |
Derrick Hammer | d48dde7bd1 | |
Derrick Hammer | 5b87187bc0 | |
Derrick Hammer | b4b30cbdea | |
Derrick Hammer | 81559342be | |
Derrick Hammer | 4e12788db5 |
|
@ -0,0 +1,13 @@
|
|||
name: Build/Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- develop-*
|
||||
|
||||
jobs:
|
||||
main:
|
||||
uses: lumeweb/github-node-deploy-workflow/.github/workflows/main.yml@master
|
||||
secrets: inherit
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"preset": [
|
||||
"@lumeweb/node-library-preset"
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
## [0.0.2-develop.1](https://git.lumeweb.com/LumeWeb/libhyperproxy/compare/v0.0.1...v0.0.2-develop.1) (2023-07-05)
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) <year> <copyright holders>
|
||||
Copyright (c) 2023 Hammer Technologies LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "@lumeweb/libhyperproxy",
|
||||
"version": "0.0.2-develop.1",
|
||||
"main": "lib/index.js",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "gitea@git.lumeweb.com:LumeWeb/libhyperproxy.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lumeweb/node-library-preset": "^0.2.7",
|
||||
"@types/serialize-error": "^4.0.1",
|
||||
"@types/streamx": "^2.9.1",
|
||||
"presetter": "*"
|
||||
},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"scripts": {
|
||||
"prepare": "presetter bootstrap",
|
||||
"build": "run build",
|
||||
"semantic-release": "semantic-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"binconv": "^0.2.0",
|
||||
"compact-encoding": "^2.12.0",
|
||||
"protomux": "^3.5.0",
|
||||
"serialize-error": "^11.0.0",
|
||||
"streamx": "^2.15.0"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import Proxy from "./proxy.js";
|
||||
import Socket from "./socket.js";
|
||||
import Peer, {
|
||||
DataSocketOptions,
|
||||
PeerOptions,
|
||||
PeerOptionsWithProxy,
|
||||
OnOpen,
|
||||
OnSend,
|
||||
OnReceive,
|
||||
OnClose,
|
||||
} from "./peer.js";
|
||||
import Server from "./server.js";
|
||||
import DummySocket from "./proxies/multiSocket/dummySocket.js";
|
||||
import TcpSocket from "./proxies/multiSocket/tcpSocket.js";
|
||||
import BasicProxy from "./proxies/basic.js";
|
||||
import MultiSocketProxy from "./proxies/multiSocket.js";
|
||||
|
||||
export {
|
||||
Proxy,
|
||||
Socket,
|
||||
Server,
|
||||
Peer,
|
||||
DataSocketOptions,
|
||||
PeerOptions,
|
||||
PeerOptionsWithProxy,
|
||||
OnOpen,
|
||||
OnSend,
|
||||
OnReceive,
|
||||
OnClose,
|
||||
DummySocket,
|
||||
TcpSocket,
|
||||
BasicProxy,
|
||||
MultiSocketProxy,
|
||||
};
|
||||
|
||||
export function createSocket(port: number, host: string): Socket {
|
||||
return new Socket({
|
||||
remotePort: port,
|
||||
remoteAddress: host,
|
||||
});
|
||||
}
|
||||
|
||||
export function createServer(): Server {
|
||||
return new Server();
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import Proxy from "./proxy.js";
|
||||
import Socket from "./socket.js";
|
||||
|
||||
export type OnOpen = (
|
||||
peer: Peer,
|
||||
socket: Socket,
|
||||
data: any,
|
||||
) =>
|
||||
| { connect: boolean }
|
||||
| Promise<{ connect: boolean }>
|
||||
| Promise<void>
|
||||
| void;
|
||||
export type OnData = (peer: Peer, data: any) => void;
|
||||
export type OnReceive = OnData;
|
||||
export type OnClose = OnData;
|
||||
export type OnSend = OnData;
|
||||
export type OnChannel = (peer: Peer, channel: any) => void;
|
||||
|
||||
export type OnOpenBound = (
|
||||
socket: Socket,
|
||||
data: any,
|
||||
) =>
|
||||
| { connect: boolean }
|
||||
| Promise<{ connect: boolean }>
|
||||
| Promise<void>
|
||||
| void;
|
||||
export type OnDataBound = (data: any) => void;
|
||||
export type OnReceiveBound = OnDataBound;
|
||||
export type OnCloseBound = OnDataBound;
|
||||
export type OnSendBound = OnDataBound;
|
||||
|
||||
export type OnChannelBound = (channel: any) => void;
|
||||
|
||||
export interface DataSocketOptions {
|
||||
onopen?: OnOpen;
|
||||
onreceive?: OnReceive;
|
||||
onsend?: OnSend;
|
||||
onclose?: OnClose;
|
||||
onchannel?: OnChannel;
|
||||
emulateWebsocket?: boolean;
|
||||
}
|
||||
|
||||
export interface PeerOptions {
|
||||
peer: any;
|
||||
muxer: any;
|
||||
}
|
||||
|
||||
export interface PeerOptionsWithProxy extends PeerOptions {
|
||||
proxy: Proxy;
|
||||
}
|
||||
|
||||
export default abstract class Peer {
|
||||
protected _proxy: Proxy;
|
||||
protected _peer: any;
|
||||
protected _muxer: any;
|
||||
protected _onopen: OnOpenBound;
|
||||
protected _onreceive: OnReceiveBound;
|
||||
protected _onsend: OnSendBound;
|
||||
protected _onclose: OnCloseBound;
|
||||
protected _onchannel: OnChannelBound;
|
||||
protected _emulateWebsocket: boolean;
|
||||
|
||||
constructor({
|
||||
proxy,
|
||||
peer,
|
||||
muxer,
|
||||
onopen,
|
||||
onreceive,
|
||||
onsend,
|
||||
onclose,
|
||||
onchannel,
|
||||
emulateWebsocket = false,
|
||||
}: PeerOptionsWithProxy & DataSocketOptions) {
|
||||
this._proxy = proxy;
|
||||
this._peer = peer;
|
||||
this._muxer = muxer;
|
||||
this._onopen = onopen?.bind(undefined, this) as OnOpenBound;
|
||||
this._onreceive = onreceive?.bind(undefined, this) as OnReceiveBound;
|
||||
this._onsend = onsend?.bind(undefined, this) as OnSendBound;
|
||||
this._onclose = onclose?.bind(undefined, this) as OnCloseBound;
|
||||
this._onchannel = onchannel?.bind(undefined, this) as OnChannelBound;
|
||||
this._emulateWebsocket = emulateWebsocket;
|
||||
}
|
||||
|
||||
protected _socket?: Socket;
|
||||
|
||||
get socket(): Socket {
|
||||
return this._socket as Socket;
|
||||
}
|
||||
|
||||
protected _channel?: any;
|
||||
|
||||
get channel(): any {
|
||||
return this._channel;
|
||||
}
|
||||
|
||||
protected abstract initSocket();
|
||||
|
||||
protected abstract handleChannelOnOpen(m: any): Promise<void>;
|
||||
protected abstract handleChannelOnClose(socket: Socket): Promise<void>;
|
||||
|
||||
protected async initChannel() {
|
||||
const self = this;
|
||||
|
||||
this._channel = await this._muxer.createChannel({
|
||||
protocol: this._proxy.protocol,
|
||||
onopen: async (m: any) => {
|
||||
await this.handleChannelOnOpen(m);
|
||||
// @ts-ignore
|
||||
await this._onopen?.(this._channel);
|
||||
},
|
||||
onclose: async (socket: Socket) => {
|
||||
await this.handleChannelOnClose(socket);
|
||||
// @ts-ignore
|
||||
await this._onclose?.();
|
||||
},
|
||||
});
|
||||
|
||||
await this.initMessages();
|
||||
|
||||
await this._onchannel?.(this._channel);
|
||||
await this._channel.open();
|
||||
this._proxy.emit("peerChannelOpen", this);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.initSocket();
|
||||
await this.initChannel();
|
||||
}
|
||||
|
||||
protected async initMessages() {}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import Proxy from "../proxy.js";
|
||||
import { DataSocketOptions, PeerOptions } from "../peer.js";
|
||||
import BasicPeer from "./basic/peer.js";
|
||||
|
||||
export default class BasicProxy extends Proxy {
|
||||
protected handlePeer({
|
||||
peer,
|
||||
muxer,
|
||||
...options
|
||||
}: DataSocketOptions & PeerOptions) {
|
||||
const conn = new BasicPeer({ proxy: this, peer, muxer, ...options });
|
||||
conn.init();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import BasePeer from "#peer.js";
|
||||
import { maybeGetAsyncProperty } from "#util.js";
|
||||
import Socket from "#socket.js";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
export default class Peer extends BasePeer {
|
||||
private _pipe?: any;
|
||||
|
||||
protected async initSocket() {
|
||||
const self = this;
|
||||
|
||||
const raw = await maybeGetAsyncProperty(self._peer.rawStream);
|
||||
this._socket = new Socket({
|
||||
remoteAddress: raw.remoteHost,
|
||||
remotePort: raw.remotePort,
|
||||
remotePublicKey: await maybeGetAsyncProperty(self._peer.remotePublicKey),
|
||||
async write(data: any, cb: Function) {
|
||||
self._pipe?.send(data);
|
||||
await self._onsend?.(data);
|
||||
cb();
|
||||
},
|
||||
emulateWebsocket: self._emulateWebsocket,
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleChannelOnOpen(m: any) {
|
||||
if (!m) {
|
||||
m = Buffer.from([]);
|
||||
}
|
||||
|
||||
if (m instanceof Uint8Array) {
|
||||
m = Buffer.from(m);
|
||||
}
|
||||
|
||||
this._socket?.on("end", () => this._channel.close());
|
||||
let ret = await this._onopen?.(this._socket as Socket, m);
|
||||
if (!ret || (ret && ret.connect === false)) {
|
||||
// @ts-ignore
|
||||
self._socket?.emit("connect");
|
||||
}
|
||||
|
||||
this._socket?.emit("data", m);
|
||||
}
|
||||
|
||||
protected async handleChannelOnClose(socket: Socket): Promise<void> {
|
||||
this._socket?.destroy();
|
||||
}
|
||||
|
||||
protected async initMessages(): Promise<void> {
|
||||
const self = this;
|
||||
this._pipe = await this._channel.addMessage({
|
||||
async onmessage(m: any) {
|
||||
if (m instanceof Uint8Array) {
|
||||
m = Buffer.from(m);
|
||||
}
|
||||
self._socket?.emit("data", m);
|
||||
await self._onreceive?.(m);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
import Proxy, { ProxyOptions } from "../proxy.js";
|
||||
import TcpSocket from "./multiSocket/tcpSocket.js";
|
||||
import { json, raw, uint } from "compact-encoding";
|
||||
import { deserializeError, serializeError } from "serialize-error";
|
||||
import b4a from "b4a";
|
||||
import type { TcpSocketConnectOpts } from "net";
|
||||
import { DataSocketOptions, PeerOptions } from "../peer.js";
|
||||
import {
|
||||
roundRobinFactory,
|
||||
idFactory,
|
||||
maybeGetAsyncProperty,
|
||||
} from "../util.js";
|
||||
import {
|
||||
CloseSocketRequest,
|
||||
ErrorSocketRequest,
|
||||
PeerEntity,
|
||||
SocketRequest,
|
||||
WriteSocketRequest,
|
||||
} from "./multiSocket/types.js";
|
||||
import DummySocket from "./multiSocket/dummySocket.js";
|
||||
import Peer from "./multiSocket/peer.js";
|
||||
import { uint8ArrayToHexString } from "binconv";
|
||||
|
||||
export interface MultiSocketProxyOptions extends ProxyOptions {
|
||||
socketClass?: any;
|
||||
server: boolean;
|
||||
allowedPorts?: number[];
|
||||
}
|
||||
|
||||
const socketEncoding = {
|
||||
preencode(state: any, m: SocketRequest) {
|
||||
uint.preencode(state, m.id);
|
||||
uint.preencode(state, m.remoteId);
|
||||
},
|
||||
encode(state: any, m: SocketRequest) {
|
||||
uint.encode(state, m.id);
|
||||
uint.encode(state, m.remoteId);
|
||||
},
|
||||
decode(state: any, m: any): SocketRequest {
|
||||
return {
|
||||
remoteId: uint.decode(state, m),
|
||||
id: uint.decode(state, m),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const writeSocketEncoding = {
|
||||
preencode(state: any, m: WriteSocketRequest) {
|
||||
socketEncoding.preencode(state, m);
|
||||
raw.preencode(state, m.data);
|
||||
},
|
||||
encode(state: any, m: WriteSocketRequest) {
|
||||
socketEncoding.encode(state, m);
|
||||
raw.encode(state, m.data);
|
||||
},
|
||||
decode(state: any, m: any): WriteSocketRequest {
|
||||
const socket = socketEncoding.decode(state, m);
|
||||
return {
|
||||
...socket,
|
||||
data: raw.decode(state, m),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const errorSocketEncoding = {
|
||||
preencode(state: any, m: ErrorSocketRequest) {
|
||||
socketEncoding.preencode(state, m);
|
||||
json.preencode(state, serializeError(m.err));
|
||||
},
|
||||
encode(state: any, m: ErrorSocketRequest) {
|
||||
socketEncoding.encode(state, m);
|
||||
json.encode(state, serializeError(m.err));
|
||||
},
|
||||
decode(state: any, m: any): ErrorSocketRequest {
|
||||
const socket = socketEncoding.decode(state, m);
|
||||
return {
|
||||
...socket,
|
||||
err: deserializeError(json.decode(state, m)),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const nextSocketId = idFactory(1);
|
||||
|
||||
export default class MultiSocketProxy extends Proxy {
|
||||
async handlePeer({
|
||||
peer,
|
||||
muxer,
|
||||
...options
|
||||
}: DataSocketOptions & PeerOptions) {
|
||||
const conn = new Peer({
|
||||
...this.socketOptions,
|
||||
proxy: this,
|
||||
peer,
|
||||
muxer,
|
||||
...options,
|
||||
});
|
||||
await conn.init();
|
||||
this.emit("peer", conn);
|
||||
}
|
||||
|
||||
private socketClass: any;
|
||||
private _peers: Map<string, PeerEntity> = new Map<string, PeerEntity>();
|
||||
private _nextPeer;
|
||||
private _server = false;
|
||||
private _allowedPorts: number[] = [];
|
||||
|
||||
constructor(options: MultiSocketProxyOptions) {
|
||||
super(options);
|
||||
if (options.socketClass) {
|
||||
this.socketClass = options.socketClass;
|
||||
} else {
|
||||
if (options.server) {
|
||||
this.socketClass = TcpSocket;
|
||||
} else {
|
||||
this.socketClass = DummySocket;
|
||||
}
|
||||
}
|
||||
if (options.server) {
|
||||
this._server = true;
|
||||
}
|
||||
this._nextPeer = roundRobinFactory(this._peers);
|
||||
}
|
||||
|
||||
private _socketMap = new Map<number, number>();
|
||||
|
||||
get socketMap(): Map<number, number> {
|
||||
return this._socketMap;
|
||||
}
|
||||
|
||||
private _sockets = new Map<number, typeof this.socketClass>();
|
||||
|
||||
get sockets(): Map<number, any> {
|
||||
return this._sockets;
|
||||
}
|
||||
|
||||
async handleNewPeerChannel(peer: Peer) {
|
||||
this.update(await this._getPublicKey(peer), {
|
||||
peer,
|
||||
});
|
||||
|
||||
await this._registerOpenSocketMessage(peer);
|
||||
await this._registerWriteSocketMessage(peer);
|
||||
await this._registerCloseSocketMessage(peer);
|
||||
await this._registerTimeoutSocketMessage(peer);
|
||||
await this._registerErrorSocketMessage(peer);
|
||||
}
|
||||
|
||||
async handleClosePeer(peer: Peer) {
|
||||
for (const item of this._sockets) {
|
||||
if (item[1].peer.peer === peer) {
|
||||
item[1].end();
|
||||
}
|
||||
}
|
||||
|
||||
const pubkey = this._toString(await this._getPublicKey(peer));
|
||||
|
||||
if (this._peers.has(pubkey)) {
|
||||
this._peers.delete(pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
public get(pubkey: Uint8Array): PeerEntity | undefined {
|
||||
if (this._peers.has(this._toString(pubkey))) {
|
||||
return this._peers.get(this._toString(pubkey)) as PeerEntity;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public update(pubkey: Uint8Array, data: Partial<PeerEntity>): void {
|
||||
const peer = this.get(pubkey) ?? ({} as PeerEntity);
|
||||
|
||||
this._peers.set(this._toString(pubkey), {
|
||||
...peer,
|
||||
...data,
|
||||
...{
|
||||
messages: {
|
||||
...peer?.messages,
|
||||
...data?.messages,
|
||||
},
|
||||
},
|
||||
} as PeerEntity);
|
||||
}
|
||||
|
||||
public createSocket(options: TcpSocketConnectOpts): typeof this.socketClass {
|
||||
if (!this._peers.size) {
|
||||
throw new Error("no peers found");
|
||||
}
|
||||
|
||||
const peer = this._nextPeer();
|
||||
const socketId = nextSocketId();
|
||||
const socket = new this.socketClass(socketId, this, peer, options);
|
||||
this._sockets.set(socketId, socket);
|
||||
|
||||
return socket;
|
||||
}
|
||||
|
||||
private async _registerOpenSocketMessage(peer: Peer) {
|
||||
const self = this;
|
||||
const message = await peer.channel.addMessage({
|
||||
encoding: {
|
||||
preencode: this._server ? socketEncoding.preencode : json.preencode,
|
||||
encode: this._server ? socketEncoding.encode : json.encode,
|
||||
decode: this._server ? json.decode : socketEncoding.decode,
|
||||
},
|
||||
async onmessage(m: SocketRequest | TcpSocketConnectOpts) {
|
||||
if (self._server) {
|
||||
if (
|
||||
self._allowedPorts.length &&
|
||||
!self._allowedPorts.includes((m as TcpSocketConnectOpts).port)
|
||||
) {
|
||||
self
|
||||
.get(await self._getPublicKey(peer))
|
||||
?.messages.errorSocket?.send({
|
||||
id: (m as SocketRequest).id,
|
||||
err: new Error(
|
||||
`port ${(m as TcpSocketConnectOpts).port} not allowed`,
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m = m as SocketRequest;
|
||||
|
||||
if (self._server) {
|
||||
new self.socketClass(
|
||||
nextSocketId(),
|
||||
m.id,
|
||||
self,
|
||||
self.get(await self._getPublicKey(peer)) as PeerEntity,
|
||||
m,
|
||||
).connect();
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = self._sockets.get(m.id);
|
||||
if (socket) {
|
||||
socket.remoteId = m.remoteId;
|
||||
// @ts-ignore
|
||||
socket.emit("connect");
|
||||
}
|
||||
},
|
||||
});
|
||||
this.update(await this._getPublicKey(peer), {
|
||||
messages: { openSocket: message },
|
||||
});
|
||||
}
|
||||
|
||||
private async _registerWriteSocketMessage(peer: Peer) {
|
||||
const self = this;
|
||||
const message = await peer.channel.addMessage({
|
||||
encoding: writeSocketEncoding,
|
||||
onmessage(m: WriteSocketRequest) {
|
||||
self._sockets.get(m.id)?.push(b4a.from(m.data));
|
||||
},
|
||||
});
|
||||
this.update(await this._getPublicKey(peer), {
|
||||
messages: { writeSocket: message },
|
||||
});
|
||||
}
|
||||
|
||||
private async _registerCloseSocketMessage(peer: Peer) {
|
||||
const self = this;
|
||||
const message = await peer.channel.addMessage({
|
||||
encoding: socketEncoding,
|
||||
onmessage(m: CloseSocketRequest) {
|
||||
self._sockets.get(m.id)?.end();
|
||||
},
|
||||
});
|
||||
this.update(await this._getPublicKey(peer), {
|
||||
messages: { closeSocket: message },
|
||||
});
|
||||
}
|
||||
|
||||
private async _registerTimeoutSocketMessage(peer: Peer) {
|
||||
const self = this;
|
||||
const message = await peer.channel.addMessage({
|
||||
encoding: socketEncoding,
|
||||
onmessage(m: SocketRequest) {
|
||||
// @ts-ignore
|
||||
self._sockets.get(m.id)?.emit("timeout");
|
||||
},
|
||||
});
|
||||
this.update(await this._getPublicKey(peer), {
|
||||
messages: { timeoutSocket: message },
|
||||
});
|
||||
}
|
||||
|
||||
private async _registerErrorSocketMessage(peer: Peer) {
|
||||
const self = this;
|
||||
const message = await peer.channel.addMessage({
|
||||
encoding: errorSocketEncoding,
|
||||
onmessage(m: ErrorSocketRequest) {
|
||||
self._sockets.get(m.id)?.emit("error", m.err);
|
||||
},
|
||||
});
|
||||
this.update(await this._getPublicKey(peer), {
|
||||
messages: { errorSocket: message },
|
||||
});
|
||||
}
|
||||
|
||||
private _toString(pubkey: Uint8Array) {
|
||||
return uint8ArrayToHexString(pubkey);
|
||||
}
|
||||
|
||||
private async _getPublicKey(peer: Peer) {
|
||||
return maybeGetAsyncProperty(peer.stream.remotePublicKey);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { Callback, Duplex } from "streamx";
|
||||
import { TcpSocketConnectOpts } from "net";
|
||||
import { clearTimeout } from "timers";
|
||||
import MultiSocketProxy from "../multiSocket.js";
|
||||
import { PeerEntity, SocketRequest, WriteSocketRequest } from "./types.js";
|
||||
import { maybeGetAsyncProperty } from "#util.js";
|
||||
import Socket, { SocketOptions } from "#socket.js";
|
||||
|
||||
export default class DummySocket extends Socket {
|
||||
private _options: TcpSocketConnectOpts;
|
||||
private _id: number;
|
||||
private _proxy: MultiSocketProxy;
|
||||
|
||||
private _connectTimeout?: number;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
manager: MultiSocketProxy,
|
||||
peer: PeerEntity,
|
||||
connectOptions: TcpSocketConnectOpts,
|
||||
socketOptions: SocketOptions,
|
||||
) {
|
||||
super(socketOptions);
|
||||
this._id = id;
|
||||
this._proxy = manager;
|
||||
this._peer = peer;
|
||||
this._options = connectOptions;
|
||||
|
||||
// @ts-ignore
|
||||
this.on("timeout", () => {
|
||||
if (this._connectTimeout) {
|
||||
clearTimeout(this._connectTimeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _remoteId = 0;
|
||||
|
||||
set remoteId(value: number) {
|
||||
this._remoteId = value;
|
||||
this._proxy.socketMap.set(this._id, value);
|
||||
}
|
||||
|
||||
private _peer;
|
||||
|
||||
get peer() {
|
||||
return this._peer;
|
||||
}
|
||||
|
||||
public async _write(data: any, cb: any): Promise<void> {
|
||||
(await maybeGetAsyncProperty(this._peer.messages.writeSocket))?.send({
|
||||
id: this._id,
|
||||
remoteId: this._remoteId,
|
||||
data,
|
||||
} as WriteSocketRequest);
|
||||
cb();
|
||||
}
|
||||
|
||||
public async _destroy(cb: Callback) {
|
||||
(await maybeGetAsyncProperty(this._peer.messages.closeSocket))?.send({
|
||||
id: this._id,
|
||||
remoteId: this._remoteId,
|
||||
} as SocketRequest);
|
||||
this._proxy.socketMap.delete(this._id);
|
||||
this._proxy.sockets.delete(this._id);
|
||||
}
|
||||
|
||||
public async connect() {
|
||||
(await maybeGetAsyncProperty(this._peer.messages.openSocket))?.send({
|
||||
...this._options,
|
||||
id: this._id,
|
||||
});
|
||||
}
|
||||
|
||||
public setTimeout(ms: number, cb: Function) {
|
||||
if (this._connectTimeout) {
|
||||
clearTimeout(this._connectTimeout);
|
||||
}
|
||||
|
||||
this._connectTimeout = setTimeout(() => {
|
||||
cb && cb();
|
||||
}, ms) as any;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import BasePeer from "#peer.js";
|
||||
import Socket from "#socket.js";
|
||||
import MultiSocketProxy from "../multiSocket.js";
|
||||
|
||||
export default class Peer extends BasePeer {
|
||||
protected async initMessages(): Promise<void> {
|
||||
await this._proxy.handleNewPeerChannel(this);
|
||||
}
|
||||
protected declare _proxy: MultiSocketProxy;
|
||||
protected async initSocket() {}
|
||||
|
||||
get stream(): any {
|
||||
return this._muxer.stream;
|
||||
}
|
||||
|
||||
protected async handleChannelOnClose(socket: Socket): Promise<void> {
|
||||
return this._proxy.handleClosePeer(this);
|
||||
}
|
||||
|
||||
protected async handleChannelOnOpen(m: any): Promise<void> {}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import { Callback } from "streamx";
|
||||
import * as net from "net";
|
||||
import { Socket, TcpSocketConnectOpts } from "net";
|
||||
import MultiSocketProxy from "../multiSocket.js";
|
||||
import { PeerEntity, SocketRequest, WriteSocketRequest } from "./types.js";
|
||||
import BaseSocket from "../../socket.js";
|
||||
|
||||
export default class TcpSocket extends BaseSocket {
|
||||
private _options;
|
||||
private _id: number;
|
||||
private _remoteId: number;
|
||||
private _proxy: MultiSocketProxy;
|
||||
|
||||
private _socket?: Socket;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
remoteId: number,
|
||||
manager: MultiSocketProxy,
|
||||
peer: PeerEntity,
|
||||
options: TcpSocketConnectOpts,
|
||||
) {
|
||||
super();
|
||||
this._remoteId = remoteId;
|
||||
this._proxy = manager;
|
||||
this._id = id;
|
||||
this._peer = peer;
|
||||
this._options = options;
|
||||
|
||||
this._proxy.sockets.set(this._id, this);
|
||||
this._proxy.socketMap.set(this._id, this._remoteId);
|
||||
}
|
||||
|
||||
private _peer;
|
||||
|
||||
get peer() {
|
||||
return this._peer;
|
||||
}
|
||||
|
||||
public _write(data: any, cb: any): void {
|
||||
this._peer.messages.writeSocket?.send({
|
||||
...this._getSocketRequest(),
|
||||
data,
|
||||
} as WriteSocketRequest);
|
||||
cb();
|
||||
}
|
||||
|
||||
public _destroy(cb: Callback) {
|
||||
this._proxy.sockets.delete(this._id);
|
||||
this._proxy.socketMap.delete(this._id);
|
||||
this._peer.messages.closeSocket?.send(this._getSocketRequest());
|
||||
}
|
||||
|
||||
public connect() {
|
||||
this.on("error", (err: Error) => {
|
||||
this._peer.messages.errorSocket?.send({
|
||||
...this._getSocketRequest(),
|
||||
err,
|
||||
});
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
this.on("timeout", () => {
|
||||
this._peer.messages.timeoutSocket?.send(this._getSocketRequest());
|
||||
});
|
||||
// @ts-ignore
|
||||
this.on("connect", () => {
|
||||
this._peer.messages.openSocket?.send(this._getSocketRequest());
|
||||
});
|
||||
|
||||
this._socket = net.connect(this._options);
|
||||
["timeout", "error", "connect", "end", "destroy", "close"].forEach(
|
||||
(event) => {
|
||||
this._socket?.on(event, (...args: any) =>
|
||||
this.emit(event as any, ...args),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
this._socket.pipe(this as any);
|
||||
this.pipe(this._socket);
|
||||
}
|
||||
|
||||
private _getSocketRequest(): SocketRequest {
|
||||
return {
|
||||
id: this._id,
|
||||
remoteId: this._remoteId,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import Peer from "#peer.js";
|
||||
|
||||
export interface SocketRequest {
|
||||
remoteId: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export type CloseSocketRequest = SocketRequest;
|
||||
|
||||
export interface WriteSocketRequest extends SocketRequest {
|
||||
data: Uint8Array;
|
||||
}
|
||||
|
||||
export interface ErrorSocketRequest extends SocketRequest {
|
||||
err: Error;
|
||||
}
|
||||
|
||||
type Message = {
|
||||
send: (pubkey: Uint8Array | any) => void;
|
||||
};
|
||||
|
||||
export interface PeerEntityMessages {
|
||||
keyExchange: Message;
|
||||
openSocket: Message;
|
||||
writeSocket: Message;
|
||||
closeSocket: Message;
|
||||
timeoutSocket: Message;
|
||||
errorSocket: Message;
|
||||
}
|
||||
|
||||
export interface PeerEntity {
|
||||
messages: PeerEntityMessages | Partial<PeerEntityMessages>;
|
||||
peer: Peer;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import Protomux from "protomux";
|
||||
import { DataSocketOptions, PeerOptions } from "./peer.js";
|
||||
import EventEmitter from "events";
|
||||
|
||||
export interface ProxyOptions extends DataSocketOptions {
|
||||
swarm: any;
|
||||
protocol: string;
|
||||
listen?: boolean;
|
||||
autostart?: boolean;
|
||||
}
|
||||
|
||||
export default abstract class Proxy extends EventEmitter {
|
||||
protected _listen: any;
|
||||
protected _autostart: boolean;
|
||||
|
||||
constructor({
|
||||
swarm,
|
||||
protocol,
|
||||
onopen,
|
||||
onreceive,
|
||||
onsend,
|
||||
onclose,
|
||||
onchannel,
|
||||
listen = false,
|
||||
autostart = false,
|
||||
emulateWebsocket = false,
|
||||
}: ProxyOptions) {
|
||||
super();
|
||||
this._swarm = swarm;
|
||||
this._protocol = protocol;
|
||||
this._listen = listen;
|
||||
this._autostart = autostart;
|
||||
this._socketOptions = {
|
||||
onopen,
|
||||
onreceive,
|
||||
onsend,
|
||||
onclose,
|
||||
onchannel,
|
||||
emulateWebsocket,
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
protected _socketOptions: DataSocketOptions;
|
||||
|
||||
get socketOptions(): DataSocketOptions {
|
||||
return this._socketOptions;
|
||||
}
|
||||
|
||||
private _swarm: any;
|
||||
|
||||
get swarm(): any {
|
||||
return this._swarm;
|
||||
}
|
||||
|
||||
private _protocol: string;
|
||||
|
||||
get protocol(): string {
|
||||
return this._protocol;
|
||||
}
|
||||
|
||||
protected abstract handlePeer({
|
||||
peer,
|
||||
muxer,
|
||||
...options
|
||||
}: DataSocketOptions & PeerOptions);
|
||||
|
||||
protected _init() {
|
||||
// Implement in subclasses
|
||||
}
|
||||
|
||||
private async init() {
|
||||
if (this._listen) {
|
||||
this._swarm.on("connection", this._handleConnection.bind(this));
|
||||
}
|
||||
await this._init();
|
||||
}
|
||||
|
||||
private _handleConnection(peer: any) {
|
||||
const muxer = Protomux.from(peer);
|
||||
const handlePeer = this.handlePeer.bind(this, {
|
||||
peer,
|
||||
muxer,
|
||||
...this._socketOptions,
|
||||
});
|
||||
|
||||
if (this._autostart) {
|
||||
handlePeer();
|
||||
return;
|
||||
}
|
||||
|
||||
muxer.pair(this._protocol, handlePeer);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import EventEmitter from "events";
|
||||
|
||||
export default class Server extends EventEmitter {
|
||||
address() {
|
||||
return {
|
||||
address: "127.0.0.1",
|
||||
family: "IPv4",
|
||||
port: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async close() {
|
||||
return;
|
||||
}
|
||||
|
||||
async getConnections() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
async listen(...args: any[]) {
|
||||
const address = this.address();
|
||||
this.emit("listening", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
get listening() {
|
||||
return false;
|
||||
}
|
||||
|
||||
set listening(value) {}
|
||||
|
||||
get maxConnections() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
set maxConnections(value) {}
|
||||
|
||||
ref() {
|
||||
return this;
|
||||
}
|
||||
|
||||
unref() {
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import { Callback, Duplex, DuplexEvents } from "streamx";
|
||||
|
||||
const IPV4 = "IPv4";
|
||||
const IPV6 = "IPv6";
|
||||
|
||||
type AddressFamily = "IPv6" | "IPv4";
|
||||
|
||||
export interface SocketOptions {
|
||||
allowHalfOpen?: boolean;
|
||||
remoteAddress?: string;
|
||||
remotePort?: number;
|
||||
remotePublicKey?: Uint8Array;
|
||||
write?: (
|
||||
this: Duplex<any, any, any, any, true, true, DuplexEvents<any, any>>,
|
||||
data: any,
|
||||
cb: Callback,
|
||||
) => void;
|
||||
emulateWebsocket?: boolean;
|
||||
}
|
||||
|
||||
export default class Socket extends Duplex {
|
||||
private _allowHalfOpen: boolean;
|
||||
public remoteAddress: any;
|
||||
public remotePort: any;
|
||||
public remoteFamily?: AddressFamily;
|
||||
|
||||
declare readable: true;
|
||||
declare writable: true;
|
||||
public remotePublicKey: Uint8Array;
|
||||
private _emulateWebsocket: boolean;
|
||||
|
||||
declare addEventListener?: typeof this.addListener;
|
||||
declare removeEventListener?: typeof this.removeListener;
|
||||
declare send?: typeof this.write;
|
||||
declare close?: typeof this.end;
|
||||
|
||||
constructor({
|
||||
allowHalfOpen = false,
|
||||
remoteAddress,
|
||||
remotePort,
|
||||
remotePublicKey,
|
||||
write,
|
||||
emulateWebsocket = false,
|
||||
}: SocketOptions = {}) {
|
||||
super({ write });
|
||||
this._allowHalfOpen = allowHalfOpen;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.remotePort = remotePort;
|
||||
this.remotePublicKey = remotePublicKey as Uint8Array;
|
||||
this._emulateWebsocket = emulateWebsocket;
|
||||
|
||||
if (remoteAddress) {
|
||||
const type = Socket.isIP(remoteAddress);
|
||||
if (!type) {
|
||||
throw Error("invalid remoteAddress");
|
||||
}
|
||||
|
||||
this.remoteFamily = type === 6 ? IPV6 : IPV4;
|
||||
}
|
||||
|
||||
if (this._emulateWebsocket) {
|
||||
this.addEventListener = this.addListener;
|
||||
this.removeEventListener = this.removeListener;
|
||||
this.send = this.write;
|
||||
this.close = this.end;
|
||||
this.addEventListener("data", (data: any) =>
|
||||
// @ts-ignore
|
||||
this.emit("message", new MessageEvent("data", { data })),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _connecting?: boolean;
|
||||
|
||||
get connecting(): boolean {
|
||||
return this._connecting as boolean;
|
||||
}
|
||||
|
||||
get readyState(): string | number {
|
||||
if (this._emulateWebsocket) {
|
||||
if (this._connecting) {
|
||||
return 0;
|
||||
} else if (this.readable && this.writable) {
|
||||
return 1;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._connecting) {
|
||||
return "opening";
|
||||
} else if (this.readable && this.writable) {
|
||||
return "open";
|
||||
} else if (this.readable && !this.writable) {
|
||||
return "readOnly";
|
||||
} else if (!this.readable && this.writable) {
|
||||
return "writeOnly";
|
||||
} else {
|
||||
return "closed";
|
||||
}
|
||||
}
|
||||
|
||||
listen() {
|
||||
throw new Error("Not supported");
|
||||
}
|
||||
|
||||
setTimeout(msecs, callback) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
_onTimeout() {
|
||||
// @ts-ignore
|
||||
this.emit("timeout");
|
||||
}
|
||||
|
||||
setNoDelay(enable: boolean) {}
|
||||
|
||||
setKeepAlive(setting: any, msecs: number) {}
|
||||
|
||||
address() {
|
||||
return {
|
||||
address: this.remoteAddress,
|
||||
port: this.remotePort,
|
||||
family: this.remoteFamily,
|
||||
};
|
||||
}
|
||||
|
||||
static isIP(input: string): number {
|
||||
if (Socket.isIPv4(input)) {
|
||||
return 4;
|
||||
} else if (Socket.isIPv6(input)) {
|
||||
return 6;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static isIPv4(input: string) {
|
||||
return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
|
||||
input,
|
||||
);
|
||||
}
|
||||
|
||||
static isIPv6(input: string) {
|
||||
return /^(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))$/.test(
|
||||
input,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
export function idFactory(start: number, step = 1, limit = 2 ** 32) {
|
||||
let id = start;
|
||||
|
||||
return function nextId() {
|
||||
const nextId = id;
|
||||
id += step;
|
||||
if (id >= limit) id = start;
|
||||
return nextId;
|
||||
};
|
||||
}
|
||||
export function roundRobinFactory<T>(list: Map<string, any>) {
|
||||
let index = 0;
|
||||
|
||||
return (): T => {
|
||||
const keys = [...list.keys()].sort();
|
||||
if (index >= keys.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
return list.get(keys[index++]);
|
||||
};
|
||||
}
|
||||
export async function maybeGetAsyncProperty(object: any) {
|
||||
if (typeof object === "function") {
|
||||
object = object();
|
||||
}
|
||||
|
||||
if (isPromise(object)) {
|
||||
object = await object;
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
export function isPromise(obj: Promise<any>) {
|
||||
return (
|
||||
!!obj &&
|
||||
(typeof obj === "object" || typeof obj === "function") &&
|
||||
typeof obj.then === "function"
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue