Problem: the hexagonal refactor churns the backend tree for nine more phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper, compose/web image) must move to its own repo first so doc/layout rewrites land once on a backend-only tree. Change: - New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/ (updateURL repointed to oikos-web releases), compose/, own CI (web + desktop jobs), own deploy script (CI-green gate, TOCTOU guard, version-tagged images, prune-to-3), own webhook receiver on :9798 + launchd unit, own compose project publishing the same 8091:80. - Cutover executed on mac-mini in order: oikos stack's web service stopped+removed, oikos-web project brought up on 8091; outer Caddy untouched (targets the published port) — serving + Authentik flow + /wails 404 quirk verified post-cutover. - Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install targets, the compose web service, oikos-web from deploy.sh's fallback prune list; wails + go-keyring dropped from go.mod, vendor synced. - README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now point at the new repo; mbse + mascot design docs carry a path note. Risk: production SPA serving depends on the new pipeline now; rollback is versioned-image re-up of the old web service from a pre-split checkout (port 8091). Desktop builds installed before the split still check dtoro/oikos releases — one manual reinstall, noted in the oikos-web release notes. Verification: go vet, make test (race), make generate-check, golangci (no new findings; baseline down 400→365); post-cutover curls — localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
140 lines
4.6 KiB
JavaScript
140 lines
4.6 KiB
JavaScript
import { watch } from "../watch/index.js";
|
|
// Helper functions for debounce and throttle
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function debounce(fn, delay) {
|
|
let timeoutId;
|
|
let lastResolve = null;
|
|
return (...args) => {
|
|
return new Promise((resolve) => {
|
|
if (lastResolve) {
|
|
lastResolve(undefined);
|
|
}
|
|
lastResolve = resolve;
|
|
clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(async () => {
|
|
const result = await fn(...args);
|
|
if (lastResolve) {
|
|
lastResolve(result);
|
|
lastResolve = null;
|
|
}
|
|
}, delay);
|
|
});
|
|
};
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function throttle(fn, delay) {
|
|
let lastRun = 0;
|
|
let lastPromise = null;
|
|
return (...args) => {
|
|
const now = Date.now();
|
|
if (lastRun && now - lastRun < delay) {
|
|
return lastPromise ?? Promise.resolve(undefined);
|
|
}
|
|
lastRun = now;
|
|
lastPromise = fn(...args);
|
|
return lastPromise;
|
|
};
|
|
}
|
|
function runResource(source, fetcher, options = {}, effectFn) {
|
|
const { lazy = false, once = false, initialValue, debounce: debounceTime, throttle: throttleTime, } = options;
|
|
// Create state
|
|
let current = $state(initialValue);
|
|
let loading = $state(false);
|
|
let error = $state(undefined);
|
|
let cleanupFns = $state([]);
|
|
// Helper function to run cleanup functions
|
|
const runCleanup = () => {
|
|
cleanupFns.forEach((fn) => fn());
|
|
cleanupFns = [];
|
|
};
|
|
// Helper function to register cleanup
|
|
const onCleanup = (fn) => {
|
|
cleanupFns = [...cleanupFns, fn];
|
|
};
|
|
// Create the base fetcher function
|
|
const baseFetcher = async (value, previousValue, refetching = false) => {
|
|
try {
|
|
loading = true;
|
|
error = undefined;
|
|
runCleanup();
|
|
// Create new AbortController for this fetch
|
|
const controller = new AbortController();
|
|
onCleanup(() => controller.abort());
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const result = await fetcher(value, previousValue, {
|
|
data: current,
|
|
refetching,
|
|
onCleanup,
|
|
signal: controller.signal,
|
|
});
|
|
current = result;
|
|
return result;
|
|
}
|
|
catch (e) {
|
|
if (!(e instanceof DOMException && e.name === "AbortError")) {
|
|
error = e;
|
|
}
|
|
return undefined;
|
|
}
|
|
finally {
|
|
loading = false;
|
|
}
|
|
};
|
|
// Apply debounce or throttle if specified
|
|
const runFetcher = debounceTime
|
|
? debounce(baseFetcher, debounceTime)
|
|
: throttleTime
|
|
? throttle(baseFetcher, throttleTime)
|
|
: baseFetcher;
|
|
// Setup effect
|
|
const sources = Array.isArray(source) ? source : [source];
|
|
let prevValues;
|
|
effectFn((values, previousValues) => {
|
|
// Skip if once and already ran
|
|
if (once && prevValues) {
|
|
return;
|
|
}
|
|
prevValues = values;
|
|
runFetcher(Array.isArray(source) ? values : values[0], Array.isArray(source) ? previousValues : previousValues?.[0]);
|
|
}, { lazy });
|
|
return {
|
|
get current() {
|
|
return current;
|
|
},
|
|
get loading() {
|
|
return loading;
|
|
},
|
|
get error() {
|
|
return error;
|
|
},
|
|
mutate: (value) => {
|
|
current = value;
|
|
},
|
|
refetch: (info) => {
|
|
const values = sources.map((s) => s());
|
|
return runFetcher(Array.isArray(source) ? values : values[0], Array.isArray(source) ? values : values[0], info ?? true);
|
|
},
|
|
};
|
|
}
|
|
// Implementation
|
|
export function resource(source, fetcher, options) {
|
|
return runResource(source, fetcher, options, (fn, options) => {
|
|
const sources = Array.isArray(source) ? source : [source];
|
|
const getters = () => sources.map((s) => s());
|
|
watch(getters, (values, previousValues) => {
|
|
fn(values, previousValues ?? []);
|
|
}, options);
|
|
});
|
|
}
|
|
// Implementation
|
|
export function resourcePre(source, fetcher, options) {
|
|
return runResource(source, fetcher, options, (fn, options) => {
|
|
const sources = Array.isArray(source) ? source : [source];
|
|
const getter = () => sources.map((s) => s());
|
|
watch.pre(getter, (values, previousValues) => {
|
|
fn(values, previousValues ?? []);
|
|
}, options);
|
|
});
|
|
}
|
|
resource.pre = resourcePre;
|