Calling
Your app reaches an outside service, such as a courier's API, through a connection: a named entry under connections: in app.jadpo that says which package to use, which of your domains may use it…
On this page
Your app reaches an outside service, such as a courier's API, through a
connection: a named entry under connections: in app.jadpo that
says which package to use, which of your domains may use it and which
credential it sends
(Connecting in app.jadpo). A domain is one
business area of your app, in its own folder under domains/. The
example below declares a connection named couriers.
There are two kinds of package (Kinds of package):
- A provider package describes one outside API at one version: its
operations, grouped into resources such as
bookings, and their types and failures. You call it through a connection. - A library package is plain shared code with no service behind it, such as a postcode parser. You call it through the package's name.
Calling a provider
You call a provider's operation as connection.resource.operation(input).
couriers.bookings.create(request) calls the create operation of the
bookings resource, through the connection couriers. A file brings the
connection in with use app { couriers }. A provider package's public
action is called the same way, as connection.action(input).
Only actions may call either. An action is code that may change things, such as writing records or calling outside, unlike a function, which only computes a value (Functions and actions).
A call can fail, so you write it with attempt or a match. attempt
passes a failure on to whoever called your action
(Passing a failure on: attempt).
After the call you list how each of the package's failures becomes one of
your app's own, written condition: Failure, as in
Turning one failure into another.
This sketch books a collection for a Shipment, a stored record of your
app that isn't shown, and saves the booking's id on it. The complete
example under A call, step by step compiles.
// domains/shipping/actions/book_collection.jadpoaction book_collection(shipment: Shipment) fails CollectionSlotGone -> Shipment { var request: AcmeCouriers.CreateBooking = AcmeCouriers.CreateBooking { postcode: shipment.postcode slot_id: shipment.slot } var booking: AcmeCouriers.Booking = attempt couriers.bookings.create(request) { slot_taken: CollectionSlotGone } return attempt Shipment.update(shipment) { set: { booking_id: booking.id } }}
slot_taken is the code of one of the failures the package declares for
the operation, under its failures:.
Calling a library package
A library package's functions are called through the package's name, with no connection, from anywhere a library may be called:
// domains/accounts/actions/save_address.jadpouse packages/postcodesvar postcode: Postcodes.UkPostcode = attempt Postcodes.uk.parse(input.postcode) { invalid_postcode: AddressInvalid}
Types
The types come from the package, and your app writes none. The checker compares what you send with what the operation takes. At runtime a response is checked only for the fields your code reads, so a provider adding or changing other fields breaks nothing. A package input may take any of your app's object types, sent as JSON, such as Postmark's template variables or Stripe's metadata.
Failures
Map a package's failure at the call to give it your app's meaning. A
package failure you don't map becomes a fault: a problem outside your
code that the runtime raises, which never appears in your fails list
(Faults). To change a package failure's code, not
just its message, map it to your app's own failure at the call.
The runtime also raises failures of its own on any outside call, whatever the package declares:
misconfiguredwhen the provider rejects the credential;unavailablewhen the provider surely did nothing;outcome_unknownwhen the provider may have acted.
A call that gets no answer in time is timed_out. You may map
timed_out to one of your own unavailable failures only on an
operation that is safe to send again
(Retrying a call, and timing out).
Idempotency keys
An idempotency key lets a provider recognise a request sent twice and act on it once. The package says where the key goes, and the runtime supplies it. You don't choose the key yourself.
Each call's key is built from what identifies this call. The action's key
is the client's Idempotency-Key header or the request's id in a route
(Idempotent writes), and the delivery key in
subscribers and schedules, which names the delivery being handled
(Delivery: at least once, safe to repeat).
The runtime never sends that key as it is. It hashes it with the
application, the environment, the connection, the caller's scope (who is
calling, so one person's key never matches another's), the call's step
number and a key generation. The result is jd1_ followed by 32 base32
characters of a SHA-256, 36 characters in all. Where the provider takes
fewer, it is cut to the package's max_length:.
So when two callers send the same Idempotency-Key, a provider that keys
requests by account never takes one for a repeat of the other.
Time limits and retries
By default a call may take 3 seconds (timeout:) and is tried up to 3
times within 30 seconds (retry: { attempts: within: }). The wait between
tries doubles each time, with some randomness (exponential backoff with
full jitter): at most 200 milliseconds before the second try and 400
before the third.
A route has 10 seconds by default, its deadline, which the route's
deadline: changes (Time limits). Three
3-second tries and their waits take less than 10 seconds, so a route
making one call needs no deadline: of its own. Retrying never runs past
the route's deadline: a wait that would end after it stops the retrying,
and the last answer stands. In background work, retrying stops at
within: or when the work's own time budget runs out, whichever comes
first.
Only safe repeats are retried: the request never left, the provider said
it did nothing (honouring Retry-After), or the call is idempotent.
Retrying a call, and timing out has
the details.
The package's rate limits space calls out before the provider refuses
them. max_concurrent: caps how many calls run at once per app
instance. Further calls wait, and a subscriber delivery that is waiting
is retried later
(How polls and max_concurrent: run).
Heavy work such as PDF rendering runs in subscribers, never in the
route.
Pages of results
A list operation returns a Page<T>
(Pages). The package says how the
provider pages, and page.next asks for the next page. Work over many
pages uses batches.each(from:, each:, finished:, key:), which pages
through in the background and resumes where it stopped
(Batches: work over many items).
Calls and writes
In an action, calls that change something come before any write. The writes commit after the last call returns. If the commit fails, the action runs again, and so do its calls, under the same keys. No transaction is open while a call is out. Reads before the call use a snapshot closed before the call, and a write to a record loaded before it checks the row is unchanged (Outside calls hold no transaction).
A changing call with no idempotency key must be the only changing call in its action. Several keyed calls follow Several changing calls in one action. Work that must follow a write emits an event (Emitting an event).
A call, step by step
Here an action calls an operation through the connection couriers,
declared in app.jadpo. The action is in shipping, a domain the
connection lists. It builds the package's own input type through the
package's name. It maps the package's slot_taken failure to the app's
CollectionSlotGone, a failure of the same kind (conflict). The
runtime's driver for the package's protocol, native code that speaks
JSON over HTTP, sends the call; how it is sent and read is under
Decoding.
use app { couriers }use shipping/failures { CollectionSlotGone }use shared/providers/acme_couriers/// Books a collection, mapping a slot the courier has given away to the/// shop's own failure.action book_collection( postcode: AcmeCouriers.Postcode, slot: AcmeCouriers.SlotId) fails CollectionSlotGone -> AcmeCouriers.Booking { var request: AcmeCouriers.CreateBooking = AcmeCouriers.CreateBooking { postcode: postcode slot_id: slot } return attempt couriers.bookings.create(request) { slot_taken: CollectionSlotGone }}
- Mapped at the call. Each arm of the call's
attemptis keyed by the code of one of the operation'sfailures:, and names the app's failure of the same kind. The action fails with the app's failure, and the provider's failure is kept as its internal cause, for the logs. A failure no arm maps is a fault when it comes, and is never passed on, so a package failure is never in the app'sfailslist. Amatchon the call handles the package's failures by name, as for a library function. - The runtime's own failures come as they would for any outside call.
misconfiguredmeans the provider rejected the credential, or the address is one the runtime won't send to. unavailablemeans the call can safely be made again. The provider surely did nothing: it answered408or429, or said the key is still in use, or the call never left. A call with an idempotency key that may have acted isunavailabletoo, since sending it again under the same key is safe.outcome_unknownmeans a call with no key may have acted: the provider answered with a server error, or didn't answer in time.- Sent once no transaction is open. The call goes out through the capability seam, the one place in the runtime that makes outside calls, where each call is logged and keyed. What the action has only read before the call is a snapshot, closed before the call goes out. Its writes run in a transaction opened after the call, which holds each record loaded before the call to the row version it was loaded at (Outside calls hold no transaction).
- A call after a write. A call made after the action's first write is
never sent: it is the fault
call_in_transaction.jadpo checkrefuses a write placed before a later call (data.write_before_call), so checked code never meets the fault. If a record changes while the call is out, the action runs again, and the call is repeated under its key. An item of a batch whoseeach:action calls a provider is a batch of its own (How a run is kept).
Retrying a call, and timing out
Here the connection sets timeout: 5s, so each try may take five
seconds, and retry:, which tries the courier up to four times within 20
seconds. Looking up a parcel changes nothing, so it is safe to send
again, and the action maps timed_out: at the call. A lookup the courier
never answers in time, on any try, is the shop's own
CourierUnavailable.
application { connections: { couriers: { package: AcmeCouriers.v1 domains: [shipping] credential: config.couriers_api_key environment: config.couriers_environment timeout: 5s retry: { attempts: 4 within: 20s } } }}
use app { couriers }use shipping/failures { CourierUnavailable }use shared/providers/acme_couriers/// Where a parcel is, or the shop's own failure when the courier doesn't/// answer in time.action parcel_status( parcel: AcmeCouriers.ParcelRef) fails CourierUnavailable -> AcmeCouriers.Status { var tracked_parcel: AcmeCouriers.Parcel = attempt couriers.parcels.get( AcmeCouriers.ParcelLookup { id: parcel } ) { timed_out: CourierUnavailable } return tracked_parcel.status}
- What is sent again. A call that never left, or that the provider
said did nothing (
408,429, its key still in use), whatever it changes; and one that may have acted (a server's error, no answer in time) only when it changes nothing or takes its package's key, sent again unchanged under the same key. A change with no key that may have acted is never sent again: it isoutcome_unknown. - How long it waits between. 200 milliseconds before the second
attempt, doubled before each after it, each wait drawn evenly from zero
to that (full jitter); a provider's
Retry-Afterin seconds instead, where it gives one. A wait that would end afterwithin:, or after what is left of the route's deadline or of background work's budget, ends the retrying, and the last answer stands. Each attempt is logged as a call of its own. timed_out:turns the runtime'stimed_outinto one of the app's own failures of kindunavailable, once every attempt has gone unanswered in time. The runtime'stimed_outis kept as its cause. You can map it only on an operation that changes nothing or isidempotent: true. Mapping it on an operation that changes something with no key is the errorfailure.unknown_mapping.- A timeout you don't map is
unavailablewhere the call was safe to send again, andoutcome_unknownwhere it wasn't.
Markdown for agents: /docs/calling.md.Built from Jadpo main at a707efb.