@moltcms/client
v0.4.0
Published
Type-safe client for the moltcms read-only sync API: a typed Server-Sent Events feed of content changes for incremental builds.
Maintainers
Readme
@moltcms/client
Type-safe client for the moltcms read-only sync API. moltcms is an incremental-build, multi-tenant headless CMS: instead of polling REST endpoints, build and frontend clients drain a Server-Sent Events feed of content changes and resume from a cursor.
This package ships:
openSyncStream— a dependency-free, typedEventSourcewrapper for the/tenants/{tenant}/syncfeed (drain-to-completion semantics, cursor resume).openServerSyncStream— afetch-based variant for Bun/Node build scripts: sends the API key in theAuthorizationheader and reconnects automatically with backoff.- Generated types (
DeliveryItem,FieldDef,FieldType, …) produced from the API's OpenAPI spec, so payloads narrow as discriminated unions. openapi-public.json— the read-only subset of the OpenAPI 3.1 spec (importable as@moltcms/client/openapi.json).
npm install @moltcms/client # or: bun add @moltcms/clientUsage
import { openSyncStream } from "@moltcms/client";
const stream = openSyncStream(
`${BASE_URL}/tenants/${TENANT_ID}/sync`,
{
onChange: (item) => {
// `item` is a DeliveryItem — narrow on the `type` discriminator:
switch (item.type) {
case "content": /* item.id, item.data, item.schema_version */ break;
case "content_deleted": /* remove item.id locally */ break;
case "schema_changed": /* item.fields: FieldDef[] */ break;
}
},
onComplete: (cursor) => save(cursor), // persist; pass back as `cursor` next run
onError: (msg) => console.error("sync error:", msg),
},
{ cursor: loadSavedCursor() },
);
// The stream closes itself after `sync-complete` (drain-to-completion).
// To stop early: stream.close();Server-side (Bun/Node)
Use openServerSyncStream in build scripts and other non-browser runtimes.
It authenticates with the Authorization: Bearer header (native EventSource
cannot) and reconnects automatically:
import { openServerSyncStream } from "@moltcms/client";
const stream = openServerSyncStream(
`${BASE_URL}/tenants/${TENANT_ID}/sync`,
{
onChange: (item) => apply(item),
onComplete: (cursor) => save(cursor),
onTransportError: (err) => console.warn("sync transport:", err),
},
{ apiKey: process.env.MOLTCMS_READ_KEY!, cursor: loadSavedCursor() },
);
// To stop early: stream.close();Reconnection contract:
- HTTP
429and5xx, and transport failures (dropped connection, network error), are retried with exponential backoff: 1s initial delay, doubling up to a 30s cap, with ±20% jitter. The backoff attempt resets once achangeorsync-completeevent is delivered — not on a bare 200 that delivers nothing. - A
Retry-Afterheader (seconds or HTTP-date) raises the wait to at least the requested time; the backoff schedule keeps advancing underneath, so a later header-less failure continues where the schedule left off. - Other 4xx (e.g.
401/403) is terminal: the stream closes and reports the status viaonTransportErroras aServerSyncHttpError. - Resume sends
Last-Event-IDlike the browser feed, which keeps replays rare. Delivery is still at-least-once — after a crash, events past the last completed cursor may be redelivered — soonChangehandlers and projections must be idempotent. stream.close()stops reconnecting immediately, including mid-backoff.
Feed semantics
changeevents carry oneDeliveryItem; the SSE event id is its rawseq, so a dropped connection resumes automatically mid-stream (Last-Event-ID).sync-completecarries an opaque cursor. Persist it and pass it as thecursoroption on the next run to sync incrementally. Do not put it inLast-Event-ID— that header only accepts a rawseq.- A server
errorevent ends the stream. The client closes itself aftersync-complete/errorby default (autoClose: falseopts out, degrading the feed into a coarse poll). - Pass
kind: "draft"to read draft content (requires a draft-capable key) — e.g. for preview builds.
Authentication
The feed is authenticated with a read-only API key (Bearer). Native
EventSource cannot send an Authorization header, so in browsers prefer a
backend-set cookie (eventSourceInit: { withCredentials: true }, mind
CORS/CSRF) or an EventSource polyfill that supports custom headers. Avoid
query-parameter keys — URLs leak into access logs, proxies and browser
history. In server-side runtimes (Node/Bun build scripts), use
openServerSyncStream, which sends the header natively.
Versioning
The package version tracks the moltcms server release. Generated code and the bundled spec always match the server version of the same number.
