subs-free.openapps.xyz

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.
Owned app · rev 1ae4feb2bebf
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 5af1a249bbbef1db1b4c5ec8
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.
← All files
src/lib/sync.ts
// Keeps the device copy of subscriptions and the person's Bounded account in step, and carries a
// guest's subscriptions into their account when they choose to keep them on all devices.
import { useSyncExternalStore } from "react";
import { get, setMany } from "@bounded-sh/client";
import { me, signIn, type Me } from "./bounded";
import { getState, replace, onLocalChange, type State } from "./store";
import { mergeSub, problem, type Sub } from "./subs";

const UPGRADE = "subs:upgrade";
const pushed = new Map<string, string>(); // doc path -> 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 changes
let readyUser: Me | null = null;
let failed = false; // the last try to connect failed (offline, or not connected yet)
const readyListeners = new Set<() => void>();

const settingsData = (s: State) => JSON.stringify({ currency: s.currency, lead: s.lead, at: s.at });

async function pull(uid: string) {
  const profile = await get(`people/${uid}`);
  const remote: Record<string, Sub> = {};
  let cursor: string | undefined;
  do {
    const page = await get(`people/${uid}/subs`, { limit: 1000, cursor });
    for (const row of page?.data || []) {
      // The account takes any text in a row, so one that is not a whole subscription is left out
      // instead of breaking sync or the pages that show it.
      let x: Sub;
      try { x = JSON.parse(row.data); if (typeof x.id !== "string" || !/^\w{8,32}$/.test(x.id) || (!x.deleted && problem(x))) continue; } catch { continue; }
      remote[x.id] = x;
      pushed.set(`people/${uid}/subs/${x.id}`, row.data);
    }
    cursor = page?.nextCursor ?? undefined;
  } while (cursor);
  if (profile?.data) pushed.set(`people/${uid}`, profile.data);
  let p: Partial<State> = {};
  try { p = JSON.parse(profile?.data || "{}"); } catch { /* unreadable settings: keep this device's */ }
  const local = getState();
  const merged: Record<string, Sub> = { ...local.subs };
  for (const [id, x] of Object.entries(remote)) merged[id] = merged[id] ? mergeSub(merged[id], x) : x;
  const newer = (p.at ?? 0) > local.at;
  replace({
    ...local,
    owner: uid,
    subs: merged,
    ...(newer ? { currency: p.currency ?? local.currency, lead: p.lead ?? local.lead, at: p.at ?? local.at } : {}),
  });
  pulledAt = Date.now();
}

async function push() {
  if (!user || !pulledAt) return;
  const uid = user.id;
  const s = getState();
  const writes: { path: string; document: Record<string, unknown> }[] = [];
  const add = (path: string, data: string) => { if (pushed.get(path) !== data) writes.push({ path, document: { data } }); };
  add(`people/${uid}`, settingsData(s));
  for (const x of Object.values(s.subs)) add(`people/${uid}/subs/${x.id}`, JSON.stringify(x));
  for (let i = 0; i < writes.length; i += 20) {
    const batch = writes.slice(i, i + 20);
    await setMany(batch);
    for (const w of batch) pushed.set(w.path, String(w.document.data));
  }
}

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);
    // Subscriptions on this device are never thrown away: they merge into whichever account is
    // signed in. (Signing out clears the device on purpose.)
    await pull(m.id).catch(() => null);
    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;
    failed = false;
    for (const l of readyListeners) l();
    return m;
  })().catch((err) => {
    ready = null;
    failed = true;
    for (const l of readyListeners) l();
    throw err;
  });
  return ready;
}

/** Reads the account's copy again (another device may have changed something), at most every 30 seconds. */
export async function refresh() {
  if (!user || Date.now() - pulledAt < 30_000) return;
  await pull(user.id);
  await push();
}

/** True once, right after a guest's subscriptions moved into their account. */
export function takeUpgraded(): boolean {
  const u = upgraded;
  upgraded = false;
  return u;
}

/** "Keep my subscriptions 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);
}

/** True when the last try to connect to the account failed. */
export function useOffline(): boolean {
  return useSyncExternalStore((cb) => { readyListeners.add(cb); return () => readyListeners.delete(cb); }, () => failed && !readyUser);
}

Change history

Point store links to openapps.free
1ae4feb2be · 18 hours ago
Bind agent address in policy
99bbab3f0c · 19 hours ago
Use OpenApps service URLs and add contribution strip
781c3c23e5 · 19 hours ago
Bounded source sync
048e3257a5 · 1 day ago
Bounded source sync
7a358a3be6 · 1 day ago
Bounded source sync
0c833fcb0e · 1 day ago