1.0.1 • Published 8 months ago

x-hook-service v1.0.1

Weekly downloads
-
License
SEE LICENSE IN LI...
Repository
github
Last release
8 months ago

X-Hook Service

A concurrency-safe "track and retry" service for Webhooks (and other things?).

You provide the event storage and webhook request mechanism. \ We provide the logic and sane defaults to retry failed webhooks with exponential backoff. \ (and a bit of random jitter to prevent duplicate events on multiple servers)

Lightweight, zero-dependency.

Install

npm install --save x-hook-service@1

Usage

To run the service which retries failed webhooks in the background:

let XHookService = require("x-hook-service");

let xhookService = XHookService.create({
  store: store,
  retryWindow: 1 * 60 * 1000,
  retryOffset: 15 * 1000,
  // backoffMap: XHooks._backoffMap,
  random: Math.random(),
});

// fetches webhook events every minute
xhookService.run();

// for graceful shutdowns
// (not always necessary since weak timeout references are used)
await xhookService.stop();

Run a webhook immediately (typically the first time):

// schedules a webhook immediately
let date = new Date();
let event = { ulid: "01H9Y080000000000000000000", retries: 0, retry_at: null };
await xhookService.immediate(date, event);

Store & Event Implementation

You must provide a Store which can retrieve and update Webhook Events and Attempts (however you choose to track those).

They will be called by the service like this (with all the logic omitted):

let events = await store.anyIncomplete(nearFutureDate);
let event = await store.oneEvent(eventId);
eventId = store.getEventId(event);

let attempt = await store.addAttempt(attemptDate, { retry_at }, event);
let result = await store.runAttempt(attemptDate, attempt, event);
await store.updateAttempt(attemptDate, attempt, event, result);

The Store Interface

let store = {};

These are used to get a list of webhooks that should be attempted again, as well as the most up-to-date version of a specific event (it is checked just before a retry).

store.anyIncomplete = async function (nearFutureDate) {
  // return events for which the webhook should be tried again

  // Example:
  //     SELECT
  //         *
  //     FROM
  //         events
  //     WHERE
  //         completed_at is NULL
  //         AND
  //         retry_at <= :near_future_date

  return [event];
};

store.oneEvent = async function (eventId) {
  // return the event by the given id
  return event;
};

store.getEventId = function (event) {
  return event.ulid;
};

The actual running and reporting of the webhook is broken into 3 steps to ensure data consistency:

store.addAttempt = async function (attemptDate, { retry_at }, event) {
  // create and return the data representing a new webhook attempt

  // Example:
  //
  //     UPDATE
  //         events
  //     SET
  //         retries = :retries,
  //         retry_at = :retry_at
  //     WHERE
  //         event.ulid = :ulid

  return attempt;
};

store.runAttempt = async function (attemptDate, attempt, event) {
  // make the webhook request and return the result

  // Example:
  //
  //     let payload = JSON.stringify(event.details)
  //     let xHubSig = xHubSign(payload);
  //     fetch(event.webhook_url, {
  //         headers: { x-hub-signature-256: xHubSig },
  //         body: payload
  //     })

  return result;
};

store.updateAttempt = async function (attemptDate, attempt, event, result) {
  // update the record of attempt with the result

  // Example:
  //
  //     UPDATE
  //         events
  //     SET
  //         completed_at = CURRENT_TIMESTAMP,
  //         retry_at = null
  //     WHERE
  //         event.ulid = :ulid

  return;
};

The Event Interface

The event must have

  • some form of id (retrieved via getEventId())
  • a retries property indicating how many times the webhook has failed
  • retry_at, indicate the next time a webhook request should be tried
  • whatever details you need to attempt the webhook request
{
  ulid: '',
  retries: 0,
  retry_at: new Date(),
  // the rest is up to you
}