SubMan async event streams (SSE) 1.0-166+8578759

Every listing endpoint in ldap-subscriber-api.yaml accepts ?subscribe=1, which turns the response into a text/event-stream that stays open and reports every change to the listed records. A single-object endpoint (/realm/{realmId} for example) returns the object and ignores subscribe. This document describes the one stream that every listing endpoint provides, and the events that the stream delivers.

Quickstart for an SSE consumer

const es = new EventSource('/realm/example.org/subscriber?subscribe=1&access_token=' + token);

es.addEventListener('ready', () => {
  // The server has finished replaying the initial state. Every later
  // `entry` is a live change.
});

es.addEventListener('entry', (e) => {
  const { entryUUID, syncOp, body } = JSON.parse(e.data);
  switch (syncOp) {
    case 'add':                                 // new record, full body
    case 'modify':  applyUpsert(body); break;   // changed record, full body
    case 'delete':  applyDelete(body); break;   // removed record, only id and _url
    case 'present':                             // unchanged record, only id and _url
      if (!haveRecord(body._url)) fetchRecord(body._url);
      break;
  }
});

The stream uses two SSE event names:

  • The server sends ready once, when the initial replay of existing records is complete. Every later entry is a live change. The payload is an empty object, and the id: line carries the resume cookie for the end of the replay.
  • The server sends entry once per record. syncOp names the kind of change, and body carries the record. Every record includes _url, the path of the REST resource of the record.

Choosing what to follow

A stream is a listing GET that stays open, so the stream accepts the same query parameters as the listing GET:

  • The id parameter and the filter_* parameters of the listing endpoint narrow the stream to the records that match the parameters. ldap-subscriber-api.yaml documents the parameters per endpoint. /realm/example.org/subscriber?subscribe=1&id=alice follows one subscriber.
  • scope=subtree widens the stream to every record below the collection, at any depth. Each event describes the record's own resource, so a subtree stream on /realm carries realm events and subscriber events. The consumer tells realm events and subscriber events apart by _url.

The server runs one directory search per open stream, and the directory server checks every write against every open search. The number of open searches is the cost of streaming. The number of connections is not a cost. The API serves HTTP/2, so a browser holds every stream on one connection. Open the fewest streams that cover the records you need. The widest streams are the subtree streams of the six top-level collections:

Stream Records carried
/realm?subscribe=1&scope=subtree realms and subscribers
/group?subscribe=1&scope=subtree subscriber groups
/service?subscribe=1&scope=subtree services, service profiles, and policies
/client?subscribe=1&scope=subtree clients and the ports and taggings of the clients
/clientGroup?subscribe=1&scope=subtree client groups
/accessDevice?subscribe=1&scope=subtree access devices and the ports and taggings of the access devices

The event body

body has the shape of the GET response for the resource that _url names, plus _url. _url therefore selects which schema in ldap-subscriber-api.yaml describes body. Decoding an event does not depend on which stream delivered the event.

Deletes

delete events carry only id and _url in body, not the attributes that the record had before the record was removed. By the time the server sends the event, the record is gone from the directory and the attributes are unrecoverable. A consumer that needs the old state keeps a copy keyed on entryUUID.

Renames

The URL of a resource contains the id of the resource. When an id changes, the change is a rename, and the server sends two events for one rename rather than one modify:

  1. a delete carrying the old id and _url.
  2. an add carrying the new id, _url, and the full body.

Both events share the same entryUUID, so a consumer that treats a rename as one operation correlates the two events by entryUUID. A consumer that only tracks the current URL of a record treats the two events as a removed record followed by a new record.

A move between collections, for example a subscriber re-homed to another realm, produces the same two events. The URL changes, so the server sends delete for the old URL and add for the new URL.

Resuming a dropped stream

The ready event and every entry event carry an SSE id: line holding an opaque resume cookie. EventSource records the most recent cookie and sends the cookie back as Last-Event-ID when EventSource reconnects, so a browser client needs no extra code. A client that does not use EventSource resumes in two steps:

  1. Remember the last id: value received.
  2. On reconnect, send the remembered value in Last-Event-ID on the new request.

The server accepts a cookie of 1024 bytes or fewer with no control characters, and answers any other cookie with 400 Bad Request.

On a reconnect, the server replays only the changes since the cookie, as long as the directory server still holds history back to the cookie. When the cookie is older than the history that the directory server keeps, the server instead sends one present event for every record that still exists and has not changed. Each present event carries id and _url only. The server then sends the changes. A present event means that the record still exists and has not changed since the cookie. Keep the copy you have, and fetch _url if you have none.

Authentication

The stream needs the same bearer token as every other request. Send the token in Authorization: Bearer <token>. A client that cannot set request headers, such as the browser EventSource API, sends the token in the access_token query parameter of a GET. The token then appears in the URL, so the response to a GET with access_token carries Cache-Control: private.

Where the events come from

A syncrepl persistent search (RFC 4533) on the LDAP directory drives the stream. The syncOp values (present, add, modify, delete) and the entryUUID field come from syncrepl. Using the stream requires no knowledge of LDAP. The events describe REST resources, and body contains the JSON shape that the GET endpoint of the resource returns.

Servers

  • sse://localhost:3000/ssedefault

    The host that serves the REST API. A stream is a GET of any listing endpoint of ldap-subscriber-api.yaml with ?subscribe=1 appended, plus the scope, id, and filter_* query parameters that the endpoint accepts.

    Security:
    • HTTP
      • Scheme: bearer
      • Bearer format: JWT

      Bearer token, sent as Authorization: Bearer <token> or, for a client that cannot set request headers, as the access_token query parameter of a GET.

Operations

  • RECEIVE {listPath}?subscribe=1

    Changes to the records that the listing endpoint at listPath returns. Every listing endpoint of ldap-subscriber-api.yaml provides the stream. The scope, id, and filter_* query parameters select the records. The receive operation documents the parameters.

    Open the change stream of a listing endpoint

    A GET of the listing endpoint with subscribe=1. The response is a text/event-stream that delivers a ready event once and then an entry event per change. Last-Event-ID resumes a dropped stream. See 'Resuming a dropped stream' in the document description.

    Operation IDreceive

    Available only on servers:

    object
    listPathstring
    required

    Path of a listing endpoint of ldap-subscriber-api.yaml, with the path parameters of the endpoint filled in. The examples list every listing endpoint.

      Examples values:
    • "/realm"
    • "/realm/example.org/subscriber"
    • "/group"
    • "/service"
    • "/service/fixedLine/profile"
    • "/service/fixedLine/profile/INETG1M/policy"
    • "/client"
    • "/client/192.0.2.1/port"
    • "/client/192.0.2.1/port/1%2F2%2F3/qinq"
    • "/client/192.0.2.1/port/1%2F2%2F3/stag"
    • "/clientGroup"
    • "/accessDevice"
    • "/accessDevice/dslam-03/port"
    • "/accessDevice/dslam-03/port/1%2F2%2F3/qinq"
    • "/accessDevice/dslam-03/port/1%2F2%2F3/stag"
    object

    Accepts one of the following messages:

    • #0Initial replay complete

      Marks the boundary between the initial replay and the live changes.

      Message IDready
      object

      Examples

    • #1Record event

      One change to one record. `_url` names the resource, and `body` has the shape of the GET response of the resource.

      Message IDentry
      allOf

      Examples

Messages

  • #1Initial replay complete

    Marks the boundary between the initial replay and the live changes.

    Message IDready
    object
  • #2Record event

    One change to one record. `_url` names the resource, and `body` has the shape of the GET response of the resource.

    Message IDentry
    allOf

Schemas

  • object
  • object

    Body of a delete or present event. The tombstone holds only enough to identify the record.

  • object
  • allOf
  • allOf
  • allOf
  • allOf
  • allOf
  • allOf
  • allOf
  • allOf
  • allOf
  • allOf
  • allOf
  • allOf