Module: Wurk::API::Page

Defined in:
lib/wurk/api/page.rb

Overview

Offset paging for the observe plane's listings.

Deliberately not the dashboard's Wurk::Api::Pagination. That module is autoloaded out of app/ by the engine, and the API has to page in standalone mode where Rails is never loaded (CLAUDE.md); requiring an autoloadable file by hand is exactly what Zeitwerk forbids. The contracts differ too — the dashboard's may change whenever the SPA does, everything under /v1 may not — so one shared module would tie a frozen contract to a mutable one.

Defined Under Namespace

Classes: Window

Constant Summary collapse

DEFAULT_COUNT =
25
MAX_COUNT =
200
MAX_PAGE =

Offset paging reaches page N by walking N*count members through Ruby, and their Redis round trips with them, so an unclamped page lets one request drag an entire million-row set through this process. A thousand pages is past any real client's depth and still bounds the worst case.

1_000

Class Method Summary collapse

Class Method Details

.integer!(raw, name, default) ⇒ Object

A repeated parameter (?page=1&page=2) parses to an Array, which is the TypeError arm — an ambiguous request, answered rather than guessed.



68
69
70
71
72
73
74
# File 'lib/wurk/api/page.rb', line 68

def integer!(raw, name, default)
  return default if raw.nil? || raw == ''

  Integer(raw, 10)
rescue ::ArgumentError, ::TypeError
  raise Validation::Invalid, "The '#{name}' parameter must be an integer."
end

.query!(request) ⇒ Object

Rack::Request#params merges the POST body in, which the paged routes must never read. parse_query is also flat: ?page[]=1 arrives as the key it literally is instead of nesting into a Hash that reaches Integer().

Rack's own refusals (InvalidParameterError on bad %-encoding, QueryLimitError on an oversized one) are ArgumentError and RangeError respectively; caught here so a malformed query string is the 400 it is rather than the 500 App's catch-all would make of it.



60
61
62
63
64
# File 'lib/wurk/api/page.rb', line 60

def query!(request)
  ::Rack::Utils.parse_query(request.query_string)
rescue ::ArgumentError, ::TypeError, ::RangeError
  raise Validation::Invalid, 'The query string could not be parsed.'
end

.slice(enumerable, window) ⇒ Object

Walks enumerable, skips window.offset members, and hands at most window.count of them to the block, which returns the row to emit.

Skipped members are never yielded, so nothing before the offset is serialized: on a queue page that is a JobRecord whose JSON is never parsed, which is most of the cost of a deep page.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/wurk/api/page.rb', line 82

def slice(enumerable, window)
  rows = []
  skipped = 0
  enumerable.each do |member|
    if skipped < window.offset
      skipped += 1
      next
    end

    rows << yield(member)
    break if rows.size >= window.count
  end
  rows
end

.window(query) ⇒ Object

The parsed-query half, for a route that reads a parameter of its own (?filter=) beside the paging ones: it parses once and hands the same Hash to both, rather than re-parsing the query string per parameter.



44
45
46
47
48
49
# File 'lib/wurk/api/page.rb', line 44

def window(query)
  Window.new(
    page: integer!(query['page'], 'page', 0).clamp(0, MAX_PAGE),
    count: integer!(query['count'], 'count', DEFAULT_COUNT).clamp(1, MAX_COUNT)
  )
end

.window!(request) ⇒ Object

Raises:

  • (Validation::Invalid)

    when a paging parameter is present but is not an integer. Out of range is clamped instead of refused — a client asking for more rows than the API will give is not a client bug — and every listing echoes the effective values back, so the clamp is visible rather than silent.



39
# File 'lib/wurk/api/page.rb', line 39

def window!(request) = window(query!(request))