OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / core / core / transient-core-errors.js

74 lines · javascript ref:
/**
 * Detect transient Hypercore / Corestore errors that warrant a retryable
 * 503 response instead of an opaque 400.
 *
 * Background: when a relay's corestore is closed or one of its cores is
 * mid-close (e.g. self-heal cycle, store recreation after error, session
 * leak), HTTP routes that call into seedApp / publishCustodyIntent surface
 * the underlying error verbatim. Consumers can't distinguish "your request
 * was malformed" from "the relay's storage subsystem is in a closing
 * transition state" — both bubble up as opaque 400s.
 *
 * Detection here is by message-string match because corestore + hypercore
 * don't throw typed errors. The strings cover:
 *
 *   - "The corestore is closed" — corestore.close() has been called;
 *     all subsequent .get() / .append() throw this.
 *   - "SESSION_CLOSED: Cannot make sessions on a closing core" — a
 *     specific core is mid-close; session-creation attempts are rejected
 *     while it transitions.
 *   - "Cannot make sessions on a closing core" — same condition,
 *     without the SESSION_CLOSED prefix (older hypercore versions).
 *   - "CORE_CLOSED" / "SESSION_CLOSED" prefixes — typed-ish error codes
 *     from newer hypercore releases.
 *   - "Mutex has been destroyed" — thrown when an operation runs against
 *     a hypercore whose internal mutex has been torn down. Same class of
 *     recoverable failure as a closed core, just at a different layer of
 *     the shutdown chain. Observed live against a production relay's
 *     publish-channel intent submit in 2026-05.
 *
 * If you add new detection patterns, please also add a unit case in
 * test/unit/transient-core-errors.test.js.
 */

const TRANSIENT_MARKERS = [
  'The corestore is closed',
  'Cannot make sessions on a closing core',
  'SESSION_CLOSED',
  'CORE_CLOSED',
  'Mutex has been destroyed'
]

/**
 * Returns true if the error appears to be a transient corestore/hypercore
 * lifecycle issue that the client should retry against the same relay.
 *
 * @param {Error|string|null|undefined} err
 * @returns {boolean}
 */
export function isTransientCoreError (err) {
  if (!err) return false
  const msg = typeof err === 'string'
    ? err
    : (err && typeof err.message === 'string' ? err.message : '')
  if (!msg) return false
  for (const marker of TRANSIENT_MARKERS) {
    if (msg.includes(marker)) return true
  }
  // Some hypercore error-code constants come through as err.code instead
  // of err.message — accept both surfaces.
  if (err && typeof err.code === 'string') {
    for (const marker of TRANSIENT_MARKERS) {
      if (err.code === marker || err.code.startsWith(marker)) return true
    }
  }
  return false
}

/**
 * Suggested Retry-After header value (seconds) for a transient core
 * error. 5 seconds gives self-heal restarts (or in-flight session close
 * transitions) time to complete before the client tries again.
 */
export const TRANSIENT_RETRY_AFTER_SECONDS = 5