
Product
Introducing Webhook Events for Alert Changes
Add real-time Socket webhook events to your workflows to automatically receive software supply chain alert changes in real time.
effect-zero
Advanced tools
// 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.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.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
});
});
// 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.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.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));
});
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.makeimplement theEqualtrait, soAtom.familywould properly cache the results when using queries as arguments.
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:
FAQs
```ts // mutators.ts
We found that effect-zero demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Add real-time Socket webhook events to your workflows to automatically receive software supply chain alert changes in real time.

Security News
ENISA has become a CVE Program Root, giving the EU a central authority for coordinating vulnerability reporting, disclosure, and cross-border response.

Product
Socket now scans OpenVSX extensions, giving teams early detection of risky behaviors, hidden capabilities, and supply chain threats in developer tools.