npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

event-watermark-store

v0.1.0

Published

Deterministic event-time watermarks with terminal precedence and atomic storage providers

Readme

event-watermark-store

event-watermark-store keeps the newest event-time state for each key while a terminal watermark prevents delayed events from restoring an already-ended state.

It provides a pure decision function, an atomic provider contract, an in-memory provider, a Redis Lua provider, and a small coalescing cache. The package has no runtime dependencies and supports both ECMAScript modules and CommonJS on Node.js 20.19 or newer.

Ordering policy

Each key is absent, active at an event time, or terminal at an event time.

| Current record | Incoming transition | Result | | --- | --- | --- | | absent | any valid transition | applied | | upsert at T | older than T | stale | | upsert at T | upsert at T | refreshed | | upsert at T | terminal at T | applied | | terminal at T | time at or below T | blocked-by-terminal | | any record | newer event time | applied |

Terminal therefore wins an equal-time race regardless of which receiver executes first. A later upsert can intentionally resume the key.

Install

npm install event-watermark-store

Memory provider

import { createEventWatermarkStore } from "event-watermark-store";
import { createMemoryEventWatermarkProvider } from "event-watermark-store/memory";

const provider = createMemoryEventWatermarkProvider({
  stateTtlSeconds: 300,
  terminalTtlSeconds: 7 * 24 * 60 * 60,
});
const store = createEventWatermarkStore({ provider });

await store.transition({
  key: "device-a",
  kind: "upsert",
  eventTime: 100,
  value: { reading: 42 },
});

await store.transition({
  key: "device-a",
  kind: "terminal",
  eventTime: 100,
  reason: "completed",
});

The same API is available through require():

const { createEventWatermarkStore } = require("event-watermark-store");
const {
  createMemoryEventWatermarkProvider,
} = require("event-watermark-store/memory");

Transition results

Every transition returns:

{
  status: "applied",
  accepted: true,
  changed: true,
  previous: null,
  current: { kind: "upsert", eventTime: 100 }
}

Statuses have fixed flag meanings:

| Status | accepted | changed | | --- | ---: | ---: | | applied | true | true | | refreshed | true | true | | replayed | true | false | | stale | false | false | | blocked-by-terminal | false | false |

An optional operationId classifies a retry of the current transition as replayed. Identity covers kind, event time, value presence and content, and reason presence and content. Reusing an ID with different identity throws OperationIdConflictError. receivedAt is deliberately excluded so a retry using a receiver clock can still replay. A replay does not extend TTL.

The memory provider fingerprints values with a canonical, type-aware encoding. Values used with operationId cannot contain cycles, functions, symbols, or custom class instances. Supported mutable values include arrays, plain objects, dates, regular expressions, maps, sets, array buffers, and typed binary views. They are snapshotted when stored and cloned on each read, so caller mutation cannot change recorded operation content.

The Redis provider fingerprints the configured codec output. Its default JSON codec sorts object keys recursively, so equivalent JSON content does not depend on property insertion order. Custom encoders use exact string identity and must return the same well-formed Unicode string for values the application considers identical.

Trigger non-idempotent downstream work only when changed is true. The store cannot make a database write, message publication, or HTTP response atomic with its own provider transition.

Redis provider

The Redis adapter accepts a structural script executor, so the package does not depend on a particular Redis client:

import { createEventWatermarkStore } from "event-watermark-store";
import {
  createIoRedisExecutor,
  createRedisEventWatermarkProvider,
} from "event-watermark-store/redis";

const provider = createRedisEventWatermarkProvider({
  execute: createIoRedisExecutor(redisClient),
  prefix: "events:",
  stateTtlSeconds: 300,
  terminalTtlSeconds: 604800,
});
const store = createEventWatermarkStore({ provider });

For node-redis, use createNodeRedisExecutor(redisClient). Both helpers only adapt the client's eval call; client construction and connection lifecycle remain application responsibilities.

The default codec is stable-key JSON. Supply encode(value): string and decode(encoded): value for another format. Encoding finishes before Redis is called, so serialization failures cannot partially apply a transition.

Each logical key becomes one Redis hash:

<prefix>{<base64url UTF-8 key>}:record

The hash tag is safe for Redis Cluster and leaves room for related keys in the same slot. Keys and prefixes must contain well-formed Unicode; this prevents UTF-8 encoders from collapsing lone UTF-16 surrogates into the replacement character. Prefixes containing { or } are rejected.

Keys, operation IDs, reasons, Redis prefixes, and custom codec output reject lone UTF-16 surrogates. Ordinary Unicode strings, including the actual U+FFFD replacement character and valid surrogate pairs, retain the same key format.

stateTtlSeconds and terminalTtlSeconds are non-negative safe integers. Zero means persistent. Expired or evicted records no longer reject delayed events.

Stored schema, kind, time, and presence flags are validated before a write. Malformed hashes and wrong Redis types throw CorruptStateError without repairing or replacing the record.

Redis numeric validation rejects NaN and positive or negative infinity in both incoming script arguments and stored hashes before mutation. Corrupt terminal watermarks therefore cannot be overwritten by exploiting non-finite Lua comparisons.

Value and reason presence are stored separately, so an explicitly empty reason round-trips as reason: "" rather than becoming absent.

Coalesced snapshot reads

import { createCoalescedSnapshotReader } from "event-watermark-store/cache";

const readSnapshot = createCoalescedSnapshotReader({
  read: () => loadSnapshot(),
  ttlMs: 1000,
});

await Promise.all([readSnapshot(), readSnapshot()]); // one underlying read
readSnapshot.invalidate();

Failures are not cached. Invalidation during an in-flight read prevents that older result from repopulating the cache, and the next call starts a fresh read without waiting for the older generation. peek() returns an unexpired cached entry without refreshing it.

Guarantees and limits

  • Atomicity is per key and depends on the provider implementation. A batch across keys is not a transaction.
  • Redis Lua execution is atomic, but Redis persistence, eviction, replication, and failover settings determine durability.
  • Redis Cluster can lose an acknowledged write in documented failover windows.
  • TTL defines how long an old event remains blocked; it is not archival retention.
  • Event times are JavaScript numbers and Redis Lua numbers. Values outside exactly distinguishable numeric ranges can compare as the same number.
  • Equal-time upserts intentionally refresh and replace the active value.
  • Memory values without an operationId retain reference semantics. Values bound to an operation ID are snapshotted and cloned on read.
  • Cached values are returned by reference and are not cloned.

For a bounded receiver migration profile, see service compatibility. The provider model and Redis schema are described in design notes.

Verification

npm run check

The default suite tests the executor contract without requiring Redis. An explicit local Redis can be checked when redis-cli is available:

npm run test:redis

This opt-in test uses a random prefix and deletes only its exact test key.

License

ISC