Skip to content

swnb/ocev

Repository files navigation

Jest coverage github workflow typescript license npm

event library with promise/stream support

proxy all web element event

What is ocev

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

English Docs | 📑中文

Install

npm install ocev
# or
yarn add ocev
# or
pnpm i 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"])

What can ocev do

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

1. Simplified 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. 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.

2. Promise/Stream

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
}

Observe all the events of a web object

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

More

If you want to know more about EventProxy and SyncEvent, see docs