@lue-bird/elm-state-interface-experimental
Version:
fast-moving, less tested version of elm-state-interface
878 lines • 39.1 kB
JavaScript
export function programStart(appConfig) {
appConfig.ports.toJs.subscribe(function (fromElm) {
function sendToElm(eventData) {
const toElm = {
id: fromElm.id,
eventData: eventData
};
appConfig.ports.fromJs.send(toElm);
}
interfaceDiffImplementation(fromElm.diff.tag, sendToElm, fromElm.id)(fromElm.diff.value);
});
function interfaceDiffImplementation(tag, sendToElm, id) {
switch (tag) {
case "Add": return (config) => {
const abortController = new AbortController();
abortControllers.set(id, abortController);
interfaceAddImplementation(id, config.tag, sendToElm, abortController.signal)(config.value);
};
case "Edit": return (config) => {
interfaceEditImplementation(id, config.tag, sendToElm)(config.value);
};
case "Remove": return (_config) => {
const abortController = abortControllers.get(id);
if (abortController !== undefined) {
abortController.abort();
abortControllers.delete(id);
}
else {
warn("bug: trying to remove an interface that was already aborted");
}
};
}
}
function interfaceAddImplementation(id, tag, sendToElm, abortSignal) {
switch (tag) {
case "DocumentTitleReplaceBy": return (config) => {
window.document.title = config;
};
case "DocumentAuthorSet": return (config) => {
getOrAddMeta("author").content = config;
};
case "DocumentKeywordsSet": return (config) => {
getOrAddMeta("keywords").content = config;
};
case "DocumentDescriptionSet": return (config) => {
getOrAddMeta("description").content = config;
};
case "DocumentEventListen": return (config) => {
window.document.addEventListener(config, sendToElm, { signal: abortSignal });
};
case "ConsoleLog": return (message) => {
window?.console.log(message);
};
case "ConsoleWarn": return (message) => {
window?.console.warn(message);
};
case "ConsoleError": return (message) => {
window?.console.error(message);
};
case "NavigationReplaceUrl": return (appUrl) => {
window.history.replaceState({ appUrl: appUrl }, "", window.location.origin + appUrl);
};
case "NavigationPushUrl": return (appUrl) => {
if (window.history.state === null || (history.state.appUrl !== appUrl)) {
window.history.pushState({ appUrl: appUrl }, "", window.location.origin + appUrl);
}
};
case "NavigationGo": return (config) => {
window.history.go(config);
};
case "NavigationLoad": return (url) => {
try {
window.location.href = url;
}
catch (_error) {
window.document.location.reload();
}
};
case "NavigationReload": return (_config) => {
window.document.location.reload();
};
case "NavigationUrlRequest": return (_config) => {
sendToElm(window.location.href);
};
case "FileDownload": return (config) => {
fileDownloadBytes(config);
};
case "ClipboardReplaceBy": return (config) => {
window.navigator.clipboard.writeText(config);
};
case "ClipboardRequest": return (_config) => {
window.navigator.clipboard.readText()
.catch(_notAllowed => {
warn("clipboard cannot be read");
})
.then(sendToElm);
};
case "AudioSourceLoad": return (config) => {
audioSourceLoad(config, sendToElm, abortSignal);
};
case "AudioPlay": return (config) => {
const audioBuffer = audioBuffers.get(config.url);
if (audioBuffer !== undefined) {
const createdAudioPlaying = createAudio(config, audioBuffer);
audioPlaying.set(id, createdAudioPlaying);
abortSignal.addEventListener("abort", _event => {
createdAudioPlaying.sourceNode.stop();
createdAudioPlaying.sourceNode.disconnect();
createdAudioPlaying.gainNode.disconnect();
createdAudioPlaying.stereoPanNode.disconnect();
createdAudioPlaying.processingNodes.forEach(node => { node.disconnect(); });
});
}
else {
warn("tried to play audio from source that isn't loaded. Did you use Web.audioSourceLoad?");
}
};
case "DomNodeRender": return (node) => {
while (appConfig.domElement.lastChild !== null) {
appConfig.domElement.removeChild(appConfig.domElement.lastChild);
}
const createdRealDomNode = createDomNode([], node, sendToElm);
appConfig.domElement.appendChild(createdRealDomNode);
abortSignal.addEventListener("abort", _event => {
domListenAbortControllers = new WeakMap();
appConfig.domElement.removeChild(createdRealDomNode);
});
};
case "NotificationAskForPermission": return (_config) => {
askForNotificationPermissionIfNotAsked();
};
case "NotificationShow": return (config) => {
askForNotificationPermissionIfNotAsked().then(status => {
switch (status) {
case "denied": break;
case "granted": {
const newNotification = new Notification(config.message, {
body: config.details,
tag: config.id
});
newNotification.addEventListener("click", _event => { sendToElm("Clicked"); });
abortSignal.addEventListener("abort", _event => {
newNotification.close();
});
}
}
});
};
case "HttpRequestSend": return (config) => {
httpFetch(config, abortSignal).then(sendToElm);
};
case "TimePosixRequest": return (_config) => {
sendToElm(Date.now());
};
case "TimezoneOffsetRequest": return (_config) => {
sendToElm(new Date().getTimezoneOffset());
};
case "TimezoneNameRequest": return (_config) => {
sendToElm(Intl.DateTimeFormat().resolvedOptions().timeZone);
};
case "TimePeriodicallyListen": return (config) => {
const timePeriodicallyListenId = window.setInterval(() => { sendToElm(Date.now()); }, config.milliSeconds);
abortSignal.addEventListener("abort", _event => {
window.clearInterval(timePeriodicallyListenId);
});
};
case "TimeOnce": return (config) => {
const timeOnceId = window.setTimeout(() => { sendToElm(Date.now()); }, config.pointInTime - Date.now());
abortSignal.addEventListener("abort", _event => {
window.clearTimeout(timeOnceId);
});
};
case "RandomUnsignedInt32sRequest": return (config) => {
sendToElm(Array.from(window.crypto.getRandomValues(new Uint32Array(config))));
};
case "WindowSizeRequest": return (_config) => {
sendToElm({ width: Math.trunc(window.innerWidth), height: Math.trunc(window.innerHeight) });
};
case "WindowPreferredLanguagesRequest": return (_config) => {
sendToElm(window.navigator.languages);
};
case "WindowEventListen": return (config) => {
window.addEventListener(config, sendToElm, { signal: abortSignal });
};
case "WindowVisibilityChangeListen": return (_config) => {
window.document.addEventListener("visibilitychange", _eventWhichDoesNotContainTheNewVisibility => {
sendToElm(window.document.visibilityState);
}, { signal: abortSignal });
};
case "WindowAnimationFrameListen": return (_config) => {
let runningAnimationFrameLoopId;
function addAnimationFrameListen() {
return window.requestAnimationFrame(_timestamp => {
if (!abortSignal.aborted) {
sendToElm(Date.now());
runningAnimationFrameLoopId = addAnimationFrameListen();
}
});
}
runningAnimationFrameLoopId = addAnimationFrameListen();
abortSignal.addEventListener("abort", _event => {
window.cancelAnimationFrame(runningAnimationFrameLoopId);
});
};
case "WindowPreferredLanguagesChangeListen": return (_config) => {
window.addEventListener("languagechange", _event => { sendToElm(window.navigator.languages); }, { signal: abortSignal });
};
case "SocketListen": return (config) => {
const socket = getExistingOrOpenSocket(config.address);
socket.addEventListener("open", (_event) => {
sendToElm({ tag: "SocketOpened", value: null });
}, { signal: abortSignal });
socket.addEventListener("close", (event) => {
sendToElm({ tag: "SocketClosed", value: event });
}, { signal: abortSignal });
socket.addEventListener("message", (event) => {
sendToElm({ tag: "SocketDataReceived", value: event.data });
}, { signal: abortSignal });
abortSignal.addEventListener("abort", (_event) => {
socket.close();
sockets.delete(config.address);
});
};
case "SocketDataSend": return (config) => {
const addressedSocket = getExistingOrOpenSocket(config.address);
addressedSocket.send(config.data);
};
case "LocalStorageSet": return (config) => {
try {
if (config.value === null) {
window.localStorage.removeItem(config.key);
}
else {
window.localStorage.setItem(config.key, config.value);
}
}
catch (disallowedByUserOrQuotaExceeded) {
warn("local storage cannot be written to: " + disallowedByUserOrQuotaExceeded);
}
};
case "LocalStorageRequest": return (config) => {
sendToElm(window.localStorage.getItem(config.key));
};
case "LocalStorageRemoveOnADifferentTabListen": return (config) => {
window.addEventListener("storage", storageEvent => {
if (storageEvent.key === config.key && storageEvent.newValue === null) {
sendToElm(storageEvent.url);
}
}, { signal: abortSignal });
};
case "LocalStorageSetOnADifferentTabListen": return (config) => {
window.addEventListener("storage", storageEvent => {
if (storageEvent.key === config.key && storageEvent.newValue !== null) {
sendToElm({ url: storageEvent.url, oldValue: storageEvent.oldValue, newValue: storageEvent.newValue });
}
}, { signal: abortSignal });
};
case "GeoLocationRequest": return (_config) => {
window.navigator.geolocation.getCurrentPosition(geoPosition => { sendToElm(geoPosition.coords); }, error => {
warn("geo location cannot be read: " + error);
}, { timeout: 10000 });
};
case "GeoLocationChangeListen": return (_config) => {
const geoLocationChangeListenId = navigator.geolocation.watchPosition(geoPosition => { sendToElm(geoPosition.coords); }, error => {
warn("geo location cannot be read: " + error);
});
abortSignal.addEventListener("abort", _event => {
navigator.geolocation.clearWatch(geoLocationChangeListenId);
});
};
case "GamepadsRequest": return (_config) => {
sendToElm(window.navigator.getGamepads());
};
case "GamepadsChangeListen": return (_config) => {
let gamepadsFromLastPoll = null;
const gamepadsChangePollingIntervalId = window.setInterval(function () {
const newGamepads = window.navigator.getGamepads();
if (gamepadsFromLastPoll !== newGamepads) {
sendToElm(newGamepads);
gamepadsFromLastPoll = newGamepads;
}
}, 14);
abortSignal.addEventListener("abort", _event => {
window.clearInterval(gamepadsChangePollingIntervalId);
});
};
default: return (_config) => {
notifyOfUnknownMessageKind("Add." + tag);
};
}
}
function interfaceEditImplementation(id, tag, sendToElm) {
switch (tag) {
case "EditDom": return (config) => {
const realRootDomNode = appConfig.domElement.firstChild;
if (realRootDomNode === null) {
warn("I wanted to edit a DOM node but it appears the root element has never been initialized or has been removed. Try to disable potential interfering extensions");
}
else {
for (const diffAtPath of config) {
const realDomNodeToEdit = domNodeInNodeAtPath(realRootDomNode, diffAtPath.path);
if (realDomNodeToEdit === null) {
warn("I wanted to edit a DOM node but it appears it has been moved or removed. Try to disable potential interfering extensions");
}
else {
editDom(diffAtPath.path, realDomNodeToEdit, diffAtPath.edit, sendToElm);
}
}
}
};
case "EditAudio": return (config) => {
for (const diffAtPath of config.edits) {
editAudio(id, diffAtPath);
}
};
case "EditNotification": return (config) => {
const newNotification = new Notification(config.message, {
body: config.details,
tag: config.id
});
newNotification.addEventListener("click", _event => { sendToElm("Clicked"); });
};
default: return (_config) => {
notifyOfUnknownMessageKind("Edit." + tag);
};
}
}
function domNodeInNodeAtPath(overallParent, path) {
let soFar = overallParent;
for (const indexInPath of path) {
if (soFar === null) {
return null;
}
else if (soFar instanceof Element) {
soFar = soFar.childNodes.item(indexInPath);
}
else {
return null;
}
}
return soFar;
}
function editDom(path, realDomNodeToEdit, edit, sendToElm) {
switch (edit.tag) {
case "ReplaceNode": {
if (realDomNodeToEdit instanceof Element) {
domListenAbortControllers.delete(realDomNodeToEdit);
}
realDomNodeToEdit.replaceWith(createDomNode(path, edit.value, sendToElm));
break;
}
case "AppendSubs": {
if (realDomNodeToEdit instanceof Element) {
const indexToStartAppendingFrom = realDomNodeToEdit.childNodes.length;
const subsAsRealDomNodes = edit.value.map((subNode, subIndex) => createDomNode([...path, indexToStartAppendingFrom + subIndex], subNode, sendToElm));
realDomNodeToEdit.append(...subsAsRealDomNodes);
}
else {
warn("the DOM node I wanted to append sub-nodes to has been replaced by text. Try to disable potential interfering extensions");
}
break;
}
case "RemoveLastNSubs": {
if (realDomNodeToEdit instanceof Element) {
for (let counter = 0; counter <= edit.value - 1; counter++) {
if (realDomNodeToEdit.lastChild !== null) {
domListenAbortControllers.delete(realDomNodeToEdit);
realDomNodeToEdit.removeChild(realDomNodeToEdit.lastChild);
}
}
}
else {
warn("the DOM node I wanted to remove sub-nodes from has been replaced by text. Try to disable potential interfering extensions");
}
break;
}
case "SetStyles":
case "SetAttributes":
case "SetAttributesNamespaced":
case "SetStringProperties":
case "SetBoolProperties":
case "SetScrollToPosition":
case "SetScrollToShow":
case "RequestScrollPosition":
case "SetEventListens": {
if (realDomNodeToEdit instanceof Element) {
editDomModifiers(path, realDomNodeToEdit, { tag: edit.tag, value: edit.value }, sendToElm);
}
else {
warn("the DOM node I wanted to edit has been replaced by text. Try to disable potential interfering extensions");
}
break;
}
}
}
}
const abortControllers = new Map();
let domListenAbortControllers = new WeakMap();
const audioPlaying = new Map();
const audioBuffers = new Map();
let audioContext = null;
const sockets = new Map();
function getExistingOrOpenSocket(address) {
const socket = sockets.get(address);
if (socket !== undefined) {
return socket;
}
else {
const createdSocket = new WebSocket(address);
sockets.set(address, createdSocket);
return createdSocket;
}
}
function getOrInitializeAudioContext() {
if (audioContext !== null) {
return audioContext;
}
else {
audioContext = new AudioContext();
return audioContext;
}
}
function editDomModifiers(path, domElementToEdit, replacement, sendToElm) {
switch (replacement.tag) {
case "SetStyles": {
if ((domElementToEdit instanceof HTMLElement) || (domElementToEdit instanceof SVGElement)) {
replacement.value.remove.forEach((styleKey) => {
domElementToEdit?.style.removeProperty(styleKey);
});
domElementAddStyles(domElementToEdit, replacement.value.edit);
}
break;
}
case "SetAttributes": {
replacement.value.remove.forEach((attributeKey) => {
domElementToEdit.removeAttribute(attributeKey);
});
domElementAddAttributes(domElementToEdit, replacement.value.edit);
break;
}
case "SetAttributesNamespaced": {
replacement.value.remove.forEach((attributeNamespacedId) => {
domElementToEdit.removeAttributeNS(attributeNamespacedId.namespace, attributeNamespacedId.key);
});
domElementAddAttributesNamespaced(domElementToEdit, replacement.value.edit);
break;
}
case "SetStringProperties": {
replacement.value.remove.forEach((propertyKey) => {
domElementToEdit[propertyKey] = "";
});
domElementSetStringProperties(domElementToEdit, replacement.value.edit);
break;
}
case "SetBoolProperties": {
replacement.value.remove.forEach((propertyKey) => {
domElementToEdit[propertyKey] = false;
});
domElementSetBoolProperties(domElementToEdit, replacement.value.edit);
break;
}
case "SetScrollToPosition": {
if (replacement.value !== null) {
domElementToEdit.scrollTo({ top: replacement.value.fromTop, left: replacement.value.fromLeft });
}
break;
}
case "SetScrollToShow": {
if (replacement.value !== null) {
domElementToEdit.scrollIntoView({ inline: replacement.value.x, block: replacement.value.y });
}
break;
}
case "RequestScrollPosition": {
domElementAddScrollPositionRequest(path, domElementToEdit, sendToElm);
break;
}
case "SetEventListens": {
const domListenAbortController = domListenAbortControllers.get(domElementToEdit);
if (domListenAbortController !== undefined) {
domListenAbortController.abort();
domListenAbortControllers.delete(domElementToEdit);
}
domElementAddEventListens(path, domElementToEdit, replacement.value, sendToElm);
break;
}
}
}
function getOrAddMeta(name) {
const maybeExistingMeta = Array.from(window.document.head.getElementsByTagName("meta"))
.find(meta => meta.name === name);
if (maybeExistingMeta !== undefined) {
return maybeExistingMeta;
}
else {
const meta = window.document.createElement("meta");
meta.name = name;
window.document.head.appendChild(meta);
return meta;
}
}
function createDomNode(path, node, sendToElm) {
switch (node.tag) {
case "Text": {
return document.createTextNode(node.value);
}
case "Element": {
return createDomElement(path, node.value, sendToElm);
}
}
}
function createDomElement(path, node, sendToElm) {
const realDomElement = node.header.namespace !== null ?
document.createElementNS(node.header.namespace, noScript(node.header.tag))
:
document.createElement(noScript(node.header.tag));
domElementAddAttributes(realDomElement, node.header.attributes);
domElementAddAttributesNamespaced(realDomElement, node.header.attributesNamespaced);
if ((realDomElement instanceof HTMLElement) || (realDomElement instanceof SVGElement)) {
domElementAddStyles(realDomElement, node.header.styles);
}
domElementSetStringProperties(realDomElement, node.header.stringProperties);
domElementSetBoolProperties(realDomElement, node.header.boolProperties);
if (node.header.scrollToPosition !== null) {
realDomElement.scrollTo({ top: node.header.scrollToPosition.fromTop, left: node.header.scrollToPosition.fromLeft });
}
if (node.header.scrollToShow !== null) {
realDomElement.scrollIntoView({ inline: node.header.scrollToShow.x, block: node.header.scrollToShow.y });
}
if (node.header.scrollPositionRequest === true) {
domElementAddScrollPositionRequest(path, realDomElement, sendToElm);
}
domElementAddEventListens(path, realDomElement, node.header.eventListens, sendToElm);
const subsAsRealDomNodes = node.subs.map((subNode, subIndex) => createDomNode([...path, subIndex], subNode, sendToElm));
realDomElement.append(...subsAsRealDomNodes);
return realDomElement;
}
function domElementAddScrollPositionRequest(path, domElement, sendToElm) {
window.requestAnimationFrame(_timestamp => {
window.requestAnimationFrame(_timestamp => {
sendToElm({
tag: "ScrollPositionRequest",
value: {
path: path,
fromLeft: domElement.scrollLeft,
fromTop: domElement.scrollTop
}
});
});
});
}
function domElementAddStyles(domElement, styles) {
styles.forEach(styleSingle => {
domElement?.style.setProperty(styleSingle.key, styleSingle.value);
});
}
function domElementSetStringProperties(domElement, properties) {
const domElementIndexable = domElement;
properties.forEach(property => {
if ((Object.hasOwn(domElement, property.key))
&& (typeof domElementIndexable[property.key] !== "string")) {
warn(`tried to set the existing non-string dom element property "${property.key}" to a string.`);
}
else if ((property.key === "innerHTML") || (property.key === "outerHTML")) {
window?.console.error("This is an XSS vector. Please parse the html string instead and construct the dom from that.");
}
else if (RE_js_html.test(property.value)) {
window?.console.error("This is an XSS vector. Please use an interface instead.");
}
else if (property.key === "src" && RE_js_html.test(property.value)) {
window?.console.error("This is an XSS vector. Please use an interface instead.");
}
else if (property.key === "action" || property.key === "href" && RE_js.test(property.value)) {
window?.console.error("This is an XSS vector. Please use an interface instead.");
}
else {
try {
domElementIndexable[property.key] = property.value;
}
catch (error) {
warn("tried to set the string property " + property.key + " failed: " + error);
}
}
});
}
function domElementSetBoolProperties(domElement, properties) {
const domElementIndexable = domElement;
properties.forEach(property => {
if ((Object.hasOwn(domElement, property.key))
&& (typeof domElementIndexable[property.key] !== "boolean")) {
warn(`tried to set the existing non-boolean dom element property "${property.key}" to a boolean.`);
}
else {
try {
domElementIndexable[property.key] = property.value;
}
catch (error) {
warn("tried to set the string property " + property.key + " failed: " + error);
}
}
});
}
function domElementAddAttributes(domElement, attributes) {
attributes.forEach(attribute => {
if (RE_js_html.test(attribute.value)) {
console.error("This is an XSS vector. Please use an interface instead.");
}
else if (attribute.key === "src" && RE_js_html.test(attribute.value)) {
console.error("This is an XSS vector. Please use an interface instead.");
}
else if (attribute.key === "action" || attribute.key === "href" && RE_js.test(attribute.value)) {
console.error("This is an XSS vector. Please use an interface instead.");
}
else {
domElement.setAttribute(noOnOrFormAction(attribute.key), attribute.value);
}
});
}
function domElementAddAttributesNamespaced(domElement, attributesNamespaced) {
attributesNamespaced.forEach(attributeNamespaced => {
domElement.setAttributeNS(attributeNamespaced.namespace, attributeNamespaced.key, attributeNamespaced.value);
});
}
function domElementAddEventListens(path, domElement, eventListens, sendToElm) {
if (eventListens.length >= 1) {
const abortController = new AbortController();
eventListens.forEach(eventListen => {
domElement.addEventListener(eventListen.name, (triggeredEvent) => {
triggeredEvent.stopPropagation();
sendToElm({
tag: "EventListen", value: {
name: eventListen.name,
path: path,
event: triggeredEvent
}
});
switch (eventListen.defaultActionHandling) {
case "DefaultActionPrevent": {
triggeredEvent.preventDefault();
break;
}
case "DefaultActionExecute": {
break;
}
}
}, {
signal: abortController.signal,
passive: eventListen.defaultActionHandling === "DefaultActionExecute"
});
});
domListenAbortControllers.set(domElement, abortController);
}
}
const RE_script = /^script$/i;
const RE_on_formAction = /^(on|formAction$)/i;
const RE_js = /^\s*j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:/i;
const RE_js_html = /^\s*(j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:|d\s*a\s*t\s*a\s*:\s*t\s*e\s*x\s*t\s*\/\s*h\s*t\s*m\s*l\s*(,|;))/i;
function noScript(tag) {
return RE_script.test(tag) ? "p" : tag;
}
function noOnOrFormAction(key) {
return RE_on_formAction.test(key) ? "data-" + key : key;
}
function fileDownloadBytes(config) {
const temporaryAnchorDomElement = window.document.createElement("a");
const blob = new Blob([asciiStringToBytes(config.contentAsciiString)], { type: config.mimeType });
const objectUrl = URL.createObjectURL(blob);
temporaryAnchorDomElement.href = objectUrl;
temporaryAnchorDomElement.download = config.name;
const event = new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true
});
document.body.appendChild(temporaryAnchorDomElement);
temporaryAnchorDomElement.dispatchEvent(event);
document.body.removeChild(temporaryAnchorDomElement);
URL.revokeObjectURL(objectUrl);
}
function httpFetch(request, abortSignal) {
return fetch(request.url, {
method: request.method,
body: request.bodyAsciiString === null ?
null
: asciiStringToBytes(request.bodyAsciiString),
headers: new Headers(request.headers.map(header => {
const tuple = [header.name, header.value];
return tuple;
})),
signal: abortSignal
})
.then((response) => response
.arrayBuffer()
.then((bodyArrayBuffer) => ({
tag: "Ok",
value: {
statusCode: response.status,
statusText: response.statusText,
headers: Array.from(response.headers.entries())
.map(([name, value]) => ({ name: name, value: value })),
bodyAsciiString: bytesToAsciiString(new Uint8Array(bodyArrayBuffer))
}
})))
.catch((error) => ({ tag: "Err", value: error }));
}
function audioSourceLoad(url, sendToElm, abortSignal) {
const audioContext = getOrInitializeAudioContext();
fetch(url, { signal: abortSignal })
.then(data => data.arrayBuffer())
.then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
.then(buffer => {
audioBuffers.set(url, buffer);
sendToElm({
tag: "Ok", value: { durationInSeconds: buffer.length / buffer.sampleRate }
});
})
.catch(error => {
sendToElm({ tag: "Err", value: error?.message !== undefined ? error.message : "NetworkError" });
});
}
function audioParameterTimelineApplyTo(audioParam, timeline) {
const audioContext = getOrInitializeAudioContext();
const currentTime = audioContext.currentTime;
audioParam.cancelScheduledValues(currentTime);
audioParam.setValueAtTime(timeline.startValue, 0);
const fullTimeline = [
{ time: currentTime, value: timeline.startValue },
...timeline.keyFrames.map(keyframe => { return { value: keyframe.value, time: posixToContextTime(keyframe.time, currentTime) }; })
];
forEachConsecutive(fullTimeline, pair => {
if (currentTime >= pair.current.time) {
audioParam.setValueAtTime(linearlyInterpolate(pair.current.value, pair.next.value, (currentTime - pair.current.time) / (pair.next.time - pair.current.time)), 0);
}
audioParam.linearRampToValueAtTime(pair.next.value, pair.next.time - pair.current.time);
});
return audioParam;
}
function createAudio(config, buffer) {
const audioContext = getOrInitializeAudioContext();
const source = audioContext.createBufferSource();
source.buffer = buffer;
audioParameterTimelineApplyTo(source.playbackRate, config.speed);
const gainNode = audioContext.createGain();
audioParameterTimelineApplyTo(gainNode.gain, config.volume);
const stereoPannerNode = new StereoPannerNode(audioContext);
audioParameterTimelineApplyTo(stereoPannerNode.pan, config.stereoPan);
const processingNodes = createProcessingNodes(config.processing);
forEachConsecutive([source, gainNode, stereoPannerNode, ...processingNodes, audioContext.destination], pair => { pair.current.connect(pair.next); });
const currentTime = new Date().getTime();
if (config.startTime >= currentTime) {
source.start(posixToContextTime(config.startTime, currentTime), 0);
}
else {
source.start(0, (currentTime - config.startTime) / 1000);
}
return {
sourceNode: source,
gainNode: gainNode,
stereoPanNode: stereoPannerNode,
processingNodes: processingNodes,
};
}
function createProcessingNodes(processingFirstToLast) {
const audioContext = getOrInitializeAudioContext();
return processingFirstToLast
.map(processing => {
switch (processing.tag) {
case "Lowpass": {
const biquadNode = new BiquadFilterNode(audioContext);
biquadNode.type = "lowpass";
audioParameterTimelineApplyTo(biquadNode.frequency, processing.value.cutoffFrequency);
return biquadNode;
}
case "Highpass": {
const biquadNode = new BiquadFilterNode(audioContext);
biquadNode.type = "highpass";
audioParameterTimelineApplyTo(biquadNode.frequency, processing.value.cutoffFrequency);
return biquadNode;
}
case "LinearConvolution": {
const convolverNode = new ConvolverNode(audioContext);
const buffer = audioBuffers.get(processing.value.sourceUrl);
if (buffer !== undefined) {
convolverNode.buffer = buffer;
}
else {
warn("tried to create a linear convolution from source that isn't loaded. Did you use Web.audioSourceLoad?");
}
return convolverNode;
}
}
});
}
function editAudio(id, edit) {
const audioPlayingToEdit = audioPlaying.get(id);
if (audioPlayingToEdit !== undefined) {
switch (edit.tag) {
case "Volume": {
audioParameterTimelineApplyTo(audioPlayingToEdit.gainNode.gain, edit.value);
break;
}
case "Speed": {
audioParameterTimelineApplyTo(audioPlayingToEdit.sourceNode.playbackRate, edit.value);
break;
}
case "StereoPan": {
audioParameterTimelineApplyTo(audioPlayingToEdit.stereoPanNode.pan, edit.value);
break;
}
case "Processing": {
const audioContext = getOrInitializeAudioContext();
audioPlayingToEdit.stereoPanNode.disconnect();
audioPlayingToEdit.processingNodes.forEach(node => { node.disconnect(); });
forEachConsecutive([audioPlayingToEdit.stereoPanNode, ...audioPlayingToEdit.processingNodes, audioContext.destination], pair => { pair.current.connect(pair.next); });
break;
}
}
}
}
function askForNotificationPermissionIfNotAsked() {
switch (Notification.permission) {
case "granted": return Promise.resolve("granted");
case "denied": return Promise.resolve("denied");
case "default":
return Notification.requestPermission()
.then(permission => {
switch (permission) {
case "granted": return "granted";
case "denied": return "denied";
case "default": return "denied";
}
})
.catch(_error => "denied");
}
}
function asciiStringToBytes(string) {
const result = new Uint8Array(string.length);
for (let i = 0; i < string.length; i++) {
result[i] = string.charCodeAt(i);
}
return result;
}
function bytesToAsciiString(bytes) {
let result = "";
for (let i = 0; i < bytes.length; i++) {
result += String.fromCharCode(bytes[i]);
}
return result;
}
function warn(warning) {
window?.console.warn(warning + " (lue-bird/elm-state-interface-experimental)");
}
function notifyOfUnknownMessageKind(messageTag) {
notifyOfBug("Unknown message kind " + messageTag + " from elm. The associated js implementation is missing");
}
function notifyOfBug(bugDescription) {
window?.console.error("bug: " + bugDescription + ". Please open an issue on github.com/lue-bird/elm-state-interface-experimental");
}
function posixToContextTime(posix, currentTimePosix) {
const audioContext = getOrInitializeAudioContext();
return (posix - currentTimePosix) / 1000 + audioContext.currentTime;
}
function linearlyInterpolate(startValue, endValue, progress) {
return Number.isFinite(progress) ?
progress * (endValue - startValue) + startValue
:
startValue;
}
function forEachConsecutive(array, forPair) {
for (let i = 0; i <= array.length - 2; i++) {
const current = array[i];
const next = array[i + 1];
if (current !== undefined && next !== undefined) {
forPair({ current: current, next: next });
}
}
}
//# sourceMappingURL=web.js.map