Shared UI kit
data.js 177 lines
Files · 11
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177import { useCallback, useEffect, useRef, useState } from "react";
export function useStore({ setup, load, listeners = {} }) {
const [rows, setRows] = useState([]);
const [ready, setReady] = useState(false);
const [error, setError] = useState("");
const [pending, setPending] = useState(0);
const callbacks = useRef({ setup, load, listeners });
callbacks.current = { setup, load, listeners };
const queue = useRef(Promise.resolve());
const mounted = useRef(false);
const initialized = useRef(false);
const enqueue = useCallback((work) => {
const result = queue.current.then(async () => {
setPending((n) => n + 1);
setError("");
try {
if (!initialized.current) {
await callbacks.current.setup();
initialized.current = true;
}
const value = await work();
const next = await callbacks.current.load();
if (mounted.current) {
setRows(next);
setReady(true);
}
return value;
} catch (err) {
if (mounted.current) setError(err.message || String(err));
throw err;
} finally {
if (mounted.current) setPending((n) => n - 1);
}
});
queue.current = result.catch(() => {});
return result;
}, []);
const act = useCallback(
async (work) => {
try {
await enqueue(work);
return true;
} catch {
return false;
}
},
[enqueue],
);
useEffect(() => {
mounted.current = true;
act(async () => {});
for (const name of Object.keys(callbacks.current.listeners)) {
window.lattice.events.listen(name, (data, envelope) =>
enqueue(() => callbacks.current.listeners[name](data, envelope)),
);
}
return () => {
mounted.current = false;
};
}, [act, enqueue]);
return {
rows,
ready,
error,
busy: pending > 0,
act,
};
}
export async function inserted(table, sql, params) {
await window.lattice.db.exec(sql, params);
const rows = await window.lattice.db.query(
`SELECT * FROM ${table} WHERE rowid = last_insert_rowid()`,
);
if (!rows[0])
throw Error(
"The saved item could not be read. Reload before trying again.",
);
return rows[0];
}
export const fieldsOf = (row, keys) =>
Object.fromEntries(keys.map((key) => [key, row[key] ?? ""]));
export const tagsOf = (text) => [
...new Set(
String(text || "")
.split(",")
.map((tag) => tag.trim())
.filter(Boolean),
),
];
export function localDate(date = new Date()) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
}
export function displayTime(value) {
if (!value) return "";
const utc = /^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/.test(value)
? value.replace(" ", "T") + "Z"
: value;
const date = new Date(utc);
return Number.isNaN(date.getTime())
? value
: date.toLocaleString([], { dateStyle: "medium", timeStyle: "short" });
}
export function httpUrl(value) {
try {
const url = new URL(value);
return ["https:", "http:"].includes(url.protocol) ? url.href : null;
} catch {
return null;
}
}
export function useModel(model) {
const [data, setData] = useState(null),
[error, setError] = useState(""),
[busy, setBusy] = useState(false);
useEffect(() => {
let alive = true;
const refresh = () =>
model
.load()
.then((v) => {
if (alive) setData(v);
})
.catch((e) => {
if (alive) setError(e.message);
});
const off = model.subscribe(refresh);
refresh();
return () => {
alive = false;
off();
};
}, [model]);
const act = async (fn) => {
setBusy(true);
setError("");
try {
await fn();
return true;
} catch (e) {
setError(e.message);
return false;
} finally {
setBusy(false);
}
};
return { data, error, busy, act, clearError: () => setError("") };
}
export function useOperations(refresh) {
const [busy, setBusy] = useState(false),
[error, setError] = useState("");
const guard = useRef(false);
const act = async (task) => {
if (guard.current) return false;
guard.current = true;
setBusy(true);
setError("");
try {
await task();
await refresh();
return true;
} catch (e) {
setError(e.message || String(e));
return false;
} finally {
guard.current = false;
setBusy(false);
}
};
return { busy, error, setError, act };
}