packages/sqrl-redis-functions/src/lua/sessionizeLua.ts (87 lines of code) (raw):

/** * Copyright 2019 Twitter, Inc. * Licensed under the Apache License, Version 2.0 * http://www.apache.org/licenses/LICENSE-2.0 */ import { MockRedisDatabase } from "../mocks/MockRedisDatabase"; export function sessionizeLua() { // It is likely inefficient to update the expiry time on every ratelimit, // change, that could be optimized a lot. return ( ` local maxAmount = tonumber(ARGV[1]) local take = tonumber(ARGV[2]) local at = tonumber(ARGV[3]) local refillTime = tonumber(ARGV[4]) local refillAmount = tonumber(ARGV[5]) local current = redis.call("get", KEYS[1]); local tokens local lastRefill local sessionStart if not current then tokens = maxAmount lastRefill = at else sessionStart, tokens, lastRefill = struct.unpack("i8i8i8", current) end if take == 0 then return redis.error_reply("Take must be non-zero for sessionize()") end if tokens < take then local refills = math.floor((at - lastRefill) / refillTime); lastRefill = lastRefill + refills * refillTime tokens = math.min(maxAmount, tokens + refills * refillAmount) end if tokens >= take then sessionStart = at end local returnedSession = nil if tokens == 0 then lastRefill = at returnedSession = sessionStart else tokens = math.max(0, tokens - take) end local expiry = math.ceil((maxAmount - tokens) / refillAmount) * refillTime local value = struct.pack("i8i8i8", sessionStart, tokens, lastRefill) redis.call("set", KEYS[1], value, "px", expiry) return returnedSession `.trim() + "\n" ); } /** * An exact replica of the lua function but running on a mock in memory database * for testing. */ export function mockSessionize( redis: MockRedisDatabase, key: string, maxAmount: number, take: number, at: number, refillTime: number, refillAmount: number ) { const current = redis.db[key]; let tokens; let lastRefill; let sessionStart; if (!current) { tokens = maxAmount; lastRefill = at; } else { [sessionStart, tokens, lastRefill] = JSON.parse(current); } if (take === 0) { throw new Error("Take must be non-zero for sessionize()"); } if (tokens < take) { const refills = Math.floor((at - lastRefill) / refillTime); lastRefill = lastRefill + refills * refillTime; tokens = Math.min(maxAmount, tokens + refills * refillAmount); } if (tokens >= take) { sessionStart = at; } let returnedSession = null; if (tokens === 0) { lastRefill = at; returnedSession = sessionStart; } else { tokens = Math.max(0, tokens - take); } // @todo: expiry // let expiry = math.ceil((maxAmount - tokens) / refillAmount) * refillTime; const value = JSON.stringify([sessionStart, tokens, lastRefill]); redis.db[key] = value; return returnedSession; }