Class: X::Cursor
- Inherits:
-
Object
- Object
- X::Cursor
- Includes:
- Enumerable
- Defined in:
- x-objects/lib/x/objects/cursor.rb
Overview
A lazily paginated, cached, thread-safe collection of resources
Instance Attribute Summary collapse
-
#client ⇒ Object
readonly
The client the resources hold, which also fetches the pages.
-
#resource_class ⇒ Class
readonly
The class of the resources in this collection.
Instance Method Summary collapse
-
#any?(*pattern) {|Resource| ... } ⇒ Boolean
Check whether the collection holds any resource, requesting one.
-
#as_json ⇒ void
Refuse to write the collection as JSON, which would read every page of it.
-
#each {|Resource| ... } ⇒ Enumerator, Cursor
Iterate over every resource, fetching pages as needed.
-
#each_page {|Page| ... } ⇒ Enumerator, Cursor
Iterate over every page, fetching pages as needed.
-
#empty? ⇒ Boolean
Check whether the collection is empty, requesting one resource.
-
#encode_with(_coder) ⇒ void
Refuse to write the cursor as YAML, as it refuses Marshal.
-
#first(count = nil) ⇒ Resource, ...
The first resource, or the first few, requesting pages no larger than needed.
-
#ids ⇒ Array<Integer, String>
The identifiers of every resource, requesting nothing but identifiers.
-
#inspect ⇒ String
Summarize the cursor for the console.
-
#marshal_dump ⇒ void
Refuse to write the cursor with Marshal, as it refuses to write it as JSON.
-
#none?(*pattern) {|Resource| ... } ⇒ Boolean
Check whether the collection holds no resource, requesting one.
-
#one?(*pattern) {|Resource| ... } ⇒ Boolean
Check whether the collection holds one resource, requesting two.
-
#page(index) ⇒ Page?
Fetch a page by index, using the cache when possible.
-
#prefetch ⇒ Cursor
Return a new cursor over the same collection with prefetching enabled.
-
#prefetch? ⇒ Boolean
Check whether the next page is fetched in the background.
-
#published_count ⇒ Integer?
The number the API publishes for the collection, without reading any of it.
-
#refresh ⇒ Cursor
Return a new cursor over the same collection with an empty page cache.
-
#stubs ⇒ Cursor
Return a new cursor over the same collection that yields stubs.
-
#take(count) ⇒ Array<Resource>
The first few resources, requesting pages no larger than needed, as first does.
-
#to_a ⇒ Array<Resource>
(also: #entries)
Every resource, fetching every page.
-
#to_h {|resource| ... } ⇒ Hash
A Hash of the pair a block returns for each resource, reading every page.
-
#to_json(_state = nil) ⇒ void
Refuse to write the collection as a JSON array, which would read every page.
Instance Attribute Details
#client ⇒ Object (readonly)
The client the resources hold, which also fetches the pages
The pages of an endpoint that refuses OAuth 1.0a, such as the posts of a space, are fetched with the app-only client of a client that signs with it, while the resources hold the client itself.
39 40 41 |
# File 'x-objects/lib/x/objects/cursor.rb', line 39 def client @client end |
#resource_class ⇒ Class (readonly)
The class of the resources in this collection
28 29 30 |
# File 'x-objects/lib/x/objects/cursor.rb', line 28 def resource_class @resource_class end |
Instance Method Details
#any?(*pattern) {|Resource| ... } ⇒ Boolean
Check whether the collection holds any resource, requesting one
Without a pattern or a block, this asks for a single resource, raised to the endpoint's minimum, rather than a full page.
229 230 231 232 233 |
# File 'x-objects/lib/x/objects/cursor.rb', line 229 def any?(*pattern, &block) return super unless pattern.empty? && block.nil? !first.nil? end |
#as_json ⇒ void
This method returns an undefined value.
Refuse to write the collection as JSON, which would read every page of it
Serializing a cursor would read every page of the collection, a request per page, and the API bills each resource it returns, from a call that says nothing of it, such as a cursor in a Hash that a log or a render writes. It raises instead, as ActiveSupport would otherwise read a cursor as the Enumerable it is. Serialize what first(n) or to_a reads instead, each of which says at the call how much it reads.
317 |
# File 'x-objects/lib/x/objects/cursor.rb', line 317 def as_json(*) = raise(UnsupportedOperation, SERIALIZATION_MESSAGE) |
#each {|Resource| ... } ⇒ Enumerator, Cursor
Iterate over every resource, fetching pages as needed
88 89 90 91 92 |
# File 'x-objects/lib/x/objects/cursor.rb', line 88 def each(&block) return to_enum unless block each_page { |page| page.each(&block) } end |
#each_page {|Page| ... } ⇒ Enumerator, Cursor
Iterate over every page, fetching pages as needed
The pages are those #page reads, as they were fetched, so a page need not hold as many resources as the largest page the endpoint allows.
104 105 106 107 108 109 110 111 112 113 |
# File 'x-objects/lib/x/objects/cursor.rb', line 104 def each_page return to_enum(:each_page) unless block_given? index = 0 while (current = page(index)) yield current index += 1 end self end |
#empty? ⇒ Boolean
Check whether the collection is empty, requesting one resource
Like none? without a pattern or a block, this asks for a single resource, raised to the endpoint's minimum, rather than a full page.
261 |
# File 'x-objects/lib/x/objects/cursor.rb', line 261 def empty? = first.nil? |
#encode_with(_coder) ⇒ void
This method returns an undefined value.
Refuse to write the cursor as YAML, as it refuses Marshal
YAML reads no marshal_dump, and would write every instance variable of the cursor, its client and the credentials it holds among them, so it raises as #marshal_dump does. Write what first(n) or to_a reads, or a page, instead.
371 |
# File 'x-objects/lib/x/objects/cursor.rb', line 371 def encode_with(_coder) = raise(TypeError, SERIALIZATION_MESSAGE) |
#first(count = nil) ⇒ Resource, ...
The first resource, or the first few, requesting pages no larger than needed
Iterating a cursor requests the largest page an endpoint allows, which costs the least in requests. The API bills each resource returned, so first asks for a page of the size it needs instead, raised to the endpoint's minimum, and each page after the first asks for no more than the pages before it left. The API may serve an empty page with the token of the next, having left out what it filters, such as suspended users, so first reads on until it finds a resource. The cursor keeps the pages first reads, as it keeps every page, so a cursor whose pages already hold what is asked for answers from them, without a request, and an iteration after first requests only what first left.
185 186 187 188 189 190 191 |
# File 'x-objects/lib/x/objects/cursor.rb', line 185 def first(count = nil) resources = @pages.read(count.nil? ? 1 : Utils.count!(count)) #: Array[untyped] return resources unless count.nil? resource, = resources resource end |
#ids ⇒ Array<Integer, String>
The identifiers of every resource, requesting nothing but identifiers
303 |
# File 'x-objects/lib/x/objects/cursor.rb', line 303 def ids = stubs.map(&:id).freeze |
#inspect ⇒ String
Summarize the cursor for the console
379 |
# File 'x-objects/lib/x/objects/cursor.rb', line 379 def inspect = "#<#{self.class} resource_class=#{resource_class} path=#{path.inspect}>" |
#marshal_dump ⇒ void
This method returns an undefined value.
Refuse to write the cursor with Marshal, as it refuses to write it as JSON
A cursor holds its client, and the threads and locks that fetch its pages, none of which Marshal can write, and caching the collection it names would mean reading every page of it, as #as_json says. It raises the TypeError Marshal raises for what it cannot write, with the message of #as_json, rather than the one Marshal would raise from within the cursor. Marshal what first(n) or to_a reads, or a page, instead.
357 |
# File 'x-objects/lib/x/objects/cursor.rb', line 357 def marshal_dump = raise(TypeError, SERIALIZATION_MESSAGE) |
#none?(*pattern) {|Resource| ... } ⇒ Boolean
Check whether the collection holds no resource, requesting one
Without a pattern or a block, this asks for a single resource, raised to the endpoint's minimum, rather than a full page.
246 247 248 249 250 |
# File 'x-objects/lib/x/objects/cursor.rb', line 246 def none?(*pattern, &block) return super unless pattern.empty? && block.nil? first.nil? end |
#one?(*pattern) {|Resource| ... } ⇒ Boolean
Check whether the collection holds one resource, requesting two
Without a pattern or a block, this asks for two resources, raised to the endpoint's minimum, rather than a full page.
274 275 276 277 278 |
# File 'x-objects/lib/x/objects/cursor.rb', line 274 def one?(*pattern, &block) return super unless pattern.empty? && block.nil? take(2).size.eql?(1) end |
#page(index) ⇒ Page?
Fetch a page by index, using the cache when possible
The pages before the one asked for are read first, since the token of each asks for the next. A page is the page as it was fetched and kept, whatever fetched it: iterating fetches the largest page the endpoint allows, but first, take, any?, and empty? fetch pages no larger than they need, which the cursor keeps too, so that an iteration after them does not pay again for what they read. After user.followers.first, the first page holds one follower, and the pages an iteration fetches after it as many as the largest page does. The API may serve a page with fewer resources than it was asked for, or none, so no page has a size to rely on.
131 |
# File 'x-objects/lib/x/objects/cursor.rb', line 131 def page(index) = @pages.at(index) |
#prefetch ⇒ Cursor
Return a new cursor over the same collection with prefetching enabled
A page the background thread fails to fetch is not requested again when it is reached: the error the thread failed with is raised there, once, and a page asked for again after it is requested again.
153 |
# File 'x-objects/lib/x/objects/cursor.rb', line 153 def prefetch = self.class.__send__(:build, resource_class, path, client:, params: own_params, prefetch: true, token_param:, min_results:, app_only: app_only?, total: @total, ids_only: ids_only?) |
#prefetch? ⇒ Boolean
Check whether the next page is fetched in the background
79 |
# File 'x-objects/lib/x/objects/cursor.rb', line 79 def prefetch? = @prefetch |
#published_count ⇒ Integer?
The number the API publishes for the collection, without reading any of it
The API publishes a number for a user's followers, followed users, and list memberships, and for a list's members and followers. Reading it costs no request when the user or list holds it, and one lookup when it is a stub. The number counts what the collection holds, which can differ from what count reads, since the endpoint leaves out what the authenticated user cannot see, such as private lists and suspended users. count instead reads every page of the collection, a request per page, and the API bills each resource. A cursor answers no size, so that Ruby's own methods, such as each_slice and lazy, do not page a collection to size it.
293 |
# File 'x-objects/lib/x/objects/cursor.rb', line 293 def published_count = @total&.call |
#refresh ⇒ Cursor
Return a new cursor over the same collection with an empty page cache
The number the API publishes for the collection is read again too, once, the first time published_count asks for it, since the collection it counts may have changed.
142 |
# File 'x-objects/lib/x/objects/cursor.rb', line 142 def refresh = self.class.__send__(:build, resource_class, path, client:, params: own_params, prefetch: prefetch?, token_param:, min_results:, app_only: app_only?, total: fresh_total, ids_only: ids_only?) |
#stubs ⇒ Cursor
Return a new cursor over the same collection that yields stubs
The requests ask for nothing but identifiers, and each resource is a stub holding only its identifier, which hydrates on demand, even when the API returns a few default fields alongside it.
165 |
# File 'x-objects/lib/x/objects/cursor.rb', line 165 def stubs = self.class.__send__(:build, resource_class, path, client:, params: id_only_params, prefetch: prefetch?, token_param:, min_results:, app_only: app_only?, total: @total, ids_only: true) |
#take(count) ⇒ Array<Resource>
The first few resources, requesting pages no larger than needed, as first does
216 |
# File 'x-objects/lib/x/objects/cursor.rb', line 216 def take(count) = first(Utils.count!(count)) |
#to_a ⇒ Array<Resource> Also known as: entries
Every resource, fetching every page
The array is frozen, as the arrays first and take return are, since a cursor keeps the pages it read and a caller that changed what it returned would change nothing the cursor holds.
202 |
# File 'x-objects/lib/x/objects/cursor.rb', line 202 def to_a = super.freeze |
#to_h {|resource| ... } ⇒ Hash
A Hash of the pair a block returns for each resource, reading every page
Without a block it raises as #as_json does, before it reads a page, since a page of the collection is read as a Hash only by its as_json, rather than raise TypeError from the to_h of Enumerable once it has read one.
331 |
# File 'x-objects/lib/x/objects/cursor.rb', line 331 def to_h(&block) = block ? super() : raise(UnsupportedOperation, SERIALIZATION_MESSAGE) |
#to_json(_state = nil) ⇒ void
This method returns an undefined value.
Refuse to write the collection as a JSON array, which would read every page
It raises as #as_json does, for the reason that says.
343 |
# File 'x-objects/lib/x/objects/cursor.rb', line 343 def to_json(_state = nil) = raise(UnsupportedOperation, SERIALIZATION_MESSAGE) |