Dry run, manual run, retry
Preview what a reflex would do, run it by hand, and retry a failed task.
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.
The gateway is down for the first charge it is asked to make.
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';
let charges = 0;
const tide = createTide({
store: createMemoryStore(),
transform: evaluate,
select: () => [{ id: 'ada', cents: 4600 }, { id: 'linus', cents: 2300 }],
effects: {
'card.charge': {
writes: [],
run: (input) => {
if ((charges += 1) === 1) throw new Error('the gateway is down');
return { charged: 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 disabled reflex
enabled: false means its schedule never runs it. retry.max: 0 means a charge that throws is not retried.
await tide.load(
[
{
id: 'pass.renew',
intent: 'Charge every season pass that is due.',
on: { clock: { every: 'month', on: 1, at: '03:00', tz: 'Europe/Vienna' } },
select: { query: 'passes due', mode: 'each', unitKey: 'id' },
effect: { name: 'card.charge', input: { member: { $ref: '$.row.id' }, cents: { $ref: '$.row.cents' } } },
policy: { retry: { max: 0 } },
enabled: false,
},
],
{ at: vienna(20, '09:00') },
);A dry run
preview runs the selection and fills in each input. The effect’s run is not called, and nothing is written.
const preview = await tide.preview('pass.renew', { now: vienna(20, '10:00') });
console.log('would run:', preview.fired, '· for', preview.units.length, 'rows');
for (const unit of preview.units) console.log(' ', unit.unit, JSON.stringify(unit.input));
console.log('charges so far:', charges);Run it by hand
fire triggers a reflex now, whatever its own trigger and even when it is disabled, and the next advance runs it. Who did it is stored with the event, as by.
await tide.fire('pass.renew', { now: vienna(20, '10:05'), by: 'box-office' });
await tide.advance({ now: vienna(20, '10:05') });
const taskStates = async (): Promise<string> => (await tide.ledger.tasks({})).map((task) => `${task.unit} ${task.state}`).join(', ');
console.log('after firing:', await taskStates());Retry the failed task
A failed task stays in the ledger as failed. retry reopens it, and the next advance runs it.
const [failed] = await tide.ledger.tasks({ state: 'failed' });
if (failed !== undefined) await tide.retry(failed.id, vienna(20, '10:30'));
await tide.advance({ now: vienna(20, '10:30') });
console.log('after retry:', await taskStates());Run it.
The preview lists two charges and makes none. Fired by hand, one charge fails and one goes through. Retried, both are done.
npx tsx try.mtswould run: true · for 2 rows
ada {"member":"ada","cents":4600}
linus {"member":"linus","cents":2300}
charges so far: 0
after firing: ada failed, linus done
after retry: ada done, linus done