UNPKG

@lue-bird/elm-state-interface-experimental

Version:
888 lines 39.6 kB
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 "MediaQueryRequest": return (queryString) => { if (window.matchMedia !== undefined) { sendToElm(window.matchMedia(queryString).matches); } }; case "MediaQueryChangeListen": return (queryString) => { if (window.matchMedia !== undefined) { window.matchMedia(queryString).addEventListener("change", event => { sendToElm(event.matches); }, { 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