@hashgraphonline/standards-sdk
Version:
The Hashgraph Online Standards SDK provides a complete implementation of the Hashgraph Consensus Standards (HCS), giving developers all the tools needed to build applications on Hedera.
691 lines (690 loc) • 26.4 kB
JavaScript
import { Logger } from "./standards-sdk.es22.js";
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
class HCS {
constructor() {
this.modelViewerLoaded = false;
this.modelViewerLoading = null;
this.config = {
cdnUrl: "https://kiloscribe.com/api/inscription-cdn/",
network: "mainnet",
retryAttempts: 3,
retryBackoff: 300,
debug: false,
showLoadingIndicator: false,
loadingCallbackName: null
};
this.configMapping = {
hcsCdnUrl: "cdnUrl",
hcsNetwork: "network",
hcsRetryAttempts: "retryAttempts",
hcsRetryBackoff: "retryBackoff",
hcsDebug: "debug",
hcsShowLoadingIndicator: "showLoadingIndicator",
hcsLoadingCallbackName: "loadingCallbackName"
};
this.LoadedScripts = {};
this.LoadedWasm = {};
this.LoadedImages = {};
this.LoadedVideos = {};
this.LoadedAudios = {};
this.LoadedAudioUrls = {};
this.LoadedGLBs = {};
this.scriptLoadedEvent = new Event("HCSScriptLoaded");
this.loadQueue = [];
this.isProcessingQueue = false;
try {
this.logger = Logger.getInstance({
module: "HCS-3",
level: this.config.debug ? "debug" : "error"
});
} catch (e) {
this.logger = this.createFallbackLogger();
}
}
createFallbackLogger() {
const fallbackLogger = {
debug: (...args) => this.config.debug && console.debug("[HCS-3]", ...args),
info: (...args) => this.config.debug && console.info("[HCS-3]", ...args),
warn: (...args) => console.warn("[HCS-3]", ...args),
error: (...args) => console.error("[HCS-3]", ...args),
setLogLevel: (level) => {
this.config.debug = level === "debug";
}
};
return fallbackLogger;
}
log(...args) {
if (args.length === 0) {
this.logger.debug("");
} else if (args.length === 1) {
this.logger.debug(String(args[0]));
} else {
const message = String(args[0]);
const data = args.slice(1);
this.logger.debug(message, data);
}
}
error(...args) {
if (args.length === 0) {
this.logger.error("");
} else if (args.length === 1) {
this.logger.error(String(args[0]));
} else {
const message = String(args[0]);
const data = args.slice(1);
this.logger.error(message, data);
}
}
loadConfigFromHTML() {
const configScript = document.querySelector(
"script[data-hcs-config]"
);
if (configScript) {
Object.keys(this.configMapping).forEach((dataAttr) => {
if (configScript.dataset[dataAttr]) {
const configKey = this.configMapping[dataAttr];
let value = configScript.dataset[dataAttr];
if (value === "true") value = true;
if (value === "false") value = false;
if (!isNaN(Number(value)) && value !== "") value = Number(value);
this.config[configKey] = value;
}
});
this.logger.setLogLevel(this.config.debug ? "debug" : "error");
}
this.log("Loaded config:", this.config);
}
updateLoadingStatus(id, status) {
if (this.LoadedScripts[id] === "loaded") {
return;
}
if (this.config.showLoadingIndicator) {
console.log("[HCS Loading] " + id + " : " + status);
}
this.LoadedScripts[id] = status;
if (this.config.loadingCallbackName && typeof window[this.config.loadingCallbackName] === "function") {
const callback = window[this.config.loadingCallbackName];
if (typeof callback === "function") {
callback(id, status);
}
}
}
async fetchWithRetry(url, retries = this.config.retryAttempts, backoff = this.config.retryBackoff) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("HTTP error! status: " + response.status);
}
return response;
} catch (error) {
if (retries > 0) {
this.log(
"Retrying fetch for " + url + " Attempts left: " + (retries - 1)
);
await this.sleep(backoff);
return this.fetchWithRetry(url, retries - 1, backoff * 2);
}
throw error;
}
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
isDuplicate(topicId) {
return !!this.LoadedScripts[topicId];
}
async retrieveHCS1Data(topicId, cdnUrl = this.config.cdnUrl, network = this.config.network) {
const cleanNetwork = network.replace(/['"]+/g, "");
const response = await this.fetchWithRetry(
cdnUrl + topicId + "?network=" + cleanNetwork
);
return await response.blob();
}
async loadScript(scriptElement) {
const src = scriptElement.getAttribute("data-src");
const scriptId = scriptElement.getAttribute("data-script-id");
const topicId = src?.split("/").pop();
const type = scriptElement.getAttribute("type");
const isRequired = scriptElement.hasAttribute("data-required");
const isModule = scriptElement.getAttribute("type") === "module";
if (this.isDuplicate(topicId || "")) {
return;
}
this.updateLoadingStatus(scriptId, "loading");
try {
const cdnUrl = scriptElement.getAttribute("data-cdn-url") || this.config.cdnUrl;
const network = scriptElement.getAttribute("data-network") || this.config.network;
const blob = await this.retrieveHCS1Data(topicId, cdnUrl, network);
if (type === "wasm") {
const arrayBuffer = await blob.arrayBuffer();
const wasmModule = await WebAssembly.compile(arrayBuffer);
this.LoadedWasm[scriptId] = await WebAssembly.instantiate(wasmModule, {
env: {},
...scriptElement.dataset
});
this.updateLoadingStatus(scriptId, "loaded");
window.dispatchEvent(this.scriptLoadedEvent);
this.log("Loaded wasm: " + scriptId);
} else {
const content = await blob.text();
const script = document.createElement("script");
script.textContent = content;
script.className = "hcs-inline-script";
if (scriptId) {
script.setAttribute("data-loaded-script-id", scriptId);
}
if (isModule) {
script.type = "module";
const moduleBlob = new Blob([content], {
type: "application/javascript"
});
script.src = URL.createObjectURL(moduleBlob);
}
document.body.appendChild(script);
this.updateLoadingStatus(scriptId, "loaded");
window.dispatchEvent(this.scriptLoadedEvent);
this.log("Loaded script: " + scriptId);
script.onerror = (error) => {
this.error("Failed to load " + type + ": " + scriptId, error);
this.updateLoadingStatus(scriptId, "failed");
if (isRequired) {
throw error;
}
};
}
} catch (error) {
this.error("Failed to load " + type + ": " + scriptId, error);
this.updateLoadingStatus(scriptId, "failed");
if (isRequired) {
throw error;
}
}
}
async loadModuleExports(scriptId) {
const script = document.querySelector(
'script[data-loaded-script-id="' + scriptId + '"]'
);
if (!script) {
throw new Error("Module script with id " + scriptId + " not found");
}
const scriptSrc = script.getAttribute("src");
if (!scriptSrc) {
throw new Error("Module script " + scriptId + " has no src attribute");
}
try {
return await import(
/* webpackIgnore: true */
scriptSrc
);
} catch (error) {
this.error("Failed to import module", error);
throw error;
}
}
async loadStylesheet(linkElement) {
const src = linkElement.getAttribute("data-src");
const stylesheetId = linkElement.getAttribute("data-script-id");
const topicId = src?.split("/").pop();
const isRequired = linkElement.hasAttribute("data-required");
if (this.isDuplicate(topicId || "")) {
return;
}
this.updateLoadingStatus(stylesheetId, "loading");
try {
const cdnUrl = linkElement.getAttribute("data-cdn-url") || this.config.cdnUrl;
const network = linkElement.getAttribute("data-network") || this.config.network;
const blob = await this.retrieveHCS1Data(topicId, cdnUrl, network);
const cssContent = await blob.text();
const style = document.createElement("style");
style.textContent = cssContent;
document.head.appendChild(style);
this.updateLoadingStatus(stylesheetId, "loaded");
window.dispatchEvent(this.scriptLoadedEvent);
this.log("Loaded and inlined stylesheet: " + stylesheetId);
} catch (error) {
this.error("Failed to load stylesheet: " + stylesheetId, error);
this.updateLoadingStatus(stylesheetId, "failed");
if (isRequired) {
throw error;
}
}
}
async loadImage(imageElement) {
const src = imageElement.getAttribute("data-src");
const topicId = src?.split("/").pop();
this.log("Loading image: " + topicId);
this.updateLoadingStatus("Image: " + topicId, "loaded");
try {
const cdnUrl = imageElement.getAttribute("data-cdn-url") || this.config.cdnUrl;
const network = imageElement.getAttribute("data-network") || this.config.network;
const blob = await this.retrieveHCS1Data(topicId, cdnUrl, network);
const objectURL = URL.createObjectURL(blob);
imageElement.src = objectURL;
this.LoadedImages[topicId] = objectURL;
this.updateLoadingStatus("Image: " + topicId, "loaded");
this.log("Loaded image: " + topicId);
} catch (error) {
this.error("Failed to load image: " + topicId, error);
this.updateLoadingStatus("Image: " + topicId, "failed");
}
}
async loadMedia(mediaElement, mediaType) {
const src = mediaElement.getAttribute("data-src");
const topicId = src?.split("/").pop();
this.log("Loading " + mediaType + ": " + topicId);
this.updateLoadingStatus(mediaType + ": " + topicId, "loading");
try {
const cdnUrl = mediaElement.getAttribute("data-cdn-url") || this.config.cdnUrl;
const network = mediaElement.getAttribute("data-network") || this.config.network;
const blob = await this.retrieveHCS1Data(topicId, cdnUrl, network);
const objectURL = URL.createObjectURL(blob);
mediaElement.src = objectURL;
if (mediaType === "video") {
this.LoadedVideos[topicId] = objectURL;
} else {
this.LoadedAudioUrls[topicId] = objectURL;
}
this.updateLoadingStatus(mediaType + ": " + topicId, "loaded");
this.log("Loaded " + mediaType + ": " + topicId);
} catch (error) {
this.error("Failed to load " + mediaType + ": " + topicId, error);
this.updateLoadingStatus(mediaType + ": " + topicId, "failed");
}
}
async loadModelViewer() {
if (this.modelViewerLoading) return this.modelViewerLoading;
if (this.modelViewerLoaded) return Promise.resolve();
this.modelViewerLoading = new Promise((resolve) => {
const modelViewerScript = document.createElement("script");
modelViewerScript.setAttribute("data-src", "hcs://1/0.0.7293044");
modelViewerScript.setAttribute("data-script-id", "model-viewer");
modelViewerScript.setAttribute("type", "module");
window.addEventListener(
"HCSScriptLoaded",
() => {
this.modelViewerLoaded = true;
resolve();
},
{ once: true }
);
this.loadScript(modelViewerScript);
});
return this.modelViewerLoading;
}
async loadGLB(glbElement) {
await this.loadModelViewer();
const src = glbElement.getAttribute("data-src");
const topicId = src?.split("/").pop();
this.log("Loading GLB: " + topicId);
this.updateLoadingStatus("GLB: " + topicId, "loading");
try {
const cdnUrl = glbElement.getAttribute("data-cdn-url") || this.config.cdnUrl;
const network = glbElement.getAttribute("data-network") || this.config.network;
let modelViewer;
if (glbElement.tagName.toLowerCase() !== "model-viewer") {
modelViewer = document.createElement("model-viewer");
Array.from(glbElement.attributes).forEach((attr) => {
modelViewer.setAttribute(attr.name, attr.value);
});
modelViewer.setAttribute("camera-controls", "");
modelViewer.setAttribute("auto-rotate", "");
modelViewer.setAttribute("ar", "");
glbElement.parentNode?.replaceChild(modelViewer, glbElement);
} else {
modelViewer = glbElement;
}
const blob = await this.retrieveHCS1Data(topicId, cdnUrl, network);
const objectURL = URL.createObjectURL(blob);
modelViewer.setAttribute("src", objectURL);
this.LoadedGLBs[topicId] = objectURL;
this.updateLoadingStatus("GLB: " + topicId, "loaded");
this.log("Loaded GLB: " + topicId);
} catch (error) {
this.error("Failed to load GLB: " + topicId, error);
this.updateLoadingStatus("GLB: " + topicId, "failed");
}
}
async loadResource(element, type, order) {
return new Promise((resolve) => {
this.loadQueue.push({ element, type, order, resolve });
this.processQueue();
});
}
async processQueue() {
if (this.isProcessingQueue) return;
this.isProcessingQueue = true;
while (this.loadQueue.length > 0) {
const item = this.loadQueue.shift();
try {
if (item.type === "script") {
await this.loadScript(item.element);
} else if (item.type === "image") {
await this.loadImage(item.element);
} else if (item.type === "video" || item.type === "audio") {
await this.loadMedia(item.element, item.type);
} else if (item.type === "glb") {
await this.loadGLB(item.element);
} else if (item.type === "css") {
await this.loadStylesheet(item.element);
}
item.resolve();
} catch (error) {
this.error("Error processing queue item:", error);
if (item.type === "script" && item.element.hasAttribute("data-required")) {
break;
}
}
}
this.isProcessingQueue = false;
}
async replaceHCSInStyle(styleContent) {
let newContent = styleContent;
let startIndex = newContent.indexOf("hcs://");
while (startIndex !== -1) {
let endIndex = startIndex;
while (endIndex < newContent.length && !["'", '"', " ", ")"].includes(newContent[endIndex])) {
endIndex++;
}
const hcsUrl = newContent.substring(startIndex, endIndex);
const topicId = hcsUrl.split("/").pop();
try {
const cdnUrl = this.config.cdnUrl;
const network = this.config.network;
const blob = await this.retrieveHCS1Data(topicId, cdnUrl, network);
const objectURL = URL.createObjectURL(blob);
newContent = newContent.substring(0, startIndex) + objectURL + newContent.substring(endIndex);
this.LoadedImages[topicId] = objectURL;
this.log("Replaced CSS HCS URL: " + hcsUrl + " with " + objectURL);
} catch (error) {
this.error("Failed to load CSS image: " + topicId, error);
}
startIndex = newContent.indexOf("hcs://", startIndex + 1);
}
return newContent;
}
async processInlineStyles() {
const elementsWithStyle = document.querySelectorAll('[style*="hcs://"]');
this.log(
"Found " + elementsWithStyle.length + " elements with HCS style references"
);
for (const element of Array.from(elementsWithStyle)) {
const style = element.getAttribute("style");
if (style) {
this.log("Processing style: " + style);
const newStyle = await this.replaceHCSInStyle(style);
if (style !== newStyle) {
element.setAttribute("style", newStyle);
this.log("Updated style to: " + newStyle);
}
}
}
const styleTags = document.querySelectorAll("style");
for (const styleTag of Array.from(styleTags)) {
if (styleTag.textContent?.includes("hcs://")) {
const newContent = await this.replaceHCSInStyle(styleTag.textContent);
if (styleTag.textContent !== newContent) {
styleTag.textContent = newContent;
}
}
}
}
async init() {
this.loadConfigFromHTML();
return new Promise((resolve) => {
const initializeObserver = async () => {
const scriptElements = document.querySelectorAll(
'script[data-src^="hcs://"]'
);
const imageElements = document.querySelectorAll(
'img[data-src^="hcs://"], img[src^="hcs://"]'
);
const videoElements = document.querySelectorAll(
'video[data-src^="hcs://"], video[src^="hcs://"]'
);
const audioElements = document.querySelectorAll(
'audio[data-src^="hcs://"], audio[src^="hcs://"]'
);
const glbElements = document.querySelectorAll(
'model-viewer[data-src^="hcs://"]'
);
const cssElements = document.querySelectorAll(
'link[data-src^="hcs://"]'
);
document.querySelectorAll('[src^="hcs://"]').forEach((element) => {
const src = element.getAttribute("src");
if (src) {
element.setAttribute("data-src", src);
element.removeAttribute("src");
}
});
await this.processInlineStyles();
const loadPromises = [];
[
{ elements: scriptElements, type: "script" },
{ elements: imageElements, type: "image" },
{ elements: videoElements, type: "video" },
{ elements: audioElements, type: "audio" },
{ elements: glbElements, type: "glb" },
{ elements: cssElements, type: "css" }
].forEach(({ elements, type }) => {
elements.forEach((element) => {
const order = parseInt(element.getAttribute("data-load-order") || "") || Infinity;
loadPromises.push(
this.loadResource(
element,
type,
order
)
);
});
});
await Promise.all(loadPromises);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
if (element.getAttribute("style")?.includes("hcs://")) {
this.processInlineStyles();
}
if (element.tagName.toLowerCase() === "style" && element.textContent?.includes("hcs://")) {
this.processInlineStyles();
}
if (element.getAttribute("src")?.startsWith("hcs://")) {
const src = element.getAttribute("src");
element.setAttribute("data-src", src);
element.removeAttribute("src");
const tagName = element.tagName.toLowerCase();
switch (tagName) {
case "img":
this.loadResource(element, "image", Infinity);
break;
case "video":
this.loadResource(element, "video", Infinity);
break;
case "audio":
this.loadResource(element, "audio", Infinity);
break;
case "script":
this.loadResource(element, "script", Infinity);
break;
}
}
if (element.matches('script[data-src^="hcs://"]')) {
this.loadResource(element, "script", Infinity);
} else if (element.matches('img[data-src^="hcs://"]')) {
this.loadResource(element, "image", Infinity);
} else if (element.matches('video[data-src^="hcs://"]')) {
this.loadResource(element, "video", Infinity);
} else if (element.matches('audio[data-src^="hcs://"]')) {
this.loadResource(element, "audio", Infinity);
} else if (element.matches('model-viewer[data-src^="hcs://"]')) {
this.loadResource(element, "glb", Infinity);
} else if (element.matches('link[data-src^="hcs://"]')) {
this.loadResource(element, "css", Infinity);
}
const childrenWithHCS = element.querySelectorAll(
'[data-src^="hcs://"], [src^="hcs://"]'
);
childrenWithHCS.forEach((child) => {
const childElement = child;
const tagName = childElement.tagName.toLowerCase();
const src = childElement.getAttribute("src");
if (src?.startsWith("hcs://")) {
childElement.setAttribute("data-src", src);
childElement.removeAttribute("src");
}
switch (tagName) {
case "script":
this.loadResource(childElement, "script", Infinity);
break;
case "img":
this.loadResource(childElement, "image", Infinity);
break;
case "video":
this.loadResource(childElement, "video", Infinity);
break;
case "audio":
this.loadResource(childElement, "audio", Infinity);
break;
case "model-viewer":
this.loadResource(childElement, "glb", Infinity);
break;
case "link":
this.loadResource(childElement, "css", Infinity);
break;
}
});
}
});
if (mutation.type === "attributes") {
const element = mutation.target;
if (mutation.attributeName === "style" && element.getAttribute("style")?.includes("hcs://")) {
this.processInlineStyles();
} else if (mutation.attributeName === "src") {
const src = element.getAttribute("src");
if (src?.startsWith("hcs://")) {
element.setAttribute("data-src", src);
element.removeAttribute("src");
const type = element.tagName.toLowerCase();
if (["img", "video", "audio"].includes(type)) {
this.loadResource(element, type, Infinity);
}
}
}
}
});
});
if (document.body) {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "src", "data-src"]
});
} else {
document.addEventListener("DOMContentLoaded", () => {
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["style", "src", "data-src"]
});
});
}
resolve();
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeObserver);
} else {
initializeObserver();
}
});
}
async preloadImage(topicId) {
this.log("Loading image:" + topicId);
this.updateLoadingStatus("image: " + topicId, "loading");
const blob = await this.retrieveHCS1Data(topicId);
const objectURL = URL.createObjectURL(blob);
this.LoadedImages[topicId] = objectURL;
this.updateLoadingStatus("image: " + topicId, "loaded");
return objectURL;
}
async preloadAudio(topicId) {
const audioElement = document.createElement("audio");
audioElement.setAttribute("data-topic-id", topicId);
audioElement.setAttribute("data-src", "hcs://1/" + topicId);
document.body.appendChild(audioElement);
await this.loadMedia(audioElement, "audio");
const cachedAudio = document.querySelector(
'audio[data-topic-id="' + topicId + '"]'
);
if (cachedAudio) {
this.LoadedAudioUrls[topicId] = cachedAudio.src;
} else {
console.error("Failed to preload audio: " + topicId);
}
return this.LoadedAudioUrls[topicId];
}
async playAudio(topicId, volume = 1) {
const audioUrl = this.LoadedAudioUrls[topicId];
if (audioUrl) {
const audio = new Audio(audioUrl);
audio.volume = volume;
this.LoadedAudios[topicId] = audio;
audio.play().catch((error) => {
console.error("Failed to play audio:", error);
});
audio.addEventListener("ended", () => {
audio.remove();
delete this.LoadedAudios[topicId];
});
} else {
console.error("Audio not preloaded: " + topicId);
}
}
async pauseAudio(topicId) {
const audioElement = document.querySelector(
'audio[data-topic-id="' + topicId + '"]'
);
if (audioElement) {
console.log("found element", audioElement);
audioElement.pause();
this.LoadedAudios[topicId]?.pause();
} else {
this.LoadedAudios[topicId]?.pause();
}
}
async loadAndPlayAudio(topicId, autoplay = false, volume = 1) {
let existingAudioElement = document.querySelector(
'audio[data-topic-id="' + topicId + '"]'
);
if (existingAudioElement) {
existingAudioElement.volume = volume;
await existingAudioElement.play();
} else {
const audioElement = document.createElement("audio");
audioElement.volume = volume;
if (autoplay) {
audioElement.setAttribute("autoplay", "autoplay");
}
audioElement.setAttribute("data-topic-id", topicId);
audioElement.setAttribute("data-src", "hcs://1/" + topicId);
document.body.appendChild(audioElement);
await this.loadMedia(audioElement, "audio");
existingAudioElement = document.querySelector(
'audio[data-topic-id="' + topicId + '"]'
);
if (!autoplay) {
await existingAudioElement.play();
}
}
}
}
export {
HCS,
sleep
};
//# sourceMappingURL=standards-sdk.es2.js.map