Back to Atlas map viewer
SOURCE / PINNED RELEASE

Made of little things.

Atlas map viewer

Release
1d2807e80b44…
Author-recorded commit
43810ed2b368…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-4757dbabc86

Archive hash verified: 8db6876d96c460ac…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

packages/hashtree/src/reader.ts
import { decodeTreeNode, LinkType, toHex } from '@hashtree/core';
import { RemoteSource } from '../../format/src/remote-source';
import { sha256, type Block } from '../../format/src/index';
import type { BlockStore } from '../../format/src/store';
import { httpUrl, mapReference } from '../../format/src/urls';
import {
  FANOUT,
  MAX_BLOB_BYTES,
  parseTree,
  parseFipsProviders,
  type TreeOffer,
  type FipsProvider,
} from './offer';
/** Immutable public Hashtree bytes, independent of delivery transport.
 * Provider failures never become a permanent negative cache. Every blob is verified. */
export class HashtreeReader extends RemoteSource {
  readonly tree: TreeOffer;
  networkBytes = 0;
  requests = 0;
  failovers = 0;
  private cache = new Map<string, Uint8Array>();
  private cacheBytes = 0;
  constructor(
    url: string,
    tree: TreeOffer,
    api: string | undefined,
    private options: {
      store?: BlockStore;
      offline?: () => boolean;
      memoryBytes?: number;
      /** Per-blob transport deadline, including time spent in an adapter's queue. Defaults to 10 seconds. */
      requestTimeoutMs?: number;
      /** Replaces all advertised providers; failures and misses never fall back to HTTP. */
      fetchBlob: (hash: string, signal: AbortSignal) => Promise<Uint8Array | null>;
    },
  ) {
    super(url, api);
    if (
      options.requestTimeoutMs !== undefined &&
      (!Number.isSafeInteger(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)
    )
      throw new Error('Invalid Hashtree request timeout');
    this.tree = parseTree(tree, tree.bytes, !!options.fetchBlob);
    if (url.startsWith('htree:')) mapReference(url, undefined, this.tree.root);
  }
  addProviders(providers: string[]) {
    this.tree.providers = [
      ...new Set([...this.tree.providers, ...providers.map((p) => httpUrl(p).replace(/\/$/, ''))]),
    ].slice(0, 8);
  }
  addFipsProviders(providers: FipsProvider[]) {
    this.tree.fips = parseFipsProviders([...(this.tree.fips ?? []), ...providers].slice(0, 8));
  }
  clear() {
    this.cache.clear();
    this.cacheBytes = 0;
  }
  private descriptor(hash: string): Block {
    return { key: `hashtree/${hash}`, hash, offset: 0, length: 0, features: 0 };
  }
  private remember(hash: string, bytes: Uint8Array) {
    const old = this.cache.get(hash);
    if (old) this.cacheBytes -= old.length;
    this.cache.delete(hash);
    this.cache.set(hash, bytes);
    this.cacheBytes += bytes.length;
    const limit = this.options.memoryBytes ?? 4 * 1024 ** 2;
    while (this.cacheBytes > limit && this.cache.size) {
      const key = this.cache.keys().next().value!;
      this.cacheBytes -= this.cache.get(key)!.length;
      this.cache.delete(key);
    }
  }
  /** Adapters that already count individual provider failures can suppress double counting. */
  protected recordBlobFailure(_error: unknown) {
    this.failovers++;
  }
  private async blob(hash: string, signal?: AbortSignal) {
    signal?.throwIfAborted();
    const cached = this.cache.get(hash);
    if (cached) {
      this.remember(hash, cached);
      return cached;
    }
    const saved = await this.options.store?.get('hashtree-v1', this.descriptor(hash));
    if (saved && saved.length <= MAX_BLOB_BYTES && (await sha256(saved)) === hash) {
      this.remember(hash, saved);
      return saved;
    }
    if (this.options.offline?.()) throw new Error(`Missing cached Hashtree blob ${hash}`);
    this.requests++;
    const timeout = AbortSignal.timeout(this.options.requestTimeoutMs ?? 10000);
    const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
    let bytes: Uint8Array;
    try {
      const received = await this.options.fetchBlob(hash, requestSignal);
      requestSignal.throwIfAborted();
      if (!received) throw new Error('Blob unavailable');
      if (received.length > MAX_BLOB_BYTES) throw new Error('Oversized blob');
      this.networkBytes += received.length;
      if ((await sha256(received)) !== hash) throw new Error('Blob hash mismatch');
      requestSignal.throwIfAborted();
      bytes = received;
    } catch (error) {
      // Caller cancellation is not a failed provider attempt. A transport deadline is.
      signal?.throwIfAborted();
      this.recordBlobFailure(error);
      throw error;
    }
    this.remember(hash, bytes);
    await this.options.store?.put('hashtree-v1', this.descriptor(hash), bytes);
    return bytes;
  }

  async read(start: number, end: number, signal?: AbortSignal) {
    if (
      !Number.isSafeInteger(start) ||
      !Number.isSafeInteger(end) ||
      start < 0 ||
      end < start ||
      end > this.tree.bytes ||
      end - start > 64 * 1024 ** 2
    )
      throw new Error('Invalid Hashtree byte range');
    const output = new Uint8Array(end - start);
    if (!output.length) return output;
    let visits = 0;
    const walk = async (
      hash: string,
      size: number,
      at: number,
      type: number,
      depth: number,
    ): Promise<void> => {
      if (at >= end || at + size <= start) return;
      if (++visits > 10000 || depth > 16) throw new Error('Hashtree traversal budget exceeded');
      const bytes = await this.blob(hash, signal);
      if (type === LinkType.Blob) {
        if (bytes.length !== size) throw new Error('Hashtree leaf length mismatch');
        const from = Math.max(start, at),
          to = Math.min(end, at + size);
        output.set(bytes.subarray(from - at, to - at), from - start);
        return;
      }
      const node = decodeTreeNode(bytes);
      if (node.type !== LinkType.File || !node.links.length || node.links.length > FANOUT)
        throw new Error('Unsupported or oversized Hashtree file node');
      let total = 0;
      for (const link of node.links) {
        if (
          !(link.hash instanceof Uint8Array) ||
          link.hash.length !== 32 ||
          link.key ||
          link.name !== undefined ||
          ![LinkType.Blob, LinkType.File].includes(link.type) ||
          !Number.isSafeInteger(link.size) ||
          link.size <= 0
        )
          throw new Error('Invalid Hashtree file link');
        total += link.size;
        if (!Number.isSafeInteger(total) || total > size)
          throw new Error('Hashtree subtree length mismatch');
      }
      if (total !== size) throw new Error('Hashtree subtree length mismatch');
      let offset = at;
      for (const link of node.links) {
        await walk(toHex(link.hash), link.size, offset, link.type, depth + 1);
        offset += link.size;
      }
    };
    await walk(
      this.tree.root,
      this.tree.bytes,
      0,
      this.tree.bytes <= this.tree.chunkBytes ? LinkType.Blob : LinkType.File,
      0,
    );
    return output;
  }
}