lists-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 ebccaf0b13fe
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 ee59501e91bae6d2c23a8370
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
// Changes to lists are saved on the device first (the outbox) and sent in order when there is a
// connection, so checking off and adding work in a store with no signal. A change the rules refuse
// (the item was cleared, or you were removed) is dropped and said out loud; anything else waits.
import { useSyncExternalStore } from "react";
import { setMany, get } from "@bounded-sh/client";
import { meSoon } from "./bounded";
import { read, write, useStored } from "./local";

export type Doc = Record<string, unknown>;
export interface Write { path: string; document: Doc | null }
export interface Op { id: string; listId: string; writes: Write[]; create?: boolean }

const OUTBOX = "lists:outbox";
const outbox = () => read<Op[]>(OUTBOX, []);
const opId = () => Math.random().toString(36).slice(2, 10);

export const useOutbox = () => useStored<Op[]>(OUTBOX, []);

/** Saves a change on the device and starts sending it. Repeated changes to one document fold into one. */
export function enqueue(listId: string, writes: Write[], create = false) {
  const ops = outbox();
  const last = ops[ops.length - 1];
  if (!create && writes.length === 1 && last && !last.create && last.listId === listId && last.writes.length === 1 && last.writes[0].path === writes[0].path && last.writes[0].document && writes[0].document) {
    write(OUTBOX, [...ops.slice(0, -1), { ...last, writes: [{ path: last.writes[0].path, document: { ...last.writes[0].document, ...writes[0].document } }] }]);
  } else {
    write(OUTBOX, [...ops, { id: opId(), listId, writes, create }]);
  }
  void flush();
}

/** Forgets every unsent change for a list (when it is gone, or removed from this device). */
export function dropList(listId: string) {
  write(OUTBOX, outbox().filter((o) => o.listId !== listId));
}

// ---------- Sending ----------

/** sending: changes are on their way. stuck: the last try could not reach the server. notice: a change that was refused. */
type Status = { sending: boolean; stuck: boolean; notice: string | null };
let status: Status = { sending: false, stuck: false, notice: null };
const statusListeners = new Set<() => void>();
function setStatus(patch: Partial<Status>) {
  status = { ...status, ...patch };
  statusListeners.forEach((l) => l());
}
export const useSyncStatus = () => useSyncExternalStore((l) => { statusListeners.add(l); return () => statusListeners.delete(l); }, () => status);
export const clearNotice = () => setStatus({ notice: null });

/** The server answered and refused the write; sending it again would not help. A rate limit is not a refusal. */
function refused(err: unknown): boolean {
  const e = err as { writeOutcome?: string; statusCode?: number; status?: number };
  return e?.writeOutcome === "not_applied" && (e.statusCode ?? e.status) !== 429;
}

const withTimeout = <T,>(p: Promise<T>, ms: number) =>
  Promise.race([p, new Promise<never>((_, no) => setTimeout(() => no(new Error("timeout")), ms))]);

let running: Promise<void> | null = null;
let retryTimer: number | undefined;
let attempts = 0;
const sentListeners = new Set<(op: Op) => void>();
/** Called with each change the server accepted, so the saved copy shows it before the live update arrives. */
export const onSent = (fn: (op: Op) => void) => { sentListeners.add(fn); };

/** Sends waiting changes in order. Stops at the first one that cannot reach the server and tries again later. */
export function flush(): Promise<void> {
  running ??= (async () => {
    const run = async () => {
      if (!outbox().length) return;
      const uid = await meSoon();
      if (!uid) throw new Error("offline");
      setStatus({ sending: true });
      for (let op = outbox()[0]; op; op = outbox()[0]) {
        try {
          // A list made offline whose first send did arrive (the answer was lost) is already there.
          const done = op.create && (await withTimeout(get(`lists/${op.listId}`), 15000).catch(() => null));
          if (!done) await withTimeout(setMany(op.writes), 20000);
          sentListeners.forEach((fn) => fn(op));
        } catch (err) {
          if (!refused(err)) throw err;
          // The refusal itself is generic: a change is refused when its item was removed, or its sender was.
          setStatus({ notice: "A change could not be saved: someone removed that item, or you are no longer on this list." });
        }
        write(OUTBOX, outbox().filter((o) => o.id !== op.id));
      }
    };
    let clean = false;
    try {
      const locks = (navigator as Navigator & { locks?: { request: (n: string, f: () => Promise<void>) => Promise<void> } }).locks;
      await (locks ? locks.request("lists-outbox", run) : run());
      attempts = 0;
      clean = true;
      setStatus({ sending: false, stuck: false });
    } catch {
      setStatus({ sending: false, stuck: true });
      // Bounded: back off up to a minute, and only while the page is open and the device says it is online.
      // Coming back online or to the page tries again at once.
      attempts++;
      window.clearTimeout(retryTimer);
      if (attempts <= 8 && navigator.onLine && document.visibilityState === "visible") {
        retryTimer = window.setTimeout(() => void flush(), Math.min(60000, 2000 * 2 ** attempts));
      }
    } finally {
      running = null;
    }
    // A change saved while this run was finishing has not been sent yet.
    if (clean && outbox().length) void flush();
  })();
  return running;
}

if (typeof window !== "undefined") {
  const again = () => { attempts = 0; void flush(); };
  window.addEventListener("online", again);
  document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") again(); });
}

Change history

Point OpenApps links to openapps.free
ebccaf0b13 · 17 hours ago
Bind agent address in policy constants
519855687c · 19 hours ago
Use OpenApps service URLs and add contribution strip
2c353edfdc · 19 hours ago
Bounded source sync
58ac7e08ec · 1 day ago
Bounded source sync
150962f3d9 · 1 day ago
Bounded source sync
e52e4676b9 · 1 day ago