Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
485 lines
14 KiB
485 lines
14 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import DEBUG from "../../config/debug"; |
|
import ctx from "../../environment/ctx"; |
|
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; |
|
import { IS_SERVICE_WORKER, IS_WORKER, notifyAll } from "../../helpers/context"; |
|
import EventListenerBase from "../../helpers/eventListenerBase"; |
|
import pause from "../../helpers/schedulers/pause"; |
|
import { Awaited, WorkerTaskTemplate, WorkerTaskVoidTemplate } from "../../types"; |
|
import { logger } from "../logger"; |
|
|
|
type SuperMessagePortTask = WorkerTaskTemplate & { |
|
transfer?: Transferable[] |
|
}; |
|
|
|
interface InvokeTask extends SuperMessagePortTask { |
|
type: 'invoke', |
|
payload: WorkerTaskVoidTemplate & {withAck?: boolean, void?: boolean} |
|
} |
|
|
|
interface ResultTask extends SuperMessagePortTask { |
|
type: 'result', |
|
payload: { |
|
taskId: number, |
|
result?: any, |
|
error?: any |
|
} |
|
} |
|
|
|
interface AckTask extends SuperMessagePortTask { |
|
type: 'ack', |
|
payload: { |
|
cached: boolean, |
|
taskId: number |
|
result?: any, |
|
error?: any, |
|
} |
|
} |
|
|
|
interface PingTask extends SuperMessagePortTask { |
|
type: 'ping' |
|
} |
|
|
|
interface PongTask extends SuperMessagePortTask { |
|
type: 'pong' |
|
} |
|
|
|
interface BatchTask extends SuperMessagePortTask { |
|
type: 'batch', |
|
payload: Task[] |
|
} |
|
|
|
type Task = InvokeTask | ResultTask | AckTask | PingTask | PongTask | BatchTask; |
|
type TaskMap = { |
|
[type in Task as type['type']]?: (task: Extract<Task, type>) => void | Promise<any> |
|
}; |
|
|
|
export type AckedResult<T> = { |
|
cached: boolean, |
|
result: Promise<T> |
|
}; |
|
// export type AckedResult<T> = { |
|
// cached: true, |
|
// result: T |
|
// } | { |
|
// cached: false, |
|
// result: Promise<T> |
|
// }; |
|
|
|
type ListenPort = WindowProxy | MessagePort | ServiceWorker | Worker | ServiceWorkerContainer; |
|
type SendPort = WindowProxy | MessagePort | ServiceWorker | Worker; |
|
|
|
type ListenerCallback = (payload: any, source: MessageEventSource, event: MessageEvent<any>) => any; |
|
type Listeners = Record<string, ListenerCallback>; |
|
|
|
const PING_INTERVAL = DEBUG || true ? 0x7FFFFFFF : 1000; |
|
const PING_TIMEOUT = DEBUG || true ? 0x7FFFFFFF : 5000; |
|
|
|
export default class SuperMessagePort< |
|
Workers extends Listeners, |
|
Masters extends Listeners, |
|
IsMaster extends boolean, |
|
Receive extends Listeners = IsMaster extends true ? Masters : Workers, |
|
Send extends Listeners = IsMaster extends true ? Workers : Masters |
|
> extends EventListenerBase<Receive> { |
|
protected listenPorts: Array<ListenPort>; |
|
protected sendPorts: Array<SendPort>; |
|
protected pingResolves: Map<SendPort, () => void>; |
|
|
|
protected taskId: number; |
|
protected awaiting: { |
|
[id: number]: { |
|
resolve: any, |
|
reject: any, |
|
taskType: string |
|
} |
|
}; |
|
protected pending: Map<SendPort, Task[]>; |
|
|
|
protected log: ReturnType<typeof logger>; |
|
protected debug: boolean; |
|
protected releasingPending: boolean; |
|
|
|
public _constructor() { |
|
super._constructor(false); |
|
|
|
this.listenPorts = []; |
|
this.sendPorts = []; |
|
this.pingResolves = new Map(); |
|
this.taskId = 0; |
|
this.awaiting = {}; |
|
this.pending = new Map(); |
|
this.log = logger('MP'); |
|
this.debug = DEBUG; |
|
} |
|
|
|
public attachPort(port: MessageEventSource) { |
|
this.attachListenPort(port); |
|
this.attachSendPort(port); |
|
} |
|
|
|
public attachListenPort(port: ListenPort) { |
|
this.listenPorts.push(port); |
|
port.addEventListener('message', this.onMessage as any); |
|
} |
|
|
|
public attachSendPort(port: SendPort) { |
|
if((port as MessagePort).start) { |
|
(port as MessagePort).start(); |
|
} |
|
|
|
this.sendPorts.push(port); |
|
this.sendPing(port); |
|
} |
|
|
|
protected sendPing(port: SendPort, loop = IS_WORKER) { |
|
let timeout: number; |
|
const promise = new Promise<void>((resolve, reject) => { |
|
this.pingResolves.set(port, resolve); |
|
this.pushTask(this.createTask('ping', undefined), port); |
|
|
|
timeout = ctx.setTimeout(() => { |
|
reject(); |
|
}, PING_TIMEOUT); |
|
}); |
|
|
|
promise.then(() => { |
|
clearTimeout(timeout); |
|
this.pingResolves.delete(port); |
|
|
|
if(loop) { |
|
this.sendPingWithTimeout(port); |
|
} |
|
}, () => { |
|
this.pingResolves.delete(port); |
|
|
|
indexOfAndSplice(this.listenPorts, port); |
|
indexOfAndSplice(this.sendPorts, port); |
|
if((port as MessagePort).close) { |
|
(port as MessagePort).close(); |
|
} |
|
}); |
|
} |
|
|
|
protected sendPingWithTimeout(port: SendPort, timeout = PING_INTERVAL) { |
|
ctx.setTimeout(() => { |
|
if(!this.sendPorts.includes(port)) { |
|
return; |
|
} |
|
|
|
this.sendPing(port); |
|
}, timeout); |
|
} |
|
|
|
protected postMessage(port: SendPort | SendPort[], task: Task) { |
|
const ports = Array.isArray(port) ? port : (port ? [port] : this.sendPorts); |
|
ports.forEach((port) => { |
|
port.postMessage(task, task.transfer as any); |
|
}); |
|
} |
|
|
|
protected onMessage = (event: MessageEvent) => { |
|
const task: Task = event.data; |
|
// this.log('got message', task); |
|
|
|
const source: MessageEventSource = event.source || event.currentTarget as any; // can have no source |
|
if(task.type === 'batch') { |
|
const newEvent: MessageEvent = {data: event.data, source: event.source, currentTarget: event.currentTarget} as any; |
|
task.payload.forEach((task) => { |
|
// @ts-ignore |
|
newEvent.data = task; |
|
this.onMessage(newEvent); |
|
}); |
|
} else if(task.type === 'result') { |
|
this.processResultTask(task); |
|
} else if(task.type === 'ack') { |
|
this.processAckTask(task); |
|
} else if(task.type === 'invoke') { |
|
this.processInvokeTask(task, source, event); |
|
} else if(task.type === 'ping') { |
|
this.processPingTask(task, source, event); |
|
} else if(task.type === 'pong') { |
|
this.processPongTask(task, source, event); |
|
} |
|
}; |
|
|
|
protected async releasePending() { |
|
//return; |
|
|
|
if(!this.listenPorts.length || this.releasingPending) { |
|
return; |
|
} |
|
|
|
this.releasingPending = true; |
|
// const perf = performance.now(); |
|
await pause(0); |
|
|
|
this.debug && this.log.debug('releasing tasks, length:', this.pending.size/* , performance.now() - perf */); |
|
|
|
this.pending.forEach((portTasks, port) => { |
|
let batchTask: BatchTask; |
|
const tasks: Task[] = []; |
|
portTasks.forEach((task) => { |
|
if(task.transfer) { |
|
batchTask = undefined; |
|
tasks.push(task); |
|
} else { |
|
if(!batchTask) { |
|
batchTask = this.createTask('batch', []); |
|
tasks.push(batchTask); |
|
} |
|
|
|
batchTask.payload.push(task); |
|
} |
|
}); |
|
|
|
// const tasks = portTasks; |
|
|
|
tasks.forEach((task) => { |
|
// if(task.type === 'batch') { |
|
// this.log(`batching ${task.payload.length} tasks`); |
|
// } |
|
|
|
try { |
|
if(IS_SERVICE_WORKER) { |
|
notifyAll(task); |
|
} else { |
|
this.postMessage(port, task); |
|
} |
|
} catch(err) { |
|
this.log.error('postMessage error:', err, task, port); |
|
} |
|
}); |
|
}); |
|
|
|
this.debug && this.log.debug('released tasks'); |
|
this.pending.clear(); |
|
|
|
this.releasingPending = false; |
|
} |
|
|
|
protected processResultTask(task: ResultTask) { |
|
const {taskId, result, error} = task.payload; |
|
const deferred = this.awaiting[taskId]; |
|
if(!deferred) { |
|
return; |
|
} |
|
|
|
this.debug && this.log.debug('done', deferred.taskType, result, error); |
|
error ? deferred.reject(error) : deferred.resolve(result); |
|
delete this.awaiting[taskId]; |
|
} |
|
|
|
protected processAckTask(task: AckTask) { |
|
const payload = task.payload; |
|
const deferred = this.awaiting[payload.taskId]; |
|
if(!deferred) { |
|
return; |
|
} |
|
|
|
// * will finish the init promise with incoming result |
|
const previousResolve: (acked: AckedResult<any>) => void = deferred.resolve; |
|
// const previousReject = deferred.reject; |
|
|
|
// if(payload.cached) { |
|
// if('result' in payload) { |
|
// previousResolve({ |
|
// cached: true, |
|
// result: payload.result |
|
// }); |
|
// } else { |
|
// previousReject(payload.error); |
|
// } |
|
// } else { |
|
// const ret: AckedResult<any> = { |
|
// cached: false, |
|
// result: new Promise((resolve, reject) => { |
|
// deferred.resolve = resolve; |
|
// deferred.reject = reject; |
|
// }) |
|
// }; |
|
|
|
// previousResolve(ret); |
|
// } |
|
|
|
const ret: AckedResult<any> = { |
|
cached: payload.cached, |
|
result: payload.cached ? ('result' in payload ? Promise.resolve(payload.result) : Promise.reject(payload.error)) : new Promise((resolve, reject) => { |
|
deferred.resolve = resolve; |
|
deferred.reject = reject; |
|
}) |
|
}; |
|
|
|
previousResolve(ret); |
|
} |
|
|
|
protected processPingTask(task: PingTask, source: MessageEventSource, event: MessageEvent) { |
|
this.pushTask(this.createTask('pong', undefined), event.source); |
|
} |
|
|
|
protected processPongTask(task: PongTask, source: MessageEventSource, event: MessageEvent) { |
|
const pingResolve = this.pingResolves.get(source); |
|
if(pingResolve) { |
|
this.pingResolves.delete(source); |
|
pingResolve(); |
|
} |
|
} |
|
|
|
protected async processInvokeTask(task: InvokeTask, source: MessageEventSource, event: MessageEvent) { |
|
const id = task.id; |
|
const innerTask = task.payload; |
|
|
|
let resultTaskPayload: ResultTask['payload']; |
|
let resultTask: ResultTask, ackTask: AckTask; |
|
if(!innerTask.void) { |
|
resultTaskPayload = {taskId: id}; |
|
resultTask = this.createTask('result', resultTaskPayload); |
|
} |
|
|
|
if(innerTask.withAck) { |
|
ackTask = this.createTask('ack', { |
|
taskId: id, |
|
cached: true |
|
}); |
|
} |
|
|
|
let isPromise: boolean; |
|
try { |
|
const listeners = this.listeners[innerTask.type]; |
|
if(!listeners?.length) { |
|
throw new Error('no listener'); |
|
} |
|
|
|
const listener = listeners[0]; |
|
|
|
// @ts-ignore |
|
let result = this.invokeListenerCallback(innerTask.type, listener, innerTask.payload, source, event); |
|
if(innerTask.void) { |
|
return; |
|
} |
|
|
|
isPromise = result instanceof Promise; |
|
|
|
if(ackTask) { |
|
const cached = !isPromise; |
|
ackTask.payload.cached = cached; |
|
if(cached) ackTask.payload.result = result; |
|
this.pushTask(ackTask, source); |
|
|
|
if(cached) { |
|
return; |
|
} |
|
} |
|
|
|
if(isPromise) { |
|
result = await result; |
|
} |
|
|
|
resultTaskPayload.result = result; |
|
} catch(error) { |
|
this.log.error('worker task error:', error, task); |
|
if(innerTask.void) { |
|
return; |
|
} |
|
|
|
if(ackTask && ackTask.payload.cached) { |
|
ackTask.payload.error = error; |
|
this.pushTask(ackTask, source); |
|
return; |
|
} |
|
|
|
resultTaskPayload.error = error; |
|
} |
|
|
|
this.pushTask(resultTask, source); |
|
} |
|
|
|
protected createTask<T extends Task['type'], K extends Task = Parameters<TaskMap[T]>[0]>(type: T, payload: K['payload'], transfer?: Transferable[]): K { |
|
return { |
|
type, |
|
payload, |
|
id: this.taskId++, |
|
transfer |
|
} as K; |
|
} |
|
|
|
protected createInvokeTask(type: string, payload: any, withAck?: boolean, _void?: boolean, transfer?: Transferable[]): InvokeTask { |
|
return this.createTask('invoke', { |
|
type, |
|
payload, |
|
withAck, |
|
void: _void |
|
}, transfer); |
|
} |
|
|
|
protected pushTask(task: Task, port?: SendPort) { |
|
let tasks = this.pending.get(port); |
|
if(!tasks) { |
|
this.pending.set(port, tasks = []); |
|
} |
|
|
|
tasks.push(task); |
|
this.releasePending(); |
|
} |
|
|
|
public invokeVoid<T extends keyof Send>(type: T, payload: Parameters<Send[T]>[0], port?: SendPort, transfer?: Transferable[]) { |
|
const task = this.createInvokeTask(type as string, payload, undefined, true, transfer); |
|
this.pushTask(task, port); |
|
} |
|
|
|
public invoke<T extends keyof Send>(type: T, payload: Parameters<Send[T]>[0], withAck?: false, port?: SendPort, transfer?: Transferable[]): Promise<Awaited<ReturnType<Send[T]>>>; |
|
public invoke<T extends keyof Send>(type: T, payload: Parameters<Send[T]>[0], withAck?: true, port?: SendPort, transfer?: Transferable[]): Promise<AckedResult<Awaited<ReturnType<Send[T]>>>>; |
|
public invoke<T extends keyof Send>(type: T, payload: Parameters<Send[T]>[0], withAck?: boolean, port?: SendPort, transfer?: Transferable[]) { |
|
this.debug && this.log.debug('start', type, payload); |
|
|
|
let task: InvokeTask; |
|
const promise = new Promise<Awaited<ReturnType<Send[T]>>>((resolve, reject) => { |
|
task = this.createInvokeTask(type as string, payload, withAck, undefined, transfer); |
|
this.awaiting[task.id] = {resolve, reject, taskType: type as string}; |
|
this.pushTask(task, port); |
|
}); |
|
|
|
if(IS_WORKER) { |
|
promise.finally(() => { |
|
clearInterval(interval); |
|
}); |
|
|
|
const interval = ctx.setInterval(() => { |
|
this.log.error('task still has no result', task, port); |
|
}, 5e3); |
|
} else if(false) { |
|
// let timedOut = false; |
|
const startTime = Date.now(); |
|
promise.finally(() => { |
|
const elapsedTime = Date.now() - startTime; |
|
if(elapsedTime >= TIMEOUT) { |
|
this.log.error(`task was processing ${Date.now() - startTime}ms`, task.payload.payload, port); |
|
}/* else { |
|
clearTimeout(timeout); |
|
} */ |
|
}); |
|
|
|
const TIMEOUT = 10; |
|
// const timeout = ctx.setTimeout(() => { |
|
// timedOut = true; |
|
// // this.log.error(`task is processing more than ${TIMEOUT} milliseconds`, task, port); |
|
// }, TIMEOUT); |
|
} |
|
|
|
return promise; |
|
} |
|
|
|
public invokeExceptSource<T extends keyof Send>(type: T, payload: Parameters<Send[T]>[0], source?: SendPort) { |
|
const ports = this.sendPorts.slice(); |
|
indexOfAndSplice(ports, source); |
|
|
|
ports.forEach((target) => { |
|
this.invokeVoid(type, payload, target); |
|
}); |
|
} |
|
}
|
|
|