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

effect-zero

v0.3.3

Published

```ts // mutators.ts

Readme

effect-zero

Custom mutators

Defining mutators schema

// mutators.ts

import * as Mutators from "effect-zero/mutators";

// Define your mutators schema
// Both root-level and nested mutators are supported (up to 1 level of nesting)
export const mutatorSchema = Mutators.schema({
  // root-level mutator
  /*
  foo: Schema.Struct({
    message: Schema.String,
  }),
  // mutator without arguments
  /*
  bar: Schema.Void,
  */
  todo: {
    // nested mutator
    create: Schema.Struct({
      id: Schema.String,
      title: Schema.String,
    }),
    toggle: Schema.Struct({
      id: Schema.String,
      done: Schema.Boolean,
    }),
  },
});

Server setup

// server.ts

import { zeroPostgresJS } from "@rocicorp/zero/server/adapters/postgresjs";
import { PushResponse, PushParams, PushBody } from "effect-zero/types/push";
import * as ServerTransaction from "effect-zero/server-transaction";
import * as Server from "effect-zero/server";
import * as Schema from "effect/Schema";
import * as Effect from "effect/Effect";
import postgres from "postgres";
import { clientTransaction, clientMutators } from "./client"; // see below
import { mutatorSchema } from "./mutators";

// zero schema (define or use something like drizzle-zero)
import { schema } from "./schema";

// setup connection
// for more driver options, see: https://zero.rocicorp.dev/docs/zql-on-the-server#creating-a-database
const database = zeroPostgresJS(
  schema,
  postgres(process.env.ZERO_UPSTREAM_DB!),
);

// The "server-side" transaction
const serverTransaction = ServerTransaction.make(
  "ServerTransaction",
  database,
  // passing a client transaction allows us to use the client mutators on the server side
  clientTransaction,
);

// define server mutators
export const serverMutators = Mutators.make(mutatorSchema, {
  todo: {
    create: ({ id, title }) => Effect.gen(function* () {
      // Note, we can run arbitrary logic before/after performing the zero transaction
      // this is a unique feature which is not supported by the default zero push processor implementation
      // shipped with the base `@rocicorp/zero` package
      
      // before the transaction
      yield* Effect.log("before the transaction");

      Effect.gen(function* () {
        // during the transaction
        yield* Effect.log("during the transaction");
        yield* serverTransaction.use((tx) => tx.mutate.TodoTable.insert({ id, title, createdAt: Date.now() }));
        yield* serverTransaction.use((tx) => tx.mutate.TodoTable.update({ id, done: false }));
        // ...
      }).pipe(serverTransaction.execute);

      // after the transaction
      yield* Effect.log("after the transaction");
    }),
    toggle: Effect.fn(function* ({ id, done }) {
      // You can also reuse client mutators on the server
      yield* clientMutators.todo.toggle({ id, done }).pipe(serverTransaction.execute);
    }),
  },
});

// handler for push endpoint
// Note: this is framework-agnostic so that is why Effect.runPromise is used below, however this is of course not needed
// if your server framework is effect-based (like Effect HTTP module)
export async function handleZeroPush(req: Request): Promise<Response> {
  const url = new URL(req.url);
  const urlParams = Schema.decodeSync(PushParams)({
    schema: url.searchParams.get("schema")!,
    appID: url.searchParams.get("appID")!,
  });
  const payload = Schema.decodeSync(PushBody)(await req.json());

  const result = await Effect.runPromise(Server.processPush(serverTransaction, serverMutators, urlParams, payload));
  const responseBody = Schema.encodeSync(PushResponse)(result);
  return new Response(JSON.stringify(responseBody), { status: 200, headers: { "content-type": "application/json" } });
}

Client setup

// client.ts
import { Zero } from "@rocicorp/zero";
import * as ClientTransaction from "effect-zero/client-transaction";
import * as Effect from "effect/Effect";
import { schema } from "./schema"; // your schema
import { mutatorSchema } from "./mutators"; // see below

// The "client-side" transaction
const clientTransaction = ClientTransaction.make("ClientTransaction", schema);

export const clientMutators = Mutators.make(mutatorSchema, {
  todo: {
    create: Effect.fn(function* ({ id, title }) {
      yield* clientTransaction.use((tx) => tx.mutate.TodoTable.insert({ id, title, createdAt: Date.now() }));
    }),
    toggle: Effect.fn(function* ({ id, done }) {
      yield* clientTransaction.use((tx) => tx.mutate.TodoTable.update({ id, done }));
    }),
  },
});

// Helper to create a vanilla Zero client instance for querying and mutating
export const createZero = Effect.fn(function* (opts: { userID: string; auth?: string; server: string }) {
  // `Client.make` returns an Effect containing a Zero client instance
  return yield* Client.make(clientTransaction, clientMutators, {
    userID: opts.userID,
    auth: opts.auth,
    server: opts.server, // your push/pull endpoint base URL
    kvStore: "idb", // or "mem" for in-memory
    // "mutators" and "schema" are inferred from other arguments, so we don't need to pass them here
  });
});

Synced queries

Defining queries

// queries.ts

import { createBuilder } from "@rocicorp/zero";
import * as Query from "effect-zero/query";
import { schema } from "./schema"; // your schema

const builder = createBuilder(schema);

export const getTodoByIdQuery = Query.make({
  name: "listTodos",
  payload: Schema.Tuple(Schema.String),
  query: Effect.fn(function* (id) {
    return yield* Effect.succeed(builder.todos.where("id", id).one());
  }),
});

// Add all your queries here
export const queries = [
  getTodoByIdQuery,
];

Server setup

// server.ts

import * as Server from "effect-zero/server";
import { TransformRequestMessage } from "effect-zero/types/queries";
import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
import { queries } from "./queries";

// See `handleZeroPush` notes
export async function handleZeroGetQueries(req: Request): Promise<Response> {
  const payload = Schema.decodeSync(TransformRequestMessage)(await req.json());
  const result = await Effect.runPromise(Server.handleGetQueries(queries, schema, payload));
  return new Response(JSON.stringify(result), { status: 200, headers: { "content-type": "application/json" } });
}

Client setup

// client.ts

import * as Effect from 'effect/Effect';
import * as Query from "effect-zero/query";
import { getTodoByIdQuery } from "./queries";

const getTodoById = Effect.fn(function* (id: string) {
  // Create the query instance
  const query = yield* getTodoByIdQuery(id);

  // For `createZero` implementation, see "Custom mutators" -> "Client setup"
  const zero = yield* createZero({ ... });

// `Query.subscribe` creates an Effect's Stream from a query
  const stream = Query.stream(zero, query);

  // `Query.subscribe` creates an Effect's Subscribable from a query
  const sub = Query.subscribable(zero, query);

  // You can also use the query with the Zero client as usual
  const view = yield* Effect.sync(() => zero.materialize(query));
});

Usage with effect-atom

You might want to create atoms with query results. To do that, you can implement a queryAtom helper like this:

import { Atom } from "@effect-atom/atom";
import * as Effect from "effect/Effect";
import * as Subscribable from "effect/Subscribable";
import * as Query from "effect-zero/query";

import type { schema } from "./schema"; // your schema
import { zeroAtom } from "./zero"; // you can create zeroAtom using `createZero` from the "Client setup" section as a reference

type Schema = typeof schema;

export const queryAtom = Atom.family(
  <T extends keyof Schema["tables"] & string, R>(query: Query.Query<Schema, T, R>) => {
    return Atom.subscribable(
      Effect.fn(function* (get) {
        const zero = yield* get.result(zeroAtom);
        return Query.subscribable(zero, query).pipe(Subscribable.map(({ data }) => data));
      }),
    );
  },
);
// Usage

import { Atom } from "@effect-atom/atom";

const todoAtom = Atom.fn(Effect.fn(function* (id: string, get: Atom.FnContext) {
  const query = yield* getTodoByIdQuery(id);
  return yield* get.result(queryAtom(query));
});

Note: Queries created via Query.make implement the Equal trait, so Atom.family would properly cache the results when using queries as arguments.

Differences from the original implementation

One key difference is that effect-zero requires you to manually wrap your DB-related logic in a transaction inside a mutator code, whereas the original implementation automatically wraps the whole mutation in a transaction. This allows you to define some logic outside of transaction (either before or after), but it also creates some edge cases that are not possible in the original implementation, because now the transaction might succeed, but the code outside of it might fail. Below are the edge case rules that effect-zero follows during the mutation execution:

  1. "One transaction and succeed" -> successful response from the push endpoint (normal flow).
  2. "One transaction then fail" (code after the transaction produces an error) -> successful response, despite the mutation failing. This is essential to maintain integrity of Zero's internal state: the transaction has already succeeded (and altered the state of the database), thus the result from the push endpoint must coincide. Relatedly, the user must be careful with work performed after the transaction, it is considered "fire and forget".
  3. "Two or more transactions" -> This is a sub-case of the "One transaction then fail (#2)" scenario; the first transaction will succeed (and thus we must return a successful response from the push endpoint), and the second one will fail. We must be careful of performing multiple transactions in the mutator for this reason.
  4. "Zero transactions then succeed" -> error response, because all mutations must have a transaction.
  5. "Zero transactions then fail" -> error response containing the first error encountered.
  6. "Fail before transaction" -> same as #5.