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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
186 lines · 5.3 KB Raw History
// In-memory ActorStorage and SharedStorage ports, enough to run a
// PersonalDataServer without a database. Shared by the tests that exercise
// repo mechanics: what a commit carries, and rebuilding the tree.

export function createMemoryStorage() {
  /** @type {Map<string, Uint8Array>} */
  const blocks = new Map();
  /** @type {Map<string, {cid: string, value: Uint8Array}>} */
  const records = new Map();
  /** @type {Array<{seq: number, cid: string, rev: string, prev: string|null}>} */
  const commits = [];
  /** @type {Array<{seq: number, commit_cid: string, evt: Uint8Array}>} */
  const events = [];
  /** @type {Record<string, *>} */
  const meta = {};

  return {
    async getRecord(/** @type {string} */ uri) {
      return records.get(uri) || null;
    },
    async putRecord(
      /** @type {string} */ uri,
      /** @type {string} */ cid,
      /** @type {string} */ _collection,
      /** @type {string} */ _rkey,
      /** @type {any} */ value,
    ) {
      records.set(uri, { cid, value });
    },
    async listRecords() {
      return { records: [], cursor: null };
    },
    async listAllRecords() {
      return [...records.entries()]
        .map(([uri, rec]) => {
          const parts = uri.replace('at://', '').split('/');
          return { key: `${parts[1]}/${parts[2]}`, cid: rec.cid };
        })
        .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
    },
    async deleteRecord(/** @type {string} */ uri) {
      records.delete(uri);
    },
    async getBlock(/** @type {string} */ cid) {
      return blocks.get(cid) || null;
    },
    async putBlock(/** @type {string} */ cid, /** @type {Uint8Array} */ data) {
      if (!blocks.has(cid)) blocks.set(cid, data);
    },
    async deleteBlocks(/** @type {string[]} */ cids) {
      for (const cid of cids) blocks.delete(cid);
    },
    async listBlocks(
      /** @type {string|null} */ cursor,
      /** @type {number} */ limit,
    ) {
      const page = [...blocks.keys()]
        .sort()
        .filter((cid) => !cursor || cid > cursor)
        .slice(0, limit)
        .map((cid) => ({
          cid,
          data: /** @type {Uint8Array} */ (blocks.get(cid)),
        }));
      const last = page.length === limit ? page[limit - 1].cid : null;
      return { blocks: page, cursor: last };
    },
    async recordCidsAmong(/** @type {string[]} */ cids) {
      const held = new Set([...records.values()].map((r) => r.cid));
      return cids.filter((cid) => held.has(cid));
    },
    async getBlocksCollected() {
      return meta.blocksCollected === true;
    },
    async setBlocksCollected(/** @type {boolean} */ collected) {
      meta.blocksCollected = collected;
    },
    async getLatestCommit() {
      return commits.length ? commits[commits.length - 1] : null;
    },
    async putCommit(
      /** @type {number} */ seq,
      /** @type {string} */ cid,
      /** @type {string} */ rev,
      /** @type {string|null} */ prev,
    ) {
      commits.push({ seq, cid, rev, prev });
    },
    async getEvents() {
      return { events, cursor: null };
    },
    async putEvent(
      /** @type {number} */ seq,
      /** @type {string} */ _did,
      /** @type {string} */ commitCid,
      /** @type {Uint8Array} */ evt,
    ) {
      events.push({ seq, commit_cid: commitCid, evt });
    },
    async getLatestSeq() {
      return events.length ? events[events.length - 1].seq : 0;
    },
    async getBlob() {
      return null;
    },
    async putBlob() {},
    async deleteBlob() {},
    async listBlobs() {
      return { cids: [], cursor: null };
    },
    async getOrphanedBlobs() {
      return [];
    },
    async linkBlobToRecord() {},
    async unlinkBlobsFromRecord() {},
    async getDid() {
      return meta.did ?? null;
    },
    async setDid(/** @type {string} */ did) {
      meta.did = did;
    },
    async getPrivateKey() {
      return meta.privateKey ?? null;
    },
    async setPrivateKey(/** @type {any} */ key) {
      meta.privateKey = key;
    },
    async getKemKey() {
      return meta.kemKey ?? null;
    },
    async setKemKey(/** @type {any} */ key) {
      meta.kemKey = key;
    },
    async getHandle() {
      return meta.handle ?? null;
    },
    async setHandle(/** @type {string} */ handle) {
      meta.handle = handle;
    },
    async getAccountStatus() {
      return meta.accountStatus ?? null;
    },
    async setAccountStatus(/** @type {any} */ status) {
      meta.accountStatus = status;
    },
    async getPreferences() {
      return [];
    },
    async setPreferences() {},
    _events: events,
    _blocks: blocks,
  };
}

export function createMemorySharedStorage() {
  /** @type {Map<string, *>} */
  const actors = new Map();
  return {
    async putActor(
      /** @type {string} */ did,
      /** @type {string} */ handle,
      /** @type {any} */ createdAt,
    ) {
      actors.set(did, { did, handle, createdAt });
    },
    async getActor(/** @type {string} */ did) {
      return actors.get(did) || null;
    },
    async resolveHandle() {
      return null;
    },
    async checkAndStoreDpopJti() {
      return true;
    },
    async putOAuthRequest() {},
    async getOAuthRequest() {
      return null;
    },
    async deleteOAuthRequest() {},
    async putOAuthToken() {},
    async getOAuthToken() {
      return null;
    },
    async deleteOAuthToken() {},
  };
}