@mpgd/platform 0.26.0 → 0.27.1
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/dist/commerce-wallet.d.ts +9 -2
- package/dist/commerce-wallet.js +165 -24
- package/dist/notifications-conformance.d.ts +31 -114
- package/dist/notifications-conformance.js +25 -1332
- package/dist/notifications-runtime-support.d.ts +20 -0
- package/dist/notifications-runtime-support.js +96 -0
- package/dist/notifications-runtime.d.ts +262 -0
- package/dist/notifications-runtime.js +1715 -0
- package/package.json +5 -1
|
@@ -112,8 +112,13 @@ export declare function isCreditLotEligible(lot: CreditLot, criteria: CreditLotE
|
|
|
112
112
|
export declare function selectEligibleCreditLots(lots: readonly CreditLot[], criteria: CreditLotEligibility): readonly CreditLot[];
|
|
113
113
|
export declare function sumCreditLotRemaining(lots: readonly CreditLot[]): number;
|
|
114
114
|
export declare function sumCreditDebits(debits: readonly CreditDebit[]): number;
|
|
115
|
+
/**
|
|
116
|
+
* True when two credit amounts differ only by representational float noise.
|
|
117
|
+
* The allowance does not grow with the amount, so a one-credit gap is never
|
|
118
|
+
* equal, and a zero and a positive price are not equal.
|
|
119
|
+
*/
|
|
115
120
|
export declare function sameCreditAmount(left: number, right: number): boolean;
|
|
116
|
-
/** The whole number of `unit` prices in `amount`, or `undefined` when `amount`
|
|
121
|
+
/** The whole number of `unit` prices in `amount`, or `undefined` when `amount` does not cover a positive multiple. */
|
|
117
122
|
export declare function creditPriceMultiple(amount: number, unit: number): number | undefined;
|
|
118
123
|
export type CreditDebitPlan = {
|
|
119
124
|
readonly status: 'debited';
|
|
@@ -125,7 +130,9 @@ export type CreditDebitPlan = {
|
|
|
125
130
|
/**
|
|
126
131
|
* Plans the debits of `amount` across eligible lots in spend order without
|
|
127
132
|
* mutating them. A spend the lots cannot cover is rejected with
|
|
128
|
-
* `INSUFFICIENT_BALANCE` and plans nothing.
|
|
133
|
+
* `INSUFFICIENT_BALANCE` and plans nothing. A shortfall is never a debit,
|
|
134
|
+
* including one credit of a large price, and an empty debit list is not a
|
|
135
|
+
* debit of a positive amount.
|
|
129
136
|
*/
|
|
130
137
|
export declare function planCreditDebits(eligible: readonly CreditLot[], amount: number): CreditDebitPlan;
|
|
131
138
|
/** Prefix of every scheduled membership debit key; a client spend cannot use it. */
|
package/dist/commerce-wallet.js
CHANGED
|
@@ -328,28 +328,156 @@ export function selectEligibleCreditLots(lots, criteria) {
|
|
|
328
328
|
return lots.filter((lot) => isCreditLotEligible(lot, criteria)).sort(compareCreditLotSpendOrder);
|
|
329
329
|
}
|
|
330
330
|
export function sumCreditLotRemaining(lots) {
|
|
331
|
-
return lots.
|
|
331
|
+
return sumCreditAmounts(lots.map((lot) => lot.remaining));
|
|
332
332
|
}
|
|
333
333
|
export function sumCreditDebits(debits) {
|
|
334
|
-
return debits.
|
|
334
|
+
return sumCreditAmounts(debits.map((debit) => debit.amount));
|
|
335
335
|
}
|
|
336
|
-
/**
|
|
337
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Compensated sum. A long run of fractional lots (`100 × 0.1`) is the price
|
|
338
|
+
* it adds up to, not a shortfall left by naive addition. A non-finite term
|
|
339
|
+
* makes the sum non-finite.
|
|
340
|
+
*/
|
|
341
|
+
function sumCreditAmounts(amounts) {
|
|
342
|
+
let sum = 0;
|
|
343
|
+
let compensation = 0;
|
|
344
|
+
for (const amount of amounts) {
|
|
345
|
+
if (!Number.isFinite(amount)) {
|
|
346
|
+
return Number.NaN;
|
|
347
|
+
}
|
|
348
|
+
const corrected = amount - compensation;
|
|
349
|
+
const next = sum + corrected;
|
|
350
|
+
compensation = (next - sum) - corrected;
|
|
351
|
+
sum = next;
|
|
352
|
+
}
|
|
353
|
+
return sum;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Representational noise around a credit amount, in units in the last place.
|
|
357
|
+
* A sum of binary fractions can land one ULP off (`0.7 + 0.1` vs `0.8`).
|
|
358
|
+
* A few ULPs cover that. The count does not scale with the price.
|
|
359
|
+
*/
|
|
360
|
+
const creditAmountUlpSlop = 4;
|
|
361
|
+
/**
|
|
362
|
+
* Absolute ceiling on that noise. It does not grow with the price, so a
|
|
363
|
+
* one-credit shortfall is never float residue. Below this magnitude the ULP
|
|
364
|
+
* bound is tighter, and a zero never matches a positive amount
|
|
365
|
+
* (`sameCreditAmount(0, 1e-9)`).
|
|
366
|
+
*/
|
|
367
|
+
const creditAmountAbsoluteCap = 1e-9;
|
|
368
|
+
/** Spacing to the next representable value at `value`'s magnitude. */
|
|
369
|
+
function creditAmountUlp(value) {
|
|
370
|
+
const magnitude = Math.abs(value);
|
|
371
|
+
if (!Number.isFinite(magnitude) || magnitude === 0) {
|
|
372
|
+
return Number.MIN_VALUE;
|
|
373
|
+
}
|
|
374
|
+
// Math.log2 can round a value just under 2^n up to n. Step back when the
|
|
375
|
+
// reconstructed boundary is above the magnitude.
|
|
376
|
+
const exponent = Math.floor(Math.log2(magnitude));
|
|
377
|
+
const boundary = 2 ** exponent;
|
|
378
|
+
const binade = magnitude < boundary ? exponent - 1 : exponent;
|
|
379
|
+
const ulp = 2 ** (binade - 52);
|
|
380
|
+
return Number.isFinite(ulp) && ulp > 0 ? ulp : Number.MIN_VALUE;
|
|
381
|
+
}
|
|
382
|
+
/** Noise two amounts may differ by. Capped so it cannot grow into a credit. */
|
|
383
|
+
function creditAmountNoise(left, right) {
|
|
384
|
+
const scale = Math.max(Math.abs(left), Math.abs(right));
|
|
385
|
+
// A few ULPs of a subnormal price are most of that price. The relative
|
|
386
|
+
// bound underflows to zero there, so only an exact payment covers it.
|
|
387
|
+
const relative = scale * Number.EPSILON;
|
|
388
|
+
if (!Number.isFinite(relative) || relative === 0) {
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
return Math.min(creditAmountUlpSlop * creditAmountUlp(scale), creditAmountAbsoluteCap, relative);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* True when two credit amounts differ only by representational float noise.
|
|
395
|
+
* The allowance does not grow with the amount, so a one-credit gap is never
|
|
396
|
+
* equal, and a zero and a positive price are not equal.
|
|
397
|
+
*/
|
|
338
398
|
export function sameCreditAmount(left, right) {
|
|
339
|
-
|
|
399
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
const difference = Math.abs(left - right);
|
|
403
|
+
if (difference === 0) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (left === 0 || right === 0) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
return difference <= creditAmountNoise(left, right);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* True when `paid` meets `price`. A shortfall is not a payment: zero does not
|
|
413
|
+
* meet a positive price, and a gap that is not float residue does not either.
|
|
414
|
+
* Residue may leave `paid` a few ULPs under `price`; that allowance is capped
|
|
415
|
+
* and does not grow with the price, so a large price cannot be underpaid by a
|
|
416
|
+
* whole credit.
|
|
417
|
+
*/
|
|
418
|
+
function creditAmountCovers(paid, price) {
|
|
419
|
+
if (!Number.isFinite(paid) || !Number.isFinite(price) || !(paid > 0) || !(price > 0)) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if (paid >= price) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
const shortfall = price - paid;
|
|
426
|
+
return shortfall < price && shortfall <= creditAmountNoise(paid, price);
|
|
340
427
|
}
|
|
341
|
-
/** The whole number of `unit` prices in `amount`, or `undefined` when `amount`
|
|
428
|
+
/** The whole number of `unit` prices in `amount`, or `undefined` when `amount` does not cover a positive multiple. */
|
|
342
429
|
export function creditPriceMultiple(amount, unit) {
|
|
343
430
|
if (!(unit > 0) || !(amount > 0) || !Number.isFinite(amount) || !Number.isFinite(unit)) {
|
|
344
431
|
return undefined;
|
|
345
432
|
}
|
|
346
433
|
const multiple = Math.round(amount / unit);
|
|
347
|
-
|
|
434
|
+
if (!Number.isFinite(multiple) || multiple < 1) {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
const product = multiple * unit;
|
|
438
|
+
// `amount` must cover the units. Float residue may put `product` a few ULPs
|
|
439
|
+
// above `amount`; a one-credit gap is not a whole number of units.
|
|
440
|
+
if (!creditAmountCovers(amount, product) || !sameCreditAmount(product, amount)) {
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
return multiple;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* True when the lots meet `amount` without adding a lot that would overflow
|
|
447
|
+
* the running total. A lot that meets the unpaid gap covers the price by
|
|
448
|
+
* itself; summing it with an already huge total is not required.
|
|
449
|
+
*/
|
|
450
|
+
function creditLotsCover(lots, amount) {
|
|
451
|
+
let sum = 0;
|
|
452
|
+
let compensation = 0;
|
|
453
|
+
for (const lot of lots) {
|
|
454
|
+
const remaining = lot.remaining;
|
|
455
|
+
if (!Number.isFinite(remaining) || !(remaining > 0)) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const unpaid = amount - sum;
|
|
459
|
+
if (!(unpaid > 0) || remaining >= unpaid) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
const corrected = remaining - compensation;
|
|
463
|
+
const next = sum + corrected;
|
|
464
|
+
if (!Number.isFinite(next)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
compensation = (next - sum) - corrected;
|
|
468
|
+
sum = next;
|
|
469
|
+
if (creditAmountCovers(sum, amount)) {
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return creditAmountCovers(sum, amount);
|
|
348
474
|
}
|
|
349
475
|
/**
|
|
350
476
|
* Plans the debits of `amount` across eligible lots in spend order without
|
|
351
477
|
* mutating them. A spend the lots cannot cover is rejected with
|
|
352
|
-
* `INSUFFICIENT_BALANCE` and plans nothing.
|
|
478
|
+
* `INSUFFICIENT_BALANCE` and plans nothing. A shortfall is never a debit,
|
|
479
|
+
* including one credit of a large price, and an empty debit list is not a
|
|
480
|
+
* debit of a positive amount.
|
|
353
481
|
*/
|
|
354
482
|
export function planCreditDebits(eligible, amount) {
|
|
355
483
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
@@ -362,31 +490,40 @@ export function planCreditDebits(eligible, amount) {
|
|
|
362
490
|
},
|
|
363
491
|
};
|
|
364
492
|
}
|
|
493
|
+
const insufficient = (available) => ({
|
|
494
|
+
status: 'rejected',
|
|
495
|
+
error: {
|
|
496
|
+
code: 'INSUFFICIENT_BALANCE',
|
|
497
|
+
retryable: false,
|
|
498
|
+
message: `Eligible balance ${available} cannot cover ${amount}.`,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
365
501
|
const ordered = [...eligible].sort(compareCreditLotSpendOrder);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
code: 'INSUFFICIENT_BALANCE',
|
|
372
|
-
retryable: false,
|
|
373
|
-
message: `Eligible balance ${available} cannot cover ${amount}.`,
|
|
374
|
-
},
|
|
375
|
-
};
|
|
502
|
+
// Stop once a lot meets the unpaid gap. Summing every lot first overflows
|
|
503
|
+
// to Infinity when two finite lots are `Number.MAX_VALUE`, and then a
|
|
504
|
+
// price the first lot covers is rejected.
|
|
505
|
+
if (!creditLotsCover(ordered, amount)) {
|
|
506
|
+
return insufficient(sumCreditLotRemaining(ordered));
|
|
376
507
|
}
|
|
377
508
|
const debits = [];
|
|
378
509
|
let remaining = amount;
|
|
379
510
|
for (const lot of ordered) {
|
|
380
|
-
if (remaining
|
|
511
|
+
if (!(remaining > 0)) {
|
|
381
512
|
break;
|
|
382
513
|
}
|
|
383
514
|
const debit = Math.min(lot.remaining, remaining);
|
|
384
|
-
if (debit
|
|
515
|
+
if (!(debit > 0)) {
|
|
385
516
|
continue;
|
|
386
517
|
}
|
|
387
518
|
debits.push({ lotId: lot.lotId, amount: debit });
|
|
388
519
|
remaining -= debit;
|
|
389
520
|
}
|
|
521
|
+
const paid = sumCreditDebits(debits);
|
|
522
|
+
// The loop must not report a debit it did not take. A positive price with
|
|
523
|
+
// an empty list, or a sum that does not cover, is still insufficient.
|
|
524
|
+
if (debits.length === 0 || !creditAmountCovers(paid, amount)) {
|
|
525
|
+
return insufficient(paid);
|
|
526
|
+
}
|
|
390
527
|
return { status: 'debited', debits };
|
|
391
528
|
}
|
|
392
529
|
// Membership keys and subscription events.
|
|
@@ -1459,10 +1596,14 @@ export function createCreditCommerceProvider(client, options) {
|
|
|
1459
1596
|
if (spend.status === 'rejected') {
|
|
1460
1597
|
return failed(spend.error);
|
|
1461
1598
|
}
|
|
1462
|
-
// A debited answer that does not
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1599
|
+
// A debited answer that does not cover the price is not a purchase.
|
|
1600
|
+
// An empty debit list never covers a positive price, and a one-credit
|
|
1601
|
+
// shortfall is still a shortfall. The allowance does not grow with price.
|
|
1602
|
+
const paid = sumCreditDebits(spend.debits);
|
|
1603
|
+
if (spend.debits.length === 0
|
|
1604
|
+
|| !creditAmountCovers(paid, spendInput.amount)
|
|
1605
|
+
|| !sameCreditAmount(paid, spendInput.amount)) {
|
|
1606
|
+
return failed(invalidResponse('spend', new RangeError(`Debits for ${offer.logicalId} sum to ${paid}, not ${spendInput.amount}.`)).toCommerceErrorInfo());
|
|
1466
1607
|
}
|
|
1467
1608
|
return {
|
|
1468
1609
|
outcome: 'purchased',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type LocalNotificationEntry, type LocalNotificationRequest, type NotificationCapabilities, type NotificationConsentRecord, type NotificationDeliveryContent, type NotificationDeliveryOutcome, type NotificationDeliveryProviderEntry, type NotificationDeployment, type NotificationNotice, type NotificationNoticeIdentity, type NotificationNoticeSettings, type NotificationPermissionState, type NotificationProvider, type NotificationTemplateData, type NotificationTopicDeclaration, type NotificationTopicMapping } from './notifications.js';
|
|
2
|
+
import { type NotificationRuntimeBackend, type NotificationRuntimeController, type NotificationRuntimePersistedRegistration, type NotificationRuntimeReminderSetStatus, type NotificationRuntimeSession } from './notifications-runtime.js';
|
|
2
3
|
import { notificationConformanceDefaultLocale, type NotificationConformanceDeliverInput, type NotificationConformanceEndpointFixture, type NotificationConformanceHostFixture, type NotificationConformanceInstallationFixture, type NotificationConformanceProfile, type NotificationConformanceProfileLimits, type NotificationConformancePromptResult, type NotificationConformanceRejectionReason, type NotificationConformanceTapInput, type NotificationConformanceVector } from './notifications-vectors.js';
|
|
3
4
|
/**
|
|
4
5
|
* Notifications v2 conformance runner (RFC "Conformance vectors") with the
|
|
@@ -6,7 +7,8 @@ import { notificationConformanceDefaultLocale, type NotificationConformanceDeliv
|
|
|
6
7
|
* with its fixture `NotificationProvider`; a fake backend that keeps sessions,
|
|
7
8
|
* consent records, endpoints, server reminders, notices, and an idempotency
|
|
8
9
|
* ledger, and applies the pure policy gate before every provider call; and the
|
|
9
|
-
*
|
|
10
|
+
* notifications runtime the runner drives as the `game` actor, which is the
|
|
11
|
+
* production one from `@mpgd/platform/notifications-runtime`. Pure
|
|
10
12
|
* in-process code: no Phaser, DOM, network, or SDK imports. Nothing here sends
|
|
11
13
|
* a real notification or grants anything.
|
|
12
14
|
*
|
|
@@ -121,49 +123,25 @@ export declare function notificationConformanceRegistryEntry(input: {
|
|
|
121
123
|
readonly limits?: NotificationConformanceProfileLimits;
|
|
122
124
|
}): NotificationDeliveryProviderEntry | undefined;
|
|
123
125
|
/** An authenticated game-services session; its installation id is the session's, never a request's. */
|
|
124
|
-
export interface NotificationConformanceSession {
|
|
125
|
-
readonly id: string;
|
|
126
|
-
readonly player: string;
|
|
126
|
+
export interface NotificationConformanceSession extends NotificationRuntimeSession {
|
|
127
127
|
readonly installationId: string;
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
-
* What setting a server reminder answers
|
|
131
|
-
*
|
|
132
|
-
* reminders, or `unavailable` when no provider the player can be reached on
|
|
133
|
-
* maps the topic. The fake backend answers only the first six.
|
|
130
|
+
* What setting a server reminder answers (`NotificationRuntimeReminderSetStatus`).
|
|
131
|
+
* The fake backend answers only the first six.
|
|
134
132
|
*/
|
|
135
|
-
export type NotificationConformanceReminderSetStatus =
|
|
133
|
+
export type NotificationConformanceReminderSetStatus = NotificationRuntimeReminderSetStatus;
|
|
136
134
|
export type NotificationConformanceNoticeChannel = 'in-game' | 'push' | 'other';
|
|
137
|
-
/**
|
|
138
|
-
|
|
135
|
+
/**
|
|
136
|
+
* The backend surface the runtime calls for the authenticated session
|
|
137
|
+
* (`NotificationRuntimeBackend`), with the fixture session's installation and
|
|
138
|
+
* every notice channel a backend under test records.
|
|
139
|
+
*/
|
|
140
|
+
export interface NotificationConformanceBackendClient extends NotificationRuntimeBackend {
|
|
139
141
|
/** The authenticated session; `undefined` before authentication and after the session ends. */
|
|
140
142
|
session(): NotificationConformanceSession | undefined;
|
|
141
143
|
/** Calls `listener` whenever a session starts; returns the unsubscribe. */
|
|
142
144
|
onAuthenticated(listener: (session: NotificationConformanceSession) => void): () => void;
|
|
143
|
-
/** Appends a record for the session's player; the backend stamps `at` with its own clock. */
|
|
144
|
-
recordConsent(record: NotificationConsentRecord): Promise<void>;
|
|
145
|
-
/** The session player's consent records, oldest first. */
|
|
146
|
-
listConsent(): Promise<readonly NotificationConsentRecord[]>;
|
|
147
|
-
/**
|
|
148
|
-
* Binds a device registration to the session's installation and player, or
|
|
149
|
-
* keeps a platform-user registration's locale and zone for the player.
|
|
150
|
-
*/
|
|
151
|
-
registerEndpoint(registration: NotificationEndpointRegistration): Promise<void>;
|
|
152
|
-
/**
|
|
153
|
-
* Unbinds the session's installation from `player`, the player whose
|
|
154
|
-
* session sent the unbind; an installation another player took over since
|
|
155
|
-
* stays bound.
|
|
156
|
-
*/
|
|
157
|
-
unregisterEndpoint(input: {
|
|
158
|
-
readonly provider: string;
|
|
159
|
-
readonly player: string;
|
|
160
|
-
}): Promise<void>;
|
|
161
|
-
/** Checks gate steps 3 to 5, then stores or replaces the session player's reminder with this key. */
|
|
162
|
-
setReminder(input: SetNotificationReminderInput): Promise<NotificationConformanceReminderSetStatus>;
|
|
163
|
-
/** Whether a pending reminder with this key, or a waiting send of one, existed. */
|
|
164
|
-
cancelReminder(input: CancelNotificationReminderInput): Promise<boolean>;
|
|
165
|
-
/** The session player's listed notices; none unless the deployment applies the Korean rules. */
|
|
166
|
-
listNotices(): Promise<readonly NotificationNotice[]>;
|
|
167
145
|
/**
|
|
168
146
|
* Records a notice given to the session's player, named by its identity
|
|
169
147
|
* fields, while that notice is listed; recording one that is not listed
|
|
@@ -368,31 +346,25 @@ export interface CreateNotificationConformanceBackendInput {
|
|
|
368
346
|
* is retried on the next run, and one failing item never stops the others.
|
|
369
347
|
*/
|
|
370
348
|
export declare function createNotificationConformanceBackend(input: CreateNotificationConformanceBackendInput): NotificationConformanceFakeBackend;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
/** Drops listeners, buffered events, and both subscriptions. */
|
|
379
|
-
stop(): void;
|
|
380
|
-
/** Waits for event-driven work (records, registrations, local reminders) and rethrows its first failure. */
|
|
381
|
-
runUntilIdle(): Promise<void>;
|
|
382
|
-
}
|
|
383
|
-
/** What the runtime keeps across restarts. */
|
|
349
|
+
/**
|
|
350
|
+
* The reference runtime the runner drives as the `game` actor: the
|
|
351
|
+
* production runtime (`createNotificationRuntime` in
|
|
352
|
+
* `@mpgd/platform/notifications-runtime`), so every vector exercises it.
|
|
353
|
+
*/
|
|
354
|
+
export type NotificationConformanceRuntime = NotificationRuntimeController;
|
|
355
|
+
/** What the runtime keeps across restarts; the runner passes the same object to each restart. */
|
|
384
356
|
export interface NotificationConformanceRuntimeStorage {
|
|
385
357
|
/** The last registration the backend acknowledged. */
|
|
386
358
|
registration?: NotificationConformancePersistedRegistration;
|
|
387
359
|
/** Set by `unregister()`; `register()` or the next authentication clears it. */
|
|
388
360
|
unbound?: boolean;
|
|
361
|
+
/** With `unbound`: the session signed in when `unregister()` ran. */
|
|
362
|
+
unboundSession?: {
|
|
363
|
+
readonly player: string;
|
|
364
|
+
readonly session: string;
|
|
365
|
+
};
|
|
389
366
|
}
|
|
390
|
-
export
|
|
391
|
-
readonly registration: NotificationEndpointRegistration;
|
|
392
|
-
readonly player: string;
|
|
393
|
-
readonly session: string;
|
|
394
|
-
readonly at: string;
|
|
395
|
-
}
|
|
367
|
+
export type NotificationConformancePersistedRegistration = NotificationRuntimePersistedRegistration;
|
|
396
368
|
export interface CreateNotificationConformanceRuntimeInput {
|
|
397
369
|
readonly provider: NotificationProvider;
|
|
398
370
|
readonly backend: NotificationConformanceBackendClient;
|
|
@@ -404,7 +376,7 @@ export interface CreateNotificationConformanceRuntimeInput {
|
|
|
404
376
|
readonly defaultLocale: string;
|
|
405
377
|
/** The device locale for local reminder copy; defaults to `defaultLocale`. */
|
|
406
378
|
readonly locale?: string;
|
|
407
|
-
/** Survives restarts; a fresh object when absent. */
|
|
379
|
+
/** Survives restarts; a fresh object when absent. The runtime writes it after each change. */
|
|
408
380
|
readonly storage?: NotificationConformanceRuntimeStorage;
|
|
409
381
|
/**
|
|
410
382
|
* The backend's registry entry for this provider. When the provider's
|
|
@@ -414,64 +386,9 @@ export interface CreateNotificationConformanceRuntimeInput {
|
|
|
414
386
|
readonly registryEntry?: NotificationDeliveryProviderEntry;
|
|
415
387
|
}
|
|
416
388
|
/**
|
|
417
|
-
* The
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* - Reads return the provider snapshot. A topic the host reads `unknown`
|
|
421
|
-
* takes the latest record, `sources` says which answered, and a
|
|
422
|
-
* game-rendered consent that is not opted in reads `prompt` until a
|
|
423
|
-
* decision is recorded and `denied` after it. A topic whose mapping needs no
|
|
424
|
-
* agreement reads `granted`. A read that observes a changed decided state
|
|
425
|
-
* records it, and every read still answers when the backend cannot.
|
|
426
|
-
* - Reads, requests, and revocations take each topic once, however often it
|
|
427
|
-
* is listed.
|
|
428
|
-
* - Requests throw `NOTIFICATIONS_USER_GESTURE_REQUIRED` without a gesture on
|
|
429
|
-
* a gesture-only provider. An unsupported provider answers `unavailable`, a
|
|
430
|
-
* provider in `action-required` answers that with its `requiredAction`, an
|
|
431
|
-
* undeclared or unmapped topic answers `unavailable`, a topic that needs no
|
|
432
|
-
* agreement answers `already-granted`, and a topic the host reads `blocked`
|
|
433
|
-
* answers `blocked`, all without reaching the host; every other topic
|
|
434
|
-
* reaches the prompt.
|
|
435
|
-
* - Records: decided states that differ from the latest record of their kind
|
|
436
|
-
* and scope, never `dismissed` or `action-required` answers. Device
|
|
437
|
-
* providers write none (the endpoint holds the permission), other app-scope
|
|
438
|
-
* providers write `delivery-permission`, and topic-scope providers write
|
|
439
|
-
* `host-agreement` with the host evidence, plus a `topic-preference` of
|
|
440
|
-
* `granted` when the player accepts a `requested` topic's agreement.
|
|
441
|
-
* Per-send acceptances are appended every time. A record belongs to the
|
|
442
|
-
* player signed in when the call or event began: one observed before
|
|
443
|
-
* authentication waits for it (only the latest per kind and scope), and one
|
|
444
|
-
* observed for a player waits for that player's session. A player action
|
|
445
|
-
* (`setTopicPreference`, `setAdvertisingConsent`) needs an authenticated
|
|
446
|
-
* player and throws `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE` without one, as
|
|
447
|
-
* does every other backend call (a server reminder, `reminders.cancel`'s
|
|
448
|
-
* server part, and `notices`). A backend call belongs to the player signed
|
|
449
|
-
* in when it began and is refused when another player signed in since. A
|
|
450
|
-
* record the backend would refuse is dropped and its error surfaces.
|
|
451
|
-
* - Registrations: the provider's registration goes to the backend at
|
|
452
|
-
* authentication, on `register()`, on `endpoint-changed` (a rotated device
|
|
453
|
-
* address, or a platform user's changed locale or zone), and when a read
|
|
454
|
-
* or request, or a revocation, changes the device's decided permission; and
|
|
455
|
-
* at start and on reads once the last one the backend acknowledged is older
|
|
456
|
-
* than 7 days. An identical one for the same player and session is skipped
|
|
457
|
-
* for 7 days, except on `register()`. `unregister()` unbinds the
|
|
458
|
-
* installation from the player signed in when it was called and cancels the
|
|
459
|
-
* pending local reminders, even when the unbind fails, and nothing binds it
|
|
460
|
-
* again, across restarts, until `register()` or the next authentication.
|
|
461
|
-
* - Reminders: one key across both routes, and `cancel` removes it from each
|
|
462
|
-
* route that has it. Local ones pass the client gate steps (permission,
|
|
463
|
-
* then the topic preference from the backend, then quiet hours, which move
|
|
464
|
-
* them or refuse a topic that is not deferrable, then the topic's daily
|
|
465
|
-
* cap); server ones are checked and stored by the backend. Turning a topic off cancels its local
|
|
466
|
-
* reminders, a known permission other than granted cancels them, and start,
|
|
467
|
-
* authentication, and every backend read reconcile them with the catalog
|
|
468
|
-
* (a topic that no longer takes the local route loses its reminders), the
|
|
469
|
-
* latest preference, and permission.
|
|
470
|
-
* - Game listeners run after the runtime's own handling of an event; a
|
|
471
|
-
* listener that throws stops neither the others nor the runtime, and its
|
|
472
|
-
* error surfaces from `runUntilIdle`, as do failures of background work:
|
|
473
|
-
* the first one, or an `AggregateError` that counts them all. `stop()` and
|
|
474
|
-
* a later `start()` buffer events for the next first subscriber again.
|
|
389
|
+
* The production runtime over the conformance inputs: `storage` is a plain
|
|
390
|
+
* object the runtime reads when it is created and rewrites after each
|
|
391
|
+
* change, so a restart that passes the same object resumes from it.
|
|
475
392
|
*/
|
|
476
393
|
export declare function createNotificationConformanceRuntime(input: CreateNotificationConformanceRuntimeInput): NotificationConformanceRuntime;
|
|
477
394
|
export interface NotificationConformanceHarness {
|