RocketMonsters.Heimdall.ServiceBus.Client
1.0.7
dotnet add package RocketMonsters.Heimdall.ServiceBus.Client --version 1.0.7
NuGet\Install-Package RocketMonsters.Heimdall.ServiceBus.Client -Version 1.0.7
<PackageReference Include="RocketMonsters.Heimdall.ServiceBus.Client" Version="1.0.7" />
<PackageVersion Include="RocketMonsters.Heimdall.ServiceBus.Client" Version="1.0.7" />
<PackageReference Include="RocketMonsters.Heimdall.ServiceBus.Client" />
paket add RocketMonsters.Heimdall.ServiceBus.Client --version 1.0.7
#r "nuget: RocketMonsters.Heimdall.ServiceBus.Client, 1.0.7"
#:package RocketMonsters.Heimdall.ServiceBus.Client@1.0.7
#addin nuget:?package=RocketMonsters.Heimdall.ServiceBus.Client&version=1.0.7
#tool nuget:?package=RocketMonsters.Heimdall.ServiceBus.Client&version=1.0.7
RocketMonsters.Heimdall.ServiceBus.Client
First-party .NET client for the Heimdall ServiceBus public dispatch API. Dispatch a typed subscriber message (e.g. a print job) to a provisioned device in two lines. Devices are addressed by their subscriber key — the operator-chosen, account-unique identifier shown in the Heimdall dashboard and on the device UI.
The payload can be a pre-serialized JsonElement or any object — the object overload serializes it with reflection
(member names verbatim); AOT-compiled consumers should pre-serialize and use the JsonElement constructor.
Install
dotnet add package RocketMonsters.Heimdall.ServiceBus.Client
Supported frameworks
.NET Core 3.1+, .NET 5+, and .NET Framework 4.6.2+ via the netstandard2.0 asset; a net10.0
asset is used automatically on .NET 10.
Usage
services.AddHeimdallServiceBus("token"); // registration
var result = await client.DispatchAsync(
new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson), ct); // usage
if (result.Succeeded && result.EventId is { } eventId)
{
// Polling cadence stays application-owned; this call performs one logical lookup.
var current = await client.GetDispatchStatusAsync(eventId, ct);
if (current.Succeeded)
Console.WriteLine($"{current.Snapshot!.Status} at {current.Snapshot.CompletedAt}");
}
The first argument is the account id — the numeric core.account.id of the target account (wire field accountId).
It is required: every dispatch names its target explicitly. Pass a sub-client account id to dispatch into the server
token's token subtree, or your own account id to dispatch to yourself. Your account id is shown in the Heimdall
dashboard.
An account id outside the token's subtree returns 404 dispatch.subscriber_not_provisioned — the same uniform response
as an unknown subscriber, so the surface cannot be used to enumerate accounts. A non-positive account id throws
ArgumentOutOfRangeException from the constructor, before any HTTP call.
ExternalId is an optional free-text tag you set to cross-reference your own events with the Heimdall event rows they
produced (wire field externalId). Set it as an init property or pass it as a trailing constructor argument —
existing 5-argument call sites are unaffected either way:
new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson)
{
ExternalId = "POS-ORDER-4471",
}
// or, equivalently
new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson, "POS-ORDER-4471")
The value is trimmed, and 20 characters after trimming is the limit. Null, empty, and whitespace-only are all the
same as omitting it: the property stores null and the field is left out of the request body entirely. A longer value
throws ArgumentOutOfRangeException from the property setter — including via message with { ExternalId = ... } —
before any HTTP call; the server enforces the same limit independently and returns 422 dispatch.external_id_too_long.
Heimdall attaches no meaning to the value, does not require it to be unique, and passes it through to the event row and
on to the device inbox.
TtlSeconds is an optional dispatch time-to-live, in whole seconds, minimum 1 (wire field ttlSeconds). Set it as an
init property:
new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson)
{
TtlSeconds = 30,
}
Omitted (null) means the event never expires — the field is left out of the request body entirely. A value less
than 1 throws ArgumentOutOfRangeException from the property setter — including via message with { TtlSeconds = ... }
— before any HTTP call. Expiry is measured from the event's server-side created_at, not from when the device
receives the message. An event that expires before delivery is reported back as the terminal status Expired and is
not printed.
Breaking change
AccountId used to be an optional init property that defaulted to the token's own account. It is now the required
first constructor argument, so every existing call site must add it:
// before
new PrintLayoutMessage("front-desk", "kitchen-1", "order-ticket", payloadJson)
// after — 4210 is your own account id if you are not targeting a sub-client
new PrintLayoutMessage(4210, "front-desk", "kitchen-1", "order-ticket", payloadJson)
The break is a compile error at every call site, never a silent change of target. The wire now always carries
accountId; the server has accepted that field since the token-subtree dispatch release and treats an explicit
home-account id exactly as it treated the omitted field.
AddHeimdallServiceBus targets https://heimdall.rocket-monsters.com by default; use the
Action<HeimdallServiceBusOptions> overload to override BaseUrl, MaxRetries, or
AttemptTimeout for staging/local or tuned resilience:
services.AddHeimdallServiceBus(o =>
{
o.Token = "token";
o.BaseUrl = "https://staging.example.test";
o.MaxRetries = 3; // default — additional attempts after the first
o.AttemptTimeout = TimeSpan.FromSeconds(15); // default — per-attempt time budget
});
Retry and never-throws contract
DispatchAsync does not throw for any HTTP-level outcome, transport failure, or attempt timeout — every one of those,
including an exhausted retry, is a DispatchResult. Check
DispatchResult.Succeeded and DispatchResult.Error. Cancellation of the caller's own
CancellationToken (mid-attempt or mid-backoff) always propagates.
A 2xx response whose body cannot be read as the accepted payload — a captive-portal HTML page returned with a 200/202, a
truncated body, or an exotic, disabled or unregistered response charset — does not throw and does not report
success. It returns
DispatchError(<status>, "invalid_response", null), because the eventId is the sole message identity and a success
carrying a null eventId would look like an accepted dispatch the caller can never poll for.
A transient failure — a connection-level exception, a per-attempt timeout, or an HTTP 408/429/5xx response — is retried
up to MaxRetries additional times with exponential backoff and full jitter (base 0.5s, doubling, 8s cap). A
Retry-After header on 429/503 is honored verbatim (capped at 8s, no jitter) in preference to the computed backoff.
Every other 4xx response (401, 403, 404, 413, 422, etc. — everything except 408/429) is returned immediately, unretried.
Once retries are exhausted: a transient HTTP response returns its ordinary HTTP-shaped result; a transport exception or
attempt-timeout returns DispatchResult.Error with Status = 0 and
Message "transport_error" or "timeout".
At-least-once, not exactly-once: retrying a request whose response was lost in transit (the server received and processed it, but the acknowledgement never arrived) may create a duplicate message. This is an accepted tradeoff for restaurant-network reliability — there is no idempotency key.
Dispatch outcomes
GetDispatchStatusAsync(eventId) returns one compact Current Snapshot and uses the same transport/timeout/HTTP retry
policy as DispatchAsync. It does not hide a polling loop or total poll timeout. A Completed status means the thermal
printer explicitly confirmed physical completion (or a non-thermal File/CUPS sink completed). SentUnconfirmed means
the dispatch is resolved and the queue may continue, but printer completion was disabled or its response was
irretrievably lost. WaitingForPrinter is non-terminal and includes the latest bounded printer diagnostic when
available.
Cadence is caller-owned — this client does not implement a poll loop or backoff schedule for status lookups — but it is
not unbounded: the lookup endpoint sits behind its own per-IP rate-limit policy, separate from the Heimdall public API's
general 30/min window, sized for a polling workload (120 requests/minute per IP at the time of writing; the server is
the source of truth for the current value). Exceeding it returns HTTP 429 with the standard
ErrorResponse(429, "rate_limited") envelope and a Retry-After header — GetDispatchStatusAsync
already treats a 429 as transient and honors Retry-After on its own retry path (see "Retry and never-throws contract"
above), so a caller polling faster than the limit will see individual lookups retried/delayed rather than fail outright,
up to MaxRetries. Callers polling multiple concurrent dispatches from the same outbound IP should budget their overall
cadence against this shared per-IP ceiling, not just per-dispatch.
A malformed or non-JSON 2xx response (e.g. a proxy or captive-portal page) never throws; it returns
DispatchStatusResult.Succeeded = false with DispatchError(200, "invalid_response", null).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.Http (>= 10.0.10)
- System.Net.Http.Json (>= 10.0.10)
- System.Text.Json (>= 10.0.10)
-
net10.0
- Microsoft.Extensions.Http (>= 10.0.10)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.