@@ -325,7 +325,7 @@ route AuthApi where
325 POST {pubkey: PubkeyHex, challenge: Text, signature: SignatureHex, revokeOthers: Bool} /auth/verify headers {clientIp: Text} -> {token: Text, expiresAt: Timestamp} = VerifyAuth
325 POST {pubkey: PubkeyHex, challenge: Text, signature: SignatureHex, revokeOthers: Bool} /auth/verify headers {clientIp: Text} -> {token: Text, expiresAt: Timestamp} = VerifyAuth
327route ClientApi where
327route 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
329 POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {results: [{error: Text}]} = SendMessages
329 POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {results: [{error: Text}]} = SendMessages
331route FederationApi where
331route FederationApi where
@@ -404,22 +404,6 @@ capList = \items maxN getKey -> dropRelation (max 0 (count items - maxN)) (sortB
404capPerKey : [a] -> (a -> Bool) -> Int -> (a -> b) -> [a]
404capPerKey : [a] -> (a -> Bool) -> Int -> (a -> b) -> [a]
405capPerKey = \items pred maxN getKey -> union (removeWhere items pred) (capList (filter items pred) (maxN - 1) getKey)
405capPerKey = \items pred maxN getKey -> union (removeWhere items pred) (capList (filter items pred) (maxN - 1) getKey)
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 -> {
415− heartbeat: False {},
417− cursorReset: cursorWasReset,
419− cursorStale: False {},
423-- Sequence an IO action over each item, in order. Recurses through the list
407-- Sequence an IO action over each item, in order. Recurses through the list
424-- one element at a time; this is the closest equivalent of `mapM_` here.
408-- one element at a time; this is the closest equivalent of `mapM_` here.
425forEach : [a] -> (a -> IO {} {}) -> IO {} {}
409forEach : [a] -> (a -> IO {} {}) -> IO {} {}
@@ -937,50 +921,35 @@ handleVerifyAuth = \pubkey challenge signature revokeOthers clientIp -> do
938-- POST /poll: top-level dispatch. Validates the global rate gate, detects a
922-- POST /poll: top-level dispatch. Validates the global rate gate, detects a
939-- generation mismatch (server restart) to force a cursor reset, then hands
923-- generation mismatch (server restart) to force a cursor reset, then hands
940−-- off to `pollOnce` for the actual long-poll body.
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.
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})
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})
942handlePoll = \cursor generation authorization clientIp -> do
927handlePoll = \cursor generation authorization clientIp -> do
944 serverGen <- *serverGeneration
929 serverGen <- *serverGeneration
945 g <- checkGlobalRate t
930 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
951− then er (False {}) (True {}) globalRateWindowMs
932+ then yield (Err {error: {status: 429, message: "rate_limited"}})
934+ let cursorWasReset = generation != serverGen && cursor > 0
952− else pollOnce authorization (if cursorWasReset then 0 else cursor) cursorWasReset clientIp serverGen t
935+ pollOnce authorization (if cursorWasReset then 0 else cursor) clientIp serverGen t
954-- Body of a single poll. Treats the cursor advance as an implicit ack
937-- Body of a single poll. Treats the cursor advance as an implicit ack
955-- (`ackPolledMessages`), then runs `doPoll` to collect events (or block
938-- (`ackPolledMessages`), then runs `doPoll` to collect events (or block
956-- until wakeup) and assembles the response.
939-- 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})
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})
958−pollOnce = \authorization cursor cursorWasReset clientIp serverGen t -> do
941+pollOnce = \authorization cursor clientIp serverGen t -> do
959 setup <- atomic withSessionAuth authorization t clientIp (yield (Nothing {})) (\pubkey -> do
942 setup <- atomic withSessionAuth authorization t clientIp (yield (Nothing {})) (\pubkey -> do
960 let clampedCursor = clamp 0 t cursor
943 let clampedCursor = clamp 0 t cursor
961 when (clampedCursor > 0) (ackPolledMessages pubkey clampedCursor)
944 when (clampedCursor > 0) (ackPolledMessages pubkey clampedCursor)
962 startWakeup <- *pollWakeup
945 startWakeup <- *pollWakeup
963 yield (Just {value: {pubkey, clampedCursor, startWakeup}}))
946 yield (Just {value: {pubkey, clampedCursor, startWakeup}}))
964 case setup of
947 case setup of
965− Nothing {} -> yield (Ok {
948+ Nothing {} -> yield (Err {error: {status: 401, message: "unauthorized"}})
966− value: emptyPollResp 0 (False {}) (False {}) serverGen 0 cursorWasReset
968 Just {value: s} -> do
949 Just {value: s} -> do
969 pollResult <- doPoll s t
950 pollResult <- doPoll s t
970 yield (Ok {
951 yield (Ok {
974− authorized: True {},
975− rateLimited: False {},
976− generation: serverGen,
952+ value: {pollResult.events, pollResult.cursor, generation: serverGen, pollResult.hasMore}
977− heartbeat: count pollResult.events == 0,
979− cursorReset: cursorWasReset,
981− cursorStale: False {},
986-- One pass: collect events at `t`. If empty, retry-wait on `pollWakeup`
955-- One pass: collect events at `t`. If empty, retry-wait on `pollWakeup`