OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-rooms / lib / compose-schemas.js

49 lines · javascript ref:
/**
 * composeSchemas — combine multiple Room schemas into one.
 *
 * Lets a single Room (one Autobase + one Hyperbee view) carry several
 * op-domains. Used for "linked-core" composites — e.g. an org Room that
 * also handles issue ops, so issues replicate WITH the org autobase
 * instead of living in a separate autobase whose bootstrap key must be
 * conveyed to every joiner.
 *
 * All composed schemas must share the same op wire-encoding. jsonSchema
 * uses plain JSON, which is op-agnostic, so we reuse the first schema's
 * encodeOp/decodeOp. validateOp and apply run EVERY schema in order;
 * each domain schema's switch ignores op.types it doesn't own (no
 * default-throw), so they coexist.
 *
 * Constraint: op.types should be DISJOINT across composed schemas. A
 * shared op.type runs ALL handlers — safe only when those writes are
 * idempotent/compatible (e.g. two schemas both writing the same
 * meta:<key> value). Diverging semantics on a shared type is a bug.
 *
 * @param {Array<{encodeOp,decodeOp,validateOp,apply}>} schemas
 * @returns {{encodeOp,decodeOp,validateOp,apply}}
 */
export function composeSchemas (schemas) {
  if (!Array.isArray(schemas) || schemas.length === 0) {
    throw new Error('composeSchemas requires a non-empty array of schemas')
  }
  for (const s of schemas) {
    for (const fn of ['encodeOp', 'decodeOp', 'validateOp', 'apply']) {
      if (typeof s[fn] !== 'function') {
        throw new Error('composeSchemas: each schema must provide ' + fn)
      }
    }
  }

  const [first] = schemas

  return {
    encodeOp: (op) => first.encodeOp(op),
    decodeOp: (buf) => first.decodeOp(buf),
    validateOp (op) {
      for (const s of schemas) s.validateOp(op)
    },
    async apply (batch, op, host) {
      for (const s of schemas) await s.apply(batch, op, host)
    }
  }
}