Skip to content

v0.6.0 — subscriptions survive hibernation wake

Choose a tag to compare

@grrowl grrowl released this 27 Jul 12:16
· 41 commits to main since this release

Added

  • Typed oversize-frame handling (ADR-0018; part of
    #28).

    An oversize client mutation used to be dropped silently server-side
    (ADR-0012's maxFrameBytes guard), surfacing only as a confirmation
    timeout — and in production Cloudflare's ~1 MiB edge cap on inbound
    WebSocket messages means the frame may never reach the DO at all. The
    transport now guards before sending: a mut/call whose encoded size
    exceeds the 1 MiB edge cap rejects immediately with
    MutationRejectedError (code "FRAME_TOO_LARGE"), so the optimistic
    overlay rolls back promptly. The server's silent drop stays as defense in
    depth. Outbound gains a warn-only fixed 1 MiB threshold: a larger encoded
    frame logs a console.warn with size and collection but is still sent
    whole — column projection (#28a) remains the real fix for oversized
    full-row re-sends. The limits are infrastructure facts (Cloudflare's edge
    cap), not application preferences, so they are constants, not options.

  • Transport reconnect policy (ADR-0016; fixes #25, #26). One option,
    reconnectDelay?: number | ((attempt, closeCode?, closeReason?) => number | null)
    (null = stop), replaces reconnectDelayMs (breaking, pre-1.0: a
    number keeps the old meaning — the default policy's base delay). The
    default (defaultReconnectDelay) replaces the fixed interval with capped
    exponential backoff + full jitter (base 250 ms; cap 30 s; attempt counter
    resets on a successful open) and treats application close codes 4000-4999
    as terminal, so an accept-then-close auth rejection (e.g. 4403) no longer
    retries forever. A terminal stop surfaces through the new
    onClosed(code, reason) hook, making auth closes distinguishable from
    transient drops.

  • Cohosting docs moved to recipes/cohosting.md and re-grounded. The
    README's cohosting section shrinks to the pitch, the code sample, and a
    pointer; the recipe carries the rules, an honest account of what is verified
    and how (CI fake-host tests vs. source audit at pinned versions vs. the
    untested wake restore), and a reframed @cloudflare/actors story. Verified
    against @cloudflare/actors@0.0.1-beta.6: the Actor class hibernates fine
    on its own but won't cohost out of the box (its Sockets helper takes over
    socket connections completely and Actor claims the DO-wide auto-response
    slot), while the Actors helpers (Alarms, Storage) compose cleanly with
    sync over a plain DurableObject base — which Actors' own examples document.
    Supersedes the 0.5.0 "host defect" phrasing.

Fixed

  • Subscriptions survive hibernation wake (ADR-0019; field report against
    0.5.1).
    Hibernatable sockets survive a DO eviction by design, but the
    subscription registry was instance memory: a wake restored the socket set
    and nothing else, so an idle client's live queries went silently dead on a
    still-open socket — its own mutations still confirmed while deltas fanned
    out to nobody, and the client's only re-subscribe trigger (the close path)
    never fired. Subscriptions are now written through to a durable
    _sync_subs table keyed by a per-socket id tag stamped at accept, and
    restored onto surviving sockets during registerSync on every wake —
    before anything can dispatch or drain. No wire or client change: an
    unmodified 0.5.1 client against a fixed server recovers fully. Orphaned
    rows (a socket that dies without webSocketClose) are swept during the
    existing compaction housekeeping; no idle timers (ADR-0006 invariant
    intact). Pinned by tests/hibernation.test.ts under real evictions
    (evictDurableObject, unlocked by the vitest 4 migration — the
    eviction-based wake test issue #29 asked for), in five shapes: eviction
    after successful broadcasts, eviction before the first-ever broadcast,
    eviction of a cohosted base (tagged-restore branch, host socket untouched),
    a restored sub whose collection left the schema (reconciled: reset +
    row dropped, healthy subs untouched), and a restored predicate that no
    longer compiles (socket closed with a non-terminal code so reconnect
    re-subscribes — a reset would strand the query, adversarial review).
    Sockets accepted by a pre-fix build that survive an in-place upgrade
    carry no id tag and keep the old behavior (dead until reconnect) — a
    one-release sharp edge, documented in ADR-0019.
  • Rejection code now survives tx-dedup replay (#21). The dedup record
    persisted only the rejection message, so a client retrying the same txId
    got the reason with no machine-readable code — breaking code-based error
    handling on exactly the retry path it exists for. _sync_seen_tx gains an
    error_code column (added in place on wake for already-deployed DOs), and
    the replayed rejected frame is now shaped identically to the original:
    { code, message } when a code was recorded, { message } otherwise.
  • BLOB columns no longer corrupt to {} over the wire (ADR-0017,
    #27).

    workerd's SqlStorage returns BLOB values as bare ArrayBuffer, which the
    msgpack encoder fell through to encodeMap on (and the JSON debug codec
    stringified as {}) — clients silently received an empty object, for
    snapshots and deltas alike. Both codecs now normalize ArrayBuffer at
    emission, so BLOB columns arrive on the client as a Uint8Array with the
    exact bytes.