Tide · guide

One task per row

A reflex can run its function once for each row of a query. Each row is its own task, and succeeds, fails or is retried alone.

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

The engine, with a selection

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.

select is how Tide gets rows: it is handed the query a reflex carries and returns the rows. Here it returns three members. The effect throws the first time it is asked to write to Linus.

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

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

let tries = 0;
const tide = createTide({
  store: createMemoryStore(),
  transform: evaluate,
  select: () => [{ id: 'ada' }, { id: 'linus' }, { id: 'mia' }],
  effects: {
    'mail.send': {
      writes: [],
      run: (input) => {
        const { to } = input as { to: string };
        if (to === 'linus' && (tries += 1) === 1) throw new Error('the mail server did not answer');
        return { sent: to };
      },
    },
  },
});

// 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 with a selection

select names the query, and unitKey the field that tells one row from another. With mode: 'each' Tide makes one task for each row, and $.row is that row in the effect’s input.

A reflex with a selection
await tide.load(
  [
    {
      id: 'theatre.reminder',
      intent: 'Remind every member with a seat tonight.',
      on: { clock: { every: 'day', at: '12:00', tz: 'Europe/Vienna' } },
      select: { query: 'members with a seat tonight', mode: 'each', unitKey: 'id' },
      effect: { name: 'mail.send', input: { to: { $ref: '$.row.id' } } },
    },
  ],
  { at: vienna(23, '09:00') },
);
Step 3 of 3

Noon, and a minute later

At noon two mails go out and one throws. An effect that throws is retried with backoff: after 1, 2 and 4 minutes. One that returns is done, whatever it returned.

Tide stores one run and three tasks. ledger reads them.

Noon, and a minute later
const printLedger = async (when: string): Promise<void> => {
  const [run] = await tide.ledger.runs({});
  const tasks = await tide.ledger.tasks({});
  console.log(when, '· run', run?.state, '· done', run?.done, 'of', run?.total);
  for (const task of tasks) console.log(' ', task.unit, task.state, 'attempt', task.attempt, task.error ?? '');
};

await tide.advance({ now: vienna(23, '12:00') });
await printLedger('12:00');
await tide.advance({ now: vienna(23, '12:01') });
await printLedger('12:01');
Then

Run it.

At noon Ada and Mia are done and Linus is waiting for a retry. A minute later his second attempt goes through, and the run is settled with three of three.

Run it
npx tsx rows.mts
It prints
12:00 · run fanned · done 2 of 3
  ada done attempt 1 
  linus retrying attempt 1 the mail server did not answer
  mia done attempt 1 
12:01 · run settled · done 3 of 3
  ada done attempt 1 
  linus done attempt 2 
  mia done attempt 1