first commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# `publish` — envelope encode/decode
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module handles wire-encoding for the Redis broker. Every published frame
|
||||
gets a one-byte encoding tag (`ENCODING_JSON` for uncompressed JSON,
|
||||
`ENCODING_GZIP_JSON` for gzipped JSON), the JSON bytes for the envelope, and
|
||||
nothing else. The same module owns the inverse path — `decodeEnvelope` — and
|
||||
all the input-validation that protects subscribers from non-broker traffic
|
||||
landing on a misconfigured channel.
|
||||
|
||||
Compression is applied conditionally based on the encoded JSON byte length
|
||||
versus the broker's configured `compressionThresholdBytes`. The 4 MiB hard
|
||||
cap (`MAX_PAYLOAD_BYTES`) is enforced both before and after compression so a
|
||||
runaway gzip-stream-bomb cannot sneak past the threshold check.
|
||||
|
||||
## Features
|
||||
|
||||
- One-byte encoding magic at the head of every frame. Unknown bytes fail
|
||||
fast with `WS_BROKER_REDIS.DESERIALISE_FAILED`.
|
||||
- Zod-validated envelope schema applied during decode so a misshapen frame
|
||||
cannot reach a subscriber handler.
|
||||
- Gzip applied via `node:zlib`'s `gzipSync`/`gunzipSync` — synchronous,
|
||||
deterministic, no streaming state to leak across frames.
|
||||
- Hard 4 MiB cap enforced both pre- and post-compression.
|
||||
- Pure encode and decode functions — no I/O, trivially testable, and the
|
||||
broker's `publish` path calls these once per envelope.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { decodeEnvelope, encodeEnvelope } from "@suaveplan/api-ws-broker-redis/publish";
|
||||
|
||||
const frame = encodeEnvelope(
|
||||
{ topic: "orders", tenant: "t1" as any, payload: { id: 1 } },
|
||||
{ compressionThresholdBytes: 1024, disableCompression: false }
|
||||
);
|
||||
const back = decodeEnvelope<{ id: number }>(frame.bytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
encodeEnvelope,
|
||||
ENCODING_GZIP_JSON,
|
||||
} from "@suaveplan/api-ws-broker-redis/publish";
|
||||
|
||||
const frame = encodeEnvelope(envelope, {
|
||||
compressionThresholdBytes: 0, // compress everything
|
||||
disableCompression: false,
|
||||
});
|
||||
if (frame.bytes[0] !== ENCODING_GZIP_JSON) {
|
||||
throw new Error("expected gzip encoding for any-size payload");
|
||||
}
|
||||
// Inspect the actual ratio for tuning the threshold:
|
||||
const ratio = frame.bytes.length / frame.rawByteLength;
|
||||
metrics.compressionRatio.record(ratio);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodeEnvelope(envelope, opts)` — encode a `BrokerEnvelope` into the wire
|
||||
frame. Returns `{ bytes, compressed, rawByteLength }`. Throws
|
||||
`WS_BROKER_REDIS.PAYLOAD_TOO_LARGE` for frames above the hard cap.
|
||||
- `decodeEnvelope<Payload>(bytes)` — inverse of `encodeEnvelope`. Throws
|
||||
`WS_BROKER_REDIS.DESERIALISE_FAILED` for any framing inconsistency.
|
||||
- `EncodedFrame` — return shape of `encodeEnvelope`.
|
||||
- `ENCODING_JSON`, `ENCODING_GZIP_JSON` — magic-byte constants.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema rebuilds a brand-stripped wire envelope (`tenant: string`) on
|
||||
decode because the `TenantId` brand is a TypeScript type, not a runtime value.
|
||||
The shape is otherwise identical to the runtime envelope, and the broker
|
||||
re-applies the brand at the call site via the typed entry points.
|
||||
|
||||
Gzip is chosen over deflate or brotli because Redis pub/sub frames are
|
||||
short-lived (the broker is the storage substrate, not a long-term archive)
|
||||
and gzip's framing overhead is the lowest of the three for typical payloads
|
||||
in the 1–16 KiB range. The compression threshold defaults to 1 KiB so tiny
|
||||
events do not pay the framing cost just to land a few bytes shorter.
|
||||
Reference in New Issue
Block a user