/express-prep-temp

Primary LanguageJavaScriptMozilla Public License 2.0MPL-2.0

Express-PREP

A Connect/Express style middleware to send Per Resource Events.

Installation

Install Express-PREP and Express-Accept-Events using your favourite package manager.

npm|pnpm|yarn add express-prep express-accept-events

Usage

Consider using the express-events-negotiate package instead for a simplified notifications setup.

We are going to describe here a non-trivial implementation of Express-PREP that produces notifications with deltas.

Setup

Add the following imports to your server:

// Process the Accept-Events header
import acceptEvents from "express-accept-events";
// PREP Middleware Factory
import prep from "express-prep";
// EventID is optional but recommended
import eventID from "express-prep/event-id";
// For Custom Content Negotiation Logic
import * as negotiate from "express-prep/negotiate";
// Notification templates (or BYO)
import * as templates from "express-prep/templates";

Invocation

Invoke the middleware in your server. In case one is using an Express server:

const app = express();
app.use(acceptEvents, eventID, prep);

The Event ID middleware populates the response with a lastEventID property and a setEventID method. Using this middleware is optional but recommended.

The PREP middleware populates the response object with a events.prep object that provide methods to configure, send and trigger notifications.

Sending Notifications

We used the Accept-Events middleware to already parse the Accept-Events header field. This populates res.acceptEvents with the notifications request headers.

First configure notifications using res.events.prep.configure() in the GET handler.

To send notifications call res.events.prep.send() in your GET handler:

app.get("/foo", (req, res) => {
  // Get the response body first
  const body = getContent(req.url);
  // Get the content-* headers
  const headers = getMediaType(responseBody);

  // By default this function configures notifications to be sent as `message/rfc822`.
  let failStatus = res.events.prep.configure(
    `accept=("message/rfc822";delta="text/plain")`
  );

  // Since we want to send deltas using `message/rfc822` body
  // We provide custom logic to negotiate the `Content-Type` for the delta
  function negotiateEvents(defaultEvents) {
    const cType = defaultEvents["content-type"];
    if (cType[0].toString() === "message/rfc822" && cType.length > 2) {
      // Check the second Map for requested delta parameter
      if (cType[2].has('delta')) {
        // Negotiate Media-Type for delta
        const match = negotiate.type(
          cType[2].get('delta'),
          cType[1].get('delta'),
        )
        if (match) {
          // If match, set the matched format as the delta parameter
          cType[1].set('delta', match);
        }
        else {
          // If no match, delete the delta parameter
          cType[1].delete('delta');
        }
      }
    // Mismatches are automatically removed
    return defaultEvents;
    }
  }

  // Fail quickly if server is misconfigured
  if (!failStatus) {
    // Iterate to the first PREP notifications request
    for (const [protocol, params] of req.acceptEvents || []) {
      if (protocol === "prep") {
        const eventsStatus = res.events.prep.send({
          body, // can also be a stream
          headers,
          params,
          modifiers: {
            negotiateEvents
          }
        });

        // if notifications are sent, you can quit
        if (!eventsStatus) return;

        // Record the first failure only
        if (!failStatus) {
          failStatus = eventsStatus;
        }
      }
    }
  }

  // If notifications are not sent, send regular response
  if (failStatus) {
    // Serialize failed events as header
    headers.events = serializeDictionary(failStatus);
  }
  res.setHeaders(new Headers(headers));
  res.write(responseBody);
  res.end();
});

Triggering Notifications

Now you can trigger notification using res.events.prep.trigger, when the resource is modified, for example, in your PATCH handler.

app.patch("/foo", bodyParser.text(), (req, res, next) => {
  let patchSuccess = false;

  // ...handle the PATCH request

  // notification is triggered if response is successful
  if (patchSuccess) {
    // set success response for notifications
    res.statusCode = 200;
    // set eventID, if you support it
    res.setHeader("Event-ID", res.setEventID());
    // close the response first
    res.end();

    // IMPORTANT: Go to the next middleware when request succeeds to trigger the notification
    return next && next();
  }
});

app.patch("/foo", bodyParser.text(), (req, res) => {
  // Define a function that generates the notification to send
  function generateNotification(
    negotiatedFields,
    // which can be specific to the parsed content-* event fields
    // for a given path specified in the trigger function
    // (see npm:structured-headers for format)
  ) {
    // Generate part header from template
    const header = templates.header(negotiatedFields);

    // Check if delta is requested with the template
    let ifDiff;
    if (negotiatedFields["content-type"]?.[0] === "message/rfc822") {
      const params = negotiatedFields["content-type"][1];
      ifDiff = params.get("delta")?.[0].toString() === "text/plain";
    }

    // Generate part body from a template
    const body = templates.rfc822({
      date: res._header.match(/^Date: (.*?)$/m)?.[1],
      method: req.method,
      eventID: res.getHeader("event-id"), // (optional, but recommended)
      // location: res.getHeader("Content-Location"), // (optional)
      // diff from the last response
      delta: ifDiff && req.body, // (optional)
    });

    // Return the notification
    return `${header}\r\n${body}`;
  };

  // Trigger the notification
  res.events.prep.trigger({
    // path               // where to trigger notification
                          // (default: req.path)
    generateNotification, // function for notification to send, defined above
                          // (default: message/rfc822 notifications with only headers)
    // lastEvent          // Set to true to close stream after this notification
                          // (default: false)
  });
});

Default Template

The generateNotification() function when not specified at the time of triggering notification results in a default message/rfc822 format notification being generated.

This default notification is also exposed as res.events.prep.defaultNotification(). Users may use this function to modify default values rather than calling the template:

  res.events.prep.trigger({
    generateNotification(negotiatedFields) {
      // ... determine if the diff exists as before
      return res.events.prep.defaultNotification({
        delta: ifDiff && req.body
      }),
    },
  });

Copyright and License

(c) 2024, Rahul Gupta

The source code in this repository is released under the Mozilla Public License v2.0.