Add Relay, Pool
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
export type Deferred<T> = Promise<T> & {
|
||||||
|
resolve: (arg: T) => void
|
||||||
|
reject: (arg: T) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defer = (): Deferred<any> => {
|
||||||
|
let resolve, reject
|
||||||
|
const p = new Promise((resolve_, reject_) => {
|
||||||
|
resolve = resolve_
|
||||||
|
reject = reject_
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.assign(p, {resolve, reject})
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
import {Relay} from "./Relay"
|
||||||
|
|
||||||
|
const normalizeUrl = url => url.replace(/\/+$/, "").toLowerCase().trim()
|
||||||
|
|
||||||
|
export class Pool {
|
||||||
|
constructor() {
|
||||||
|
this.relays = new Map()
|
||||||
|
}
|
||||||
|
add(url) {
|
||||||
|
url = normalizeUrl(url)
|
||||||
|
|
||||||
|
if (!this.relays.has(url)) {
|
||||||
|
this.relays.set(url, new Relay(url))
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.relays.get(url)
|
||||||
|
}
|
||||||
|
remove(url) {
|
||||||
|
url = normalizeUrl(url)
|
||||||
|
|
||||||
|
this.relays.get(url)?.disconnect()
|
||||||
|
this.relays.delete(url)
|
||||||
|
}
|
||||||
|
async waitFor(url) {
|
||||||
|
const relay = this.add(url)
|
||||||
|
|
||||||
|
await relay.connect()
|
||||||
|
|
||||||
|
return relay.status === Relay.STATUS.READY ? relay : null
|
||||||
|
}
|
||||||
|
async execute(urls, callback) {
|
||||||
|
const results = await Promise.all([
|
||||||
|
urls.map(async url => {
|
||||||
|
const relay = await this.waitFor(url)
|
||||||
|
|
||||||
|
if (!relay) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [relay, callback(relay)]
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
return results.filter(Boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
+160
@@ -0,0 +1,160 @@
|
|||||||
|
import {WebSocket} from "ws"
|
||||||
|
import {Deferred, defer} from "./Deferred"
|
||||||
|
|
||||||
|
export class Relay {
|
||||||
|
ws?: WebSocket
|
||||||
|
url: string
|
||||||
|
ready?: Deferred<void>
|
||||||
|
queue: string[]
|
||||||
|
error: string
|
||||||
|
status: string
|
||||||
|
timeout?: number
|
||||||
|
bus: EventBus
|
||||||
|
static STATUS = {
|
||||||
|
NEW: "new",
|
||||||
|
PENDING: "pending",
|
||||||
|
CLOSED: "closed",
|
||||||
|
ERROR: "error",
|
||||||
|
READY: "ready",
|
||||||
|
}
|
||||||
|
static ERROR = {
|
||||||
|
CONNECTION: "connection",
|
||||||
|
UNAUTHORIZED: "unauthorized",
|
||||||
|
FORBIDDEN: "forbidden",
|
||||||
|
}
|
||||||
|
constructor(url) {
|
||||||
|
if (connections[url]) {
|
||||||
|
error(`Connection to ${url} already exists`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws = null
|
||||||
|
this.url = url
|
||||||
|
this.ready = null
|
||||||
|
this.queue = []
|
||||||
|
this.timeout = null
|
||||||
|
this.bus = new EventBus()
|
||||||
|
this.error = null
|
||||||
|
this.status = Relay.STATUS.NEW
|
||||||
|
}
|
||||||
|
async connect() {
|
||||||
|
if (this.status === Relay.STATUS.NEW) {
|
||||||
|
if (this.ws) {
|
||||||
|
error("Attempted to connect when already connected", this)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ready = defer()
|
||||||
|
this.ws = new WebSocket(this.url)
|
||||||
|
this.status = Relay.STATUS.PENDING
|
||||||
|
|
||||||
|
this.ws.addEventListener("open", () => {
|
||||||
|
log(`Opened connection to ${this.url}`)
|
||||||
|
|
||||||
|
this.status = Relay.STATUS.READY
|
||||||
|
this.ready.resolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.addEventListener("message", e => {
|
||||||
|
this.queue.push(e.data)
|
||||||
|
|
||||||
|
if (!this.timeout) {
|
||||||
|
this.timeout = global.setTimeout(() => this.handleMessages(), 10)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.addEventListener("error", e => {
|
||||||
|
log(`Error on connection to ${this.url}`)
|
||||||
|
|
||||||
|
this.disconnect()
|
||||||
|
this.ready.reject()
|
||||||
|
this.error = Relay.ERROR.CONNECTION
|
||||||
|
this.status = Relay.STATUS.CLOSED
|
||||||
|
})
|
||||||
|
|
||||||
|
this.ws.addEventListener("close", () => {
|
||||||
|
log(`Closed connection to ${this.url}`)
|
||||||
|
|
||||||
|
this.disconnect()
|
||||||
|
this.ready.reject()
|
||||||
|
this.status = Relay.STATUS.CLOSED
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.ready.catch(() => null)
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
if (this.ws) {
|
||||||
|
log(`Disconnecting from ${this.url}`)
|
||||||
|
|
||||||
|
this.ws.close()
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleMessages() {
|
||||||
|
for (const json of this.queue.splice(0, 10)) {
|
||||||
|
let message
|
||||||
|
try {
|
||||||
|
message = JSON.parse(json)
|
||||||
|
} catch (e) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bus.handle(...message)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout = this.queue.length > 0 ? global.setTimeout(() => this.handleMessages(), 10) : null
|
||||||
|
}
|
||||||
|
send(...payload) {
|
||||||
|
if (this.ws?.readyState !== 1) {
|
||||||
|
console.warn("Send attempted before socket was ready", this)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.send(JSON.stringify(payload))
|
||||||
|
}
|
||||||
|
subscribe(filters, id, {onEvent, onEose}) {
|
||||||
|
const [eventChannel, eoseChannel] = [
|
||||||
|
this.bus.on("EVENT", (subid, e) => subid === id && onEvent(e)),
|
||||||
|
this.bus.on("EOSE", subid => subid === id && onEose()),
|
||||||
|
]
|
||||||
|
|
||||||
|
this.send("REQ", id, ...filters)
|
||||||
|
|
||||||
|
return {
|
||||||
|
conn: this,
|
||||||
|
unsub: () => {
|
||||||
|
if (this.status === Relay.STATUS.READY) {
|
||||||
|
this.send("CLOSE", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bus.off("EVENT", eventChannel)
|
||||||
|
this.bus.off("EOSE", eoseChannel)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publish(event, {onOk, onError}) {
|
||||||
|
const withCleanup = cb => k => {
|
||||||
|
if (k === event.id) {
|
||||||
|
cb()
|
||||||
|
this.bus.off("OK", okChannel)
|
||||||
|
this.bus.off("ERROR", errorChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [okChannel, errorChannel] = [
|
||||||
|
this.bus.on("OK", withCleanup(onOk)),
|
||||||
|
this.bus.on("ERROR", withCleanup(onError)),
|
||||||
|
]
|
||||||
|
|
||||||
|
this.send("EVENT", event)
|
||||||
|
}
|
||||||
|
count(filter, id, {onCount}) {
|
||||||
|
const channel = this.bus.on("COUNT", (subid, ...payload) => {
|
||||||
|
if (subid === id) {
|
||||||
|
onCount(...payload)
|
||||||
|
|
||||||
|
this.bus.off("COUNT", channel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.send("COUNT", id, ...filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-1
@@ -1 +1,4 @@
|
|||||||
export * from './EventBus'
|
export * from "./EventBus"
|
||||||
|
export * from "./Deferred"
|
||||||
|
export * from "./Relay"
|
||||||
|
export * from "./Pool"
|
||||||
|
|||||||
Generated
+24
-2151
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -17,7 +17,6 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check:ts": "tsc --lib esnext lib/*.ts --noEmit",
|
|
||||||
"check:fmt": "prettier --check lib/*",
|
"check:fmt": "prettier --check lib/*",
|
||||||
"check": "run-p check:*",
|
"check": "run-p check:*",
|
||||||
"format": "prettier --write lib/*"
|
"format": "prettier --write lib/*"
|
||||||
@@ -28,12 +27,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/recommended": "^1.0.2",
|
"@tsconfig/recommended": "^1.0.2",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.7",
|
||||||
"vite": "^4.2.1",
|
"vite": "^4.2.1"
|
||||||
"vite-plugin-eslint": "^1.8.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"typescript": "^5.0.2"
|
"typescript": "^5.0.2",
|
||||||
|
"ws": "^8.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@tsconfig/recommended/tsconfig.json"
|
|
||||||
"compilerOptions": {
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": false,
|
|
||||||
"importsNotUsedAsValues": "preserve"
|
|
||||||
},
|
|
||||||
"include": ["lib/**/*.d.ts", "lib/**/*.ts"]
|
|
||||||
}
|
|
||||||
+1
-3
@@ -1,13 +1,11 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import {defineConfig} from 'vite'
|
import {defineConfig} from 'vite'
|
||||||
import eslint from 'vite-plugin-eslint'
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [eslint()],
|
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: path.resolve(__dirname, 'lib/main.ts'),
|
|
||||||
name: 'paravel',
|
name: 'paravel',
|
||||||
|
entry: path.resolve(__dirname, 'lib/main.ts'),
|
||||||
fileName: (format) => `paravel.${format}.js`
|
fileName: (format) => `paravel.${format}.js`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user