Change stream
changes.stream, a reconnecting async iterable over GET /v1/stream that resumes where it left off.
furniops.changes.stream(query?, options?) opens GET /v1/stream
and returns a ChangeStream: an async iterable of events that reconnects on its own and resumes
after the last event it delivered.
const controller = new AbortController();
const stream = furniops.changes.stream(
{ types: "offer.changed,offer.created", payload: "thin" },
{ signal: controller.signal, lastEventId: loadLastEventId() },
);
for await (const { id, type, event, apiVersion } of stream) {
console.log(type, event.id, apiVersion);
saveLastEventId(id); // a feed cursor: resume after it, on the stream or the feed
}Each event
| Field | Meaning |
|---|---|
id | The event's position: a feed cursor. Save it to resume, on the stream or the feed. |
type | The event type, offer.changed. |
event | The envelope, parsed and typed as a WebhookEvent. |
data | The envelope exactly as received, as a string: the bytes to forward or sign. |
apiVersion | The FurniOps-Version the stream answered in. |
stream.lastEventId is the last position seen, including the server's own position marks.
Query
The first argument is the stream's query, as in the API:
| Field | Meaning |
|---|---|
types | Comma-separated event types to keep. Every type by default. |
catalogues | Comma-separated cat_ IDs, at most 50: only events of those catalogues. Every catalogue you can see by default. |
payload | thin (default) or fat. See payloads. |
cursor | Start after this feed cursor. Without one (and without lastEventId), the stream starts with events committed from now on. |
Options
| Option | Default | Meaning |
|---|---|---|
signal | none | Ends the stream: the loop rejects with the signal's reason. |
lastEventId | none | Resume after this event (sent as Last-Event-ID), over the query's cursor. |
maxReconnects | unlimited | Failed connection attempts in a row (refused, 429, 5xx) before the last failure is thrown. A successful connection resets the count. |
idleTimeout | 45000 | Milliseconds without a byte before the connection is treated as dead and replaced. The server sends a comment every 15 s while it is quiet, so a healthy connection never trips it. |
reconnectDelay | 3000 | The first wait before reconnecting, until the server sends its own retry:. |
apiVersion, timeout, headers | as the client | As for any call. timeout covers the wait for the response headers. |
Reconnection
- When a connection ends, the stream reconnects after
reconnectDelay(or the server'sretry:) and sendsLast-Event-ID, so no event between the two connections is lost. Each connection asks yourapiKeyfunction again, so a refreshed token is picked up. - Failed attempts in a row back off exponentially; a
Retry-Afterup to 60 s is honoured. - A
4xxother than429(a revoked key, a missing scope) is never retried: it is thrown at once. - Breaking out of the
for awaitloop closes the connection.
A stream is not a delivery guarantee
The stream resumes from the last event it delivered while your process runs. If your process
stops, events committed meanwhile are delivered only if you resume with a saved lastEventId
within the feed's 30 days. For a system that must not miss an event, save the id after handling
each one, as in the example, or follow the feed.
When to use it
| Use | Best fit |
|---|---|
| A live dashboard, a console, a local tool | The stream |
| Keeping a database in sync | The feed, with wait |
| Reacting in a server you already run, without a loop | Webhooks |
Each connection is in the stream rate class, reconnections included.