npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

express-prep

v0.6.4

Published

A Connect/Express style middleware for the Per Resource Events Protocol

Downloads

251

Readme

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 Negotiate Events package instead for a simplified notifications setup.

We are going to describe here a non-trivial implementation of Express PREP to serve 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);

  // Configures notifications to be sent as `message/rfc822` with deltas.
  // The default is to omit the delta.
  let failStatus = res.events.prep.configure(
    `accept=("message/rfc822"; delta="text/plain")`,
  );

  // Custom logic for negotiating media-type for deltas
  // The headers are parsed "npm:structured-headers". PREP adds a second
  // Map after parameters to List Item with requested deltas, allowing
  // an implementor to negotiate against the configured parameters.
  function negotiateEvents(defaultEvents) {
    const cType = defaultEvents["content-type"];
    if (cType[0].toString() === "message/rfc822" && cType.length > 2) {
      // Check for additional map after parameters for the
      // "message/rfc822" item
      if (cType[2].has("delta")) {
        // Manually 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");
        }
      }
      // Second Map is 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 a 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());
    // you can set eventID on other paths, say, in case of side effects
    //   res.setEventID("/another/path")
    // you also set your own eventID for a given path
    //   res.setEventID({ path: req.path, id:"foo" })
    // 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("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 and Express PREP contributors.

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