Runtime and stores
Create and close the FolioSdk runtime, mount stores, read state and send actions.
Preview
This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.
Every app that uses the Folio SDK works through two kinds of objects: one runtime, FolioSdk, and
the stores mounted on it. This page describes both and the contract every store shares.
The runtime orchestrates, stores own data access
FolioSdk creates the session, the network transport, the vault and the store loop. It returns no
data. Everything your app reads or changes goes through a store:
- To read something, mount the store that owns it and read its state.
- To change something, or to load more data, send an action to the store. The result arrives as a new state of the same store.
There is no call on the runtime that fetches data, and no call on a store that returns a result directly. A store's state is the only source your UI renders from.
FolioSdk has these members:
| Member | What it does |
|---|---|
create(config, mapper) | Static. Builds the runtime and restores or starts the session. Asynchronous. |
localizeCurrent(key) | Returns the text for a TextKey in the current SDK locale. See Localization. |
shutdown() | Unmounts every store, ends the session scope and stops the runtime. Asynchronous. |
close() | Runs shutdown() once and releases the runtime. Asynchronous; later calls wait for the same close. |
How each platform spells them:
| Member | Swift | Kotlin | TypeScript |
|---|---|---|---|
| Create | try await FolioSdk.create(config:mapper:) | FolioSdk.create(config, mapper), a suspend | await FolioSdk.create(config, mapper, options) |
| Localize | try sdk.localizeCurrent(key:) | sdk.localizeCurrent(key) | sdk.localizeCurrent(key) |
| Shut down | try await sdk.shutdown() | sdk.shutdown(), a suspend | await sdk.shutdown({ signal }) |
| Close | try await sdk.close() | sdk.close(), a suspend | await sdk.close(), also [Symbol.asyncDispose]() |
| Static functions | static func on FolioSdk | @JvmStatic members of the companion object | static methods of FolioSdk |
On Android, create is a suspend member of the companion object and, unlike the static
functions, carries no @JvmStatic.
Besides these, FolioSdk is the host every store mounts on: it implements SessionHost,
AuthHost, VaultHost, InquiryHost, MrtdHost, DiagnosticsHost, FolioDocumentListHost,
FolioDocumentHost and Localizing. Code that only needs part of the runtime takes it through
these interfaces. The inquiry UI, for example, runs on a mounted
InquiryStore; on iOS and Android its InquiryPlatform takes the runtime as the MrtdHost that
reads the NFC chip. See Inquiry UI.
Create the runtime
FolioSdk.create takes a FolioSdkConfig and your
IdentityRecordMapper. It completes only when the runtime is ready: it
initializes the platform services, loads the stored locale, and restores the stored session or
starts a new one with the Folio backend. A runtime you receive from create is ready to use; there
is no separate start step and no need to wait before mounting stores.
Create one runtime for your app and keep it for as long as you use the SDK. The runtime keeps the mapper for its whole lifetime.
create also sets the host of the flag URLs that the static country helpers return, from baseUrl
and for the whole process; see
Countries and phone numbers.
import FolioSDK
let sdk = try await FolioSdk.create(config: config, mapper: SavedIdentityMapper())On Android, create is a suspend function: call it from a coroutine. Cancelling that coroutine
cancels the creation. On the web, create takes an optional third argument, { signal }, with an
AbortSignal that cancels the creation; the promise then rejects with a DOMException named
AbortError.
Android
Call XPlatform.init(context) once per process before you create a runtime. XPlatform is in
the package id.folio.sdk.platform. It loads the SDK's native library; a second call throws
"XPlatform already initialized", so call it from your Application.onCreate.
XPlatform.isInitialized tells whether it already ran. See Android setup.
In Compose, rememberFolioSdk(config, mapper) creates the runtime for you. It is scoped to the
ViewModelStoreOwner of the composition and returns a FolioSdkStartup:
FolioSdkStartup | Meaning |
|---|---|
Starting | create is still running. |
Ready | The runtime is ready. runtime holds the FolioSdk. |
Failed | create failed. error holds the Throwable it failed with. retry() runs create again. |
rememberFolioSdk closes the runtime when its ViewModel is cleared. When you pass a different
config (compared by value) or a different mapper (compared by identity), it closes the current
runtime and creates a new one once the old one has finished closing.
setContent {
when (val startup = rememberFolioSdk(config, SavedIdentityMapper)) {
FolioSdkStartup.Starting -> Unit
is FolioSdkStartup.Failed -> Button(onClick = { startup.retry() }) { Text("Retry") }
is FolioSdkStartup.Ready -> FolioSDKProvider(startup.runtime) { Inquiry() }
}
}Web
The web package runs the SDK as WebAssembly. Load it once per page with initializeFolioWasm
before you create the runtime. source identifies the WASM you load, application is your web
application's identity (see Application identity) and
load returns the bytes; loadFolioSDKWasm loads them from the manifest that ships with the
package, whose file name is FolioSDKWasmArtifact.manifestFilename. Web setup
describes where to serve the files.
import { FolioSdk, FolioSDKWasmArtifact, initializeFolioWasm, loadFolioSDKWasm } from '@folio/sdk';
const manifestUrl = `/${FolioSDKWasmArtifact.manifestFilename}`;
await initializeFolioWasm({
source: manifestUrl,
application: { identifier: window.location.origin, version: '1.0.0', build: '1' },
load: () => loadFolioSDKWasm({ manifestUrl }),
});
const sdk = await FolioSdk.create(config, savedIdentityMapper);initializeFolioWasm loads the WASM only once per page:
- A second call with the same
sourcereturns the same promise. - A call with a different
sourcerejects with "Folio WASM is already loaded from ...". - If loading fails, the next call tries again.
- An
applicationfield that is missing or blank rejects with anErrorthat names the field, such asxplatform: application.version must be a non-empty string.
A FolioSdk.create call before the WASM is loaded rejects with the Error "WASM not initialized".
Providers
Each platform has a provider that hands one runtime to the views below it, and helpers that mount a store for the lifetime of a view.
On iOS, FolioSDKProvider(runtime:content:) is a SwiftUI view that puts the runtime into the
environment. FolioSdk is an ObservableObject, so a view below reads it with
@EnvironmentObject.
FolioSDKProvider(runtime: sdk) {
RecordsView()
}
struct RecordsView: View {
@EnvironmentObject var sdk: FolioSdk
var body: some View {
RecordList(runtime: sdk)
}
}Close the runtime
shutdown() ends the runtime:
- Every mounted store is unmounted. Every open subscription of a store ends: its
next()returns no value. - The session scope is shut down.
- The store loop stops and the log journal is flushed.
shutdown()returns when both have finished, and fails with aCoreErrorwhen the store loop ended with an error or the journal could not be flushed (LogsUnavailable).
close() runs shutdown() once and then releases the runtime, also when shutdown() failed.
Every call of close() waits for that same close and ends with its result. Close the runtime when
your app no longer needs it, and close your own stores before that. After close(), every call on
the runtime throws.
try await sdk.close()Start failures
create fails when the runtime cannot start. Nothing is created in that case; fix the cause and
call create again. The call throws the platform's SDK error: FolioSDKError on iOS,
FolioSDKException on Android, and a promise rejected with FolioSDKError on the web. Its
message is the text of a SessionStartError:
| Case | Fields | When | Message starts with |
|---|---|---|---|
Backend | status (HTTP status), code, message | The Folio backend rejected the session start or refresh. code and message come from it. | backend rejected authentication |
Network | message | A request got no HTTP response, for example when the device is offline. | backend unreachable |
Scope | message | The services of the session's identity could not be started. | session scope is unavailable |
Core | error (CoreError) | A local failure: invalid configuration, missing platform services or unavailable storage. | the text of the CoreError |
The same SessionStartError is available as a value in SessionUiModel.error; see
Session for its spelling on each platform.
The most common Core cases from create:
CoreError | Cause |
|---|---|
InvalidInput | organization or an entry of serviceEnvironments in the configuration is not valid, or your application's version or build cannot be sent in a header. See Configuration. |
NotInitialized | The platform services are not available, for example because XPlatform.init did not run on Android or the platform does not report your application's identifier, version or build. |
StorageUnavailable | The device storage could not be read or written. |
LogsUnavailable | The log journal configured in FolioSdkConfig.logging could not be started, or could not be stopped after create failed for another reason; then it replaces that error. See Logging. |
On Android, rememberFolioSdk reports the failure as FolioSdkStartup.Failed. See
Errors for every error type of the SDK.
Stores
A store exists only while it is mounted. You mount it on the runtime, read its state, send it actions, and close it when the screen or feature that uses it goes away. You can mount as many stores as you need, including several instances of the same store.
| Store | Mount on | State | Action |
|---|---|---|---|
SessionStore | SessionHost | SessionUiModel | SessionAction |
AuthStore | AuthHost | AuthUiModel | AuthAction |
VaultStore | VaultHost | VaultUiModel | VaultAction |
InquiryStore | InquiryHost | InquiryUiModel | InquiryAction |
MrtdStore | MrtdHost | MrtdUiModel | MrtdAction |
LogsStore | DiagnosticsHost | LogsUiModel | LogsAction |
FolioDocumentListStore | FolioDocumentListHost | FolioDocumentListUiModel | FolioDocumentListAction |
FolioDocumentStore | FolioDocumentHost | FolioDocumentUiModel | FolioDocumentAction |
FolioSdk implements every host in the table, so you mount each store on your FolioSdk. Two
stores take a mount parameter: LogsStore the LogFilter of the journal it reads,
LogsStore.mount(runtime, init), and FolioDocumentStore the FolioDocumentInit with the id
of its document, FolioDocumentStore.mount(runtime, init). See
Folio documents.
Two more objects mount on DiagnosticsHost but are not stores: they have no state and no actions.
InteractionRecorder.mount(runtime) records the user's interactions in the SDK's log journal, and
SdkLogger.mount(runtime, target) writes your own messages to it. See Logging.
Store members
Every store has the same members:
| Member | What it does |
|---|---|
mount(runtime) | Static. Mounts a new store instance on the runtime and reads its first state. Throws on failure. |
state | The current state. Never empty while the store is mounted. |
dispatch(action) | Queues one action. Returns as soon as the action is queued; the outcome arrives as a new state. |
subscribe() | Opens a subscription that yields the store's states one by one. |
close() | Unmounts the store and cancels the work it still has running. |
How each platform exposes them:
| Member | Swift | Kotlin | TypeScript |
|---|---|---|---|
| Mount | try InquiryStore.mount(runtime: sdk) | InquiryStore.mount(sdk) | InquiryStore.mount(sdk) |
| State | @Published property state, publisher $state | state: StateFlow<InquiryUiModel>, current value state.value | Getter state |
| Dispatch | try store.dispatch(action: .close) | store.dispatch(InquiryAction.Close) | store.dispatch(InquiryAction.close) |
| Subscribe | try store.subscribe() returns an InquirySubscription | store.subscribe() returns an InquirySubscription | store.subscribe() returns an InquirySubscription; store.subscribe(listener) registers a listener and returns its remover |
| Close | store.close(), also on deinit | store.close(), the store is AutoCloseable | store.close(), also [Symbol.dispose]() |
dispatch takes an optional second argument, a LogContext, that ties the action's journal
entries to an interaction; see Logging.
On iOS, state updates arrive on the main actor, and every store is an ObservableObject. On
Android, state is a StateFlow you can collect in Compose with collectAsState(). On the web, a
listener passed to subscribe takes no arguments; read store.state inside it.
On iOS and Android the store keeps state current through a subscription of its own, opened at
mount; on the web it opens one while a listener is subscribed. If that stream fails, the store
keeps the error: streamError, a @Published Error? on iOS and a StateFlow<Throwable?> on
Android; on the web reading state throws it.
let vault = try VaultStore.mount(runtime: sdk)
let records = vault.state.recordsSubscriptions
subscribe() takes no arguments and returns a subscription. Its asynchronous next() returns a
state each time you call it:
- The first
next()returns the current state, every later one the next changed state. When the state changes several times between two calls,next()returns only the latest state. - After the store is unmounted, by
close()or byshutdown()of the runtime,next()returns no value:nilin Swift,nullin Kotlin andundefinedon the web. - Holding a subscription does not keep the store mounted. Release the subscription when you no longer read it; the store stays mounted.
Each store has its own subscription type, named after the store: InquirySubscription,
VaultSubscription and so on. Iterating a subscription releases it when the iteration ends.
| Platform | next() | Release | Iterate |
|---|---|---|---|
| Swift | try await subscription.next() | release(), also on deinit | for try await state in subscription |
| Kotlin | subscription.next(), a suspend | close(), it is AutoCloseable | subscription.asFlow() |
| Web | await subscription.next({ signal }) | dispose() or [Symbol.dispose]() | for await (const state of subscription) |
let subscription = try vault.subscribe()
for try await state in subscription {
render(state.records)
}Locale changes
A store's state carries text that is already localized. When the locale saved in the SDK changes, for example after a language pick in the inquiry, every mounted store publishes its state again in the new language. You do not need to reload anything. A change of the device language while the runtime runs does not make the stores publish again. See Localization.
Store errors
mount, dispatch and subscribe fail with a CoreError. The call throws the platform's SDK
error, FolioSDKError on iOS and the web and FolioSDKException on Android, whose message is
the text of the CoreError.
CoreError | Meaning |
|---|---|
Unmounted | The store is no longer mounted because its runtime was shut down. For a store you closed, see below. |
StoreOverflow | The action could not be queued: the runtime's action queue, shared by all stores mounted on it, is full, or the runtime has stopped. |
InvalidInput | A value you passed cannot be decoded, or the LogContext passed to dispatch belongs to another runtime; see Logging. |
ArenaFull | No more stores can be mounted on this runtime. |
SourceUnavailable | The store's last state was computed from inputs that are no longer active and no newer state is ready yet. Reported by a state read (mount, and on the web reading state while no listener is subscribed) and by dispatch while the store's inputs are revoked and no renewed state has been accepted yet; the store stays mounted, and dispatch succeeds again once it has taken the renewed state. |
The action queue holds at most 256 actions. Actions of InquiryStore and MrtdStore do not wait
in it: they go straight to the store loop, so a full queue never rejects them. The one exception is
the inquiry's camera frame, the intent PreviewFrame: it waits in the queue, and when the queue is
full the frame is dropped and dispatch does not throw.
After close(), dispatch and subscribe throw: FolioSDKError with the message "Handle
already released" on iOS and IllegalStateException with the message "Handle has been closed" on
Android. On the web, dispatch and subscribe() throw an Error such as
VaultStore has been disposed, while state keeps returning the last state and
subscribe(listener) returns a remover that does nothing. close() itself can be called more
than once.
A failure of the work behind an action, such as a rejected request, is not thrown from
dispatch. It appears in the store's state. Each store page describes where.
Static functions
The SDK also has functions that need no runtime, such as text lookup and the country list. They are
static members of FolioSdk, so you call them on the class, not on an instance. In Swift every one
of them is declared throws.
| Swift | Kotlin | TypeScript |
|---|---|---|
try FolioSdk.localize(key:locale:) | FolioSdk.localize(key, locale) | FolioSdk.localize(key, locale) |
try FolioSdk.localizeWithArgs(key:locale:args:) | FolioSdk.localizeWithArgs(key, locale, args) | FolioSdk.localizeWithArgs(key, locale, args) |
try FolioSdk.localizePlural(group:count:locale:) | FolioSdk.localizePlural(group, count, locale) | FolioSdk.localizePlural(group, count, locale) |
try FolioSdk.localizeCount(group:count:locale:) | FolioSdk.localizeCount(group, count, locale) | FolioSdk.localizeCount(group, count, locale) |
try FolioSdk.localizeWithCount(key:group:count:locale:) | FolioSdk.localizeWithCount(key, group, count, locale) | FolioSdk.localizeWithCount(key, group, count, locale) |
try FolioSdk.formatFileSize(bytes:locale:) | FolioSdk.formatFileSize(bytes, locale) | FolioSdk.formatFileSize(bytes, locale) |
try FolioSdk.availableLocales() | FolioSdk.availableLocales() | FolioSdk.availableLocales() |
try FolioSdk.countries() | FolioSdk.countries() | FolioSdk.countries() |
try FolioSdk.country(code:) | FolioSdk.country(code) | FolioSdk.country(code) |
try FolioSdk.parseCountry(code:) | FolioSdk.parseCountry(code) | FolioSdk.parseCountry(code) |
try FolioSdk.countryFlag(country:) | FolioSdk.countryFlag(country) | FolioSdk.countryFlag(country) |
try FolioSdk.countryByPhone(phone:) | FolioSdk.countryByPhone(phone) | FolioSdk.countryByPhone(phone) |
try FolioSdk.subdivisions(country:) | FolioSdk.subdivisions(country) | FolioSdk.subdivisions(country) |
try FolioSdk.phoneDialCode(country:) | FolioSdk.phoneDialCode(country) | FolioSdk.phoneDialCode(country) |
try FolioSdk.formatPhoneNational(raw:country:) | FolioSdk.formatPhoneNational(raw, country) | FolioSdk.formatPhoneNational(raw, country) |
try FolioSdk.phoneCountryOptions() | FolioSdk.phoneCountryOptions() | FolioSdk.phoneCountryOptions() |
try FolioSdk.currencies() | FolioSdk.currencies() | FolioSdk.currencies() |
try FolioSdk.addressLayout(format:locale:) | FolioSdk.addressLayout(format, locale) | FolioSdk.addressLayout(format, locale) |
try FolioSdk.addressForm(address:fallbackCountryCode:locale:) | FolioSdk.addressForm(address, fallbackCountryCode, locale) | FolioSdk.addressForm(address, fallbackCountryCode, locale) |
try FolioSdk.addressEmpty() | FolioSdk.addressEmpty() | FolioSdk.addressEmpty() |
try FolioSdk.addressChangeCountry(address:country:) | FolioSdk.addressChangeCountry(address, country) | FolioSdk.addressChangeCountry(address, country) |
try FolioSdk.addressChangePart(address:key:value:) | FolioSdk.addressChangePart(address, key, value) | FolioSdk.addressChangePart(address, key, value) |
try FolioSdk.addressDisplay(address:) | FolioSdk.addressDisplay(address) | FolioSdk.addressDisplay(address) |
try FolioSdk.applyDateInputMask(input:format:) | FolioSdk.applyDateInputMask(input, format) | FolioSdk.applyDateInputMask(input, format) |
try FolioSdk.decodeLink(link:) | FolioSdk.decodeLink(link) | FolioSdk.decodeLink(link) |
try FolioSdk.isValidEmail(value:) | FolioSdk.isValidEmail(value) | FolioSdk.isValidEmail(value) |
try FolioSdk.extractRecoveryCode(text:) | FolioSdk.extractRecoveryCode(text) | FolioSdk.extractRecoveryCode(text) |
try FolioSdk.loggingConfig(consoleLevel:) | FolioSdk.loggingConfig(consoleLevel) | FolioSdk.loggingConfig(consoleLevel) |
try FolioSdk.mrtdDataGroupsStandard() | FolioSdk.mrtdDataGroupsStandard() | FolioSdk.mrtdDataGroupsStandard() |
try FolioSdk.mrtdDataGroupsAll() | FolioSdk.mrtdDataGroupsAll() | FolioSdk.mrtdDataGroupsAll() |
try FolioSdk.inquiryTheme() | FolioSdk.inquiryTheme() | FolioSdk.inquiryTheme() |
try FolioSdk.barcodeImage(barcodeType:value:) | FolioSdk.barcodeImage(barcodeType, value) | FolioSdk.barcodeImage(barcodeType, value) |
try FolioSdk.computeMetrics(format:data:width:height:strideYOpt:) | FolioSdk.computeMetrics(format, data, width, height, strideYOpt) | FolioSdk.computeMetrics(format, data, width, height, strideYOpt) |
try FolioSdk.rotateImage(argbData:width:height:direction:) | FolioSdk.rotateImage(argbData, width, height, direction) | FolioSdk.rotateImage(argbData, width, height, direction) |
try FolioSdk.sdkBuildInfo() | FolioSdk.sdkBuildInfo() | FolioSdk.sdkBuildInfo() |
try FolioSdk.iconAsset(icon:) | FolioSdk.iconAsset(icon) | FolioSdk.iconAsset(icon) |
try FolioSdk.statusPalette() | FolioSdk.statusPalette() | FolioSdk.statusPalette() |
iconAsset returns the asset name of an Icon, such as bed for Bed, and statusPalette the
colors of the document status styles, for an app that draws a document card itself. The
Folio documents stores need neither of them.
Your app finds an icon image by its asset name:
On iOS, the images are template images in the resource bundle of the FolioInquiryUI library,
FolioResourceAssets.bundle. Each is named resource- followed by the asset name, such as
resource-bed. Link FolioInquiryUI to use them.
import FolioInquiryUI
import FolioSDK
import SwiftUI
let name = try FolioSdk.iconAsset(icon: .bed)
let icon = Image("resource-" + name, bundle: FolioResourceAssets.bundle)Where each group is described:
- Text, locales, countries, phone numbers, addresses, date input masks and links: Localization, with the exact signatures.
currenciesreturns theCurrencyCatalog, whoseitemshold oneCurrencyInfo { currency, name, minorUnitDigits }perCurrency.nameis aLocalizedText: itsvaluesmap a locale to the name, with the ISO 4217 name under the key_.minorUnitDigitsis absent when ISO 4217 assigns the currency no minor unit.isValidEmailchecks the shape of an email address, ignoring surrounding whitespace.extractRecoveryCodereturns the first recovery code (four groups of four letters or digits joined by hyphens, read in upper case) found in a text, or an empty string.loggingConfig: Logging.mrtdDataGroupsStandardandmrtdDataGroupsAll: NFC chip reading.inquiryTheme: Web setup.sdkBuildInfo: Versions and build info.
Image helpers
Three functions work on images without a runtime. When the input is not valid, they fail with an
ImageProcessingError, thrown as the platform's SDK error.
| Function | Returns | What it does |
|---|---|---|
barcodeImage(barcodeType, value) | ImageContent | Encodes value as a barcode of the BarcodeType and returns it as a PNG, one pixel per module and without a quiet zone: scale it up without smoothing and draw the light border yourself. |
computeMetrics(format, data, width, height, strideYOpt) | ImageMetrics | Measures the luminance of a camera frame in the PixelFormat RGBA, NV12 or NV21: mean brightness, variance, standard deviation, median, minimum, maximum and dynamic range. A strideYOpt of 0 means the default row stride. |
rotateImage(argbData, width, height, direction) | RotatedImageResult | Rotates ARGB pixel data, four bytes per pixel, by a RotationDirection. The result is an object: data(), width() and height() return the rotated pixels and their size. |
BarcodeType is QR, CODE128, PDF417, AZTEC, DATA_MATRIX, CODE39, CODE93, CODABAR,
INTERLEAVED2_OF5, EAN13, EAN8, UPC_A or UNKNOWN. QR carries its error correction
level, a QrLevel: L, M, Q or H. Swift writes the cases in lower camel case:
.qr(level: .m), .dataMatrix, .upcA. Kotlin uses the sealed class BarcodeType:
BarcodeType.Qr(QrLevel.M), BarcodeType.DataMatrix, BarcodeType.UpcA. TypeScript uses the
tagged union with the type values above and the builders BarcodeType.qr(QrLevel.M) and
BarcodeType.dataMatrix. barcodeImage throws for an empty value, for UNKNOWN and for a value
the barcode type cannot encode.
RotationDirection is NONE, CLOCKWISE90, COUNTER_CLOCKWISE90 or ROTATE180 (.none,
.clockwise90, .counterClockwise90 and .rotate180 in Swift). Release a
RotatedImageResult when you have read it: release() in Swift, close() in Kotlin and
dispose() on the web.