Deduplicate a webhook
A payment provider delivers the same webhook twice. Give it a `dedupeKey` and Tide processes it once.
pnpm add @niscorp/tide @niscorp/prism @niscorp/strata zodThe 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.
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`);A reflex on a signal
A signal is an event from outside your database, under a name you choose. Its body is $.fact.payload.
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') },
);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.
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);Run it.
The second delivery is dropped, and the seats are released once.
npx tsx twice.mtsfirst: taken in
second: dropped
seat.release { booking: 'B-2041' }
runs: 1