Table of Contents

Class StatementOutbox

Namespace
Xapi.Client.Outbox
Assembly
Xapi.Client.Outbox.dll

A durable queue in front of an XapiClient: Statements are stored before EnqueueAsync(Statement, CancellationToken) returns, and delivered later, in order and in batches, by FlushAsync(CancellationToken) or by a background loop (RunAsync(CancellationToken)).

public sealed class StatementOutbox : IAsyncDisposable, IDisposable
Inheritance
StatementOutbox
Implements
Inherited Members

Remarks

Delivery is at least once. Every Statement gets its id before it is stored, so a Statement sent again after a lost response is the same Statement to the LRS, which keeps the first copy: on a conforming LRS the result is exactly once.

Failed sends are classified (docs/outbox.md has the full table). Network errors, timeouts, 408, 429 and 5xx are retried with exponential backoff and jitter, honouring Retry-After. A 400 for a batch splits it in halves until the invalid Statement is found; only that one becomes a dead letter. A 409 is resolved by fetching the stored Statement and comparing it with StatementEquivalence. A 413 halves the batch size. 401, 403 and responses that mean the endpoint is wrong pause the outbox; nothing is dead-lettered.

All members are thread-safe. One flush runs at a time; Statements can be enqueued while it runs. Events are raised on the thread that runs the flush, which is not the Unity main thread; exceptions thrown by handlers are caught and ignored, so a handler cannot stop delivery.

Attachments are not supported: the outbox does not store attachment data, so Statements with an Attachment that has no fileUrl are rejected by EnqueueAsync(Statement, CancellationToken). Send those with SaveStatementAsync(Statement, IReadOnlyList<AttachmentPayload>?, CancellationToken).

Constructors

StatementOutbox(XapiClient, OutboxStore, OutboxOptions?)

Creates an outbox.

public StatementOutbox(XapiClient client, OutboxStore store, OutboxOptions? options = null)

Parameters

client XapiClient

The client to send with.

store OutboxStore

Where to keep the Statements: a FileOutboxStore, usually.

options OutboxOptions

The settings, or null for the defaults. They are copied.

Exceptions

ArgumentException

An option is out of range.

Methods

Dispose()

Tells the background loop started with RunAsync(CancellationToken) to stop, without waiting for it. Prefer DisposeAsync(), which waits. The store and the client are not disposed.

public void Dispose()

DisposeAsync()

Stops the background loop started with RunAsync(CancellationToken) and waits for it to end. The store and the client are not disposed. A flush started with FlushAsync(CancellationToken) is not waited for.

public ValueTask DisposeAsync()

Returns

ValueTask

A task that completes when the loop has stopped.

EnqueueAsync(Statement, CancellationToken)

Stores a Statement for delivery. When the returned task completes, the Statement is in the store; with a FileOutboxStore, on disk.

public ValueTask<Guid> EnqueueAsync(Statement statement, CancellationToken cancellationToken = default)

Parameters

statement Statement

The Statement.

cancellationToken CancellationToken

Cancels the operation.

Returns

ValueTask<Guid>

The Statement id.

Remarks

A Statement without an id gets a new UUID version 7, and one without a timestamp gets the current time (to the millisecond), so that a Statement delivered hours later still says when it happened. The Statement is then validated with the xAPI 2.0.0 rules, which accept everything 1.0.3 does; a Statement that uses 2.0-only properties and is later sent to a 1.0.3 LRS becomes a dead letter.

Enqueueing a Statement whose id is already waiting does nothing. The background loop, if running, is woken.

Exceptions

XapiValidationException

The Statement is invalid, or has an Attachment without a fileUrl (attachment data is not supported).

OutboxFullException

MaxQueueSize Statements are already waiting.

FlushAsync(CancellationToken)

Sends pending Statements now, batch after batch, until none are left, a transient error occurs, or the LRS refuses the request. Waits for a flush already in progress first.

public Task<OutboxFlushResult> FlushAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Cancels the flush. A request in flight is abandoned; its Statements stay queued.

Returns

Task<OutboxFlushResult>

What the flush did and the state it left the outbox in.

Remarks

A manual flush does not wait for the retry delay of an earlier failure. While the outbox is Paused it sends nothing; call Resume() first. Errors are not thrown; they are in Status. Exceptions from the store are thrown.

GetDeadLettersAsync(CancellationToken)

Returns the dead letters, oldest first.

public ValueTask<IReadOnlyList<OutboxDeadLetter>> GetDeadLettersAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Cancels the operation.

Returns

ValueTask<IReadOnlyList<OutboxDeadLetter>>

The dead letters.

GetStatusAsync(CancellationToken)

Returns what the outbox is doing, how much is waiting, and the last error.

public ValueTask<OutboxStatus> GetStatusAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Cancels the operation.

Returns

ValueTask<OutboxStatus>

The status.

RemoveDeadLetterAsync(Guid, CancellationToken)

Deletes a dead letter for good.

public ValueTask<bool> RemoveDeadLetterAsync(Guid statementId, CancellationToken cancellationToken = default)

Parameters

statementId Guid

The Statement id.

cancellationToken CancellationToken

Cancels the operation.

Returns

ValueTask<bool>

false when there is no dead letter with that id.

RequeueAllDeadLettersAsync(CancellationToken)

Moves every dead letter back to the end of the queue, oldest first, and wakes the background loop.

public ValueTask<int> RequeueAllDeadLettersAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Cancels the operation.

Returns

ValueTask<int>

The number of dead letters requeued.

RequeueDeadLetterAsync(Guid, CancellationToken)

Moves a dead letter back to the end of the queue, with no failed attempts, and wakes the background loop. Use it after fixing what made the LRS reject the Statement.

public ValueTask<bool> RequeueDeadLetterAsync(Guid statementId, CancellationToken cancellationToken = default)

Parameters

statementId Guid

The Statement id.

cancellationToken CancellationToken

Cancels the operation.

Returns

ValueTask<bool>

false when there is no dead letter with that id.

Resume()

Ends a pause (Paused), for example after the credentials were fixed, and wakes the background loop. Does nothing when the outbox is not paused.

public void Resume()

RunAsync(CancellationToken)

Delivers Statements in the background until cancellationToken is cancelled or the outbox is disposed: right away, whenever a Statement is enqueued, and at RetryAt after a transient failure. While the outbox is paused it waits for Resume().

public Task RunAsync(CancellationToken cancellationToken = default)

Parameters

cancellationToken CancellationToken

Stops the loop. A request in flight is abandoned; its Statements stay queued.

Returns

Task

A task that completes, without an exception, when the loop has stopped.

Remarks

Needs no host or dependency injection, so it also suits Unity and desktop apps: start it once (_ = outbox.RunAsync(token)) and cancel the token, or dispose the outbox, on exit. On net10.0, AddOutbox runs it as a hosted service. FlushAsync(CancellationToken) can still be called while it runs.

Errors never end the loop. Failures of the store (a full disk, for example) are handled like transient failures and shown in LastError.

Exceptions

InvalidOperationException

The loop is already running.

Events

DeadLettered

Raised after a Statement was moved to the dead letters.

public event EventHandler<OutboxDeadLetteredEventArgs>? DeadLettered

Event Type

EventHandler<OutboxDeadLetteredEventArgs>

Delivered

Raised after Statements were delivered, or found already stored and equivalent at the LRS.

public event EventHandler<OutboxDeliveredEventArgs>? Delivered

Event Type

EventHandler<OutboxDeliveredEventArgs>

Paused

Raised when the LRS refused a request and the outbox paused (Paused).

public event EventHandler<OutboxPausedEventArgs>? Paused

Event Type

EventHandler<OutboxPausedEventArgs>