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

@sideffect/lint

v0.3.0

Published

ESLint and Oxlint rules for Sideffect and Cloudflare Workflows.

Readme

@sideffect/lint

Lint rules for Cloudflare Workflows and Sideffect workflows.

@sideffect/lint catches workflow bugs that are easy to miss in review: unowned step promises, mutation of replayed event payloads, unsafe races between steps, unstable step names, and step.do timeouts that exceed Cloudflare's recommended limit.

The same rules run in ESLint and Oxlint, and they work with both native Cloudflare WorkflowEntrypoint classes and Sideffect Workflow.make(...).toLayer(...) definitions.

This package is experimental. Rule behavior and public exports may change before 1.0.

Install

bun add -D @sideffect/lint

Install the runner you use as well:

bun add -D eslint
# or
bun add -D oxlint

eslint and oxlint are optional peer dependencies so you only need the one your project runs.

Quick Start

ESLint

Use the recommended flat config:

// eslint.config.js
import sideffect from "@sideffect/lint/eslint";

export default [...sideffect.configs.recommended];

Use strict if you also want step-name determinism and timeout checks:

// eslint.config.js
import sideffect from "@sideffect/lint/eslint";

export default [...sideffect.configs.strict];

Oxlint

Register the JavaScript plugin and enable the rules you want:

// .oxlintrc.json
{
  "jsPlugins": [
    {
      "name": "sideffect",
      "specifier": "@sideffect/lint/oxlint",
    },
  ],
  "rules": {
    "sideffect/await-step-calls": "error",
    "sideffect/no-event-mutation": "error",
    "sideffect/no-step-race-any-outside-step": "warn",
  },
}

For Vite+ projects, you can import the bundled Oxlint preset:

// vite.config.ts
import { oxlintRecommended } from "@sideffect/lint";
import { defineConfig } from "vite-plus";

export default defineConfig({
  lint: oxlintRecommended,
});

Use oxlintStrict to enable every rule:

// vite.config.ts
import { oxlintStrict } from "@sideffect/lint";
import { defineConfig } from "vite-plus";

export default defineConfig({
  lint: oxlintStrict,
});

Presets

recommended enables the rules that protect core workflow correctness:

sideffect/await-step-calls: error
sideffect/no-event-mutation: error
sideffect/no-step-race-any-outside-step: warn

strict includes recommended and adds rules for deploy-time hygiene:

sideffect/deterministic-step-names: warn
sideffect/max-step-timeout-30m: error

Supported Workflow Shapes

Native Cloudflare Workflows

Rules run inside run(event, step) methods on classes that extend WorkflowEntrypoint from cloudflare:workers:

import { WorkflowEntrypoint } from "cloudflare:workers";

export class BillingWorkflow extends WorkflowEntrypoint {
  async run(event, step) {
    await step.do("charge customer", async () => {
      return chargeCustomer(event.payload.customerId);
    });
  }
}

Namespace imports are supported too:

import * as cf from "cloudflare:workers";

export class BillingWorkflow extends cf.WorkflowEntrypoint {
  async run(event, step) {
    return step.sleep("pause", "1 second");
  }
}

Sideffect Workflows

Rules also run inside Sideffect workflow layers:

import { Schema, Step, Workflow } from "sideffect";

const billingWorkflow = Workflow.make({
  name: "billing",
  payload: Schema.Struct({ customerId: Schema.String }),
});

const chargeCustomer = Step.make("charge customer", {
  payload: Schema.Struct({ customerId: Schema.String }),
  result: Schema.Void,
  run: async ({ customerId }, ctx) => {
    await ctx.env.PAYMENTS.charge(customerId);
  },
});

export const billingWorkflowLayer = billingWorkflow.toLayer(async ({ payload }, step) => {
  await step.do(chargeCustomer, payload);
});

The plugin recognizes direct Workflow.make(...).toLayer(...) calls, local workflow constants, exported workflow constants, and imported local definitions whose names end in Workflow.

Effect-style layer bodies are supported when the step promise is owned by Effect.promise or Effect.tryPromise:

Workflow.make({ name: "billing" }).toLayer(
  Effect.fn(function* ({ payload }, step) {
    return yield* Effect.promise(() => step.do(chargeCustomer, payload));
  }),
);

Rules

sideffect/await-step-calls

Workflow step calls must be owned. A call to step.do, step.sleep, step.sleepUntil, or step.waitForEvent should be awaited, returned, yielded, chained from a returned promise, or collected by an awaited promise aggregate.

// Bad: the workflow can continue before the step is tracked.
step.do("charge customer", async () => chargeCustomer(event.payload.customerId));

// Good: the workflow owns the step promise.
await step.do("charge customer", async () => chargeCustomer(event.payload.customerId));

Owned promise aggregates are accepted:

const [customer, invoice] = await Promise.all([
  step.do("fetch customer", async () => fetchCustomer(event.payload.customerId)),
  step.do("fetch invoice", async () => fetchInvoice(event.payload.invoiceId)),
]);

The rule accepts Promise.all, Promise.allSettled, Promise.race, and Promise.any when the aggregate itself is owned. Pair it with no-step-race-any-outside-step if you want to block replay-unsafe races.

sideffect/no-event-mutation

Workflow events and payloads are replay inputs. Mutating them can make retries, restarts, and local reasoning disagree about what state actually exists.

// Bad: mutates replay input.
event.payload.status = "charged";

// Good: derive a new value and persist it through a step.
const nextStatus = await step.do("charge", async () => "charged");

The rule catches direct assignments, updates, deletes, and common mutating methods through simple aliases:

const { payload } = event;

payload.items.push("charged");
delete payload.pendingChargeId;

sideffect/no-step-race-any-outside-step

Promise.race and Promise.any over step promises can replay inconsistently when the race happens in workflow code. Put the race inside a step.do callback so the result is captured as one step outcome.

// Bad: races workflow steps directly.
const winner = await Promise.race([
  step.do("slow provider", async () => callSlowProvider()),
  step.do("fast provider", async () => callFastProvider()),
]);

// Good: captures the race result inside one step.
const winner = await step.do("race providers", async () => {
  return Promise.race([callSlowProvider(), callFastProvider()]);
});

sideffect/deterministic-step-names

Step names are stable identifiers for persisted workflow state. Do not build them from time, randomness, or generated IDs.

// Bad: creates a different step name on replay.
await step.do(`run at ${Date.now()}`, async () => event.payload);

// Good: stable name, variable data stays in the payload.
await step.do("process payload", async () => event.payload);

The rule checks names passed to native step calls and names passed to Step.make(...). It detects Date.now(), new Date(), Math.random(), and crypto.randomUUID().

sideffect/max-step-timeout-30m

Cloudflare recommends step.do timeouts of 30 minutes or less. Use step.waitForEvent or split the workflow when work needs to wait longer.

// Bad: longer than 30 minutes.
await step.do("call provider", { timeout: "2 hours" }, async () => callProvider());

// Good: within the recommended limit.
await step.do("call provider", { timeout: "30 minutes" }, async () => callProvider());

For Sideffect step definitions, the timeout object is the third argument:

await step.do(chargeCustomer, payload, { timeout: "30 minutes" });

The rule handles string durations, static template literals, and numeric millisecond literals.

Manual Rule Configuration

If you do not want a preset, register the plugin and choose rules directly.

// eslint.config.js
import sideffect from "@sideffect/lint/eslint";

export default [
  {
    plugins: { sideffect },
    rules: {
      "sideffect/await-step-calls": "error",
      "sideffect/no-event-mutation": "error",
      "sideffect/no-step-race-any-outside-step": "warn",
      "sideffect/deterministic-step-names": "warn",
      "sideffect/max-step-timeout-30m": "error",
    },
  },
];

Limitations

@sideffect/lint is syntax-only. It does not run TypeScript type resolution, execute modules, or prove that arbitrary functions are deterministic.

That keeps the plugin fast and lets the same rule implementations run in both ESLint and Oxlint, but it also means recognition is intentionally conservative. Imported local workflow definitions are detected with syntax and naming heuristics rather than full runtime value tracking.

License

Apache-2.0