Tide · guide

Deduplicate a webhook

A payment provider delivers the same webhook twice. Give it a `dedupeKey` and Tide processes it once.

Install it
pnpm add @niscorp/tide @niscorp/prism @niscorp/strata zod
Step 1 of 3

The engine

createTide makes the engine. It takes a store for its state (this one is in memory), Prism’s evaluate to fill in templates, and the effects: the functions a reflex may call by name.

Tide takes the time as an argument and never reads the system clock, so this file prints the same on any day.

The engine
import { createTide, createMemoryStore } from '@niscorp/tide';
import { evaluate } from '@niscorp/prism';

const tide = createTide({
  store: createMemoryStore(),
  transform: evaluate,
  effects: { 'seat.release': { writes: [], run: (input) => console.log('seat.release', input) } },
});

// an instant: a day of October 2026 and a local time in Vienna
const vienna = (day: number, time: string): number => Date.parse(`2026-10-${day}T${time}:00+02:00`);
Step 2 of 3

A reflex on a signal

A signal is an event from outside your database, under a name you choose. Its body is $.fact.payload.

A reflex on a signal
await tide.load(
  [
    {
      id: 'payment.refunded',
      intent: 'Release the seats of a booking that was refunded.',
      on: { fact: { signal: 'payments' } },
      effect: { name: 'seat.release', input: { booking: { $ref: '$.fact.payload.booking' } } },
    },
  ],
  { at: vienna(23, '09:00') },
);
Step 3 of 3

The webhook, twice

dedupeKey is the provider’s own id for the event. The first ingest returns the stored event. The second returns nothing, because it is already stored.

Without a key Tide cannot know they are the same, and the seats would be released twice.

The webhook, twice
const event = { kind: 'signal', name: 'payments', payload: { booking: 'B-2041' }, dedupeKey: 'evt_81', at: vienna(23, '10:00') } as const;

console.log('first:', (await tide.ingest(event)) === undefined ? 'dropped' : 'taken in');
console.log('second:', (await tide.ingest(event)) === undefined ? 'dropped' : 'taken in');

await tide.advance({ now: vienna(23, '10:00') });
console.log('runs:', (await tide.ledger.runs({})).length);
Then

Run it.

The second delivery is dropped, and the seats are released once.

Run it
npx tsx twice.mts
It prints
first: taken in
second: dropped
seat.release { booking: 'B-2041' }
runs: 1