Introduction
what is ocev
ocev is an event library designed to simplify the complexity of event processing, while supporting promise/stream mode 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
Install
npm install ocev
Basic Usage
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"])
Reasons for using ocev
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
ocev has the following characteristics
1. Simplifying web event handling
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.
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) => {}) // Support complete type hints
.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 this section are based on EventProxy
, which is an encapsulation of SyncEvent
. For more information on both, see the documentation.
2. Promise support
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
- timeout
- race emit
- config race
- map to error
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 })
return ws
}
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
const ws = new WebSocket(url)
// Race waits for either an 'open' event or an error to trigger first
const { event } = await EventProxy.new(ws).waitUtilRace(["open", "error"])
if (event === "error") {
throw new Error("websocket connect error")
}
return ws
}
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
const ws = new WebSocket(url)
// race waits for a trigger in 'open' , 'error' and then resolves, timeout rejects exception
const { event } = await EventProxy.new(ws).waitUtilRace([
{ event: "open", timeout },
"error",
])
if (event === "error") {
throw new Error("websocket connect error")
}
return ws
}
import { EventProxy } from "ocev"
async function setupWebSocket(url: string, timeout: number) {
const ws = new WebSocket(url)
// Race waits for 'open' event to trigger resolve, 'error', 'timeout', whichever triggers exception first
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"),
},
])
// transform to event stream
const eventStream = ws.createEventStream(["close", "message", "error"])
// another way create ReadableStream
// const readableStream = ws.createEventReadableStream(["close", "message", "error"])
// All events are pushed into a queue and triggered in turn
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": {
// 支持类型提示
const message = value[0].data
// handle message
break
}
default:
throw new Error("unreachable")
}
}
}
With asynchronous iterators, you can convert events into streams, and when processing message streams, you can also use strategy to drop some messages to deal with backpressure problems.
With Promise/Stream, the code becomes legible, and 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 write something like this
import { EventProxy } from "ocev"
async function connect(timeout: number) {
const connection = new RTCPeerConnection()
await EventProxy.new(connection).waitUtil("connectionstatechange", {
timeout,
// resolve only if where returns true
where: (ev) => connection.connectionState === "connected",
})
return connection
}
For more waitUtil configuration, please see the documentation;
3. Observe all events of a web object
Do you know what events 'video' triggers when it plays? Use ocev to directly observe all events of a web object
- first method
- second method
import { EventProxy } from "ocev"
// support complete type prompt !
EventProxy.new(videoDom, { proxyAllEvent: true }).any((eventName, ...args) => {
console.log(eventName)
})
import { EventProxy } from "ocev"
EventProxy.new(videoDom).proxyAllEvent().any((eventName, ...args) => {
console.log(eventName)
})
Open the console and you will see all the events triggered by video
and its order
Almost all web elements can be proxied by EventProxy
.
More
The above is just a partial API usage of ocev, if you want to know more, please see the documentation
If you have good suggestions and ideas, welcome pr and issue