virtual-dom: create App type and rocScript function

This commit is contained in:
Brian Carroll 2022-10-30 19:43:29 +00:00
parent fbc8e684fd
commit 9beaedc536
No known key found for this signature in database
GPG Key ID: 5C7B2EC4101703C0
2 changed files with 320 additions and 0 deletions

View File

@ -1,5 +1,6 @@
interface Html.Internal
exposes [
App,
Node,
Attribute,
CyclicStructureAccessor,
@ -9,6 +10,7 @@ interface Html.Internal
insertHandler,
replaceHandler,
dispatchEvent,
rocScript,
appendRenderedStatic,
nodeSize,
]
@ -219,3 +221,42 @@ dispatchEvent = \lookup, handlerId, eventData, state ->
Ok (Custom handler) ->
handler state eventData
HtmlId : Str
App state : {
static : Node [],
initDynamic : Str -> state,
renderDynamic : state -> Dict HtmlId (Node state),
}
rocScript : Str, List HtmlId, Str -> Result (Node []) [InvalidUtf8]*
rocScript = \initData, dynamicRootIds, wasmUrl ->
toJs = \data ->
data
|> Encode.toBytes Json.toUtf8
|> Str.fromUtf8
encInitData = toJs initData
encDynamicRootIds = toJs dynamicRootIds
encWasmUrl = toJs wasmUrl
when { encInitData, encDynamicRootIds, encWasmUrl } is
{ encInitData: Ok jsInitData, encDynamicRootIds: Ok jsDynamicRootIds, encWasmUrl: Ok jsWasmUrl } ->
elem : Node []
elem = (element "script") [] [
Text
"""
\(virtualDomJavaScript)
(function() {
const initData = \(jsInitData);
const dynamicRootIds = \(jsDynamicRootIds);
const wasmUrl = \(jsWasmUrl);
window.roc.init(initData, dynamicRootIds, wasmUrl);
})();
""",
]
Ok elem
_ ->
Err InvalidUtf8

View File

@ -0,0 +1,279 @@
interface Html.VdomJs
exposes [virtualDomJavaScript]
imports []
virtualDomJavaScript : Str
virtualDomJavaScript =
"""
/** @typedef {(e: Event) => void} JsEventDispatcher */
/** @typedef {[string, JsEventDispatcher]} Listener */
/**
* @typedef {Object} RocWasmExports
* @property {(size: number, alignment: number) => number} roc_alloc
* @property {(jsonListAddr: number, jsonListLength: number, handlerId: number) => void} roc_dispatch_event
* @property {() => number} main
*/
/**
* @typedef {{ObjectField: [string, CyclicStructureAccessor]} | {ArrayIndex: [number, CyclicStructureAccessor]} | {SerializableValue: []}} CyclicStructureAccessor
*/
/** @type {Array<Node | null>} */
const nodes = [];
/** @type {Array<Listener | null>} */
const listeners = [];
const utf8Decoder = new TextDecoder();
const utf8Encoder = new TextEncoder();
/**
* @param {string} wasmFilename
*/
export const init = async (wasmFilename) => {
const effects = {
/**
* @param {number} tagAddr
*/
createElement: (tagAddr) => {
const tagName = decodeRocStr(tagAddr);
const node = document.createElement(tagName);
return insertNode(node);
},
/**
* @param {number} contentAddr
*/
createTextNode: (contentAddr) => {
const content = decodeRocStr(contentAddr);
const node = document.createTextNode(content);
return insertNode(node);
},
/**
* @param {number} parentId
* @param {number} childId
*/
appendChild: (parentId, childId) => {
const parent = nodes[parentId];
const child = nodes[childId];
parent.appendChild(child);
},
/**
* @param {number} id
*/
removeNode: (id) => {
var _a;
const node = nodes[id];
nodes[id] = null;
node.parentElement.removeChild(node);
},
/**
* @param {number} nodeId
* @param {number} typeAddr
* @param {number} valueAddr
*/
setAttribute: (nodeId, typeAddr, valueAddr) => {
const node = nodes[nodeId];
const name = decodeRocStr(typeAddr);
const value = decodeRocStr(valueAddr);
node.setAttribute(name, value);
},
/**
* @param {number} nodeId
* @param {number} typeAddr
*/
removeAttribute: (nodeId, typeAddr) => {
const node = nodes[nodeId];
const name = decodeRocStr(typeAddr);
node.removeAttribute(name);
},
/**
* @param {number} nodeId
* @param {number} propNameAddr
* @param {number} jsonAddr
*/
setProperty: (nodeId, propNameAddr, jsonAddr) => {
const node = nodes[nodeId];
const propName = decodeRocStr(propNameAddr);
const json = decodeRocListUtf8(jsonAddr);
const value = JSON.parse(json);
node[propName] = value;
},
/**
* @param {number} nodeId
* @param {number} propNameAddr
*/
removeProperty: (nodeId, propNameAddr) => {
const node = nodes[nodeId];
const propName = decodeRocStr(propNameAddr);
node[propName] = null;
},
/**
* @param {number} nodeId
* @param {number} eventTypeAddr
* @param {number} accessorsJsonAddr
* @param {number} handlerId
*/
setListener: (nodeId, eventTypeAddr, accessorsJsonAddr, handlerId) => {
const element = nodes[nodeId];
const eventType = decodeRocStr(eventTypeAddr);
const accessorsJson = decodeRocStr(accessorsJsonAddr);
const accessors = JSON.parse(accessorsJson);
// Dispatch a DOM event to the specified handler function in Roc
const dispatchEvent = (ev) => {
const { roc_alloc, roc_dispatch_event } = app.exports;
const outerListRcAddr = roc_alloc(4 + accessors.length * 12, 4);
memory32[outerListRcAddr >> 2] = 1;
const outerListBaseAddr = outerListRcAddr + 4;
let outerListIndex32 = outerListBaseAddr >> 2;
accessors.forEach((accessor) => {
const json = accessCyclicStructure(accessor, ev);
const length16 = json.length;
// Due to UTF-8 encoding overhead, a few code points go from 2 bytes in UTF-16 to 3 bytes in UTF-8!
const capacity8 = length16 * 3; // Extremely "worst-case", but simple, and the allocation is short-lived.
const rcAddr = roc_alloc(4 + capacity8, 4);
memory32[rcAddr >> 2] = 1;
const baseAddr = rcAddr + 4;
// Write JSON to the heap allocation of the inner `List U8`
const allocation = memory8.subarray(baseAddr, baseAddr + capacity8);
const { written } = utf8Encoder.encodeInto(json, allocation);
const length = written || 0; // TypeScript claims that `written` can be undefined, though I don't see this in the spec.
// Write the fields of the inner `List U8` into the heap allocation of the outer List
memory32[outerListIndex32++] = baseAddr;
memory32[outerListIndex32++] = length;
memory32[outerListIndex32++] = capacity8;
});
roc_dispatch_event(outerListBaseAddr, accessors.length, handlerId);
};
// Make things easier to debug
dispatchEvent.name = `dispatchEvent${handlerId}`;
element.setAttribute("data-roc-event-handler-id", `${handlerId}`);
listeners[handlerId] = [eventType, dispatchEvent];
element.addEventListener(eventType, dispatchEvent);
},
/**
* @param {number} nodeId
* @param {number} handlerId
*/
removeListener: (nodeId, handlerId) => {
const element = nodes[nodeId];
const [eventType, dispatchEvent] = findListener(element, handlerId);
listeners[handlerId] = null;
element.removeAttribute("data-roc-event-handler-id");
element.removeEventListener(eventType, dispatchEvent);
},
};
/**
* decode a Roc `Str` to a JavaScript string
* @param {number} strAddr8
*/
const decodeRocStr = (strAddr8) => {
const lastByte = memory8[strAddr8 + 12];
const isSmall = lastByte >= 0x80;
if (isSmall) {
const len = lastByte & 0x7f;
const bytes = memory8.slice(strAddr8, strAddr8 + len);
return utf8Decoder.decode(bytes);
} else {
return decodeRocListUtf8(strAddr8);
}
};
/**
* decode a Roc List of UTF-8 bytes to a JavaScript string
* @param {number} listAddr8
*/
const decodeRocListUtf8 = (listAddr8) => {
const listIndex32 = listAddr8 >> 2;
const bytesAddr8 = memory32[listIndex32];
const len = memory32[listIndex32 + 1];
const bytes = memory8.slice(bytesAddr8, bytesAddr8 + len);
return utf8Decoder.decode(bytes);
};
const wasmImports = { effects };
const promise = fetch(wasmFilename);
const instanceAndModule = await WebAssembly.instantiateStreaming(
promise,
wasmImports
);
const app = instanceAndModule.instance;
const memory = app.exports.memory;
const memory8 = new Uint8Array(memory.buffer);
const memory32 = new Uint32Array(memory.buffer);
const { main } = app.exports;
const exitCode = main();
if (exitCode) {
throw new Error(`Roc exited with error code ${exitCode}`);
}
};
/**
* @param {Node} node
*/
const insertNode = (node) => {
let i = 0;
for (; i < nodes.length; i++) {
if (!nodes[i]) break;
}
nodes[i] = node;
return i;
};
/**
* @param {CyclicStructureAccessor} accessor
* @param {any} structure
*/
const accessCyclicStructure = (accessor, structure) => {
while (true) {
if ("SerializableValue" in accessor) {
return JSON.stringify(structure);
} else if ("ObjectField" in accessor) {
const [field, childAccessor] = accessor.ObjectField;
structure = structure[field];
accessor = childAccessor;
} else if ("ArrayIndex" in accessor) {
const [index, childAccessor] = accessor.ArrayIndex;
structure = structure[index];
accessor = childAccessor;
}
throw new Error("Invalid CyclicStructureAccessor");
}
};
/**
* @param {Element} element
* @param {number} handlerId
*/
const findListener = (element, handlerId) => {
const listener = listeners[handlerId];
if (listener) {
return listener;
} else {
throw new Error(
`Event listener #${handlerId} not found. This is a bug in virtual-dom, not your app!` +
"It should have been on this node:\n" +
element.outerHTML
);
}
};
"""