47 lines · 1.7 KB
import { env } from "cloudflare:workers";
import { forgetScope } from "./scoped-db/locators";

const ID_PATTERN = /^[0-9a-z]{20}$/;

/**
 * Credentials minted after the AccountState move carry their account id: `<prefix>_<accountId>_<secret>`.
 * Returns the embedded id, or null for older tokens (resolve those through a locator).
 */
export function embeddedAccountId(token: string, prefix: string): string | null {
  if (!token.startsWith(`${prefix}_`)) return null;
  const parts = token.slice(prefix.length + 1).split("_");
  if (parts.length !== 2) return null;
  return ID_PATTERN.test(parts[0]) ? parts[0] : null;
}

export function accountToken(prefix: string, accountId: string, secretHex: string): string {
  return `${prefix}_${accountId}_${secretHex}`;
}

/** Wipes the account's AccountState and every locator pointing into it. */
export async function dropAccountState(accountId: string): Promise<void> {
  try {
    const stub = env.ACCOUNT_STATE.get(env.ACCOUNT_STATE.idFromName(accountId)) as unknown as {
      destroy(): Promise<void>;
    };
    await stub.destroy();
  } catch (error) {
    console.error("AccountState destroy failed", accountId, error);
  }
  await forgetScope(accountId);
}

/** Runs `task` for every id with bounded concurrency. */
export async function forEachScope<T>(ids: string[], task: (id: string) => Promise<T>, limit = 8): Promise<T[]> {
  const results: T[] = new Array(ids.length);
  let next = 0;
  const workers = Array.from({ length: Math.min(limit, ids.length) }, async () => {
    while (next < ids.length) {
      const index = next++;