|
| 1 | +# Usage |
| 2 | + |
| 3 | +## Single provider |
| 4 | + |
| 5 | +When there is only one section per invocation point, no lift is needed — the |
| 6 | +dispatch short-circuits before the lift is ever called. Provide a no-op lift |
| 7 | +as a placeholder: |
| 8 | + |
| 9 | +```ts |
| 10 | +import { M } from '@endo/patterns'; |
| 11 | +import { makeDefaultExo } from '<internal>'; |
| 12 | +import { sheafify } from '@metamask/kernel-utils'; |
| 13 | + |
| 14 | +const noop = async function* (germs) { |
| 15 | + yield* germs; |
| 16 | +}; |
| 17 | + |
| 18 | +const priceGuard = M.interface('PriceService', { |
| 19 | + getPrice: M.callWhen(M.await(M.string())).returns(M.await(M.number())), |
| 20 | +}); |
| 21 | + |
| 22 | +const priceExo = makeDefaultExo('PriceService', priceGuard, { |
| 23 | + async getPrice(token) { |
| 24 | + return fetchPrice(token); |
| 25 | + }, |
| 26 | +}); |
| 27 | + |
| 28 | +const sheaf = sheafify({ |
| 29 | + name: 'PriceService', |
| 30 | + sections: [{ exo: priceExo }], |
| 31 | +}); |
| 32 | + |
| 33 | +const section = sheaf.getSection({ guard: priceGuard, lift: noop }); |
| 34 | +// section is a revocable dispatch exo; call it like any capability |
| 35 | +const price = await E(section).getPrice('ETH'); |
| 36 | +``` |
| 37 | + |
| 38 | +## Multiple providers with a lift |
| 39 | + |
| 40 | +When the stalk at a given invocation point contains more than one germ, the |
| 41 | +sheaf calls the lift to choose. The lift is an `async function*` coroutine that |
| 42 | +yields candidates in preference order; it receives accumulated errors as the |
| 43 | +argument to each subsequent `.next()` so it can adapt its ranking. |
| 44 | + |
| 45 | +The idiomatic pattern is a generator that `yield*`s candidates filtered by |
| 46 | +metadata, expressing priority tiers in source order: |
| 47 | + |
| 48 | +```ts |
| 49 | +import { sheafify, constant } from '@metamask/kernel-utils'; |
| 50 | +import type { Lift } from '@metamask/kernel-utils'; |
| 51 | + |
| 52 | +type WalletMeta = { mode: 'fast' | 'reliable' }; |
| 53 | + |
| 54 | +const preferFast: Lift<WalletMeta> = async function* (germs) { |
| 55 | + yield* germs.filter((g) => g.metadata?.mode === 'fast'); |
| 56 | + yield* germs.filter((g) => g.metadata?.mode === 'reliable'); |
| 57 | +}; |
| 58 | + |
| 59 | +const sheaf = sheafify<WalletMeta>({ |
| 60 | + name: 'Wallet', |
| 61 | + sections: [ |
| 62 | + { exo: fastExo, metadata: constant({ mode: 'fast' }) }, |
| 63 | + { exo: reliableExo, metadata: constant({ mode: 'reliable' }) }, |
| 64 | + ], |
| 65 | +}); |
| 66 | + |
| 67 | +// guard restricts which methods callers may invoke |
| 68 | +const section = sheaf.getSection({ guard: clientGuard, lift: preferFast }); |
| 69 | +``` |
| 70 | + |
| 71 | +The sheaf drives the generator: it primes it with `gen.next([])`, calls the |
| 72 | +chosen candidate, then passes any thrown errors back as `gen.next(errors)` so |
| 73 | +the lift can adapt before yielding the next candidate. |
| 74 | + |
| 75 | +Use the `constant`, `source`, or `callable` helpers to build metadata specs: |
| 76 | + |
| 77 | +```ts |
| 78 | +import { constant, source, callable } from '@metamask/kernel-utils'; |
| 79 | + |
| 80 | +// static value known at construction time |
| 81 | +constant({ mode: 'fast' }); |
| 82 | + |
| 83 | +// JS source string compiled once in the sheaf's compartment at construction time |
| 84 | +source(`(args) => ({ cost: args[0] > 9000 ? 'high' : 'low' })`); |
| 85 | + |
| 86 | +// live function evaluated at each dispatch — useful when cost varies by argument, |
| 87 | +// e.g. a swap whose metadata encodes volume-based cost tiers |
| 88 | +callable((args) => ({ cost: Number(args[0]) > 9000 ? 'high' : 'low' })); |
| 89 | +``` |
| 90 | + |
| 91 | +## Discoverable sections |
| 92 | + |
| 93 | +`getDiscoverableSection` works like `getSection` but the returned exo exposes |
| 94 | +its guard — it can be introspected by the caller to discover what methods and |
| 95 | +argument shapes it accepts. Use this when the recipient needs to advertise |
| 96 | +capability to a third party. It requires a `schema` map describing each method: |
| 97 | + |
| 98 | +```ts |
| 99 | +import type { MethodSchema } from '@metamask/kernel-utils'; |
| 100 | + |
| 101 | +const schema: Record<string, MethodSchema> = { |
| 102 | + getPrice: { description: 'Get the current price of a token.' }, |
| 103 | +}; |
| 104 | + |
| 105 | +const section = sheaf.getDiscoverableSection({ |
| 106 | + guard: clientGuard, |
| 107 | + lift, |
| 108 | + schema, |
| 109 | +}); |
| 110 | +``` |
| 111 | + |
| 112 | +`getSection` is the non-discoverable variant (no `schema` required). |
| 113 | + |
| 114 | +`getGlobalSection` and `getDiscoverableGlobalSection` derive the guard |
| 115 | +automatically from the union of all presheaf sections. They are `@deprecated` |
| 116 | +as a nudge toward explicit guards once the caller knows the section set — |
| 117 | +explicit guards make the capability's scope visible at the call site. When |
| 118 | +sections are assembled dynamically (e.g., rebuilt at runtime from a set of |
| 119 | +grants that changes) and the union guard isn't known until after `sheafify` |
| 120 | +runs, the global variants are the right choice. |
| 121 | + |
| 122 | +## Revocation |
| 123 | + |
| 124 | +```ts |
| 125 | +// revoke every granted section whose guard covers this invocation point |
| 126 | +sheaf.revokePoint('getPrice', 'ETH'); |
| 127 | + |
| 128 | +// revoke all granted sections at once |
| 129 | +sheaf.revokeAll(); |
| 130 | + |
| 131 | +// union guard of all currently active (non-revoked) sections |
| 132 | +const exported = sheaf.getExported(); |
| 133 | +``` |
| 134 | + |
| 135 | +## Remote sections |
| 136 | + |
| 137 | +`makeRemoteSection` wraps a CapTP remote reference as a `PresheafSection`, |
| 138 | +fetching the remote's guard once at construction and forwarding all calls via |
| 139 | +`E()`. This lets you mix local exos and remote capabilities in the same sheaf: |
| 140 | + |
| 141 | +```ts |
| 142 | +import { makeRemoteSection, constant } from '@metamask/kernel-utils'; |
| 143 | + |
| 144 | +const remoteSection = await makeRemoteSection( |
| 145 | + 'RemoteWallet', // name for the wrapper exo |
| 146 | + remoteCapRef, // CapTP reference |
| 147 | + constant({ mode: 'remote' }), // optional metadata |
| 148 | +); |
| 149 | + |
| 150 | +const sheaf = sheafify({ |
| 151 | + name: 'Mixed', |
| 152 | + sections: [localSection, remoteSection], |
| 153 | +}); |
| 154 | +``` |
| 155 | + |
| 156 | +## Lift composition |
| 157 | + |
| 158 | +`@metamask/kernel-utils` exports helpers for building lifts from composable |
| 159 | +parts, useful when lift logic would otherwise be duplicated across callers: |
| 160 | + |
| 161 | +```ts |
| 162 | +import { |
| 163 | + proxyLift, |
| 164 | + withFilter, |
| 165 | + withRanking, |
| 166 | + fallthrough, |
| 167 | +} from '@metamask/kernel-utils'; |
| 168 | +``` |
| 169 | + |
| 170 | +- **`withRanking(comparator, inner)`** — sort germs by comparator before |
| 171 | + passing to `inner` |
| 172 | +- **`withFilter(predicate, inner)`** — remove germs that fail `predicate` |
| 173 | + before passing to `inner` |
| 174 | +- **`fallthrough(liftA, liftB)`** — try all candidates from `liftA` first; |
| 175 | + if all fail, try `liftB` |
| 176 | +- **`proxyLift(inner)`** — forward yielded candidates up and error arrays down; |
| 177 | + useful when wrapping a lift in middleware |
0 commit comments