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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
97 lines · 2.9 KB Raw History
// Every call is same-origin against the PDS serving this page.

/** Thrown with the message the server chose, so screens can show it as-is. */
export class ApiError extends Error {
  /** @param {string} message @param {number} status */
  constructor(message, status) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
  }
}

/**
 * @param {string} path
 * @param {RequestInit} [init]
 * @returns {Promise<any>}
 */
async function request(path, init) {
  const response = await fetch(`/account/api${path}`, {
    headers: { 'Content-Type': 'application/json' },
    ...init,
  });
  const body = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw new ApiError(
      body.error || `Request failed (${response.status})`,
      response.status,
    );
  }
  return body;
}

/** @param {string} path */
export const get = (path) => request(path);

/**
 * Call one of this server's own XRPC methods, on the account session. A
 * refusal names its reason in `message`.
 * @param {string} method - the lexicon method NSID
 * @param {RequestInit} [init]
 * @param {Record<string, string|undefined|null>} [params] - query parameters; an empty one is left out
 * @returns {Promise<any>}
 */
async function xrpcRequest(method, init, params) {
  const query = new URLSearchParams();
  for (const [key, value] of Object.entries(params ?? {})) {
    if (value) query.set(key, value);
  }
  const suffix = query.size ? `?${query}` : '';
  const response = await fetch(`/xrpc/${method}${suffix}`, {
    headers: { 'Content-Type': 'application/json' },
    ...init,
  });
  const body = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw new ApiError(
      body.message || body.error || `Request failed (${response.status})`,
      response.status,
    );
  }
  return body;
}

/** @param {string} method @param {Record<string, string|undefined|null>} [params] */
export const xrpcGet = (method, params) =>
  xrpcRequest(method, undefined, params);

/** @param {string} method @param {unknown} [body] */
export const xrpcPost = (method, body) =>
  xrpcRequest(method, { method: 'POST', body: JSON.stringify(body ?? {}) });

/** @param {string} path @param {unknown} [body] */
export const post = (path, body) =>
  request(path, { method: 'POST', body: JSON.stringify(body ?? {}) });

/**
 * Send one file as the request body. The server streams it into blob storage,
 * so the file is never re-encoded as JSON or a multipart form.
 * @param {string} path
 * @param {File} file
 * @returns {Promise<any>}
 */
export async function upload(path, file) {
  const response = await fetch(`/account/api${path}`, {
    method: 'POST',
    headers: { 'Content-Type': file.type || 'application/octet-stream' },
    body: file,
  });
  const body = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw new ApiError(
      body.error || `Upload failed (${response.status})`,
      response.status,
    );
  }
  return body;
}