EdgeAIG's picture
download
raw
10.9 kB
import * as Arr from "../../Array.js";
import * as Cause from "../../Cause.js";
import * as Channel from "../../Channel.js";
import * as ChannelSchema from "../../ChannelSchema.js";
import * as Data from "../../Data.js";
import * as Duration from "../../Duration.js";
import * as Effect from "../../Effect.js";
import { hasProperty } from "../../Predicate.js";
import * as Pull from "../../Pull.js";
import * as Result from "../../Result.js";
import * as Schema from "../../Schema.js";
import * as SchemaTransformation from "../../SchemaTransformation.js";
/**
* Creates a channel that parses Server-Sent Events text chunks into `Event` values.
*
* **Details**
*
* SSE `retry` directives are emitted as `Retry` failures so callers can
* reconnect with the requested delay.
*
* @category decoding
* @since 4.0.0
*/
export const decode = () => Channel.fromTransform((upstream, _scope) => Effect.sync(() => {
let buffer = [];
let retry;
const parser = makeParser(event => {
if (event._tag === "Retry") {
retry = event;
} else {
buffer.push(event);
}
});
const pump = Effect.flatMap(upstream, arr => {
for (let i = 0; i < arr.length; i++) {
parser.feed(arr[i]);
}
return Effect.void;
});
return Effect.suspend(function loop() {
if (Arr.isArrayNonEmpty(buffer)) {
const out = buffer;
buffer = [];
return Effect.succeed(out);
} else if (retry) {
return Effect.fail(retry);
}
return Effect.flatMap(pump, loop);
});
}));
/**
* Creates an SSE decoder channel that decodes each parsed event with a schema.
*
* **Details**
*
* The schema receives the untagged event shape containing `id`, `event`, and
* string `data`.
*
* @category decoding
* @since 4.0.0
*/
export const decodeSchema = schema => Channel.pipeTo(decode(), ChannelSchema.decode(EventEncoded.pipe(Schema.decodeTo(schema)))());
/**
* Creates an SSE decoder channel that JSON-decodes each event `data` field with a schema.
*
* **Details**
*
* The output preserves the SSE `event` name and optional `id` while replacing
* `data` with the decoded value.
*
* @category decoding
* @since 4.0.0
*/
export const decodeDataSchema = schema => {
const eventSchema = Schema.Struct({
...EventEncoded.fields,
data: Schema.fromJsonString(schema)
});
return Channel.pipeTo(decode(), Channel.map(ChannelSchema.decode(eventSchema)(), Arr.map(event => ({
...event,
id: event.id
}))));
};
/**
* Creates a stateful Server-Sent Events parser.
*
* **Details**
*
* Call `feed` with text chunks to parse `Event` and `Retry` values through the
* callback, and call `reset` to clear any buffered event state.
*
* @category decoding
* @since 4.0.0
*/
export function makeParser(onParse) {
// Processing state
let isFirstChunk;
let buffer;
let startingPosition;
let startingFieldLength;
// Event state
let eventId;
let lastEventId;
let eventName;
let data;
reset();
return {
feed,
reset
};
function reset() {
isFirstChunk = true;
buffer = "";
startingPosition = 0;
startingFieldLength = -1;
eventId = undefined;
eventName = undefined;
data = "";
}
function feed(chunk) {
buffer = buffer ? buffer + chunk : chunk;
// Strip any UTF8 byte order mark (BOM) at the start of the stream.
// Note that we do not strip any non - UTF8 BOM, as eventsource streams are
// always decoded as UTF8 as per the specification.
if (isFirstChunk && hasBom(buffer)) {
buffer = buffer.slice(BOM.length);
}
isFirstChunk = false;
// Set up chunk-specific processing state
const length = buffer.length;
let position = 0;
let discardTrailingNewline = false;
// Read the current buffer byte by byte
while (position < length) {
// EventSource allows for carriage return + line feed, which means we
// need to ignore a linefeed character if the previous character was a
// carriage return
// @todo refactor to reduce nesting, consider checking previous byte?
// @todo but consider multiple chunks etc
if (discardTrailingNewline) {
if (buffer[position] === "\n") {
++position;
}
discardTrailingNewline = false;
}
let lineLength = -1;
let fieldLength = startingFieldLength;
let character;
for (let index = startingPosition; lineLength < 0 && index < length; ++index) {
character = buffer[index];
if (character === ":" && fieldLength < 0) {
fieldLength = index - position;
} else if (character === "\r") {
discardTrailingNewline = true;
lineLength = index - position;
} else if (character === "\n") {
lineLength = index - position;
}
}
if (lineLength < 0) {
startingPosition = length - position;
startingFieldLength = fieldLength;
break;
} else {
startingPosition = 0;
startingFieldLength = -1;
}
parseEventStreamLine(buffer, position, fieldLength, lineLength);
position += lineLength + 1;
}
if (position === length) {
// If we consumed the entire buffer to read the event, reset the buffer
buffer = "";
} else if (position > 0) {
// If there are bytes left to process, set the buffer to the unprocessed
// portion of the buffer only
buffer = buffer.slice(position);
}
}
function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) {
if (lineLength === 0) {
// We reached the last line of this event
if (data.length > 0) {
onParse({
_tag: "Event",
id: eventId,
event: eventName ?? "message",
data: data.slice(0, -1) // remove trailing newline
});
data = "";
eventId = undefined;
}
eventName = undefined;
return;
}
const noValue = fieldLength < 0;
const field = lineBuffer.slice(index, index + (noValue ? lineLength : fieldLength));
let step = 0;
if (noValue) {
step = lineLength;
} else if (lineBuffer[index + fieldLength + 1] === " ") {
step = fieldLength + 2;
} else {
step = fieldLength + 1;
}
const position = index + step;
const valueLength = lineLength - step;
const value = lineBuffer.slice(position, position + valueLength).toString();
if (field === "data") {
data += value ? `${value}\n` : "\n";
} else if (field === "event") {
eventName = value;
} else if (field === "id" && !value.includes("\u0000")) {
eventId = value;
lastEventId = value;
} else if (field === "retry") {
const retry = parseInt(value, 10);
if (!Number.isNaN(retry)) {
onParse(new Retry({
duration: Duration.millis(retry),
lastEventId
}));
}
}
}
}
const BOM = [239, 187, 191];
function hasBom(buffer) {
return BOM.every((charCode, index) => buffer.charCodeAt(index) === charCode);
}
/**
* Creates a channel that encodes `Event` values as Server-Sent Events text.
*
* **Details**
*
* If the upstream channel fails with `Retry`, the retry directive is written and
* the encoder completes.
*
* @category encoding
* @since 4.0.0
*/
export const encode = () => Channel.fromTransform((upstream, _scope) => Effect.sync(() => {
let done = false;
const pull = upstream.pipe(Effect.map(Arr.map(encoder.write)), Effect.catchFilter(Retry.filter, retry => {
done = true;
return Effect.succeed(Arr.of(encoder.write(retry)));
}), Pull.catchDone(() => Cause.done()));
return Effect.suspend(() => done ? Cause.done() : pull);
}));
/**
* Creates an SSE encoder channel for values accepted by a schema.
*
* **Details**
*
* Values are schema-encoded to the untagged SSE event shape, transformed to
* `Event`, and then written as Server-Sent Events text.
*
* @category encoding
* @since 4.0.0
*/
export const encodeSchema = schema => ChannelSchema.encode(Event.pipe(Schema.decodeTo(schema, transformEvent)))().pipe(Channel.pipeTo(encode()));
/**
* Schema for the untagged Server-Sent Events payload shape containing an optional `id`, `event`, and string `data` fields.
*
* @category models
* @since 4.0.0
*/
export const EventEncoded = /*#__PURE__*/Schema.Struct({
id: /*#__PURE__*/Schema.optional(Schema.String),
event: Schema.String,
data: Schema.String
});
/**
* Schema for the tagged Server-Sent Events message model that adds `_tag: "Event"` to the event name, optional event ID, and string data payload.
*
* @category models
* @since 4.0.0
*/
export const Event = /*#__PURE__*/Schema.Struct({
_tag: /*#__PURE__*/Schema.tag("Event"),
id: /*#__PURE__*/Schema.UndefinedOr(Schema.String),
event: Schema.String,
data: Schema.String
});
/**
* Schema for transforming untagged SSE event payloads into tagged `Event`
* models.
*
* @category models
* @since 4.0.0
*/
export const transformEvent = /*#__PURE__*/SchemaTransformation.transform({
decode: event => event,
encode: event => ({
_tag: "Event",
id: event.id,
event: event.event ?? "message",
data: event.data
})
});
const RetryTypeId = "~effect/encoding/Sse/Retry";
/**
* Represents a Server-Sent Events retry directive.
*
* **Details**
*
* Decoders surface this value as a failure to request reconnection after
* `duration`; encoders serialize an upstream `Retry` failure as a `retry:` line.
*
* @category models
* @since 4.0.0
*/
export class Retry extends /*#__PURE__*/Data.TaggedClass("Retry") {
/**
* Marks this value as an SSE retry directive for runtime guards.
*
* @since 4.0.0
*/
[RetryTypeId] = RetryTypeId;
/**
* Returns `true` when the value is an SSE retry directive.
*
* @since 4.0.0
*/
static is(u) {
return hasProperty(u, RetryTypeId);
}
/**
* Separates SSE retry directives from regular event values.
*
* @since 4.0.0
*/
static filter(u) {
return Retry.is(u) ? Result.succeed(u) : Result.fail(u);
}
}
/**
* Default Server-Sent Events encoder.
*
* **Details**
*
* It renders `Event` values as `id`, `event`, and `data` lines and renders
* `Retry` values as `retry:` directives.
*
* @category encoding
* @since 4.0.0
*/
export const encoder = {
write(event) {
switch (event._tag) {
case "Event":
{
let data = "";
if (event.id !== undefined) {
data += `id: ${event.id}\n`;
}
if (event.event !== "message") {
data += `event: ${event.event}\n`;
}
if (event.data !== "") {
data += `data: ${event.data.replace(/\n/g, "\ndata: ")}\n`;
}
return data + "\n";
}
case "Retry":
{
return `retry: ${Duration.toMillis(event.duration)}\n\n`;
}
}
}
};
//# sourceMappingURL=Sse.js.map

Xet Storage Details

Size:
10.9 kB
·
Xet hash:
43adfe45c49ac1e47177a48851a2331fc285b67d11b1882bfc35d6b5e9ab08b7

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.