OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / spacedust.test.mjs

161 lines · javascript ref:
// spacedust client — URL building, event parsing, filter framing, backpressure.
// Run: node test/spacedust.test.mjs
//
// The socket is injected, so the queue/drop/reconnect story is tested without a network. That matters
// because these are precisely the behaviours you cannot observe in production: you cannot prove after
// the fact that a bounded queue stopped us being lag-dropped.
import {
  buildSubscribeUrl, parseLinkEvent, parseAtUri, optionsUpdateFrame, backoffDelay,
  SpacedustClient, SOURCES, CONTENT_BEARING
} from '../lib/spacedust.js'

let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m); pass++ }

// ── URL building ─────────────────────────────────────────────────────────────────────────────────
{
  const u = new URL(buildSubscribeUrl({ subjectDids: ['did:plc:a', 'did:plc:b'], sources: [SOURCES.LIKE] }))
  ok(u.searchParams.getAll('wantedSubjectDids').length === 2, 'params REPEAT rather than comma-separate — spacedust parses repeats')
  ok(u.searchParams.get('wantedSources') === SOURCES.LIKE, 'sources ride the same encoding')
  ok(u.searchParams.get('instant') === null, 'instant is ABSENT by default — the 21s buffer is what stops us retaining undone interactions')
}
{
  const u = new URL(buildSubscribeUrl({ subjects: ['at://did:plc:a/app.bsky.feed.post/1'] }, { instant: true }))
  ok(u.searchParams.get('instant') === 'true', 'instant is opt-in and explicit')
  ok(decodeURIComponent(u.searchParams.get('wantedSubjects')).startsWith('at://'), 'at-uris are url-encoded in the query')
}
{
  const u = new URL(buildSubscribeUrl({}))
  ok([...u.searchParams.keys()].length === 0, 'an empty filter sends no axes — server-side that means no filtering, which the caller must mean deliberately')
}

// ── event parsing ────────────────────────────────────────────────────────────────────────────────
const LIKE = {
  kind: 'link',
  origin: 'live',
  link: {
    operation: 'create',
    source: SOURCES.LIKE,
    source_record: 'at://did:plc:liker/app.bsky.feed.like/3kabc',
    source_rev: '3lzzz',
    subject: 'at://did:plc:wj62/app.bsky.feed.post/3kxyz'
  }
}
{
  const p = parseLinkEvent(JSON.stringify(LIKE))
  ok(p !== null, 'a well-formed link event parses')
  ok(p.did === 'did:plc:liker' && p.collection === 'app.bsky.feed.like' && p.rkey === '3kabc',
    'the LINKING record is decomposed into a fetchable locator — that is the whole payload, spacedust ships no record')
  ok(p.subject === LIKE.link.subject, 'the subject (our content) is preserved')
  ok(p.rev === '3lzzz', 'source_rev is kept for the backfill skip-test')
  ok(p.contentBearing === false, 'a LIKE is count-bearing — Tier C, never retained (§7.6 pressure valve)')
}
{
  const reply = JSON.parse(JSON.stringify(LIKE))
  reply.link.source = SOURCES.REPLY_PARENT
  reply.link.source_record = 'at://did:plc:replier/app.bsky.feed.post/3kdef'
  ok(parseLinkEvent(JSON.stringify(reply)).contentBearing === true, 'a REPLY is content-bearing — Tier B, worth fetch+verify+retain')
}
{
  ok(parseLinkEvent('not json') === null, 'garbage parses to null rather than throwing — one bad frame must not kill the subscription')
  ok(parseLinkEvent(JSON.stringify({ kind: 'other', link: {} })) === null, 'a non-link event is ignored')
  const noRec = JSON.parse(JSON.stringify(LIKE)); noRec.link.source_record = 'https://example.com/x'
  ok(parseLinkEvent(JSON.stringify(noRec)) === null, 'a non-at-uri source_record is unusable — we could never fetch it, so it is dropped')
  const badOp = JSON.parse(JSON.stringify(LIKE)); badOp.link.operation = 'weird'
  ok(parseLinkEvent(JSON.stringify(badOp)) === null, 'an unknown operation is refused rather than guessed')
}
{
  ok(parseLinkEvent(Buffer.from(JSON.stringify(LIKE))) !== null, 'Buffers parse too — bare-ws pushes Buffers, not strings')
}

// ── at-uri decomposition ─────────────────────────────────────────────────────────────────────────
ok(parseAtUri('at://did:plc:a/app.bsky.feed.post/3k').rkey === '3k', 'a 3-segment at-uri decomposes')
ok(parseAtUri('at://did:plc:a/app.bsky.feed.post') === null, 'a 2-segment at-uri is a collection, not a record')
ok(parseAtUri('did:plc:a') === null, 'a bare DID is a legitimate subject but is not a record')
ok(parseAtUri('at://did:plc:a//3k') === null, 'empty segments are refused')

// ── options_update framing ───────────────────────────────────────────────────────────────────────
{
  const f = JSON.parse(optionsUpdateFrame({ subjectDids: ['did:plc:a'], sources: [SOURCES.LIKE] }))
  ok(f.type === 'options_update', 'the envelope tag is snake_case (serde tag="type")')
  ok(Array.isArray(f.payload.wantedSubjectDids), 'but the payload is camelCase — the shapes differ, and getting it wrong is a silent no-op')
  ok(f.payload.wantedSubjects.length === 0 && f.payload.wantedSubjectPrefixes.length === 0,
    'every axis is sent even when empty — the server REPLACES the whole query, so a partial frame would silently clear axes')
}

// ── backoff ──────────────────────────────────────────────────────────────────────────────────────
{
  const b = { min: 1000, max: 60_000 }
  ok(backoffDelay(1, b, () => 1) === 1000, 'first retry is bounded by min')
  ok(backoffDelay(5, b, () => 1) === 16000, 'it grows exponentially')
  ok(backoffDelay(99, b, () => 1) === 60_000, 'and is clamped to max')
  ok(backoffDelay(5, b, () => 0) === 0, 'full jitter can reach zero — jitter is what stops a thundering herd when the service restarts')
}

// ── client: queue + backpressure ─────────────────────────────────────────────────────────────────
function fakeSocket () {
  const handlers = {}
  return {
    written: [],
    on (ev, cb) { (handlers[ev] = handlers[ev] || []).push(cb); return this },
    emit (ev, arg) { for (const cb of handlers[ev] || []) cb(arg) },
    write (d) { this.written.push(d) },
    end () {}
  }
}

{
  let sock
  const c = new SpacedustClient({ subjectDids: ['did:plc:wj62'] }, {
    createSocket: () => (sock = fakeSocket()),
    maxQueue: 3
  })
  c.start()
  ok(c.stats.connects === 1, 'start connects once')

  for (let i = 0; i < 5; i++) {
    const e = JSON.parse(JSON.stringify(LIKE))
    e.link.source_record = `at://did:plc:liker/app.bsky.feed.like/rk${i}`
    sock.emit('data', Buffer.from(JSON.stringify(e)))
  }
  ok(c.pending === 3, 'the queue is BOUNDED — it never grows without limit')
  ok(c.stats.dropped === 2, 'and overflow is COUNTED, not silent (§7.6 budgets are meaningless if drops are invisible)')
  const drained = c.drain()
  ok(drained.length === 3 && drained[0].rkey === 'rk2',
    'the OLDEST is dropped: losing a recoverable pointer beats being lag-disconnected and losing the stream')
  ok(c.pending === 0, 'drain empties the queue')

  sock.emit('data', Buffer.from('garbage'))
  ok(c.stats.unparseable === 1 && c.pending === 0, 'unparseable frames are counted and dropped, never enqueued')

  ok(c.updateFilter({ subjectDids: ['did:plc:wj62', 'did:plc:new'] }) === true, 'the filter updates over the SAME socket')
  ok(JSON.parse(sock.written[0]).payload.wantedSubjectDids.length === 2, 'and carries the complete new filter — no reconnect, so a mutating roster stays live')

  c.close()
  ok(c.drain().length === 0, 'close is clean')
}

{
  // A drain smaller than the queue must not lose the remainder.
  let sock
  const c = new SpacedustClient({ subjectDids: ['did:plc:sub'] }, { createSocket: () => (sock = fakeSocket()) })
  c.start()
  for (let i = 0; i < 4; i++) {
    const e = JSON.parse(JSON.stringify(LIKE))
    e.link.source_record = `at://did:plc:x/app.bsky.feed.like/rk${i}`
    sock.emit('data', Buffer.from(JSON.stringify(e)))
  }
  ok(c.drain(2).length === 2 && c.pending === 2, 'a partial drain leaves the rest queued')
  c.close()
}

// ── tier table sanity ────────────────────────────────────────────────────────────────────────────
ok(CONTENT_BEARING.has(SOURCES.REPLY_PARENT) && CONTENT_BEARING.has(SOURCES.QUOTE),
  'replies and quotes carry text — Tier B')
ok(CONTENT_BEARING.has(SOURCES.QUOTE_MEDIA) && SOURCES.QUOTE_MEDIA !== SOURCES.QUOTE,
  'recordWithMedia quotes use a DIFFERENT path (embed.record.record.uri) and must be subscribed separately — measured 2026-07-31: 376 plain + 50 media = 426, the exact quoteCount, so omitting it silently lost ~12% of quotes')
ok(!CONTENT_BEARING.has(SOURCES.LIKE) && !CONTENT_BEARING.has(SOURCES.REPOST) && !CONTENT_BEARING.has(SOURCES.FOLLOW),
  'likes/reposts/follows carry only a tally — Tier C, answered by Constellation counts on demand')

console.log(`\nSPACEDUST OK: ${pass} assertions`)