Opaque Cursor Encoding And Decoding

A cursor codec that encodes and decodes opaque cursors so API clients never depend on internal offsets or key names.

TypescriptData Fetching

Code

export type CursorDirection = "forward" | "backward";

export interface CursorFields {
  readonly id: string;
  readonly direction: CursorDirection;
}

export type CursorResult<T> =
  | { readonly ok: true; readonly value: T }
  | { readonly ok: false; readonly error: string };

function toBase64Url(text: string): string {
  return Buffer.from(text, "utf8").toString("base64url");
}

function fromBase64Url(encoded: string): string {
  return Buffer.from(encoded, "base64url").toString("utf8");
}

export function encodeCursor(fields: CursorFields): string {
  return toBase64Url(JSON.stringify(fields));
}

export function decodeCursor(cursor: string): CursorResult<CursorFields> {
  if (typeof cursor !== "string" || cursor.length === 0) {
    return { ok: false, error: "cursor must be a non-empty string" };
  }
  let parsed: unknown;
  try {
    parsed = JSON.parse(fromBase64Url(cursor));
  } catch {
    return { ok: false, error: "cursor is not valid base64url JSON" };
  }
  if (!parsed || typeof parsed !== "object") {
    return { ok: false, error: "cursor payload must be an object" };
  }
  const record = parsed as Record<string, unknown>;
  if (typeof record.id !== "string" || record.id.length === 0) {
    return { ok: false, error: "cursor payload missing a string id" };
  }
  const direction = record.direction === undefined ? "forward" : record.direction;
  if (direction !== "forward" && direction !== "backward") {
    return { ok: false, error: `cursor direction must be forward or backward, got ${String(direction)}` };
  }
  return { ok: true, value: { id: record.id, direction } };
}