Skip to content

xAPI transport

Getting a statement to an LRS over a network that is not always there. This is the part of the xAPI support with real behaviour in it, and the part worth reading before you trust a delivery guarantee.

File Job
IXApiTransport The send seam — one method, so tests can fake a network
UnityWebRequestXApiTransport The real sender
QueuedXApiTransport FIFO queue, retry with backoff, persistence
XApiDeliveryResult The tri-state outcome the queue decides on
XApiEndpointConfig Endpoint, auth, and the activity id statements hang off
XApiQueueOptions Attempt count, backoff bounds, queue cap, persistence key

The three outcomes

XApiDeliveryResult is Delivered, Retryable, or Rejected, and the whole queue turns on telling them apart:

  • Delivered — drop it, it is recorded.
  • Retryable — keep it and try again later. Timeouts, 5xx, no connection.
  • Rejected — drop it, because retrying cannot help. A malformed statement is still malformed in ten minutes, and a queue that retries it forever stops delivering everything behind it.

A 409 counts as delivered. The LRS is saying "I already have this statement id", which is the correct answer to a retry that succeeded the first time and lost the response.

The enum is internal. Callers get Task<bool> — see the xAPI overview for why.

Authentication failures halt without dropping

401, 403 and 407 are not rejections, even though they are 4xx. A wrong or expired credential is a configuration problem, not a bad statement: the statements in the queue are fine and will deliver as soon as the credential is. So the queue stops sending, keeps everything, and logs it — rather than discarding a session's worth of learner data because a token expired mid-course.

Persistence

With Persist on, the queue survives the session: PlayerPrefs, which is IndexedDB on WebGL. A failed persist is logged as an error, not a warning, because the queue will keep running and look healthy while nothing it holds would survive a reload.

BindToLearner scopes the stored queue to a learner. It only adopts a backlog that has no learner scope yet — statements belonging to a different learner are left alone rather than inherited, which is the behaviour you want on a shared machine.

Options worth setting deliberately

MaxQueuedStatements defaults to 256 and MaxAttempts to 3, with backoff from 500ms to 8s. The cap exists because an unbounded queue on a long offline session is a memory leak with a learner's progress in it; when it is reached the oldest statements go first, on the grounds that the recent ones describe where the learner actually is.