event library with promise/stream support
proxy all web element event
ocev is an event library designed to simplify the complexity of event processing. while supporting promise/stream to handle events.
supporting all events of proxy web elements, and processing with ocev api.
all api are maximized support typescript, providing the most complete type prompt
npm install ocev
# or
yarn add ocev
# or
pnpm i ocev
import { SyncEvent } from "ocev"
// define event type
type EventHandlerMap = {
event1: (arg1: string, arg2: number) => void
event2: (arg1: number, arg2: string) => void
}
const syncEvent = SyncEvent.new<EventHandlerMap>()
queueMicrotask(() => {
syncEvent.emit("event1", "1", 2)
syncEvent.emit("event2", 3, "4")
})
// register
const cancel = syncEvent
.on("event1", (arg1, arg2) => {})
.once("event2", (arg1, arg2) => {})
.on("event1", (arg1, arg2) => {}, {
debounce: {
waitMs: 200,
maxWaitMs: 500,
},
})
// cancel()
// waitUtil event emit
await syncEvent.waitUtil("event1")
// create event stream
const eventStream = syncEvent.createEventStream(["event1", "event2"])
From the above example, you can see that ocev is essentially a (pub/sub) library, but ocev can also proxy all events of web element, and use ocev to handle all events with promise/stream.
ocev has two class,SyncEvent,EventProxy, the following example is mainly based on EventProxy
I've always felt that web event handling is too complex, and if you're using react, you're probably going to write this code. I have written a lot of template code like this, it is very complicated
useEffect(() => {
const callback = () => {}
target.addEventListener("event", callback) // any event target
return () => {
target.removeEventListener("event", callback)
}
}, [target])
for multiple events
useEffect(() => {
const callback1 = () => {}
target.addEventListener("event1", callback1)
const callback2 = () => {}
target.addEventListener("event2", callback2)
// ....
return () => {
target.removeEventListener("event1", callback1)
target.removeEventListener("event2", callback2)
// ....
}
}, [target])
You have to clean up as many as you register, which is very cumbersome to write.
If you are using ocev, your code will be something like this, infinite calls, one-time cleanup
import { EventProxy } from "ocev"
useEffect(() => {
return EventProxy.new(target)
.on("event1", (...args) => {}) // type hint!
.once("event2", (...args) => {})
.on("event3", (...args) => {})
}, [target])
ocev's method on/once
returns a clean function, which can be called once,on
as an object. For more details, please see the documentation.
all examples in current section base on EventProxy, EventProxy is wrapper of SyncEvent, more detail see documentation.
Consider a scenario where you want to establish a websocket connection, wait for the connection to open, set the maximum wait time for the connection, and then handle messages and exceptions. To ensure the correct release of resources, you might write the following code
async function setupWebSocket(
url: string,
successCallback: (ws: WebSocket) => void,
errorCallback: (err: Error) => void,
timeout: number
) {
const ws = new WebSocket(url)
const timeID = setTimeout(() => {
errorCallback(new Error("timeout"))
ws.removeEventListener("open", onOpen)
ws.removeEventListener("error", onError)
}, timeout)
function onOpen() {
successCallback(ws)
clearTimeout(timeID)
}
function onError() {
errorCallback(new Error("can't connect to server"))
clearTimeout(timeID)
}
ws.addEventListener("open", onOpen)
ws.addEventListener("error", onError)
}
ocev supports Promise to handle events. If you use ocev to handle events, the code will be like this
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
const ws = new WebSocket(url)
// Wait for the 'open' event to trigger or timeout throws an exception
await EventProxy.new(ws).waitUtil("open", { timeout })
// or Race waits for either an 'open' event or an 'error' to trigger first see docs
// await EventProxy.new(ws).waitUtilRace([
// { event: "open", timeout },
// { event: "error",
// mapToError: () => new Error("websocket connect error"),
// },
// ])
return ws
}
Promise makes event handling simple and elegant, and using Promise to process code makes logic clearer
Take it a step further and see how to implement message processing (Stream) with ocev
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
const ws = EventProxy.new(new WebSocket(url))
await ws.waitUtilRace([
{ event: "open", timeout },
{
event: "error",
mapToError: () => new Error("websocket connect error"),
},
])
// convert to Event Stream
const eventStream = ws.createEventStream(["close", "message", "error"])
// another way(ReadableStream)
// const readableStream = ws.createEventReadableStream(["close", "message", "error"])
// all events are pushed into a queue
for await (const { event, value } of eventStream) {
switch (event) {
case "error": {
throw Error("websocket connect error")
}
case "close": {
throw Error("websocket connection closed")
}
case "message": {
// support type prompt
const message = value[0].data
// handle message
break
}
default:
throw new Error("unreachable")
}
}
}
With asyncIterator, you can convert events into stream, and you can use the strategy to drop messages when faced with backpressure
With Promise/Stream, when you convert all the code to async/await, you can handle the reconnection logic like this
let reconnectCount = 0
for (;;) {
try {
await setupWebSocket("", 1000)
} catch (error) {
reconnectCount += 1
}
}
If you want to establish a WebRTC connection, you can use where
import { EventProxy } from "ocev"
async function connect(timeout: number) {
const connection = new RTCPeerConnection()
await EventProxy.new(connection).waitUtil("connectionstatechange", {
timeout,
// resolve when where return true
where: (ev) => connection.connectionState === "connected",
})
return connection
}
Do you know what events video triggers when it plays?
import { EventProxy } from "ocev"
// 或者 EventProxy.new(videoDom).proxyAllEvent()
EventProxy.new(videoDom, { proxyAllEvent: true }).any((eventName, ...args) => {
console.log(eventName)
})
real example in react
import { EventProxy } from "ocev"
import { useEffect, useRef } from "react"
function Video() {
const videoDomRef = useRef<HTMLVideoElement>(null)
useEffect(() => {
return EventProxy.new(videoDomRef.current!, { proxyAllEvent: true }).any((eventName, ...args) => {
console.log(eventName)
})
}, [])
const url = "" // your video link
return <video muted autoPlay src={url} ref={videoDomRef} />
}
open the console and you will see the order of all the 'video' events
Almost all web elements can be proxied by EventProxy
. codesandbox example
If you want to know more about EventProxy and SyncEvent, see docs