OpenFeed.Client (OpenFeed v0.1.0)

Copy Markdown View Source

HTTP transport for the OpenFeed sharing API.

Handles DPoP, pagination, retries and error classification. Most callers want OpenFeed.Sharing, which wraps this with a named function per endpoint.

Retries are handled here, not by Req

DPoP proofs are single-use: the server remembers each jti and rejects reuse. Req's built-in retry replays the same request, headers and all, which would present an already-spent proof and earn a 401. So retries live here, and every attempt gets a freshly signed proof — the nonce dance, transient 5xx retries and transport retries alike.

Collection responses carry links.next, present only when more records exist. get_all/4 follows that link.

It does not infer the end from a short page. That heuristic looks equivalent and is not: with a record count that is an exact multiple of the page size, a short-page check requests one page too many, and OpenFeed answers offset >= total with 400 no_records_found_at_offset_limit — turning a perfectly good full read into a spurious failure. Following links.next sidesteps the arithmetic entirely.

Response shape

get/4 and delete/3 return the whole envelope (%{"version" => ..., "data" => ..., "meta" => ..., "links" => ...}). get_all/4 returns just the concatenated data items.

Summary

Types

Options accepted by every request function.

An access token.

Functions

DELETE a resource. Returns {:ok, envelope_or_nil} — 204 yields nil.

GET a single resource or one page of a collection.

GET every page of a collection, following links.next.

OpenFeed's maximum (and default) page size.

Lazily stream every item in a collection, one page at a time.

The authorization scheme to use for a token.

Types

opts()

@type opts() :: [
  params: keyword() | map(),
  token_type: String.t(),
  max_attempts: pos_integer(),
  receive_timeout: timeout(),
  sleep_fn: (non_neg_integer() -> any())
]

Options accepted by every request function.

token()

@type token() :: String.t() | OpenFeed.Tokens.t()

An access token.

Prefer passing the OpenFeed.Tokens struct: it carries the token_type OpenFeed actually issued, which is the only reliable source of it. A bare string works, but then the scheme has to be guessed — see token_type/2.

Functions

delete(config, access_token, path, opts \\ [])

@spec delete(OpenFeed.Config.t(), token(), String.t(), opts()) ::
  {:ok, map() | nil} | {:error, OpenFeed.Error.t()}

DELETE a resource. Returns {:ok, envelope_or_nil} — 204 yields nil.

get(config, access_token, path, opts \\ [])

@spec get(OpenFeed.Config.t(), token(), String.t(), opts()) ::
  {:ok, map()} | {:error, OpenFeed.Error.t()}

GET a single resource or one page of a collection.

Returns the full response envelope.

get_all(config, access_token, path, opts \\ [])

@spec get_all(OpenFeed.Config.t(), token(), String.t(), opts()) ::
  {:ok, [term()]} | {:error, OpenFeed.Error.t()}

GET every page of a collection, following links.next.

Returns the concatenated data lists.

max_page_size()

@spec max_page_size() :: pos_integer()

OpenFeed's maximum (and default) page size.

stream(config, access_token, path, opts \\ [])

@spec stream(OpenFeed.Config.t(), token(), String.t(), opts()) :: Enumerable.t()

Lazily stream every item in a collection, one page at a time.

Use this instead of get_all/4 when a collection could be large — a year of transactions across several accounts does not want to be a single list in memory.

Unlike the other functions here, this raises OpenFeed.Error on failure rather than returning a tuple, because a lazy stream has nowhere to put an error tuple. Wrap it if you need the tuple back:

try do
  OpenFeed.Client.stream(config, token, path) |> Enum.take(50)
rescue
  error in OpenFeed.Error -> {:error, error}
end

token_type(config, arg2)

@spec token_type(OpenFeed.Config.t(), token()) :: String.t()

The authorization scheme to use for a token.

Taken from the token itself when an OpenFeed.Tokens is given. For a bare string it falls back to the client profile's usual scheme, which is a guess — and a wrong one for client-credentials tokens, which OpenFeed issues as Bearer even to a private_key_jwt client. Pass the struct, or an explicit :token_type, whenever the type is knowable.