zep:

- is a very small event pipeline library.

- turns every addEventListener into a composable, self-cleaning pipeline.

- is framework agnostic (vanilla js), so it should fit every frontend.

Events are great, until they are not. Like when you need to debounce inputs, handle scroll spam, or fetch async data out-of-order.

zep was built for exactly those moments.

No more loose let variables, no more memory leaks, no event handling boilerplate. Just clean, composable event handling logic.

Zep is a bit of a slacker 🦥 as well- it does zero work and attaches no DOM listeners until you actually call

.on().

Without AbortSignal: use the returned cleanup function.

import { zep } from "@marsbos/zep";

import { map, filter, debounce, latest } from "@marsbos/zep/helpers";

const cleanup = zep(queryEl, "input")

.use(

map((e) => e.target.value.trim()), // <= transform input to string value

filter((query) => query.length > 2), // <= only pass if min length = 3

debounce(300), // <= wait 300 ms

latest(

(query, signal) =>

fetch(`https://dummyjson.com/products/search?q=${query}`, {

signal,

}).then((res) => res.json()), // <= auto abort previous or pending requests

),

)

.on((results) => {

console.log(results);

});

// Whenever you're ready, just call 'cleanup()'With AbortSignal passed to zep.

zep(queryEl, "input", { signal }) // <= Pass a signal to zep

.use(

map((e) => e.target.value.trim()),

filter((query) => query.length > 2),

debounce(300),

latest((query, signal) =>

fetch(`https://dummyjson.com/products/search?q=${query}`, {

signal,

}).then((res) => res.json()),

),

)

.on((results) => {

console.log(results);

});

// controller.abort() will do the cleanup via the signal passed to zep.Everything in Zep execution pipelines is 100% synchronous. When an event fires, it passes through filter, map, take, and your .on() listener instantly within the very same callstack. No microtask queues, no hidden schedulers.

The only exception is latest() (and timing operators like debounce / throttle / raf which delay execution):

- latest()introduces an asynchronous boundary because it handles Promises and async operations. It passes an- AbortSignalto your async callback and automatically cancels pending executions when a new event arrives.

- debounce: waits until mshave passed.

- distinct: ignores values if they match the previous value.

- filter: only passes values that meet a condition.

- latest: automatically abortsprevious/pending async tasks on new events.

- map: transforms the value before passing it on.

- raf: limits execution to requestAnimationFramefor smooth UI updates.

- take: stops listening after n events have fired.

- throttle: limits execution to once every ms.

export const log = (eventName) => (onDestroy, stop) => (next) => {

let count = 0;

return (value) => {

console.log(new Date().toISOString(), `${++count} events for ${eventName}`);

next(value);

};

};

// Usage: zep(myButton, 'click').use(map(...), log("myEvent")).on(...);Keep UI animations butter smooth by decoupling scroll events from rendering.

import { zep } from "@marsbos/zep";

import { raf } from "@marsbos/zep/helpers";

zep(window, "scroll")

.use(raf())

.on(() => {

const scrolled = window.scrollY;

// Perform smooth layout/UI updates here

});Unsubscribe after 1 successfull event

import { zep } from "@marsbos/zep";

import { take, filter } from "@marsbos/zep/helpers";

zep(document, "keydown")

.use(

filter((e) => e.key === "Escape"), // <= We only want to act on the Escape key

take(1), // <= Stop and unsubscribe

)

.on(() => closeModal());Avoid layout thrashing when measuring window bounds.

import { zep } from "@marsbos/zep";

import { throttle, map } from "@marsbos/zep/helpers";

zep(window, "resize")

.use(

throttle(200),

map(() => ({ width: window.innerWidth, height: window.innerHeight })),

)

.on(({ width, height }) => {

console.log(`Window resized to: ${width}x${height}`);

});zep.js is extremely lightweight and distributed as an ES module with zero dependencies:

- Core: ~417 bytes minified (265 bytes gzipped)

- Helpers: ~922 bytes minified (484 bytes gzipped)

import { zep } from "@marsbos/zep";

import { filter, etc... } from "@marsbos/zep/helpers";Marcel Bos

MIT

- Concept, architecture & core logic: Built by Marcel Bos.

- Tooling & Quality: AI was used to help me with the TypeScript definitions and the Vitest test suite.

I hate testing and TypeScript. So yeah, I am a bit of a slacker 🦥.