hyperscript.org
Version:
a small scripting language for the web
410 lines (408 loc) • 12.7 kB
JavaScript
// src/parsetree/base.js
var ParseElement = class _ParseElement {
errors = [];
collectErrors(visited) {
if (!visited) visited = /* @__PURE__ */ new Set();
if (visited.has(this)) return [];
visited.add(this);
var all = [...this.errors];
for (var key of Object.keys(this)) {
for (var item of [this[key]].flat()) {
if (item instanceof _ParseElement) {
all.push(...item.collectErrors(visited));
}
}
}
return all;
}
sourceFor() {
return this.programSource.substring(this.startToken.start, this.endToken.end);
}
lineFor() {
return this.programSource.split("\n")[this.startToken.line - 1];
}
static parseEventArgs(parser) {
var args = [];
if (parser.token(0).value === "(" && (parser.token(1).value === ")" || parser.token(2).value === "," || parser.token(2).value === ")")) {
parser.matchOpToken("(");
do {
args.push(parser.requireTokenType("IDENTIFIER"));
} while (parser.matchOpToken(","));
parser.requireOpToken(")");
}
return args;
}
};
var Feature = class extends ParseElement {
isFeature = true;
constructor() {
super();
if (this.constructor.keyword) {
this.type = this.constructor.keyword + "Feature";
}
}
install(target, source, args, runtime) {
}
/**
* Parse optional catch/finally blocks after a command list.
* Returns { errorHandler, errorSymbol, finallyHandler }
*/
static parseErrorAndFinally(parser) {
var errorSymbol, errorHandler, finallyHandler;
if (parser.matchToken("catch")) {
errorSymbol = parser.requireTokenType("IDENTIFIER").value;
errorHandler = parser.requireElement("commandList");
parser.ensureTerminated(errorHandler);
}
if (parser.matchToken("finally")) {
finallyHandler = parser.requireElement("commandList");
parser.ensureTerminated(finallyHandler);
}
return { errorHandler, errorSymbol, finallyHandler };
}
};
// src/ext/eventsource.js
async function* parseSSE(reader) {
var decoder = new TextDecoder();
var buffer = "";
var hasData = false;
var message = { data: "", event: "", id: "", retry: null };
var firstChunk = true;
try {
while (true) {
var { done, value } = await reader.read();
if (done) break;
var chunk = decoder.decode(value, { stream: true });
if (firstChunk) {
if (chunk.charCodeAt(0) === 65279) chunk = chunk.slice(1);
firstChunk = false;
}
buffer += chunk;
var lines = buffer.split(/\r\n|\r|\n/);
buffer = lines.pop() || "";
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (!line) {
if (hasData) {
yield message;
hasData = false;
message = { data: "", event: "", id: "", retry: null };
}
continue;
}
var colonIndex = line.indexOf(":");
if (colonIndex === 0) continue;
var field, val;
if (colonIndex < 0) {
field = line;
val = "";
} else {
field = line.slice(0, colonIndex);
val = line.slice(colonIndex + 1);
if (val[0] === " ") val = val.slice(1);
}
if (field === "data") {
message.data += (hasData ? "\n" : "") + val;
hasData = true;
} else if (field === "event") {
message.event = val;
} else if (field === "id") {
if (!val.includes("\0")) message.id = val;
} else if (field === "retry") {
var retryValue = parseInt(val, 10);
if (!isNaN(retryValue)) message.retry = retryValue;
}
}
}
} finally {
reader.releaseLock();
}
}
function matchesEventPattern(pattern, eventName) {
if (pattern === eventName) return true;
if (!pattern.includes("*")) return false;
var regex = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
return regex.test(eventName);
}
var EventSourceFeature = class _EventSourceFeature extends Feature {
static keyword = "eventsource";
constructor(eventSourceName, nameSpace, stub) {
super();
this.eventSourceName = eventSourceName;
this.nameSpace = nameSpace;
this.stub = stub;
}
install(target, source, args, runtime) {
this.runtime = runtime;
runtime.assignToNamespace(target, this.nameSpace, this.eventSourceName, this.stub);
}
static parse(parser) {
if (!parser.matchToken("eventsource")) return;
var urlElement;
var withCredentials = false;
var method = "GET";
var headers = null;
var name = parser.requireElement("dotOrColonPath");
var qualifiedName = name.evalStatically();
var nameSpace = qualifiedName.split(".");
var eventSourceName = nameSpace.pop();
if (parser.matchToken("from")) {
urlElement = parser.parseURLOrExpression();
}
while (parser.matchToken("with")) {
if (parser.matchToken("credentials")) {
withCredentials = true;
} else if (parser.matchToken("method")) {
method = parser.requireElement("stringLike").evalStatically().toUpperCase();
} else if (parser.matchToken("headers")) {
headers = parser.requireElement("objectLiteral");
} else {
parser.raiseExpected("credentials", "method", "headers");
}
}
var staticHeaders = null;
if (headers) {
staticHeaders = {};
for (var i = 0; i < headers.keyExpressions.length; i++) {
var key = headers.keyExpressions[i].evalStatically();
var val = headers.valueExpressions[i].evalStatically();
staticHeaders[key] = val;
}
}
var stub = createStub(withCredentials, method, staticHeaders);
var feature = new _EventSourceFeature(eventSourceName, nameSpace, stub);
while (parser.matchToken("on")) {
var eventName = parser.requireElement("stringLike").evalStatically();
var encoding = "";
if (parser.matchToken("as")) {
encoding = parser.requireElement("stringLike").evalStatically();
}
var commandList = parser.requireElement("commandList");
parser.ensureTerminated(commandList);
parser.requireToken("end");
stub.listeners.push({
type: eventName,
handler: makeHandler(encoding, commandList, stub, feature)
});
}
parser.requireToken("end");
if (urlElement != void 0) {
stub.open(urlElement.evalStatically());
}
return feature;
}
};
function createStub(withCredentials, method, headers) {
var stub = {
listeners: [],
retryCount: 0,
closed: false,
abortController: null,
reader: null,
lastEventId: null,
reconnectTimeout: null,
url: null,
withCredentials,
method,
headers,
open: function(url) {
if (url == void 0) {
if (stub.url != null) {
url = stub.url;
} else {
throw new Error("no url defined for EventSource.");
}
}
if (stub.url === url && stub.abortController && !stub.abortController.signal.aborted) {
return;
}
if (stub.abortController) {
stub.abortController.abort();
}
stub.closed = false;
stub.url = url;
startConnection(stub);
},
close: function() {
stub.closed = true;
if (stub.reconnectTimeout) {
clearTimeout(stub.reconnectTimeout);
stub.reconnectTimeout = null;
}
if (stub.abortController) {
stub.abortController.abort();
stub.abortController = null;
}
stub.retryCount = 0;
dispatch(stub, "close", { type: "close" });
},
addEventListener: function(type, handler, options) {
stub.listeners.push({ type, handler, options });
}
};
return stub;
}
function startConnection(stub) {
var ac = new AbortController();
stub.abortController = ac;
var fetchOptions = {
method: stub.method,
signal: ac.signal,
headers: Object.assign({}, stub.headers || {})
};
if (stub.withCredentials) {
fetchOptions.credentials = "include";
}
if (stub.lastEventId) {
fetchOptions.headers["Last-Event-ID"] = stub.lastEventId;
}
fetch(stub.url, fetchOptions).then(function(response) {
if (ac.signal.aborted) return;
if (!response.ok) {
throw new Error("SSE connection failed with status " + response.status);
}
stub.retryCount = 0;
dispatch(stub, "open", { type: "open" });
return readStream(stub, response.body.getReader(), ac);
}).catch(function(err) {
if (ac.signal.aborted) return;
dispatch(stub, "error", { type: "error", error: err });
scheduleReconnect(stub);
});
}
async function readStream(stub, reader, ac) {
stub.reader = reader;
var baseDelay = 500;
try {
for await (var msg of parseSSE(reader)) {
if (ac.signal.aborted) break;
if (msg.id) stub.lastEventId = msg.id;
if (msg.retry != null) baseDelay = msg.retry;
var eventType = msg.event || "message";
var evt = {
type: eventType,
data: msg.data,
lastEventId: msg.id || stub.lastEventId || ""
};
dispatch(stub, eventType, evt);
}
} catch (err) {
if (!ac.signal.aborted) {
dispatch(stub, "error", { type: "error", error: err });
}
}
stub.reader = null;
if (!stub.closed && !ac.signal.aborted) {
scheduleReconnect(stub, baseDelay);
}
}
function scheduleReconnect(stub, baseDelay) {
if (stub.closed) return;
baseDelay = baseDelay || 500;
stub.retryCount = Math.min(7, stub.retryCount + 1);
var timeout = Math.random() * 2 ** stub.retryCount * baseDelay;
stub.reconnectTimeout = setTimeout(function() {
stub.reconnectTimeout = null;
if (!stub.closed) startConnection(stub);
}, timeout);
}
function dispatch(stub, eventType, evt) {
for (var i = 0; i < stub.listeners.length; i++) {
var listener = stub.listeners[i];
if (matchesEventPattern(listener.type, eventType)) {
try {
listener.handler(evt);
} catch (e) {
console.error("Error in SSE handler for '" + listener.type + "':", e);
}
}
}
}
function makeHandler(encoding, commandList, stub, feature) {
return function(evt) {
var data = decode(evt.data, encoding);
var context = feature.runtime.makeContext(stub, feature, stub);
context.event = evt;
context.result = data;
commandList.execute(context);
};
}
function decode(data, encoding) {
if (encoding.toLowerCase() === "json") {
return JSON.parse(data);
}
return data;
}
function createStream(response, runtime, context) {
var element = context.me;
var reader = response.body.getReader();
var messages = [];
var waiting = null;
var done = false;
(async function() {
try {
for await (var msg of parseSSE(reader)) {
var eventType = msg.event || "message";
if (msg.event) {
runtime.triggerEvent(element, eventType, {
data: msg.data,
lastEventId: msg.id || ""
});
} else {
messages.push(msg.data);
if (waiting) {
waiting.resolve({ value: msg.data, done: false });
waiting = null;
}
}
}
} catch (err) {
runtime.triggerEvent(element, "stream-error", { error: err });
}
done = true;
if (waiting) {
waiting.resolve({ value: void 0, done: true });
waiting = null;
}
runtime.triggerEvent(element, "streamEnd", {});
})();
var stream = {
element,
[Symbol.asyncIterator]: function() {
var index = 0;
return {
next: function() {
if (index < messages.length) {
return Promise.resolve({ value: messages[index++], done: false });
}
if (done) {
return Promise.resolve({ value: void 0, done: true });
}
return new Promise(function(resolve) {
waiting = { resolve };
}).then(function(result) {
if (!result.done) index++;
return result;
});
}
};
}
};
return stream;
}
var streamConversion = function(response, runtime, context) {
return createStream(response, runtime, context);
};
streamConversion._rawResponse = true;
function eventsourcePlugin(_hyperscript) {
_hyperscript.addFeature(EventSourceFeature.keyword, EventSourceFeature.parse.bind(EventSourceFeature));
_hyperscript.config.conversions.Stream = streamConversion;
}
if (typeof self !== "undefined" && self._hyperscript) {
self._hyperscript.use(eventsourcePlugin);
}
export {
EventSourceFeature,
eventsourcePlugin as default
};
//# sourceMappingURL=eventsource.esm.js.map