@@ -325,7 +325,7 @@ route AuthApi where
325325 POST {pubkey: PubkeyHex, challenge: Text, signature: SignatureHex, revokeOthers: Bool} /auth/verify headers {clientIp: Text} -> {token: Text, expiresAt: Timestamp} = VerifyAuth
326326
327327route ClientApi where
328− POST {cursor: Timestamp, generation: Int} /poll headers {authorization: Text, clientIp: Text} -> {events: [PollEvent], cursor: Timestamp, authorized: Bool, rateLimited: Bool, generation: Int, heartbeat: Bool, retryAfterMs: Int, cursorReset: Bool, hasMore: Bool, cursorStale: Bool, loadPct: Int} = Poll
328+ POST {cursor: Timestamp, generation: Int} /poll headers {authorization: Text, clientIp: Text} -> {events: [PollEvent], cursor: Timestamp, generation: Int, hasMore: Bool} = Poll
329329 POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {results: [{error: Text}]} = SendMessages
330330
331331route FederationApi where
@@ -404,22 +404,6 @@ capList = \items maxN getKey -> dropRelation (max 0 (count items - maxN)) (sortB
404404capPerKey : [a] -> (a -> Bool) -> Int -> (a -> b) -> [a]
405405capPerKey = \items pred maxN getKey -> union (removeWhere items pred) (capList (filter items pred) (maxN - 1) getKey)
406406
407−-- Shared empty/error shape for /poll. Only a few fields ever vary.
408−emptyPollResp : Timestamp -> Bool -> Bool -> Int -> Int -> Bool -> {events: [PollEvent], cursor: Timestamp, authorized: Bool, rateLimited: Bool, generation: Int, heartbeat: Bool, retryAfterMs: Int, cursorReset: Bool, hasMore: Bool, cursorStale: Bool, loadPct: Int}
409−emptyPollResp = \cursor authorized rateLimited gen retryAfterMs cursorWasReset -> {
410− events: [],
411− cursor,
412− authorized,
413− rateLimited,
414− generation: gen,
415− heartbeat: False {},
416− retryAfterMs,
417− cursorReset: cursorWasReset,
418− hasMore: False {},
419− cursorStale: False {},
420− loadPct: 0
421−}
422−
423407-- Sequence an IO action over each item, in order. Recurses through the list
424408-- one element at a time; this is the closest equivalent of `mapM_` here.
425409forEach : [a] -> (a -> IO {} {}) -> IO {} {}
@@ -937,50 +921,35 @@ handleVerifyAuth = \pubkey challenge signature revokeOthers clientIp -> do
937921
938922-- POST /poll: top-level dispatch. Validates the global rate gate, detects a
939923-- generation mismatch (server restart) to force a cursor reset, then hands
940−-- off to `pollOnce` for the actual long-poll body.
941−handlePoll : Int -> Int -> Text -> Text -> IO {r *pollWakeup, r *serverGeneration, r *sessions, rw *forwards, rw *globalRateCount, rw *globalRateWindowStart, rw *messages, clock} (Result HttpError {authorized: Bool, cursor: Timestamp, cursorReset: Bool, cursorStale: Bool, events: [PollEvent], generation: Int, hasMore: Bool, heartbeat: Bool, loadPct: Int, rateLimited: Bool, retryAfterMs: Int})
924+-- off to `pollOnce` for the actual long-poll body. Clients detect a server
925+-- restart by comparing the `generation` they sent to the one returned.
926+handlePoll : Int -> Int -> Text -> Text -> IO {r *pollWakeup, r *serverGeneration, r *sessions, rw *forwards, rw *globalRateCount, rw *globalRateWindowStart, rw *messages, clock} (Result HttpError {cursor: Timestamp, events: [PollEvent], generation: Int, hasMore: Bool})
942927handlePoll = \cursor generation authorization clientIp -> do
943928 t <- now
944929 serverGen <- *serverGeneration
945930 g <- checkGlobalRate t
946− let cursorWasReset = generation != serverGen && cursor > 0
947− let er = \auth rate retryRate -> yield (Ok {
948− value: emptyPollResp 0 auth rate serverGen retryRate cursorWasReset
949− })
950931 if g
951− then er (False {}) (True {}) globalRateWindowMs
952− else pollOnce authorization (if cursorWasReset then 0 else cursor) cursorWasReset clientIp serverGen t
932+ then yield (Err {error: {status: 429, message: "rate_limited"}})
933+ else do
934+ let cursorWasReset = generation != serverGen && cursor > 0
935+ pollOnce authorization (if cursorWasReset then 0 else cursor) clientIp serverGen t
953936
954937-- Body of a single poll. Treats the cursor advance as an implicit ack
955938-- (`ackPolledMessages`), then runs `doPoll` to collect events (or block
956939-- until wakeup) and assembles the response.
957−pollOnce : Text -> Int -> Bool -> Text -> Int -> Int<Ms> -> IO {r *pollWakeup, r *sessions, rw *forwards, rw *messages, clock} (Result HttpError {authorized: Bool, cursor: Timestamp, cursorReset: Bool, cursorStale: Bool, events: [PollEvent], generation: Int, hasMore: Bool, heartbeat: Bool, loadPct: Int, rateLimited: Bool, retryAfterMs: Int})
958−pollOnce = \authorization cursor cursorWasReset clientIp serverGen t -> do
940+pollOnce : Text -> Int -> Text -> Int -> Int<Ms> -> IO {r *pollWakeup, r *sessions, rw *forwards, rw *messages, clock} (Result HttpError {cursor: Timestamp, events: [PollEvent], generation: Int, hasMore: Bool})
941+pollOnce = \authorization cursor clientIp serverGen t -> do
959942 setup <- atomic withSessionAuth authorization t clientIp (yield (Nothing {})) (\pubkey -> do
960943 let clampedCursor = clamp 0 t cursor
961944 when (clampedCursor > 0) (ackPolledMessages pubkey clampedCursor)
962945 startWakeup <- *pollWakeup
963946 yield (Just {value: {pubkey, clampedCursor, startWakeup}}))
964947 case setup of
965− Nothing {} -> yield (Ok {
966− value: emptyPollResp 0 (False {}) (False {}) serverGen 0 cursorWasReset
967− })
948+ Nothing {} -> yield (Err {error: {status: 401, message: "unauthorized"}})
968949 Just {value: s} -> do
969950 pollResult <- doPoll s t
970951 yield (Ok {
971− value: {
972− pollResult.events,
973− pollResult.cursor,
974− authorized: True {},
975− rateLimited: False {},
976− generation: serverGen,
977− heartbeat: count pollResult.events == 0,
978− retryAfterMs: 0,
979− cursorReset: cursorWasReset,
980− pollResult.hasMore,
981− cursorStale: False {},
982− loadPct: 0
983− }
952+ value: {pollResult.events, pollResult.cursor, generation: serverGen, pollResult.hasMore}
984953 })
985954
986955-- One pass: collect events at `t`. If empty, retry-wait on `pollWakeup`