Public
Edited
Apr 7
Importers
Insert cell
Insert cell
viewof cachedvalue = CachedWidget("data", dataInput({ value: [] }), {blob: true, debug: true})
Insert cell
cachedvalue
Insert cell
viewof name.value
Insert cell
viewof name = CachedWidget("name", Inputs.text({label: "Name will be cached:"}), {showControls: false})
Insert cell
Inputs.bind(Inputs.text(), viewof name)
Insert cell
CachedWidget("range",
Inputs.range([1, 10], { label: "Name will be cached:" }),
{
showControls: true
})
Insert cell
async function CachedWidget(
name,
input,
{
storageName = "CachedWidgetDB", // Name of IndexedDB storage
showControls = true,
template = (input, controls) => htl.html`<div>${input}${controls}</div>`, // how to render the input
// use (input) => input to render passed input directly
loadFromCacheCondition = (v) =>
v === "" ||
v === undefined ||
v === null ||
(Array.isArray(v) && v?.length === 0),
debug = false,
blob = false,
schema = "" // for indexedDB
} = {}
) {
// Check for input
if (
!input?.addEventListener ||
typeof input?.addEventListener !== "function"
) {
throw new Error(
"second parameter should be an input and support addEventListener"
);
}

let value = input?.value;
let originalValue = value;
let loadedFromCache = false;

// IndexedDB Initialization
const db = new Dexie(storageName);
// TODO: Handle versions?
db.version(1).stores({ data: schema });

const btnClear = htl.html`<button>Clear cache</button>`;
const info = htl.html`<output></output>`;

// if (loadFromCacheCondition(value)) {
debug &&
console.log("🤔 Value seems empty, trying to load from cache", value);
// try to load from cache
await loadFromCache();
// }

// *** Load from Cache ***
async function loadFromCache() {
const before = performance.now();
debug && console.log("Loading from cache", name);
try {
const res = await db.data.get(name);
if (!res) {
debug && console.log("No value found", name);
info.innerText = "🕳️";
return;
}
loadedFromCache = true;
debug &&
console.log(
"data loaded from cache",
res,
` in ${(performance.now() - before) / 1000}`
);
info.innerText = "🛟";
value = blob ? fromBlob(res) : res;
input.value = value;
input.dispatchEvent(new Event("input", { bubbles: true }));
} catch (err) {
info.innerText = "❌";
console.error(err.message);
}
}

function showAndCacheValue() {
input.value = value;
const before = performance.now();
debug && console.log("🛟 Caching data", value);
info.innerText = "⏳";

const valueToStore = blob
? toBlob(value)
: value;
db.data
.put(valueToStore, name)
.then((res) => {
debug &&
console.log(
"data stored",
name,
value,
`in ${(performance.now() - before) / 1000}`
);
info.innerText = "✅";
})
.catch((err) => {
console.error(err.message);
info.innerText = "❌";
});
}

btnClear.addEventListener("click", () => {
db.data.delete(name).then(() => {
value = originalValue;
info.innerText = "🕳️";
debug && console.log("Data Cleared");
});
});

const controls = showControls
? htl.html`<div>
${info}
${btnClear}</div>`
: htl.html``;
const wrappedInput = template(input, controls);

const widget = ReactiveWidget(wrappedInput, {
value,
showValue: showAndCacheValue
});

if (!loadedFromCache) showAndCacheValue();

// Listen for changes on the input to store them in the cache
input.addEventListener("input", (evt) => {
debug && console.log("✉️ Input value changed", input.value, value);
evt.stopPropagation();
info.innerText = "Input value changed";
value = input.value;
// showAndCacheValue();
widget.setValue(input.value);
});

widget.addEventListener("input", (evt) => {
debug &&
console.log("❤️ Input value changed", input.value, value, widget.value);
value = widget.value;
showAndCacheValue();
});

widget.clearCache = () => {
db.data.delete(name);

widget.setValue(originalValue);
};

return widget;
}
Insert cell
// async function CachedValue(
// name,
// { value, schema = "", storageName = "CachedWidgetDB" } = {}
// ) {
// const btnClear = htl.html`<button>Clear cache</button>`;
// const info = htl.html`<output></output>`;

// const db = new Dexie(storageName);
// db.version(1).stores({ data: schema });
// let loaded = false;

// if (value === undefined) {
// // try to load from cache
// await loadFromCache();
// }

// console.log("after load", value);

// // When the Reactive Widget gets an update value event, store the data in the indexedDB
// function showValue() {
// console.log("caching data", { id: name, data: value });

// db.data
// .put({ id: name, data: value }, name)
// .then((res) => {
// // db[name].bulkPut(value).then((res) => {
// info.innerText = "Data stored";
// console.log("data stored", name, value);
// })
// .catch((err) => console.error(err.message));
// }

// async function loadFromCache() {
// console.log("Loading from db", name);
// try {
// const res = await db.data.get(name);
// console.log("cache get", res);
// if (!res) {
// info.innerText = "No value provided and no cache found";
// value = null;
// console.log("No value found", name);
// return;
// }
// loaded = true;
// console.log("data loaded from cache", res);
// value = res.data;
// info.innerText = "Data loaded from cache";
// } catch (err) {
// console.error(err.message);
// }
// }

// btnClear.addEventListener("click", () => {
// db.data.delete(name).then(() => {
// // db[name].clear().then(() => {
// info.innerText = "Data Cleared";
// console.log("data Cleared");
// });
// });

// console.log("creating rw", name, value);
// const widget = ReactiveWidget(
// htl.html`<div>
// ${info}<br>
// ${btnClear}
// </div>`,
// {
// value,
// showValue
// }
// );

// if (!loaded) showValue();

// return widget;
// }
Insert cell
ReactiveWidget = require("reactive-widget-helper")
Insert cell
Dexie = require("dexie")
Insert cell
// Generate sample data (replace this with your actual data)
sampleData = Array.from({ length: 1000000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random() * 1000,
category: ["A", "B", "C", "D"][Math.floor(Math.random() * 4)],
timestamp: Date.now() - Math.random() * 10000000
}))
Insert cell
// {
// var db = new Dexie("FriendDatabase");

// // DB with single table "friends" with primary key "id" and
// // indexes on properties "name" and "age"
// db.version(1).stores({
// friends: `
// id,
// name,
// age`
// });

// // Now add some values.
// db.friends
// .bulkPut([
// { id: 1, name: "Josephine", age: 21 },
// { id: 2, name: "Per", age: 75 },
// { id: 3, name: "Simon", age: 5 },
// { id: 4, name: "Sara", age: 50, notIndexedProperty: "foo" }
// ])
// .then(() => {
// return db.friends.where("age").between(0, 25).toArray();
// })
// .then((friends) => {
// alert("Found young friends: " + friends.map((friend) => friend.name));

// return db.friends.orderBy("age").reverse().toArray();
// })
// .then((friends) => {
// alert(
// "Friends in reverse age order: " +
// friends.map((friend) => `${friend.name} ${friend.age}`)
// );

// return db.friends.where("name").startsWith("S").keys();
// })
// .then((friendNames) => {
// alert("Friends on 'S': " + friendNames);
// })
// .catch((err) => {
// alert("Ouch... " + err);
// });
// }
Insert cell
import {dataInput} from "@john-guerra/data-input"
Insert cell
// Gemini "js create object from blob"
function createObjectFromBlob(blob, type = "json") {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = (event) => {
const fileContent = event.target.result;

let resultObject;

switch (type) {
case "json":
try {
resultObject = JSON.parse(fileContent);
} catch (error) {
reject("Error parsing JSON: " + error);
return;
}
break;
case "text":
resultObject = fileContent;
break;
default:
reject("Unsupported type: " + type);
return;
}
resolve(resultObject);
};

reader.onerror = (error) => {
reject("Error reading Blob: " + error);
};

if (type === "json") {
reader.readAsText(blob);
} else if (type === "text") {
reader.readAsText(blob);
} else {
reader.readAsArrayBuffer(blob);
}
});
}
Insert cell
// Generated with Claude 3.7
/**
* Convert a variable to a Blob
* @param {any} data - The data to convert to a Blob
* @param {string} [type="application/octet-stream"] - The MIME type of the Blob
* @returns {Blob} - The created Blob object
*/
function toBlob(data, type = "application/octet-stream") {
// Handle different data types
let blobData;
if (data instanceof Blob) {
// Already a Blob, just return it
return data;
} else if (typeof data === "string") {
// Convert string directly
blobData = [data];
} else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
// Handle ArrayBuffer and typed arrays
blobData = [data];
} else {
// For objects and other types, convert to JSON string
try {
blobData = [JSON.stringify(data)];
// If no type was specified and we're converting an object, use JSON MIME type
if (type === "application/octet-stream") {
type = "application/json";
}
} catch (e) {
// If JSON conversion fails, use string representation
blobData = [String(data)];
}
}
// Create and return the Blob
return new Blob(blobData, { type });
}
Insert cell
// Generated with Claude 3.7
/**
* Convert a Blob back to a variable
* @param {Blob} blob - The Blob to convert
* @param {string} [outputType="auto"] - The desired output type: "text", "json", "arraybuffer", or "auto" (detects from MIME type)
* @returns {Promise<any>} - A promise that resolves to the converted data
*/
async function fromBlob(blob, outputType = "auto") {
// Determine output type based on blob's MIME type if set to auto
if (outputType === "auto") {
const mimeType = blob.type.toLowerCase();
if (mimeType.includes("json")) {
outputType = "json";
} else if (mimeType.includes("text") ||
mimeType.includes("javascript") ||
mimeType.includes("html") ||
mimeType.includes("xml") ||
mimeType.includes("css")) {
outputType = "text";
} else {
outputType = "arraybuffer";
}
}
// Read the blob according to the output type
switch (outputType) {
case "text":
return await blob.text();
case "json":
const text = await blob.text();
try {
return JSON.parse(text);
} catch (e) {
throw new Error("Failed to parse blob as JSON: " + e.message);
}
case "arraybuffer":
return await blob.arrayBuffer();
default:
throw new Error(`Unsupported output type: ${outputType}`);
}
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more