Commit graph

63 commits

Author SHA1 Message Date
04a43fd33c fix(api): unwedge abandoned probes and per-interaction rate-limit buckets
Probe abandonment wedge: a still-alive caller whose acquire_probe call
times out on a spent deadline was elected the probe server-side (reply
to a dead ref) but never settled and never died, wedging probing[route]
forever. Two layers: the caller now casts abandon_probe/1 on timeout
(server settles only if the entry's monitored pid matches, so a later
probe is never clobbered), and the server arms a force-settle timer at
election as a backstop against a lost abandon.

Interaction callbacks shared one route key: templatize collapsed the
interaction token to :id, so every callback shared one bucket and one
Remaining:0/429 stalled all acks toward the 3s deadline. Restore
per-interaction separation via the same non-reversible token digest used
for webhooks (id still collapses); secrets never stored raw.
2026-07-04 02:05:24 -03:00
1b0914d60e docs: write README, example bot, and ex_doc setup for Phase 6
Adds the full README (quickstart, interaction styles, privileged intents,
cache, REST, slash registration, event semantics, Nostrum porting guide,
reliability design), an examples/echo_bot.exs Mix.install script, ex_doc
config in mix.exs, and rewrites Dexcord.Gateway's moduledoc to describe
the finished reliability behavior instead of stale Phase 0-1 scope notes.
2026-07-04 01:45:36 -03:00
45688193d6 Phase 5: Prefix/Slash ergonomics, Registrar, presence delegate, gateway polish 2026-07-04 01:39:08 -03:00
f44d30954a fix(gateway): decode HELLO coalesced with the 101 upgrade; hermetic test env
Root cause of the order/seed-dependent integration failures: when the WS
upgrade's 101 response and the server's first frame (HELLO) arrive in one TCP
segment, Mint returns them as [status, headers, {:data, hello}, {:done}] - the
{:data} ordered BEFORE the {:done} that builds the websocket. process_responses
only decoded {:data} once the websocket existed, so the coalesced HELLO was
silently dropped and the client wedged in :hello_wait. Coalescing is far more
likely under full-suite scheduler load, which also starved the cadence-sensitive
tests - hence the flaky, seed-varying failures. Buffer pre-upgrade bytes and
decode them the instant the websocket is built.

Test hermeticity:
- Add Dexcord.EnvSandbox: snapshot all :dexcord app env in setup, restore
  wholesale on_exit. Replaces every file's hand-rolled per-key save/restore so no
  knob (timing bounds, budgets, ratelimit clock, base URLs) can leak between tests.
- gateway_fatal_test: await the directly-started supervisor's full termination
  (monitor + DOWN) instead of a fire-and-forget Process.exit, so its globally-named
  singletons and named ETS are gone before the next test's tree starts.
2026-07-04 01:25:48 -03:00
2dec62e72d Phase 4: ETS cache, event coverage, gateway send budget, member chunking
- Dexcord.Cache GenServer: creates 8 public ETS tables (me/guilds/channels/
  users sets; members/roles/presences/voice_states ordered_set keyed {guild_id,x}).
  Writes only via handle_dispatch/3 (single writer = Dispatcher, gateway-ordered);
  reads are direct ETS. Full event coverage incl. GUILD_DELETE cascade purge and
  cache_presences toggle. Read API + bang variants.
- Dispatcher: inline Cache.handle_dispatch before handler task; op-8 chunk request
  on real GUILD_CREATE when request_guild_members and :guild_members intent set.
- Gateway: send-budget token bucket (default 115/60s, heartbeats bypass); op-3/op-8
  consume tokens, queue when empty (bounded, drop+warn), drain on :send_refill ticks.
- Supervisor: Cache child after Session, before Finch.
- Config: validate cache_presences / request_guild_members are booleans.
2026-07-04 00:52:58 -03:00
92cb967a0f fix(api): harden REST rate limiter (probe death, token leak, deadline, herd)
- Monitor the elected probe caller and release parked waiters on its death;
  guarantee probe_settled via try/after around request issuance so a raising
  encode or killed caller can no longer wedge a route permanently.
- Never store secret tokens in public route keys: digest webhook tokens and
  collapse interaction-callback tokens to :id; add an idle-TTL sweep evicting
  stale bucket + route rows (:ratelimit_idle_ttl_ms / :ratelimit_sweep_interval_ms).
- Enforce a per-request deadline (:api_deadline_ms, default 30s); convert
  probe-park GenServer.call timeouts into an error tuple.
- Reset the counter to limit-1 on window elapse to avoid the reset thundering
  herd (un-deads the limit field); clamp the shared counter floor at -1.
- Preserve non-JSON bodies: {:ok, {:raw, body}} for 2xx, bounded snippet in
  error messages.
2026-07-04 00:47:12 -03:00
ef8d1fc34d fix(gateway): harden identify-gap, reidentify-watchdog, and stale-socket paths
- Enforce the identify gap on every identify path via do_identify/1, so an
  op 9 d:false reidentify in :resuming no longer bypasses it.
- Add a reidentify_pending? flag so a dispatch trickling in during the
  post-op-9 wait can't clobber the pending reidentify timer.
- Delete dead conn_gen state and correct the moduledoc.
- Extract pure stale_socket?/2 for direct unit testing.
- Add deterministic tests for the hello_wait heartbeat re-arm, the resuming
  identify gap, the reidentify clobber guard, and stale_socket?/2.
2026-07-04 00:30:48 -03:00
50a16666b4 Phase 3: REST rate limiter, typed endpoints, error struct
Add Dexcord.Api.Ratelimit (route->bucket learning, per-bucket windows via
monotonic X-RateLimit-Reset-After deadlines, global lock, injectable clock,
probe serialization for unknown routes). Rework Dexcord.Api.request/4 around
acquire/wait/update with 429 + global handling and up to 3 retries; add the
~16 typed endpoints and a Dexcord.Api.Error struct. Start Ratelimit in the
supervisor after Finch. Add unit tests (route-key table, bucket/global math
with injected clock) and integration tests against a new Dexcord.FakeRest
Bandit server.
2026-07-04 00:26:58 -03:00
12689862c8 feat(gateway): phase 2 reliability core (resume/zombie/backoff/fatal) + FakeGateway
- fix heartbeat timer loss in :hello_wait (re-arm during identify-gap wait)
- honor server op 1 in :identifying/:hello_wait, not just :connected/:resuming
- full op 9 handling: d:false in :resuming re-IDENTIFYs on the same socket after
  the mandated 1-5s; d:false elsewhere floors the reconnect; d:true keeps session
- resume-loop cap (5 consecutive failed resumes -> fresh IDENTIFY) via Session ETS
- explicit conn_gen/stale-socket guard backstopping Mint's :unknown
- bounded TCP connect + configurable handshake watchdogs so a stalled reconnect
  self-heals instead of wedging
- FakeGateway scripted Bandit/WebSock test server + TestHandler
- integration suite (happy/zombie/kill/close-codes/op7/op9/replay/cap/backoff/gap/
  stale), fatal-path end-to-end, and unit tests for resume counter + backoff math
2026-07-04 00:09:33 -03:00
9b6413371a feat: gateway gen_statem, supervisor tree, child_spec 2026-07-03 22:59:32 -03:00
9355019ee9 feat: naive REST api, handler behaviour, dispatcher 2026-07-03 22:59:32 -03:00
359bf60677 feat: pure core (intents, event names, session, payload, config) 2026-07-03 22:59:32 -03:00
868426cf4d chore: scaffold dexcord mix project with deps 2026-07-03 22:47:31 -03:00