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

@triargos/effect-procurat

v3.1.1

Published

Effect-based Procurat SDK

Readme

@triargos/effect-procurat

Effect-based SDK for the Procurat API.

Install

pnpm add @triargos/effect-procurat effect

effect@^4.0.0-beta.0 is a peer dependency.

On Effect v3

Import from the /v3 subpath and add @effect/platform:

pnpm add @triargos/effect-procurat effect@^3.18.5 @effect/platform@^0.92.1
import { ProcuratClient } from '@triargos/effect-procurat/v3';
import { CreatePerson, Person } from '@triargos/effect-procurat/v3/schemas';
import type { ProcuratError } from '@triargos/effect-procurat/v3/errors';

The subpath carries the same API, the same errors and the same schemas as the v4 entry points. Two spelling differences follow Effect itself: the transport comes from @effect/platform instead of effect/unstable/http, and a request schema such as CreatePerson is a value plus a type rather than a class.

Every release ships both. The v3 build is generated from the v4 sources and blocks the release if it does not typecheck and pass its tests — see v3/README.md.

Provide a transport

The SDK does not ship an HTTP transport. Pick one from effect/unstable/http and provide it alongside the client layer — FetchHttpClient.layer works everywhere fetch does.

import { Effect, Layer, Redacted } from 'effect';
import { FetchHttpClient } from 'effect/unstable/http';
import { ProcuratClient } from '@triargos/effect-procurat';

const ProcuratLive = ProcuratClient.layer({
  apiKey: Redacted.make(process.env.PROCURAT_API_KEY!),
  baseUrl: 'https://procurat.example.com/api',
}).pipe(Layer.provide(FetchHttpClient.layer));

Or read both from the environment (PROCURAT_API_KEY, PROCURAT_BASE_URL):

const ProcuratLive = ProcuratClient.layerConfig().pipe(Layer.provide(FetchHttpClient.layer));

Use it

Every method takes a single params object and fails with ProcuratError.

import { Effect } from 'effect';
import { ProcuratClient } from '@triargos/effect-procurat';

const program = Effect.gen(function* () {
  const procurat = yield* ProcuratClient;

  const person = yield* procurat.person.findById({ id: 42 });

  const optional = yield* procurat.person
    .findById({ id: 99 })
    .pipe(Effect.catchTag('ProcuratNotFoundError', () => Effect.succeed(null)));

  yield* procurat.person.update({ person: { ...person, comment: 'synced' } });

  return { person, optional };
});

Because every operation shares one error union, a fan-out sync collects failures that are already self-describing — each error carries the operation and endpoint it came from:

const results =
  yield *
  Effect.forEach(ids, (id) => procurat.person.findById({ id }), {
    mode: 'either',
    concurrency: 8,
  });

Errors

All failures are Data.TaggedError classes; catch them with Effect.catchTag.

| tag | cause | retried | | ------------------------- | ---------------------------------- | ------- | | ProcuratNotFoundError | 404 | no | | ProcuratBadRequestError | 400, 409, 422 | no | | ProcuratAuthError | 401, 403 | no | | ProcuratUnavailableError | 5xx, unmapped status, no response | yes | | ProcuratDecodeError | response shape drift | no |

Every error carries operation and endpoint. HTTP response failures also carry status, code, and message. ProcuratBadRequestError carries the rejected payload for dead-lettering. ProcuratUnavailableError.kind distinguishes server and transport failures, while ProcuratDecodeError carries the raw body that failed to decode.

An error body that is not Procurat's { code, error } envelope — an HTML page from a reverse proxy, say — still surfaces as a typed error, with code: null and the raw text as message.

Retries

ProcuratUnavailableError is retried 3 times with jittered exponential backoff starting at 200ms. Procurat does not send Retry-After, so none is honoured.

Install ProcuratRetry to change the policy for every call:

import { Schedule } from 'effect';
import { ProcuratRetry } from '@triargos/effect-procurat';

const NoRetries = ProcuratRetry.layer({
  while: () => false,
  schedule: Schedule.forever,
  times: 0,
});

const ProcuratLive = ProcuratClient.layer({ apiKey, baseUrl }).pipe(
  Layer.provide(NoRetries),
  Layer.provide(FetchHttpClient.layer),
);

Dates

Every date field is an IsoDate: a YYYY-MM-DD string with no time and no zone. It is display-ready, compares with ===, and sorts with <. Build one with IsoDate.make or, from a Date, with IsoDate.fromDate — the zone is yours to pick, because a Date near midnight falls on a different day in UTC than it does locally.

import { IsoDate } from '@triargos/effect-procurat';

yield * procurat.absence.create({
  absence: {
    personId: 42,
    startDate: IsoDate.make('2024-05-01'),
    endDate: IsoDate.fromDate(picker.value, 'local'),
    // ...
  },
});

Procurat is moving from timestamps (2024-05-01T00:00:00.000Z) to date-only strings. Responses in either format decode the same, so reads need no configuration. Writes do: if your installation still runs the old API, ask for the old format.

const ProcuratLive = ProcuratClient.layer({ apiKey, baseUrl, dateFormat: 'timestamp' });

The option defaults to 'iso-date' and disappears once every installation has moved.

If you do not know which format an installation wants, ask it. health.determineDateStyle() reads the build number and answers 'iso-date' or 'timestamp':

const dateFormat = yield* procurat.health.determineDateStyle();

This one is temporary too and goes away with the option.

Schemas

Response and request types live on @triargos/effect-procurat/schemasPerson, CreatePerson, Address, Group, and so on. Request types are plain object shapes: pass an object literal, no constructor call.

Health

procurat.health.get() reads GET /health — the build number, the database version, and what the installation is doing right now.

const health = yield* procurat.health.get();

health.build; // 4711
health.productionVersion; // '2024.1'
health.databaseLocked; // false

The lastUpdate* fields stay raw strings, because the API names no format for them.

The same service answers which date format the installation accepts on write — see Dates.

File downloads

file.download* answers with the content type alongside the bytes, because the endpoints stream arbitrary files and nothing in the path tells you what you got.

const { contentType, stream } = yield* procurat.file.downloadPublicFile({ path: 'info/note.pdf' });

contentType; // 'application/pdf'
yield * Stream.run(stream, sink);

When the installation sends no Content-Type, contentType is 'application/octet-stream' — what HTTP already means by an unlabeled body, and what file.upload* sends when you name no type.

There is no fileName: these endpoints address files by path, so the basename of the path you passed is the name.

File uploads

file.upload* buffers the whole stream into memory before sending it, because the endpoint takes multipart and FormData needs a materialized blob. Size uploads accordingly.