~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
105 lines · 3.3 KB Raw History
// @pdsjs/spaces/sequence - space revisions, which only the authority assigns.
//
// Each repo carries its own rev. The authority also puts every accepted update
// on one line for the whole space: a spaceRev that strictly increases across
// writers, with prevSpaceRev naming the update before it. listRepos pages by
// spaceRev, and a syncer that sees a gap in prevSpaceRev catches up from there.

import { createTid, tidToMs } from '@pdsjs/core/repo';

const TID_CHARS = '234567abcdefghijklmnopqrstuvwxyz';

/** How far ahead of the clock a reported repo rev may be. */
export const FUTURE_REV_LIMIT_MS = 5 * 60 * 1000;

// One allocation per space at a time: two updates must not both read the same
// head and claim the same predecessor. Sound for the reason writer.js gives: a
// space's authority is exactly one process.
/** @type {WeakMap<object, Map<string, Promise<unknown>>>} */
const queues = new WeakMap();

/**
 * @param {unknown} value
 * @returns {value is string}
 */
export function isTid(value) {
  return (
    typeof value === 'string' &&
    value.length === 13 &&
    [...value].every((c) => TID_CHARS.includes(c)) &&
    TID_CHARS.indexOf(value[0]) < 16
  );
}

/**
 * Whether a repo rev claims a time too far ahead of this server's clock.
 * @param {string} repoRev
 * @param {number} [now]
 * @returns {boolean}
 */
export function isFutureRev(repoRev, now = Date.now()) {
  const ms = tidToMs(repoRev);
  return ms !== null && ms > now + FUTURE_REV_LIMIT_MS;
}

/**
 * Record a writer's update and give it the next space revision.
 *
 * An update at or behind the writer's last accepted repo rev is not
 * resequenced: the answer is null, and the caller forwards nothing.
 *
 * @param {import('@pdsjs/core/ports').SpaceStoragePort} spaceStorage
 * @param {{space: string, did: string, repoRev: string, data: Uint8Array}} update
 * @returns {Promise<{spaceRev: string, prevSpaceRev: string|null}|null>}
 */
export function recordWriterUpdate(
  spaceStorage,
  { space, did, repoRev, data },
) {
  let inStorage = queues.get(spaceStorage);
  if (!inStorage) {
    inStorage = new Map();
    queues.set(spaceStorage, inStorage);
  }
  const run = async () => {
    const current = await spaceStorage.getSpaceWriter(space, did);
    if (current && current.repoRev >= repoRev) return null;
    const prevSpaceRev = await spaceStorage.getSpaceRevHead(space);
    const spaceRev = nextTid(prevSpaceRev);
    await spaceStorage.putSpaceWriter(space, did, repoRev, data, spaceRev);
    return { spaceRev, prevSpaceRev };
  };
  const prior = inStorage.get(space) ?? Promise.resolve();
  const next = prior.then(run, run);
  inStorage.set(
    space,
    next.then(
      () => undefined,
      () => undefined,
    ),
  );
  return next;
}

/**
 * A TID later than `after`. A writer recorded before space revisions existed
 * is sequenced at its repo rev, which another clock produced, so the clock
 * here alone does not guarantee that.
 *
 * @param {string|null} after
 * @returns {string}
 */
export function nextTid(after) {
  const tid = createTid();
  if (after === null || tid > after) return tid;
  let micros = 0;
  for (const c of after.slice(0, 11))
    micros = micros * 32 + TID_CHARS.indexOf(c);
  micros += 1;
  let out = '';
  for (let i = 0; i < 11; i++) {
    out = TID_CHARS[micros % 32] + out;
    micros = Math.floor(micros / 32);
  }
  return `${out}22`;
}