Commit graph

71 commits

Author SHA1 Message Date
2163ff5b2f feat(snowflake): integer-only t(), cast/cast!/dump, is_snowflake guard, fixed to_unix 2026-07-05 03:09:43 -03:00
3d5517d39e docs: add full API surface design plan (api-surface)
Completed brainstorming session. Design includes:
- Three compile-time DSLs (Struct/Enum/Flags) generating decode/encode/merge from single declarations, string-key matching with zero wire atomization
- Integer Snowflake rework, distinct channel structs + factory, first-class partials with Cache.fill hydration
- Endpoint DSL covering ~180 v10 REST endpoints + mix dexcord.coverage spec-diff gate
- 7 implementation phases
2026-07-05 00:29:29 -03:00
f3ddb9defb feat(api/cache): add channel-message history, thread creation, snowflakes, cached threads
- Api.get_channel_messages/2,3 (opts + Nostrum-compatible locator), query
  baked into the path so the rate limiter buckets it correctly
- Api.start_thread_with_message/3,4 (POST .../messages/:id/threads)
- Dexcord.Snowflake: from_datetime/to_datetime/to_unix (replaces Nostrum.Snowflake)
- Cache.threads/1: thread-type channels (10/11/12) for a guild
- FakeRest now records query_string so query building is testable

Added to support migrating Alamedya off Nostrum. 178 tests, 0 failures.
2026-07-04 13:12:54 -03:00
229cb9d9a8 update gitignore 2026-07-04 02:56:30 -03:00
99c16c52d4 rm .claude 2026-07-04 02:56:18 -03:00
c350767a7a fix(gateway): harden dispatch, session-boundary seq, and send-budget FIFO
- Tolerate op-0 frames with a non-binary `t` (e.g. `t: null`): the gateway
  no longer crashes the gen_statem via EventNames.to_atom/1's is_binary guard;
  such protocol noise is logged at debug, still bumps seq, and is not dispatched.
- Session.establish/2 resets last_seq to nil so a new session never inherits the
  previous session's sequence; READY's own frame reseeds it. Also stop bumping
  last_seq from frames replayed under reidentify_pending? (abandoned session).
- queue_or_send now follows queue-first FIFO discipline: a frame is only sent
  immediately when the queue is empty, otherwise it falls in behind existing
  queued frames and the front is drained, preventing newer op-3/op-8 frames from
  jumping older ones.
- op-3/op-8 casts outside :connected are enqueued (bounded) instead of dropped,
  so member-chunk requests issued during :resuming replay are not lost; they
  drain on the next :connected enter. Dropped only with no session context.
- Refund the token on a failed budgeted send (no more double-charge) and extract
  the shared send_one_budgeted/2 helper used by both the send-now and drain paths.
- Document the reidentify_pending? invariant at its declaration.
2026-07-04 02:17:16 -03:00
9ae463a91b fix(slash/dispatch/cache): contain registrar retries, route components/modals, guard global wipe, isolate cache crashes
- Registrar: restart: :temporary + bounded in-process retry (2s/10s, configurable)
  so a failing registration never cycles the gateway or shares the tree's
  max_restarts budget; loud error + normal exit on give-up.
- Global mode: skip the overwrite when commands/0 is empty (symmetric with guild
  mode) so a boot never wipes production commands; log how to wipe intentionally.
- Slash: route on top-level interaction type - 2 -> handle_interaction (by name),
  3 -> handle_component, 5 -> handle_modal (by custom_id). New optional callbacks
  with use-injected debug catch-alls; modules defining only handle_interaction
  still compile and work.
- Supervisor: nest Cache + Dispatcher under a :rest_for_one supervisor so a Cache
  crash recreates the ETS tables and restarts the Dispatcher after it, leaving
  the outer one_for_one Gateway/Session semantics untouched.
- Slash response helpers: merge a caller-supplied integer flags with the
  ephemeral bit via Bitwise.bor (4 -> 4; 4 + ephemeral -> 68).
2026-07-04 02:08:27 -03:00
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
4c9ab79803 test: unit tests for pure modules plus supervisor smoke test 2026-07-03 22:59:32 -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