mirror of
https://gitlab.com/upRootNutrition/zookeeper.git
synced 2025-06-17 10:55:12 -05:00
feat: init
This commit is contained in:
parent
8379d09058
commit
2cfa016090
2929 changed files with 299087 additions and 3 deletions
398
node_modules/undici/lib/web/eventsource/eventsource-stream.js
generated
vendored
Normal file
398
node_modules/undici/lib/web/eventsource/eventsource-stream.js
generated
vendored
Normal file
|
@ -0,0 +1,398 @@
|
|||
'use strict'
|
||||
const { Transform } = require('node:stream')
|
||||
const { isASCIINumber, isValidLastEventId } = require('./util')
|
||||
|
||||
/**
|
||||
* @type {number[]} BOM
|
||||
*/
|
||||
const BOM = [0xEF, 0xBB, 0xBF]
|
||||
/**
|
||||
* @type {10} LF
|
||||
*/
|
||||
const LF = 0x0A
|
||||
/**
|
||||
* @type {13} CR
|
||||
*/
|
||||
const CR = 0x0D
|
||||
/**
|
||||
* @type {58} COLON
|
||||
*/
|
||||
const COLON = 0x3A
|
||||
/**
|
||||
* @type {32} SPACE
|
||||
*/
|
||||
const SPACE = 0x20
|
||||
|
||||
/**
|
||||
* @typedef {object} EventSourceStreamEvent
|
||||
* @type {object}
|
||||
* @property {string} [event] The event type.
|
||||
* @property {string} [data] The data of the message.
|
||||
* @property {string} [id] A unique ID for the event.
|
||||
* @property {string} [retry] The reconnection time, in milliseconds.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef eventSourceSettings
|
||||
* @type {object}
|
||||
* @property {string} lastEventId The last event ID received from the server.
|
||||
* @property {string} origin The origin of the event source.
|
||||
* @property {number} reconnectionTime The reconnection time, in milliseconds.
|
||||
*/
|
||||
|
||||
class EventSourceStream extends Transform {
|
||||
/**
|
||||
* @type {eventSourceSettings}
|
||||
*/
|
||||
state = null
|
||||
|
||||
/**
|
||||
* Leading byte-order-mark check.
|
||||
* @type {boolean}
|
||||
*/
|
||||
checkBOM = true
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
crlfCheck = false
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
eventEndCheck = false
|
||||
|
||||
/**
|
||||
* @type {Buffer}
|
||||
*/
|
||||
buffer = null
|
||||
|
||||
pos = 0
|
||||
|
||||
event = {
|
||||
data: undefined,
|
||||
event: undefined,
|
||||
id: undefined,
|
||||
retry: undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {eventSourceSettings} options.eventSourceSettings
|
||||
* @param {Function} [options.push]
|
||||
*/
|
||||
constructor (options = {}) {
|
||||
// Enable object mode as EventSourceStream emits objects of shape
|
||||
// EventSourceStreamEvent
|
||||
options.readableObjectMode = true
|
||||
|
||||
super(options)
|
||||
|
||||
this.state = options.eventSourceSettings || {}
|
||||
if (options.push) {
|
||||
this.push = options.push
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} chunk
|
||||
* @param {string} _encoding
|
||||
* @param {Function} callback
|
||||
* @returns {void}
|
||||
*/
|
||||
_transform (chunk, _encoding, callback) {
|
||||
if (chunk.length === 0) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
// Cache the chunk in the buffer, as the data might not be complete while
|
||||
// processing it
|
||||
// TODO: Investigate if there is a more performant way to handle
|
||||
// incoming chunks
|
||||
// see: https://github.com/nodejs/undici/issues/2630
|
||||
if (this.buffer) {
|
||||
this.buffer = Buffer.concat([this.buffer, chunk])
|
||||
} else {
|
||||
this.buffer = chunk
|
||||
}
|
||||
|
||||
// Strip leading byte-order-mark if we opened the stream and started
|
||||
// the processing of the incoming data
|
||||
if (this.checkBOM) {
|
||||
switch (this.buffer.length) {
|
||||
case 1:
|
||||
// Check if the first byte is the same as the first byte of the BOM
|
||||
if (this.buffer[0] === BOM[0]) {
|
||||
// If it is, we need to wait for more data
|
||||
callback()
|
||||
return
|
||||
}
|
||||
// Set the checkBOM flag to false as we don't need to check for the
|
||||
// BOM anymore
|
||||
this.checkBOM = false
|
||||
|
||||
// The buffer only contains one byte so we need to wait for more data
|
||||
callback()
|
||||
return
|
||||
case 2:
|
||||
// Check if the first two bytes are the same as the first two bytes
|
||||
// of the BOM
|
||||
if (
|
||||
this.buffer[0] === BOM[0] &&
|
||||
this.buffer[1] === BOM[1]
|
||||
) {
|
||||
// If it is, we need to wait for more data, because the third byte
|
||||
// is needed to determine if it is the BOM or not
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
// Set the checkBOM flag to false as we don't need to check for the
|
||||
// BOM anymore
|
||||
this.checkBOM = false
|
||||
break
|
||||
case 3:
|
||||
// Check if the first three bytes are the same as the first three
|
||||
// bytes of the BOM
|
||||
if (
|
||||
this.buffer[0] === BOM[0] &&
|
||||
this.buffer[1] === BOM[1] &&
|
||||
this.buffer[2] === BOM[2]
|
||||
) {
|
||||
// If it is, we can drop the buffered data, as it is only the BOM
|
||||
this.buffer = Buffer.alloc(0)
|
||||
// Set the checkBOM flag to false as we don't need to check for the
|
||||
// BOM anymore
|
||||
this.checkBOM = false
|
||||
|
||||
// Await more data
|
||||
callback()
|
||||
return
|
||||
}
|
||||
// If it is not the BOM, we can start processing the data
|
||||
this.checkBOM = false
|
||||
break
|
||||
default:
|
||||
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
|
||||
// present
|
||||
if (
|
||||
this.buffer[0] === BOM[0] &&
|
||||
this.buffer[1] === BOM[1] &&
|
||||
this.buffer[2] === BOM[2]
|
||||
) {
|
||||
// Remove the BOM from the buffer
|
||||
this.buffer = this.buffer.subarray(3)
|
||||
}
|
||||
|
||||
// Set the checkBOM flag to false as we don't need to check for the
|
||||
this.checkBOM = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
while (this.pos < this.buffer.length) {
|
||||
// If the previous line ended with an end-of-line, we need to check
|
||||
// if the next character is also an end-of-line.
|
||||
if (this.eventEndCheck) {
|
||||
// If the the current character is an end-of-line, then the event
|
||||
// is finished and we can process it
|
||||
|
||||
// If the previous line ended with a carriage return, we need to
|
||||
// check if the current character is a line feed and remove it
|
||||
// from the buffer.
|
||||
if (this.crlfCheck) {
|
||||
// If the current character is a line feed, we can remove it
|
||||
// from the buffer and reset the crlfCheck flag
|
||||
if (this.buffer[this.pos] === LF) {
|
||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
||||
this.pos = 0
|
||||
this.crlfCheck = false
|
||||
|
||||
// It is possible that the line feed is not the end of the
|
||||
// event. We need to check if the next character is an
|
||||
// end-of-line character to determine if the event is
|
||||
// finished. We simply continue the loop to check the next
|
||||
// character.
|
||||
|
||||
// As we removed the line feed from the buffer and set the
|
||||
// crlfCheck flag to false, we basically don't make any
|
||||
// distinction between a line feed and a carriage return.
|
||||
continue
|
||||
}
|
||||
this.crlfCheck = false
|
||||
}
|
||||
|
||||
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
|
||||
// If the current character is a carriage return, we need to
|
||||
// set the crlfCheck flag to true, as we need to check if the
|
||||
// next character is a line feed so we can remove it from the
|
||||
// buffer
|
||||
if (this.buffer[this.pos] === CR) {
|
||||
this.crlfCheck = true
|
||||
}
|
||||
|
||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
||||
this.pos = 0
|
||||
if (
|
||||
this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
|
||||
this.processEvent(this.event)
|
||||
}
|
||||
this.clearEvent()
|
||||
continue
|
||||
}
|
||||
// If the current character is not an end-of-line, then the event
|
||||
// is not finished and we have to reset the eventEndCheck flag
|
||||
this.eventEndCheck = false
|
||||
continue
|
||||
}
|
||||
|
||||
// If the current character is an end-of-line, we can process the
|
||||
// line
|
||||
if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) {
|
||||
// If the current character is a carriage return, we need to
|
||||
// set the crlfCheck flag to true, as we need to check if the
|
||||
// next character is a line feed
|
||||
if (this.buffer[this.pos] === CR) {
|
||||
this.crlfCheck = true
|
||||
}
|
||||
|
||||
// In any case, we can process the line as we reached an
|
||||
// end-of-line character
|
||||
this.parseLine(this.buffer.subarray(0, this.pos), this.event)
|
||||
|
||||
// Remove the processed line from the buffer
|
||||
this.buffer = this.buffer.subarray(this.pos + 1)
|
||||
// Reset the position as we removed the processed line from the buffer
|
||||
this.pos = 0
|
||||
// A line was processed and this could be the end of the event. We need
|
||||
// to check if the next line is empty to determine if the event is
|
||||
// finished.
|
||||
this.eventEndCheck = true
|
||||
continue
|
||||
}
|
||||
|
||||
this.pos++
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer} line
|
||||
* @param {EventStreamEvent} event
|
||||
*/
|
||||
parseLine (line, event) {
|
||||
// If the line is empty (a blank line)
|
||||
// Dispatch the event, as defined below.
|
||||
// This will be handled in the _transform method
|
||||
if (line.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// If the line starts with a U+003A COLON character (:)
|
||||
// Ignore the line.
|
||||
const colonPosition = line.indexOf(COLON)
|
||||
if (colonPosition === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let field = ''
|
||||
let value = ''
|
||||
|
||||
// If the line contains a U+003A COLON character (:)
|
||||
if (colonPosition !== -1) {
|
||||
// Collect the characters on the line before the first U+003A COLON
|
||||
// character (:), and let field be that string.
|
||||
// TODO: Investigate if there is a more performant way to extract the
|
||||
// field
|
||||
// see: https://github.com/nodejs/undici/issues/2630
|
||||
field = line.subarray(0, colonPosition).toString('utf8')
|
||||
|
||||
// Collect the characters on the line after the first U+003A COLON
|
||||
// character (:), and let value be that string.
|
||||
// If value starts with a U+0020 SPACE character, remove it from value.
|
||||
let valueStart = colonPosition + 1
|
||||
if (line[valueStart] === SPACE) {
|
||||
++valueStart
|
||||
}
|
||||
// TODO: Investigate if there is a more performant way to extract the
|
||||
// value
|
||||
// see: https://github.com/nodejs/undici/issues/2630
|
||||
value = line.subarray(valueStart).toString('utf8')
|
||||
|
||||
// Otherwise, the string is not empty but does not contain a U+003A COLON
|
||||
// character (:)
|
||||
} else {
|
||||
// Process the field using the steps described below, using the whole
|
||||
// line as the field name, and the empty string as the field value.
|
||||
field = line.toString('utf8')
|
||||
value = ''
|
||||
}
|
||||
|
||||
// Modify the event with the field name and value. The value is also
|
||||
// decoded as UTF-8
|
||||
switch (field) {
|
||||
case 'data':
|
||||
if (event[field] === undefined) {
|
||||
event[field] = value
|
||||
} else {
|
||||
event[field] += `\n${value}`
|
||||
}
|
||||
break
|
||||
case 'retry':
|
||||
if (isASCIINumber(value)) {
|
||||
event[field] = value
|
||||
}
|
||||
break
|
||||
case 'id':
|
||||
if (isValidLastEventId(value)) {
|
||||
event[field] = value
|
||||
}
|
||||
break
|
||||
case 'event':
|
||||
if (value.length > 0) {
|
||||
event[field] = value
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {EventSourceStreamEvent} event
|
||||
*/
|
||||
processEvent (event) {
|
||||
if (event.retry && isASCIINumber(event.retry)) {
|
||||
this.state.reconnectionTime = parseInt(event.retry, 10)
|
||||
}
|
||||
|
||||
if (event.id && isValidLastEventId(event.id)) {
|
||||
this.state.lastEventId = event.id
|
||||
}
|
||||
|
||||
// only dispatch event, when data is provided
|
||||
if (event.data !== undefined) {
|
||||
this.push({
|
||||
type: event.event || 'message',
|
||||
options: {
|
||||
data: event.data,
|
||||
lastEventId: this.state.lastEventId,
|
||||
origin: this.state.origin
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
clearEvent () {
|
||||
this.event = {
|
||||
data: undefined,
|
||||
event: undefined,
|
||||
id: undefined,
|
||||
retry: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EventSourceStream
|
||||
}
|
480
node_modules/undici/lib/web/eventsource/eventsource.js
generated
vendored
Normal file
480
node_modules/undici/lib/web/eventsource/eventsource.js
generated
vendored
Normal file
|
@ -0,0 +1,480 @@
|
|||
'use strict'
|
||||
|
||||
const { pipeline } = require('node:stream')
|
||||
const { fetching } = require('../fetch')
|
||||
const { makeRequest } = require('../fetch/request')
|
||||
const { getGlobalOrigin } = require('../fetch/global')
|
||||
const { webidl } = require('../fetch/webidl')
|
||||
const { EventSourceStream } = require('./eventsource-stream')
|
||||
const { parseMIMEType } = require('../fetch/data-url')
|
||||
const { MessageEvent } = require('../websocket/events')
|
||||
const { isNetworkError } = require('../fetch/response')
|
||||
const { delay } = require('./util')
|
||||
const { kEnumerableProperty } = require('../../core/util')
|
||||
|
||||
let experimentalWarned = false
|
||||
|
||||
/**
|
||||
* A reconnection time, in milliseconds. This must initially be an implementation-defined value,
|
||||
* probably in the region of a few seconds.
|
||||
*
|
||||
* In Comparison:
|
||||
* - Chrome uses 3000ms.
|
||||
* - Deno uses 5000ms.
|
||||
*
|
||||
* @type {3000}
|
||||
*/
|
||||
const defaultReconnectionTime = 3000
|
||||
|
||||
/**
|
||||
* The readyState attribute represents the state of the connection.
|
||||
* @enum
|
||||
* @readonly
|
||||
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate-dev
|
||||
*/
|
||||
|
||||
/**
|
||||
* The connection has not yet been established, or it was closed and the user
|
||||
* agent is reconnecting.
|
||||
* @type {0}
|
||||
*/
|
||||
const CONNECTING = 0
|
||||
|
||||
/**
|
||||
* The user agent has an open connection and is dispatching events as it
|
||||
* receives them.
|
||||
* @type {1}
|
||||
*/
|
||||
const OPEN = 1
|
||||
|
||||
/**
|
||||
* The connection is not open, and the user agent is not trying to reconnect.
|
||||
* @type {2}
|
||||
*/
|
||||
const CLOSED = 2
|
||||
|
||||
/**
|
||||
* Requests for the element will have their mode set to "cors" and their credentials mode set to "same-origin".
|
||||
* @type {'anonymous'}
|
||||
*/
|
||||
const ANONYMOUS = 'anonymous'
|
||||
|
||||
/**
|
||||
* Requests for the element will have their mode set to "cors" and their credentials mode set to "include".
|
||||
* @type {'use-credentials'}
|
||||
*/
|
||||
const USE_CREDENTIALS = 'use-credentials'
|
||||
|
||||
/**
|
||||
* @typedef {object} EventSourceInit
|
||||
* @property {boolean} [withCredentials] indicates whether the request
|
||||
* should include credentials.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The EventSource interface is used to receive server-sent events. It
|
||||
* connects to a server over HTTP and receives events in text/event-stream
|
||||
* format without closing the connection.
|
||||
* @extends {EventTarget}
|
||||
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
|
||||
* @api public
|
||||
*/
|
||||
class EventSource extends EventTarget {
|
||||
#events = {
|
||||
open: null,
|
||||
error: null,
|
||||
message: null
|
||||
}
|
||||
|
||||
#url = null
|
||||
#withCredentials = false
|
||||
|
||||
#readyState = CONNECTING
|
||||
|
||||
#request = null
|
||||
#controller = null
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
* @property {string} lastEventId
|
||||
* @property {number} reconnectionTime
|
||||
* @property {any} reconnectionTimer
|
||||
*/
|
||||
#settings = null
|
||||
|
||||
/**
|
||||
* Creates a new EventSource object.
|
||||
* @param {string} url
|
||||
* @param {EventSourceInit} [eventSourceInitDict]
|
||||
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface
|
||||
*/
|
||||
constructor (url, eventSourceInitDict = {}) {
|
||||
// 1. Let ev be a new EventSource object.
|
||||
super()
|
||||
|
||||
webidl.argumentLengthCheck(arguments, 1, { header: 'EventSource constructor' })
|
||||
|
||||
if (!experimentalWarned) {
|
||||
experimentalWarned = true
|
||||
process.emitWarning('EventSource is experimental, expect them to change at any time.', {
|
||||
code: 'UNDICI-ES'
|
||||
})
|
||||
}
|
||||
|
||||
url = webidl.converters.USVString(url)
|
||||
eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict)
|
||||
|
||||
// 2. Let settings be ev's relevant settings object.
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object
|
||||
this.#settings = {
|
||||
origin: getGlobalOrigin(),
|
||||
policyContainer: {
|
||||
referrerPolicy: 'no-referrer'
|
||||
},
|
||||
lastEventId: '',
|
||||
reconnectionTime: defaultReconnectionTime
|
||||
}
|
||||
|
||||
let urlRecord
|
||||
|
||||
try {
|
||||
// 3. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings.
|
||||
urlRecord = new URL(url, this.#settings.origin)
|
||||
this.#settings.origin = urlRecord.origin
|
||||
} catch (e) {
|
||||
// 4. If urlRecord is failure, then throw a "SyntaxError" DOMException.
|
||||
throw new DOMException(e, 'SyntaxError')
|
||||
}
|
||||
|
||||
// 5. Set ev's url to urlRecord.
|
||||
this.#url = urlRecord.href
|
||||
|
||||
// 6. Let corsAttributeState be Anonymous.
|
||||
let corsAttributeState = ANONYMOUS
|
||||
|
||||
// 7. If the value of eventSourceInitDict's withCredentials member is true,
|
||||
// then set corsAttributeState to Use Credentials and set ev's
|
||||
// withCredentials attribute to true.
|
||||
if (eventSourceInitDict.withCredentials) {
|
||||
corsAttributeState = USE_CREDENTIALS
|
||||
this.#withCredentials = true
|
||||
}
|
||||
|
||||
// 8. Let request be the result of creating a potential-CORS request given
|
||||
// urlRecord, the empty string, and corsAttributeState.
|
||||
const initRequest = {
|
||||
redirect: 'follow',
|
||||
keepalive: true,
|
||||
// @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
||||
mode: 'cors',
|
||||
credentials: corsAttributeState === 'anonymous'
|
||||
? 'same-origin'
|
||||
: 'omit',
|
||||
referrer: 'no-referrer'
|
||||
}
|
||||
|
||||
// 9. Set request's client to settings.
|
||||
initRequest.client = this.#settings
|
||||
|
||||
// 10. User agents may set (`Accept`, `text/event-stream`) in request's header list.
|
||||
initRequest.headersList = [['accept', { name: 'accept', value: 'text/event-stream' }]]
|
||||
|
||||
// 11. Set request's cache mode to "no-store".
|
||||
initRequest.cache = 'no-store'
|
||||
|
||||
// 12. Set request's initiator type to "other".
|
||||
initRequest.initiator = 'other'
|
||||
|
||||
initRequest.urlList = [new URL(this.#url)]
|
||||
|
||||
// 13. Set ev's request to request.
|
||||
this.#request = makeRequest(initRequest)
|
||||
|
||||
this.#connect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state of this EventSource object's connection. It can have the
|
||||
* values described below.
|
||||
* @returns {0|1|2}
|
||||
* @readonly
|
||||
*/
|
||||
get readyState () {
|
||||
return this.#readyState
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL providing the event stream.
|
||||
* @readonly
|
||||
* @returns {string}
|
||||
*/
|
||||
get url () {
|
||||
return this.#url
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating whether the EventSource object was
|
||||
* instantiated with CORS credentials set (true), or not (false, the default).
|
||||
*/
|
||||
get withCredentials () {
|
||||
return this.#withCredentials
|
||||
}
|
||||
|
||||
#connect () {
|
||||
if (this.#readyState === CLOSED) return
|
||||
|
||||
this.#readyState = CONNECTING
|
||||
|
||||
const fetchParam = {
|
||||
request: this.#request
|
||||
}
|
||||
|
||||
// 14. Let processEventSourceEndOfBody given response res be the following step: if res is not a network error, then reestablish the connection.
|
||||
const processEventSourceEndOfBody = (response) => {
|
||||
if (isNetworkError(response)) {
|
||||
this.dispatchEvent(new Event('error'))
|
||||
this.close()
|
||||
}
|
||||
|
||||
this.#reconnect()
|
||||
}
|
||||
|
||||
// 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody...
|
||||
fetchParam.processResponseEndOfBody = processEventSourceEndOfBody
|
||||
|
||||
// and processResponse set to the following steps given response res:
|
||||
fetchParam.processResponse = (response) => {
|
||||
// 1. If res is an aborted network error, then fail the connection.
|
||||
|
||||
if (isNetworkError(response)) {
|
||||
// 1. When a user agent is to fail the connection, the user agent
|
||||
// must queue a task which, if the readyState attribute is set to a
|
||||
// value other than CLOSED, sets the readyState attribute to CLOSED
|
||||
// and fires an event named error at the EventSource object. Once the
|
||||
// user agent has failed the connection, it does not attempt to
|
||||
// reconnect.
|
||||
if (response.aborted) {
|
||||
this.close()
|
||||
this.dispatchEvent(new Event('error'))
|
||||
return
|
||||
// 2. Otherwise, if res is a network error, then reestablish the
|
||||
// connection, unless the user agent knows that to be futile, in
|
||||
// which case the user agent may fail the connection.
|
||||
} else {
|
||||
this.#reconnect()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Otherwise, if res's status is not 200, or if res's `Content-Type`
|
||||
// is not `text/event-stream`, then fail the connection.
|
||||
const contentType = response.headersList.get('content-type', true)
|
||||
const mimeType = contentType !== null ? parseMIMEType(contentType) : 'failure'
|
||||
const contentTypeValid = mimeType !== 'failure' && mimeType.essence === 'text/event-stream'
|
||||
if (
|
||||
response.status !== 200 ||
|
||||
contentTypeValid === false
|
||||
) {
|
||||
this.close()
|
||||
this.dispatchEvent(new Event('error'))
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Otherwise, announce the connection and interpret res's body
|
||||
// line by line.
|
||||
|
||||
// When a user agent is to announce the connection, the user agent
|
||||
// must queue a task which, if the readyState attribute is set to a
|
||||
// value other than CLOSED, sets the readyState attribute to OPEN
|
||||
// and fires an event named open at the EventSource object.
|
||||
// @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
|
||||
this.#readyState = OPEN
|
||||
this.dispatchEvent(new Event('open'))
|
||||
|
||||
// If redirected to a different origin, set the origin to the new origin.
|
||||
this.#settings.origin = response.urlList[response.urlList.length - 1].origin
|
||||
|
||||
const eventSourceStream = new EventSourceStream({
|
||||
eventSourceSettings: this.#settings,
|
||||
push: (event) => {
|
||||
this.dispatchEvent(new MessageEvent(
|
||||
event.type,
|
||||
event.options
|
||||
))
|
||||
}
|
||||
})
|
||||
|
||||
pipeline(response.body.stream,
|
||||
eventSourceStream,
|
||||
(error) => {
|
||||
if (
|
||||
error?.aborted === false
|
||||
) {
|
||||
this.close()
|
||||
this.dispatchEvent(new Event('error'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.#controller = fetching(fetchParam)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async #reconnect () {
|
||||
// When a user agent is to reestablish the connection, the user agent must
|
||||
// run the following steps. These steps are run in parallel, not as part of
|
||||
// a task. (The tasks that it queues, of course, are run like normal tasks
|
||||
// and not themselves in parallel.)
|
||||
|
||||
// 1. Queue a task to run the following steps:
|
||||
|
||||
// 1. If the readyState attribute is set to CLOSED, abort the task.
|
||||
if (this.#readyState === CLOSED) return
|
||||
|
||||
// 2. Set the readyState attribute to CONNECTING.
|
||||
this.#readyState = CONNECTING
|
||||
|
||||
// 3. Fire an event named error at the EventSource object.
|
||||
this.dispatchEvent(new Event('error'))
|
||||
|
||||
// 2. Wait a delay equal to the reconnection time of the event source.
|
||||
await delay(this.#settings.reconnectionTime)
|
||||
|
||||
// 5. Queue a task to run the following steps:
|
||||
|
||||
// 1. If the EventSource object's readyState attribute is not set to
|
||||
// CONNECTING, then return.
|
||||
if (this.#readyState !== CONNECTING) return
|
||||
|
||||
// 2. Let request be the EventSource object's request.
|
||||
// 3. If the EventSource object's last event ID string is not the empty
|
||||
// string, then:
|
||||
// 1. Let lastEventIDValue be the EventSource object's last event ID
|
||||
// string, encoded as UTF-8.
|
||||
// 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header
|
||||
// list.
|
||||
if (this.#settings.lastEventId !== '') {
|
||||
this.#request.headersList.set('last-event-id', this.#settings.lastEventId, true)
|
||||
}
|
||||
|
||||
// 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section.
|
||||
this.#connect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection, if any, and sets the readyState attribute to
|
||||
* CLOSED.
|
||||
*/
|
||||
close () {
|
||||
webidl.brandCheck(this, EventSource)
|
||||
|
||||
if (this.#readyState === CLOSED) return
|
||||
this.#readyState = CLOSED
|
||||
clearTimeout(this.#settings.reconnectionTimer)
|
||||
this.#controller.abort()
|
||||
|
||||
if (this.#request) {
|
||||
this.#request = null
|
||||
}
|
||||
}
|
||||
|
||||
get onopen () {
|
||||
return this.#events.open
|
||||
}
|
||||
|
||||
set onopen (fn) {
|
||||
if (this.#events.open) {
|
||||
this.removeEventListener('open', this.#events.open)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this.#events.open = fn
|
||||
this.addEventListener('open', fn)
|
||||
} else {
|
||||
this.#events.open = null
|
||||
}
|
||||
}
|
||||
|
||||
get onmessage () {
|
||||
return this.#events.message
|
||||
}
|
||||
|
||||
set onmessage (fn) {
|
||||
if (this.#events.message) {
|
||||
this.removeEventListener('message', this.#events.message)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this.#events.message = fn
|
||||
this.addEventListener('message', fn)
|
||||
} else {
|
||||
this.#events.message = null
|
||||
}
|
||||
}
|
||||
|
||||
get onerror () {
|
||||
return this.#events.error
|
||||
}
|
||||
|
||||
set onerror (fn) {
|
||||
if (this.#events.error) {
|
||||
this.removeEventListener('error', this.#events.error)
|
||||
}
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
this.#events.error = fn
|
||||
this.addEventListener('error', fn)
|
||||
} else {
|
||||
this.#events.error = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const constantsPropertyDescriptors = {
|
||||
CONNECTING: {
|
||||
__proto__: null,
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
value: CONNECTING,
|
||||
writable: false
|
||||
},
|
||||
OPEN: {
|
||||
__proto__: null,
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
value: OPEN,
|
||||
writable: false
|
||||
},
|
||||
CLOSED: {
|
||||
__proto__: null,
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
value: CLOSED,
|
||||
writable: false
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(EventSource, constantsPropertyDescriptors)
|
||||
Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors)
|
||||
|
||||
Object.defineProperties(EventSource.prototype, {
|
||||
close: kEnumerableProperty,
|
||||
onerror: kEnumerableProperty,
|
||||
onmessage: kEnumerableProperty,
|
||||
onopen: kEnumerableProperty,
|
||||
readyState: kEnumerableProperty,
|
||||
url: kEnumerableProperty,
|
||||
withCredentials: kEnumerableProperty
|
||||
})
|
||||
|
||||
webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([
|
||||
{ key: 'withCredentials', converter: webidl.converters.boolean, defaultValue: false }
|
||||
])
|
||||
|
||||
module.exports = {
|
||||
EventSource,
|
||||
defaultReconnectionTime
|
||||
}
|
37
node_modules/undici/lib/web/eventsource/util.js
generated
vendored
Normal file
37
node_modules/undici/lib/web/eventsource/util.js
generated
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
'use strict'
|
||||
|
||||
/**
|
||||
* Checks if the given value is a valid LastEventId.
|
||||
* @param {string} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidLastEventId (value) {
|
||||
// LastEventId should not contain U+0000 NULL
|
||||
return value.indexOf('\u0000') === -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given value is a base 10 digit.
|
||||
* @param {string} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isASCIINumber (value) {
|
||||
if (value.length === 0) return false
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (value.charCodeAt(i) < 0x30 || value.charCodeAt(i) > 0x39) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// https://github.com/nodejs/undici/issues/2664
|
||||
function delay (ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms).unref()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidLastEventId,
|
||||
isASCIINumber,
|
||||
delay
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue