Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 23e397d8a5d5bf7756362640
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/lib/sync.ts
// Keeps the device copy of the log and the person's Bounded account in step, and carries a guest's
// log into their account when they choose to keep it on all devices.
// Docs: lifters/<uid> (settings, your exercises, body weight), lifters/<uid>/routines/<id>,
// lifters/<uid>/workouts/<id>. Each is one JSON string plus the server time it was written (at),
// so after the first sync a device reads only the docs that changed since, and writes only its own changes.
import { useSyncExternalStore } from "react";
import { get, setMany, serverTimestamp } from "@bounded-sh/client";
import { me, signIn, type Me } from "./bounded";
import { getState, replace, saveSync, onLocalChange, type State } from "./store";
const UPGRADE = "lift:upgrade";
const MARGIN = 300; // seconds read again before the newest change seen, for writes that landed while we read
let profileSent = ""; // the profile data last written or read
let user: Me | null = null;
let timer: number | undefined;
let ready: Promise<Me> | null = null;
let upgraded = false;
let pulledAt = 0; // a push waits until the account's copy has been read once, so it never overwrites newer data
let readyUser: Me | null = null;
const readyListeners = new Set<() => void>();
const profileData = (s: State) => JSON.stringify({ settings: s.settings, custom: s.custom, bw: s.bw });
const syncOf = (uid: string) => { const x = getState().sync; return x?.uid === uid ? x : { uid, since: 0, at: {} }; };
type Stamped = { at: number };
function mergeById<T extends Stamped>(a: Record<string, T>, b: Record<string, T>): Record<string, T> {
const out = { ...a };
for (const [id, x] of Object.entries(b)) if (!out[id] || x.at > out[id].at) out[id] = x;
return out;
}
function parse<T>(data: string): T | null {
try { return JSON.parse(data) as T; } catch { return null; }
}
/** Every doc in a collection, or only those written after a server time. */
async function readAll(path: string, after: number): Promise<{ data: string; at: number }[]> {
const rows: { data: string; at: number }[] = [];
let cursor: string | undefined;
do {
const page = await get(path, { limit: 1000, cursor, ...(after > 0 ? { filter: { at: { $gt: after } } } : {}) });
for (const r of page?.data || []) rows.push({ data: r.data, at: Number(r.at) || 0 });
cursor = page?.nextCursor ?? undefined;
} while (cursor);
return rows;
}
async function pull(uid: string) {
const base = `lifters/${uid}`;
const known = syncOf(uid);
const after = known.since ? known.since - MARGIN : 0;
const [profile, routines, workouts] = await Promise.all([get(base), readAll(`${base}/routines`, after), readAll(`${base}/workouts`, after)]);
const local = getState();
const next: State = { ...local, owner: uid };
if (profile?.data) {
profileSent = profile.data;
const p = parse<Pick<State, "settings" | "custom" | "bw">>(profile.data);
if (p) {
if ((p.settings?.at ?? 0) > local.settings.at) next.settings = { ...local.settings, ...p.settings };
next.custom = mergeById(local.custom, p.custom || {});
const bw = { ...local.bw };
for (const [day, e] of Object.entries(p.bw || {})) if (!bw[day] || e.t > bw[day].t) bw[day] = e;
next.bw = bw;
}
}
const sync = { uid, since: known.since, at: { ...known.at } };
const take = <T extends { id: string; at: number }>(rows: { data: string; at: number }[], kind: "routines" | "workouts") => {
const out: Record<string, T> = {};
for (const r of rows) {
const x = parse<T>(r.data);
if (!x) continue;
out[x.id] = x;
sync.at[`${base}/${kind}/${x.id}`] = x.at;
sync.since = Math.max(sync.since, r.at);
}
return out;
};
next.routines = mergeById(local.routines, take(routines, "routines"));
next.workouts = mergeById(local.workouts, take(workouts, "workouts"));
replace({ ...next, sync });
pulledAt = Date.now();
}
async function push() {
if (!user || !pulledAt) return;
const base = `lifters/${user.id}`;
const s = getState();
const sync = syncOf(user.id);
const writes: { path: string; document: { data: string; at?: unknown }; at?: number }[] = [];
const profile = profileData(s);
if (profile !== profileSent) writes.push({ path: base, document: { data: profile } });
for (const [kind, items] of [["routines", s.routines], ["workouts", s.workouts]] as const) {
for (const x of Object.values(items)) {
const path = `${base}/${kind}/${x.id}`;
if (sync.at[path] !== x.at) writes.push({ path, document: { data: JSON.stringify(x), at: serverTimestamp() }, at: x.at });
}
}
if (!writes.length) return;
for (let i = 0; i < writes.length; i += 20) {
const batch = writes.slice(i, i + 20);
await setMany(batch.map(({ path, document }) => ({ path, document })));
const done = syncOf(user.id);
for (const w of batch) {
if (w.path === base) profileSent = w.document.data;
else done.at[w.path] = w.at!;
}
saveSync(done);
}
}
function schedulePush() {
window.clearTimeout(timer);
timer = window.setTimeout(() => void push().catch(() => { /* offline: the device copy has it, the next change retries */ }), 800);
}
/** Writes now (before leaving the page for sign-in, for example). */
export async function flush() {
window.clearTimeout(timer);
await push();
}
/** Signs in (guest or account), merges the device copy with the account copy, and starts syncing. */
export function startSync(): Promise<Me> {
ready ??= (async () => {
const m = await me();
let pending: { guest: string; at: number } | null = null;
try { pending = JSON.parse(localStorage.getItem(UPGRADE) || "null"); } catch { /* ignore */ }
user = m;
onLocalChange(schedulePush);
// The log on this device is never thrown away: it merges into whichever account is signed in.
// (Signing out clears the device on purpose.)
await pull(m.id);
await push().catch(() => null);
if (pending) {
localStorage.removeItem(UPGRADE);
if (!m.guest && m.id !== pending.guest && Date.now() - pending.at < 3600_000) upgraded = true;
}
readyUser = m;
for (const l of readyListeners) l();
return m;
})().catch((err) => { ready = null; throw err; });
return ready;
}
/** Reads what changed on the account (another device may have logged a workout), at most every 30 seconds.
* If the session could not start (first opened with no signal), it tries again. */
export async function refresh() {
if (!user) { await startSync(); return; }
if (Date.now() - pulledAt < 30_000) return;
await pull(user.id);
await push();
}
/** True once, right after a guest's log moved into their account. */
export function takeUpgraded(): boolean {
const u = upgraded;
upgraded = false;
return u;
}
/** "Keep my log on all devices": saves, then goes to the hosted email sign-in. */
export async function keepOnAllDevices() {
const m = await startSync();
if (!m.guest) return;
await flush().catch(() => null);
localStorage.setItem(UPGRADE, JSON.stringify({ guest: m.id, at: Date.now() }));
await signIn();
}
/** The signed-in person once syncing has started (null before, or offline). */
export function useUser(): Me | null {
return useSyncExternalStore((cb) => { readyListeners.add(cb); return () => readyListeners.delete(cb); }, () => readyUser);
}
Change history
Point OpenApps store links to openapps.free
Add favicon.ico, local share image and openapi.json for links and guides
Bind agent address in policy constants
Add OpenApp contribution strip and OpenApps source links
Bounded source sync
Bounded source sync
Bounded source sync