@mpgd/game-services 0.30.4 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/client.d.ts +16 -2
- package/dist/client.js +28 -3
- package/dist/commerce-reconciler.d.ts +53 -3
- package/dist/commerce-reconciler.js +240 -14
- package/dist/contract.d.ts +6 -6
- package/dist/monetization-recovery.js +1 -1
- package/dist/orpc-error-compat.d.ts +42 -0
- package/dist/orpc-error-compat.js +102 -0
- package/dist/request-headers.d.ts +22 -0
- package/dist/request-headers.js +34 -1
- package/dist/runtime.d.ts +70 -2
- package/dist/runtime.js +340 -10
- package/dist/server.d.ts +15 -6
- package/dist/server.js +26 -3
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -183,6 +183,37 @@ and retry policy are not redesigned by this observer API.
|
|
|
183
183
|
The root `pnpm test` pipeline includes compiled client JS and public declaration
|
|
184
184
|
consumption checks alongside the client and target conformance suites.
|
|
185
185
|
|
|
186
|
+
## oRPC version and error compatibility
|
|
187
|
+
|
|
188
|
+
The oRPC transport (`createGameServicesOrpcClient`, `createGameServicesRouter`,
|
|
189
|
+
`createGameServicesRpcFetchHandler`) is built on oRPC `2.0.0-beta.46`, pinned
|
|
190
|
+
exactly. Pin the same exact version if a game or Worker also uses oRPC directly.
|
|
191
|
+
|
|
192
|
+
oRPC 2.0.0-beta.34 removed the `inferable` flag from RPC error bodies, and each
|
|
193
|
+
side rejects the other's shape: an older client would see a malformed response
|
|
194
|
+
instead of `BAD_REQUEST` or `NOT_FOUND`, and so would a newer client talking to
|
|
195
|
+
an older Worker. Games and Workers deploy separately, so this package bridges
|
|
196
|
+
the change in `src/orpc-error-compat.ts`:
|
|
197
|
+
|
|
198
|
+
- `createGameServicesRpcFetchHandler` adds `inferable: false` to oRPC error
|
|
199
|
+
bodies, so clients from `@mpgd/game-services` 0.30.5 or older (oRPC
|
|
200
|
+
2.0.0-beta.14) still decode error codes;
|
|
201
|
+
- `createGameServicesOrpcClient` (0.31.0 or later) removes a boolean `inferable`
|
|
202
|
+
from error bodies before oRPC validates them, so it still decodes errors from
|
|
203
|
+
Workers on 0.30.5 or older.
|
|
204
|
+
|
|
205
|
+
Success responses are unchanged. Any other oRPC 2.0.0-beta.34+ client that calls
|
|
206
|
+
a game-services Worker's `/rpc` route (for example a plain `RPCLink` in a game)
|
|
207
|
+
must wrap its fetch with `withLegacyOrpcErrorCompat` from
|
|
208
|
+
`@mpgd/game-services/client`, or it sees the legacy flag as a malformed error. A
|
|
209
|
+
Worker that no longer serves clients from 0.30.5 or older can pass
|
|
210
|
+
`legacyOrpcErrorFlag: false` to `createGameServicesRpcFetchHandler`. The shim is
|
|
211
|
+
removed once the oldest supported game client and every supported Worker come
|
|
212
|
+
from `@mpgd/game-services` 0.31.0 or later (oRPC 2.0.0-beta.46). Other oRPC changes picked up with it: RPC routes accept
|
|
213
|
+
only POST, PUT, PATCH, and DELETE (a GET answers 404, and the kit clients always
|
|
214
|
+
POST), and `Infinity` inputs are encoded with their own meta marker, which a Worker
|
|
215
|
+
on 0.30.5 or older (oRPC 2.0.0-beta.14) cannot decode.
|
|
216
|
+
|
|
186
217
|
## Runtime backend transport
|
|
187
218
|
|
|
188
219
|
`createGameServicesRuntime` accepts `httpTransport` for its HTTP JSON backend
|
|
@@ -233,6 +264,71 @@ them into analytics or logs.
|
|
|
233
264
|
All published entrypoints use explicit internal ESM module paths and are smoke-tested
|
|
234
265
|
with native Node imports, without a bundler or TypeScript runtime loader.
|
|
235
266
|
|
|
267
|
+
## One player per runtime
|
|
268
|
+
|
|
269
|
+
A runtime, its purchase journal records, and its reconciler belong to
|
|
270
|
+
`playerId` for their whole life, while `getHeaders` and a store provider's
|
|
271
|
+
account binding follow the live session. When the session can change player
|
|
272
|
+
within a run (an account switch), pass `sessionPlayerId: () => session.current()?.playerId`:
|
|
273
|
+
|
|
274
|
+
- It is read before every backend request and again once that request's
|
|
275
|
+
headers are resolved, and by the commerce reconciler before each pass,
|
|
276
|
+
queued event, record step, store read, restore, and finish, and right
|
|
277
|
+
before a store UI opens.
|
|
278
|
+
- Another player closes the runtime for good: nothing more is verified,
|
|
279
|
+
restored, or finished for this player with the other player's credentials
|
|
280
|
+
(a backend answers that with a permanent account-mismatch rejection), and
|
|
281
|
+
no store UI opens for a purchase journaled here once the session names
|
|
282
|
+
someone else. The provider resolves its account binding a few steps after
|
|
283
|
+
that last check, so the binding resolver must follow the runtime's player
|
|
284
|
+
too (bind nothing for anyone else, as the generated game's
|
|
285
|
+
`pinStarterCommercePlayer` does) for a purchase never to be
|
|
286
|
+
journaled here and bound to the other account; a binding refused while the
|
|
287
|
+
session changed keeps the attempt retryable. Purchases
|
|
288
|
+
answer `failed` with the retryable diagnostic `GAME_SERVICES_PLAYER_CHANGED`
|
|
289
|
+
before any store UI, rewarded-ad claims answer `unavailable` before any
|
|
290
|
+
ad, recovery runs nothing, and Restore Purchases rejects with
|
|
291
|
+
`GameServicesRuntimeClosedError`.
|
|
292
|
+
- No player (signed out) refuses the same way with
|
|
293
|
+
`GAME_SERVICES_SESSION_REQUIRED` and leaves the runtime open for its player.
|
|
294
|
+
|
|
295
|
+
Refusals end no journal record: a runtime created for the player later
|
|
296
|
+
resumes them. `runtime.dispose()` closes a runtime the same way
|
|
297
|
+
(`GAME_SERVICES_RUNTIME_DISPOSED`) and resolves once a purchase whose store
|
|
298
|
+
sheet is open has journaled its answer, a recovery pass under way
|
|
299
|
+
(`monetizationRecovery.reconcile()`) has finished, and the reconciler has
|
|
300
|
+
stopped (each wait bounded by `gameServicesDisposeWaitMs`, 3 minutes); call
|
|
301
|
+
it, and wait, before creating the runtime for another player or reloading.
|
|
302
|
+
A recovery pass that outlives that wait starts no store subscription
|
|
303
|
+
afterwards. A custom
|
|
304
|
+
`httpTransport` must send only the headers the runtime resolved for each
|
|
305
|
+
request, or the after-resolution check cannot cover it. A refusal keeps its
|
|
306
|
+
`GameServicesRuntimeClosedError` through header resolution and the HTTP
|
|
307
|
+
transport (it is not turned into `GameServicesHeaderResolutionError` or
|
|
308
|
+
`GameServicesBackendTransportError`). A reconciler started while the session
|
|
309
|
+
is signed out still subscribes to the store; its work waits for the session.
|
|
310
|
+
|
|
311
|
+
Pass `commerceAccountBinding` too: this player's store account binding,
|
|
312
|
+
derived from `playerId` the way the store provider binds the player's new
|
|
313
|
+
purchases (Google Play's obfuscated account id, StoreKit's app account
|
|
314
|
+
token). The reconciler then leaves a store purchase bound to another account
|
|
315
|
+
alone (reported, never journaled or verified for this player). After a
|
|
316
|
+
restart as another player, Google Play still lists the previous player's
|
|
317
|
+
unacknowledged purchases; sending one with this player's session would be
|
|
318
|
+
rejected for good, and its derived key would then block the other player's
|
|
319
|
+
own recovery. The player's explicit Restore Purchases still takes a durable
|
|
320
|
+
bound to another account (restore sharing). StoreKit's provider already
|
|
321
|
+
filters transactions by app account token the same way.
|
|
322
|
+
|
|
323
|
+
`dispose()` is a required member of `GameServicesRuntime`: a runtime built by
|
|
324
|
+
hand (a test fake, a custom wrapper) adds `dispose: async () => undefined`.
|
|
325
|
+
|
|
326
|
+
A game adopting this creates its runtime from its server session's player
|
|
327
|
+
(never a platform placeholder), passes `sessionPlayerId` and
|
|
328
|
+
`commerceAccountBinding`, keeps its store account binding resolver on the
|
|
329
|
+
same player (bind nothing for anyone else), and disposes the runtime (and
|
|
330
|
+
waits) before it creates one for another player.
|
|
331
|
+
|
|
236
332
|
## Guest session and account binding foundation
|
|
237
333
|
|
|
238
334
|
`createGuestSessionCoordinator` from `@mpgd/game-services/guest-session`
|
package/dist/client.d.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { type AnalyticsSink } from '@mpgd/analytics';
|
|
|
2
2
|
import type { LeaderboardScoreInput, PlatformGateway, PlatformTarget } from '@mpgd/platform';
|
|
3
3
|
import type { CommerceServiceCapabilities } from '@mpgd/platform/commerce';
|
|
4
4
|
import { type ClientRewardEvidenceRegistry } from './client-reward-evidence.js';
|
|
5
|
-
import { type CommerceReconciler, type CommerceReconcilerEvent, type CommerceReconcilerProduct } from './commerce-reconciler.js';
|
|
5
|
+
import { type CommerceReconciler, type CommerceReconcilerEvent, type CommerceReconcilerProduct, type CreateCommerceReconcilerInput } from './commerce-reconciler.js';
|
|
6
6
|
import type { MonetizationOperationStore } from './monetization-recovery.js';
|
|
7
7
|
import type { GameServicesOperationClient } from './operations.js';
|
|
8
8
|
export type { GameServicesOperationClient, GameServicesPurchaseInput, GameServicesPurchaseResult, GameServicesRewardedAdInput, GameServicesRewardedAdResult, } from './operations.js';
|
|
9
9
|
import type { GameServicesContractClient } from './contract.js';
|
|
10
|
+
export { withLegacyOrpcErrorCompat } from './orpc-error-compat.js';
|
|
10
11
|
import { type GameServicesHeaderResolver } from './request-headers.js';
|
|
11
12
|
import type { ClaimAdRewardRequest, ClaimAdRewardResponse, GameServicesLedgerTarget, PurchaseLedgerStatusRequest, PurchaseLedgerStatusResponse, RecordLeaderboardScoreRequest, RecordLeaderboardScoreResponse, VerifyPurchaseRequest, VerifyPurchaseResponse } from './types.js';
|
|
12
13
|
export type { GameServicesOperationKind, GameServicesOperationLocation, GameServicesOperationOptions, GameServicesOperationProgress, GameServicesPurchaseProgress, GameServicesRewardedAdProgress, } from './operation-progress.js';
|
|
@@ -133,8 +134,21 @@ export interface CreateGameServicesClientInput {
|
|
|
133
134
|
readonly warn?: (message: string) => void;
|
|
134
135
|
/** Catalog kinds and SKUs the reconciler resolves without a store read. */
|
|
135
136
|
readonly commerceProducts?: readonly CommerceReconcilerProduct[];
|
|
137
|
+
/** The reconciler's `staleLaunchedAfterMs` (24 hours by default). */
|
|
138
|
+
readonly commerceStaleLaunchedAfterMs?: number;
|
|
136
139
|
readonly onCommerceEvent?: (event: CommerceReconcilerEvent) => void;
|
|
137
|
-
|
|
140
|
+
/** The reconciler's `sessionCheck` (see `CreateCommerceReconcilerInput`). */
|
|
141
|
+
readonly commerceSessionCheck?: CreateCommerceReconcilerInput['sessionCheck'];
|
|
142
|
+
/** The reconciler's `closed` (see `CreateCommerceReconcilerInput`). */
|
|
143
|
+
readonly commerceClosed?: CreateCommerceReconcilerInput['closed'];
|
|
144
|
+
/** The reconciler's `accountBinding` (see `CreateCommerceReconcilerInput`). */
|
|
145
|
+
readonly commerceAccountBinding?: CreateCommerceReconcilerInput['accountBinding'];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `commerceStaleLaunchedAfterMs` is checked where it enters, so a bad value
|
|
149
|
+
* fails even when no reconciler is built (a gateway without `commerce`).
|
|
150
|
+
*/
|
|
151
|
+
export declare function assertCommerceStaleLaunchedAfterMs(value: number | undefined): void;
|
|
138
152
|
export interface GameServicesLeaderboardInput extends LeaderboardScoreInput {
|
|
139
153
|
}
|
|
140
154
|
export interface GameServicesLeaderboardResult {
|
package/dist/client.js
CHANGED
|
@@ -6,7 +6,9 @@ import { createCommerceReconciler, toGameServicesPurchaseResult, } from './comme
|
|
|
6
6
|
import { createDefaultClientRewardEvidenceRegistry } from './default-client-reward-evidence.js';
|
|
7
7
|
import { createInMemoryMonetizationOperationStore } from './monetization-operation-store.js';
|
|
8
8
|
import { observeGameServicesOperation } from './operation-progress.js';
|
|
9
|
-
import {
|
|
9
|
+
import { withLegacyOrpcErrorCompat } from './orpc-error-compat.js';
|
|
10
|
+
export { withLegacyOrpcErrorCompat } from './orpc-error-compat.js';
|
|
11
|
+
import { GameServicesHeaderResolutionError, GameServicesRuntimeClosedError, resolveGameServicesRequestHeaders, } from './request-headers.js';
|
|
10
12
|
export { GameServicesHeaderResolutionError, resolveGameServicesRequestHeaders, } from './request-headers.js';
|
|
11
13
|
export const gameServicesBackendEndpoints = {
|
|
12
14
|
verifyPurchase: '/game-services/purchases/verify',
|
|
@@ -35,7 +37,17 @@ export class GameServicesBackendTransportError extends Error {
|
|
|
35
37
|
this.endpoint = endpoint;
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* `commerceStaleLaunchedAfterMs` is checked where it enters, so a bad value
|
|
42
|
+
* fails even when no reconciler is built (a gateway without `commerce`).
|
|
43
|
+
*/
|
|
44
|
+
export function assertCommerceStaleLaunchedAfterMs(value) {
|
|
45
|
+
if (value !== undefined && !(typeof value === 'number' && value > 0)) {
|
|
46
|
+
throw new RangeError('commerceStaleLaunchedAfterMs must be a positive duration in milliseconds, or Infinity.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
38
49
|
export function createGameServicesClient(input) {
|
|
50
|
+
assertCommerceStaleLaunchedAfterMs(input.commerceStaleLaunchedAfterMs);
|
|
39
51
|
const now = input.now ?? (() => new Date().toISOString());
|
|
40
52
|
const requestNow = input.requestNow ?? now;
|
|
41
53
|
const rewardEvidenceRegistry = input.rewardEvidenceRegistry ?? createDefaultClientRewardEvidenceRegistry();
|
|
@@ -62,6 +74,12 @@ export function createGameServicesClient(input) {
|
|
|
62
74
|
target: input.target,
|
|
63
75
|
...(input.deploymentTarget === undefined ? {} : { deploymentTarget: input.deploymentTarget }),
|
|
64
76
|
...(input.commerceProducts === undefined ? {} : { products: input.commerceProducts }),
|
|
77
|
+
...(input.commerceSessionCheck === undefined ? {} : { sessionCheck: input.commerceSessionCheck }),
|
|
78
|
+
...(input.commerceClosed === undefined ? {} : { closed: input.commerceClosed }),
|
|
79
|
+
...(input.commerceAccountBinding === undefined ? {} : { accountBinding: input.commerceAccountBinding }),
|
|
80
|
+
...(input.commerceStaleLaunchedAfterMs === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { staleLaunchedAfterMs: input.commerceStaleLaunchedAfterMs }),
|
|
65
83
|
now: requestNow,
|
|
66
84
|
});
|
|
67
85
|
if (input.onCommerceEvent !== undefined) {
|
|
@@ -424,9 +442,16 @@ export function createGameServicesFetchBackendTransport(input) {
|
|
|
424
442
|
};
|
|
425
443
|
}
|
|
426
444
|
export function createGameServicesOrpcClient(input) {
|
|
445
|
+
const fetcher = input.fetch;
|
|
427
446
|
const link = new RPCLink({
|
|
428
447
|
origin: input.url,
|
|
429
|
-
|
|
448
|
+
// Every response passes the oRPC error-body compatibility shim so errors from a Worker on
|
|
449
|
+
// an older oRPC beta keep their codes; the default fetch is resolved per call, as oRPC does.
|
|
450
|
+
fetch: withLegacyOrpcErrorCompat((url, init, ...rest) => {
|
|
451
|
+
return fetcher === undefined
|
|
452
|
+
? globalThis.fetch(url, init)
|
|
453
|
+
: fetcher(url, init, ...rest);
|
|
454
|
+
}),
|
|
430
455
|
...(input.headers === undefined && input.getHeaders === undefined
|
|
431
456
|
? {}
|
|
432
457
|
: {
|
|
@@ -490,7 +515,7 @@ async function sendGameServicesBackendRequest(transport, endpoint, body) {
|
|
|
490
515
|
});
|
|
491
516
|
}
|
|
492
517
|
catch (error) {
|
|
493
|
-
if (error instanceof GameServicesHeaderResolutionError) {
|
|
518
|
+
if (error instanceof GameServicesHeaderResolutionError || error instanceof GameServicesRuntimeClosedError) {
|
|
494
519
|
throw error;
|
|
495
520
|
}
|
|
496
521
|
// Do not leak SDK or native transport exceptions (which can contain
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LogicalProductId } from '@mpgd/platform';
|
|
2
|
-
import { type CommerceCapabilities, type CommerceErrorInfo, type CommerceProvider, type CommercePurchaseState, type CommercePurchaseSummaryStatus, type FetchProductsInput, type PurchaseKind, type RequestPurchaseInput, type RequestPurchaseResult, type StoreId, type StoreProduct, type StorePurchase } from '@mpgd/platform/commerce';
|
|
2
|
+
import { CommerceError, type CommerceCapabilities, type CommerceErrorInfo, type CommerceProvider, type CommercePurchaseState, type CommercePurchaseSummaryStatus, type FetchProductsInput, type PurchaseKind, type RequestPurchaseInput, type RequestPurchaseResult, type StoreId, type StoreProduct, type StorePurchase, type StorePurchaseAccountBinding } from '@mpgd/platform/commerce';
|
|
3
3
|
import type { PurchaseVerificationApi } from './client.js';
|
|
4
4
|
import { type CommerceServiceAlignment } from './commerce-evidence-registry.js';
|
|
5
5
|
import type { MonetizationOperationStore } from './monetization-recovery.js';
|
|
@@ -73,7 +73,9 @@ export declare function summarizeCommercePurchaseOperation(record: CommercePurch
|
|
|
73
73
|
* finish allow-list of its own (the removed v1 driver paired transactions in
|
|
74
74
|
* `finishGrantedTransaction`), so this is the finish gate for every `client`
|
|
75
75
|
* and `host` owner: the ledger entry id comes from the journal revision the
|
|
76
|
-
* backend grant wrote.
|
|
76
|
+
* backend grant wrote. The one other finish call is a `finished` consumable a
|
|
77
|
+
* `client` store reports again, finished with the same journaled ledger entry
|
|
78
|
+
* (see `finishReportedAgain` in `createCommerceReconciler`).
|
|
77
79
|
*/
|
|
78
80
|
export declare function assertCommerceFinishAllowed(record: CommercePurchaseOperationRecord): {
|
|
79
81
|
readonly transactionId: string;
|
|
@@ -87,7 +89,9 @@ export interface CommerceReconcilerProduct {
|
|
|
87
89
|
readonly kind: PurchaseKind;
|
|
88
90
|
readonly storeSku?: string;
|
|
89
91
|
}
|
|
90
|
-
export type CommerceReconcilerFailureStage = 'capabilities' | 'available' | 'catalog' | 'match' | 'journal' | 'verify' | 'finish' | 'ledger-status' | 'restore'
|
|
92
|
+
export type CommerceReconcilerFailureStage = 'capabilities' | 'available' | 'catalog' | 'match' | 'journal' | 'verify' | 'finish' | 'ledger-status' | 'restore'
|
|
93
|
+
/** `sessionCheck` refused: the reconciler's player is not the session's now. */
|
|
94
|
+
| 'session';
|
|
91
95
|
export type CommerceReconcilerEvent = {
|
|
92
96
|
readonly type: 'transition';
|
|
93
97
|
readonly record: CommercePurchaseOperationRecord;
|
|
@@ -153,6 +157,52 @@ export interface CreateCommerceReconcilerInput {
|
|
|
153
157
|
* ten seconds.
|
|
154
158
|
*/
|
|
155
159
|
readonly serviceCapabilitiesTimeoutMs?: number;
|
|
160
|
+
/**
|
|
161
|
+
* Whether the reconciler may act for `playerId` right now; an error says
|
|
162
|
+
* why not (`createGameServicesRuntime` passes its session player check).
|
|
163
|
+
* It is read synchronously before every pass, before each queued event and
|
|
164
|
+
* each record a pass handles, before a store read, restore, or finish, and
|
|
165
|
+
* for a purchase at its start, right before its record is reserved, and
|
|
166
|
+
* right before the store UI opens. A refusal never ends a record: a pass stops with the
|
|
167
|
+
* journal as it is and the events still queued (a `failure` event with
|
|
168
|
+
* stage `session` reports it), a purchase answers the error with no store
|
|
169
|
+
* UI (a reserved attempt stays retryable), and `restorePurchases` rejects
|
|
170
|
+
* with it. So one player's journaled purchases are never verified,
|
|
171
|
+
* restored, or finished while another player's session headers or account
|
|
172
|
+
* binding are current. Return a retryable `CommerceError`.
|
|
173
|
+
*/
|
|
174
|
+
readonly sessionCheck?: () => CommerceError | undefined;
|
|
175
|
+
/**
|
|
176
|
+
* Whether the reconciler's owner closed for good (`createGameServicesRuntime`
|
|
177
|
+
* passes its disposed or player-changed state). Read synchronously when
|
|
178
|
+
* `start()` is called and again right before it subscribes (after the
|
|
179
|
+
* journal read): while it answers true, `start()` subscribes nothing and
|
|
180
|
+
* starts nothing, and resolves without an error. So a caller that reaches
|
|
181
|
+
* `start()` after `stop()` (a recovery pass that outlived its owner's
|
|
182
|
+
* dispose), or a start that was still reading the journal when its owner
|
|
183
|
+
* stopped waiting for it, leaves no store subscription.
|
|
184
|
+
* Unlike `sessionCheck`, a signed-out session is not closed: the reconciler
|
|
185
|
+
* still subscribes then and its work waits for the session. Omitted, a
|
|
186
|
+
* stopped reconciler can be started again.
|
|
187
|
+
*/
|
|
188
|
+
readonly closed?: () => boolean;
|
|
189
|
+
/**
|
|
190
|
+
* This player's store account binding: the value the store provider binds
|
|
191
|
+
* the player's new purchases to (Google Play's obfuscated account id,
|
|
192
|
+
* StoreKit's app account token). A store purchase the journal does not
|
|
193
|
+
* hold yet whose binding names another account is another player's: it is
|
|
194
|
+
* reported (stage `match`) and left alone, never journaled or verified for
|
|
195
|
+
* this player (a backend would reject it for good as an account mismatch,
|
|
196
|
+
* and its derived key would then block that player's own recovery). The
|
|
197
|
+
* player's explicit Restore Purchases pass still takes a durable bound to
|
|
198
|
+
* another account (restore sharing). A purchase without a binding, or with
|
|
199
|
+
* a binding of another kind, is not compared. Answer `undefined` (or
|
|
200
|
+
* throw) only transiently, while this player's binding cannot be read
|
|
201
|
+
* yet: a bound purchase reported then is kept and asked about again on
|
|
202
|
+
* the next pass (a resume, an event, an explicit trigger). A runtime for a store or target whose purchases this player's
|
|
203
|
+
* binding never covers must omit the input; omitted, nothing is compared.
|
|
204
|
+
*/
|
|
205
|
+
readonly accountBinding?: () => StorePurchaseAccountBinding | undefined | Promise<StorePurchaseAccountBinding | undefined>;
|
|
156
206
|
}
|
|
157
207
|
export declare const defaultStaleLaunchedAfterMs: number;
|
|
158
208
|
/** How long one backend capabilities read may take before the last comparison is kept. */
|