Skip to content

Instantly share code, notes, and snippets.

@IanSSenne
Created June 11, 2020 03:47
Show Gist options
  • Select an option

  • Save IanSSenne/8392bffea20396087c5dfb34c2cb5cb3 to your computer and use it in GitHub Desktop.

Select an option

Save IanSSenne/8392bffea20396087c5dfb34c2cb5cb3 to your computer and use it in GitHub Desktop.
import React, { useEffect, useState } from "react";
export const NO_VALUE = Symbol("NO_VALUE");
export const Middleware = {
JSON: [{ from: JSON.parse, to: JSON.stringify }],
base64: [{ from: (v) => atob(v), to: (v) => btoa(v) }]
}
export class LocalStorageSettingsInterface {
static get defaultMiddleware() {
return Middleware.JSON
}
constructor(setting_name, defaults, middleware) {
this.middleware = (middleware || LocalStorageSettingsInterface.defaultMiddleware).flat(1);
this.defaults = defaults;
this.setting_name = setting_name;
try {
this.settings = this.middleware.reduce((a, m) => m.from(a), localStorage.getItem(this.setting_name));
} catch (e) {
this.settings = {};
}
if (typeof this.settings != "object" || this.settings === null) {
this.settings = {};
}
}
get(name) {
const parts = name.split("/");
let settings = this.settings;
let default_settings = this.defaults;
let IS_DEFAULT = false;
while (parts[0]) {
const part = parts.shift();
if (settings === undefined) {
return Promise.resolve({ IS_DEFAULT: false, value: NO_VALUE });
}
if (settings[part]) {
settings = settings[part]
} else {
IS_DEFAULT = true;
settings = default_settings[part];
}
default_settings = default_settings[part];
}
return Promise.resolve({ IS_DEFAULT, value: settings });
}
set(name, value) {
const parts = name.split("/");
const end = parts.pop();
let settings = this.settings;
while (parts[0]) {
const part = parts.shift();
if (typeof settings[part] === "object") {
settings = settings[part];
} else if (typeof settings[part] === "undefined") {
settings[part] = {};
settings = settings[part];
} else {
throw new Error(`unable to set ${name} as one of its parents is not an object`);
}
}
settings[end] = value;
localStorage.setItem(this.setting_name, this.middleware.reduce((a, m) => m.to(a), this.settings));
return Promise.resolve(name.split("/").map((_, i, a) => a.slice(0, 1 + i).join("/")));
}
}
export function createSettingsHandler(io_handlers, options) {
options = Object.assign({ future_delay: 1000 }, options)
const future_tasks = [];
let future_cache = { last_id: null };
async function processFutures() {
const updatedPaths = new Set();
const local_future_tasks = future_tasks.splice(0);
while (local_future_tasks[0]) {
const task = local_future_tasks.pop();
if (!updatedPaths.has(task.name)) {//since we are pushing to future_tasks we only want to head the latest update to a path in case its expensive to write to it.
updatedPaths.add(task.name);
await task.handler.set(task.name, task.value);
}
}
}
function createFuture(o) {
future_tasks.push(o);
clearTimeout(future_cache.last_id);
future_cache.last_id = setTimeout(() => {
processFutures();
}, options.future_delay);
}
function createFutureUpdateTask(name, value) {
for (let i = 1; i < io_handlers.length; i++) {
createFuture({ handler: io_handlers[i], name, value });
}
}
function should_propagate_down(name, value, end) {
for (let i = io_handlers.indexOf(end); i > 0; i--) {
createFuture({ handler: io_handlers[i], name, value });
}
}
async function get(name) {
let potential_return_value = NO_VALUE;
let return_value = NO_VALUE;
for (let handler of io_handlers) {
const value = await handler.get(name);
if (undefined !== value.value) {
if (io_handlers.indexOf(handler) !== 0 && !value.IS_DEFAULT && value.value !== NO_VALUE) {
return_value = { name, value, handler };
break;
} else {
potential_return_value = { name, value, handler };
}
}
}
if (return_value !== NO_VALUE) {
should_propagate_down(return_value.name, return_value.value.value, return_value.handler);
return return_value.value.value;
} else if (potential_return_value !== NO_VALUE) {
createFuture({ name: potential_return_value.name, handler: potential_return_value.handler, value: potential_return_value.value.value });
return potential_return_value.value.value;
} else {
return NO_VALUE;
}
}
function set(name, value) {
createFutureUpdateTask(name, value);
return io_handlers[0].set(name, value);
}
const updates = {};
const Setting = ({ name, option: Option, default: defaultValue }) => {
const [value, setValue] = useState(defaultValue || NO_VALUE);
const onChange = (next_value) => {
setValue(next_value);
set(name, next_value).then((updatedPaths) => {
updatedPaths.forEach((path) => {
if (updates[path]) {
updates[path].forEach(cb => cb(next_value));
}
});
});
}
useEffect(() => {
(async () => {
const value = await get(name);
if (value === NO_VALUE) {
onChange(defaultValue);
} else {
setValue(value);
}
})()
}, [defaultValue, name]);
return (value !== NO_VALUE && React.createElement(Option, { onChange, value }))
}
const useSetting = (name) => {
const [value, setValue] = useState(NO_VALUE);
useEffect(() => {
(async () => {
setValue(await get(name));
})();
updates[name] = updates[name] || [];
updates[name].push(setValue);
return () => updates[name].splice(updates[name].indexOf(setValue), 1);
}, [name]);
return value;
}
return { Setting, useSetting };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment