@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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type NotificationEvent, type NotificationNoticeIdentity, type RecordNotificationNoticeInput, type SetNotificationReminderInput } from './notifications.js';
|
|
2
|
+
/**
|
|
3
|
+
* Buffers an event for the first listener, keeping at most
|
|
4
|
+
* `eventBufferLimit`: past it the oldest event that is not an open goes first,
|
|
5
|
+
* so a cold-start open is not lost to later events.
|
|
6
|
+
*/
|
|
7
|
+
export declare function bufferEvent(buffer: NotificationEvent[], event: NotificationEvent): void;
|
|
8
|
+
/** Calls every listener with every event, in order; returns the errors they threw. */
|
|
9
|
+
export declare function deliverEach(listeners: readonly ((event: NotificationEvent) => void)[], events: readonly NotificationEvent[]): unknown[];
|
|
10
|
+
/** A well-formed `notices.recordShown` input, or `NOTIFICATIONS_INVALID_INPUT`. */
|
|
11
|
+
export declare function recordNoticeInputOf(input: unknown): RecordNotificationNoticeInput;
|
|
12
|
+
/**
|
|
13
|
+
* Only the fields that name a notice, with the record time in the canonical
|
|
14
|
+
* form; a listed notice's `record.state` and `textVersion` are left out.
|
|
15
|
+
*/
|
|
16
|
+
export declare function noticeIdentityOf(notice: NotificationNoticeIdentity): NotificationNoticeIdentity;
|
|
17
|
+
/** Why a reminder's input breaks the RFC bounds, or `undefined`. */
|
|
18
|
+
export declare function reminderInputIssue(input: SetNotificationReminderInput): string | undefined;
|
|
19
|
+
export declare function isBoundedText(value: unknown): boolean;
|
|
20
|
+
export declare function describeError(error: unknown): string;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { assertRecordNotificationNoticeInput, normalizeNotificationTimestamp, NotificationError, notificationKeyIssue, notificationTemplateDataIssue, } from './notifications.js';
|
|
2
|
+
/**
|
|
3
|
+
* Helpers the notifications runtime and the conformance runner share. Not a
|
|
4
|
+
* package entry: `@mpgd/platform/notifications-runtime` and
|
|
5
|
+
* `@mpgd/platform/notifications-conformance` are.
|
|
6
|
+
*/
|
|
7
|
+
/** Events a provider or runtime keeps for its first listener. */
|
|
8
|
+
const eventBufferLimit = 64;
|
|
9
|
+
/**
|
|
10
|
+
* Buffers an event for the first listener, keeping at most
|
|
11
|
+
* `eventBufferLimit`: past it the oldest event that is not an open goes first,
|
|
12
|
+
* so a cold-start open is not lost to later events.
|
|
13
|
+
*/
|
|
14
|
+
export function bufferEvent(buffer, event) {
|
|
15
|
+
buffer.push(event);
|
|
16
|
+
if (buffer.length > eventBufferLimit) {
|
|
17
|
+
const evicted = buffer.findIndex((buffered) => buffered.type !== 'opened');
|
|
18
|
+
buffer.splice(evicted === -1 ? 0 : evicted, 1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Calls every listener with every event, in order; returns the errors they threw. */
|
|
22
|
+
export function deliverEach(listeners, events) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
for (const event of events) {
|
|
25
|
+
for (const listener of listeners) {
|
|
26
|
+
try {
|
|
27
|
+
listener(event);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
errors.push(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return errors;
|
|
35
|
+
}
|
|
36
|
+
/** A well-formed `notices.recordShown` input, or `NOTIFICATIONS_INVALID_INPUT`. */
|
|
37
|
+
export function recordNoticeInputOf(input) {
|
|
38
|
+
try {
|
|
39
|
+
return assertRecordNotificationNoticeInput(input);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new NotificationError('NOTIFICATIONS_INVALID_INPUT', {
|
|
43
|
+
message: 'A notice is a listed notice, or its kind, the kind and time of the record it reports '
|
|
44
|
+
+ `on, and its dueBy: ${describeError(error)}`,
|
|
45
|
+
cause: error,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Only the fields that name a notice, with the record time in the canonical
|
|
51
|
+
* form; a listed notice's `record.state` and `textVersion` are left out.
|
|
52
|
+
*/
|
|
53
|
+
export function noticeIdentityOf(notice) {
|
|
54
|
+
return {
|
|
55
|
+
kind: notice.kind,
|
|
56
|
+
record: { kind: notice.record.kind, at: normalizeNotificationTimestamp(notice.record.at) },
|
|
57
|
+
dueBy: notice.dueBy,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Why a reminder's input breaks the RFC bounds, or `undefined`. */
|
|
61
|
+
export function reminderInputIssue(input) {
|
|
62
|
+
const keyIssue = notificationKeyIssue(input.key);
|
|
63
|
+
if (keyIssue !== undefined) {
|
|
64
|
+
return `key: ${keyIssue}`;
|
|
65
|
+
}
|
|
66
|
+
if (timestampOrUndefined(input.fireAt) === undefined) {
|
|
67
|
+
return 'fireAt: expected RFC 3339 with an explicit offset';
|
|
68
|
+
}
|
|
69
|
+
if (input.templateData !== undefined) {
|
|
70
|
+
const issue = notificationTemplateDataIssue(input.templateData);
|
|
71
|
+
if (issue !== undefined) {
|
|
72
|
+
return `templateData: ${issue}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (input.link !== undefined && !isBoundedText(input.link)) {
|
|
76
|
+
return 'link: expected a non-empty string';
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
export function isBoundedText(value) {
|
|
81
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 16_384;
|
|
82
|
+
}
|
|
83
|
+
function timestampOrUndefined(value) {
|
|
84
|
+
if (typeof value !== 'string') {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return normalizeNotificationTimestamp(value);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function describeError(error) {
|
|
95
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
96
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { StorageAdapter } from './index.js';
|
|
2
|
+
import { type CancelNotificationReminderInput, type NotificationConsentRecord, type NotificationDeliveryProviderEntry, type NotificationEndpointRegistration, type NotificationNotice, type NotificationNoticeIdentity, type NotificationProvider, type NotificationRuntime, type NotificationTopicDeclaration, type SetNotificationReminderInput } from './notifications.js';
|
|
3
|
+
/** The authenticated session a backend serves; the backend binds the installation to it. */
|
|
4
|
+
export interface NotificationRuntimeSession {
|
|
5
|
+
/** Stable for the session's life: refreshing the session keeps it. */
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly player: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* What setting a server reminder answers: stored, the gate step 3 to 5
|
|
11
|
+
* refusal, `capacity-exceeded` past a backend's cap on a player's pending
|
|
12
|
+
* reminders, or `unavailable` when no provider the player can be reached on
|
|
13
|
+
* maps the topic.
|
|
14
|
+
*/
|
|
15
|
+
export type NotificationRuntimeReminderSetStatus = 'scheduled' | 'replaced' | 'permission-required' | 'request-missing' | 'preference-off' | 'advertising-consent-missing' | 'capacity-exceeded' | 'unavailable';
|
|
16
|
+
/**
|
|
17
|
+
* The backend surface the runtime calls for the authenticated session. Every
|
|
18
|
+
* call acts for the session `session()` names when it is made; the backend
|
|
19
|
+
* learns the session from its own authentication, never from a request field.
|
|
20
|
+
* A call without a session, or for a session the backend no longer accepts,
|
|
21
|
+
* throws `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE`.
|
|
22
|
+
*/
|
|
23
|
+
export interface NotificationRuntimeBackend {
|
|
24
|
+
/** The authenticated session; `undefined` before authentication and after the session ends. */
|
|
25
|
+
session(): NotificationRuntimeSession | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Calls `listener` whenever a session starts, including a restored one and
|
|
28
|
+
* an account switch; returns the unsubscribe. Refreshing the same session
|
|
29
|
+
* (the same `id`) is not a start.
|
|
30
|
+
*/
|
|
31
|
+
onAuthenticated(listener: (session: NotificationRuntimeSession) => void): () => void;
|
|
32
|
+
/** Appends a record for the session's player; the backend stamps `at` with its own clock. */
|
|
33
|
+
recordConsent(record: NotificationConsentRecord): Promise<void>;
|
|
34
|
+
/** The session player's consent records, oldest first. */
|
|
35
|
+
listConsent(): Promise<readonly NotificationConsentRecord[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Binds a device registration to the session's installation and player, or
|
|
38
|
+
* keeps a platform-user registration's locale and zone for the player.
|
|
39
|
+
*/
|
|
40
|
+
registerEndpoint(registration: NotificationEndpointRegistration): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Unbinds the session's installation from `player`, the player whose
|
|
43
|
+
* session sent the unbind; an installation another player took over since
|
|
44
|
+
* stays bound. Unlike the other calls, it may answer without a request when
|
|
45
|
+
* `player` is no longer signed in: that session's end unbinds what it
|
|
46
|
+
* registered (the game-services client does).
|
|
47
|
+
*/
|
|
48
|
+
unregisterEndpoint(input: {
|
|
49
|
+
readonly provider: string;
|
|
50
|
+
readonly player: string;
|
|
51
|
+
}): Promise<void>;
|
|
52
|
+
/** Checks gate steps 3 to 5, then stores or replaces the session player's reminder with this key. */
|
|
53
|
+
setReminder(input: SetNotificationReminderInput): Promise<NotificationRuntimeReminderSetStatus>;
|
|
54
|
+
/** Whether a pending reminder with this key, or a waiting send of one, existed. */
|
|
55
|
+
cancelReminder(input: CancelNotificationReminderInput): Promise<boolean>;
|
|
56
|
+
/** The session player's listed notices; none unless the deployment applies the Korean rules. */
|
|
57
|
+
listNotices(): Promise<readonly NotificationNotice[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Records a notice the game showed the session's player, named by its
|
|
60
|
+
* identity fields, while that notice is listed; recording one that is not
|
|
61
|
+
* listed changes nothing.
|
|
62
|
+
*/
|
|
63
|
+
recordNotice(input: NotificationNoticeIdentity & {
|
|
64
|
+
readonly channel: 'in-game';
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
/** The last registration the backend acknowledged, with the player and session it was sent for. */
|
|
68
|
+
export interface NotificationRuntimePersistedRegistration {
|
|
69
|
+
readonly registration: NotificationEndpointRegistration;
|
|
70
|
+
readonly player: string;
|
|
71
|
+
readonly session: string;
|
|
72
|
+
readonly at: string;
|
|
73
|
+
}
|
|
74
|
+
/** What the runtime keeps across restarts (RFC "Runtime", "Restarts"). */
|
|
75
|
+
export interface NotificationRuntimeState {
|
|
76
|
+
/** The last registration the backend acknowledged. */
|
|
77
|
+
readonly registration?: NotificationRuntimePersistedRegistration;
|
|
78
|
+
/**
|
|
79
|
+
* Set by `unregister()`; `register()` or the next authentication clears it,
|
|
80
|
+
* including one the runtime did not observe (see `unboundSession`).
|
|
81
|
+
*/
|
|
82
|
+
readonly unbound?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* With `unbound`: the session signed in when `unregister()` ran. Another
|
|
85
|
+
* session found at start or on a read, or any session when none was signed
|
|
86
|
+
* in then, is a sign-in the runtime did not observe, so it binds again.
|
|
87
|
+
*/
|
|
88
|
+
readonly unboundSession?: {
|
|
89
|
+
readonly player: string;
|
|
90
|
+
readonly session: string;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Where the runtime keeps its state across restarts. `load` runs once, when
|
|
95
|
+
* the runtime is created, and again after a load that failed; a failed load
|
|
96
|
+
* is reported by the call that needed the state. `save` runs after each
|
|
97
|
+
* change, with the whole state. Either may answer synchronously. A value
|
|
98
|
+
* `load` answers that is not a runtime state reads as an empty one. One
|
|
99
|
+
* runtime owns a storage: a game with two runtimes gives each its own key.
|
|
100
|
+
* The state holds the last registration as sent, push address included (an
|
|
101
|
+
* FCM token): an address, not a credential, kept like other game data.
|
|
102
|
+
*/
|
|
103
|
+
export interface NotificationRuntimeStorage {
|
|
104
|
+
load(): unknown;
|
|
105
|
+
save(state: NotificationRuntimeState): void | Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
/** The clock the runtime dates its records and registrations by (RFC 3339 instants). */
|
|
108
|
+
export interface NotificationRuntimeClock {
|
|
109
|
+
now(): string;
|
|
110
|
+
}
|
|
111
|
+
export interface CreateNotificationRuntimeInput {
|
|
112
|
+
readonly provider: NotificationProvider;
|
|
113
|
+
readonly backend: NotificationRuntimeBackend;
|
|
114
|
+
/** The provider's topics, each with this target's mapping (`resolveNotificationTopicsForTarget`). */
|
|
115
|
+
readonly catalog: readonly NotificationTopicDeclaration[];
|
|
116
|
+
/** The catalog's default locale. */
|
|
117
|
+
readonly defaultLocale: string;
|
|
118
|
+
/** The device locale for local reminder copy; defaults to `defaultLocale`. */
|
|
119
|
+
readonly locale?: string;
|
|
120
|
+
/**
|
|
121
|
+
* The deployment's zone, for local quiet hours while the device has
|
|
122
|
+
* registered none; defaults to `notificationDefaultDeploymentTimeZone`.
|
|
123
|
+
*/
|
|
124
|
+
readonly timeZone?: string;
|
|
125
|
+
/** Defaults to the system clock. */
|
|
126
|
+
readonly clock?: NotificationRuntimeClock;
|
|
127
|
+
/**
|
|
128
|
+
* Survives restarts. Without it the state lives in memory, so a restart
|
|
129
|
+
* re-sends the registration and forgets an `unregister()`; pass
|
|
130
|
+
* `createNotificationRuntimeStorage({ storage: gateway.storage })` or
|
|
131
|
+
* another durable port.
|
|
132
|
+
*/
|
|
133
|
+
readonly storage?: NotificationRuntimeStorage;
|
|
134
|
+
/**
|
|
135
|
+
* The backend's registry entry for this provider. When the provider's
|
|
136
|
+
* capabilities disagree with it at start, every call throws
|
|
137
|
+
* `NOTIFICATIONS_CONFIGURATION_REQUIRED`, and nothing is registered, read,
|
|
138
|
+
* or recorded in the background either. `unregister()` is the exception:
|
|
139
|
+
* it still unbinds, since removing a device from a player is always safe. While a `loadRegistryEntry` is in
|
|
140
|
+
* flight, registrations (including `register()`, which then resolves
|
|
141
|
+
* before it is sent) and records that grant something wait without
|
|
142
|
+
* holding any queue, and go out when the load settles without a mismatch;
|
|
143
|
+
* withdrawals and refusals go out at once.
|
|
144
|
+
*/
|
|
145
|
+
readonly registryEntry?: NotificationDeliveryProviderEntry;
|
|
146
|
+
/**
|
|
147
|
+
* Loads the registry entry when `start()` runs without `registryEntry`,
|
|
148
|
+
* for example from the game-services Worker's `GET /providers`. The load
|
|
149
|
+
* runs on its own: neither `start()` nor `runUntilIdle()` waits for it.
|
|
150
|
+
* Until it answers, reads, prompts, and local reminders run without the
|
|
151
|
+
* comparison, while registrations and granting records wait for it (see
|
|
152
|
+
* `registryEntry`). A load that fails is
|
|
153
|
+
* reported through `onError` and the next `runUntilIdle()`, and the runtime
|
|
154
|
+
* goes on without the entry, so a backend it cannot reach does not turn notifications off;
|
|
155
|
+
* the next `start()` loads it again. An entry once loaded is kept for the
|
|
156
|
+
* runtime's life.
|
|
157
|
+
*/
|
|
158
|
+
readonly loadRegistryEntry?: () => Promise<NotificationDeliveryProviderEntry | undefined>;
|
|
159
|
+
/**
|
|
160
|
+
* Called with each failure of background work (records, registrations,
|
|
161
|
+
* local reminders, game listeners) as it happens; `runUntilIdle` still
|
|
162
|
+
* rethrows them. It runs synchronously: an error it throws, or a promise
|
|
163
|
+
* it returns, is ignored.
|
|
164
|
+
*/
|
|
165
|
+
readonly onError?: (error: unknown) => void;
|
|
166
|
+
}
|
|
167
|
+
export interface NotificationRuntimeController extends NotificationRuntime {
|
|
168
|
+
/**
|
|
169
|
+
* Subscribes to the provider and to authentication, compares the provider
|
|
170
|
+
* with its registry entry, and reconciles pending local reminders. Provider
|
|
171
|
+
* events are buffered for the game's first listener.
|
|
172
|
+
*/
|
|
173
|
+
start(): Promise<void>;
|
|
174
|
+
/** Drops listeners, buffered events, and both subscriptions. */
|
|
175
|
+
stop(): void;
|
|
176
|
+
/** Waits for event-driven work (records, registrations, local reminders) and rethrows its first failure. */
|
|
177
|
+
runUntilIdle(): Promise<void>;
|
|
178
|
+
}
|
|
179
|
+
/** The storage key `createNotificationRuntimeStorage` uses by default. */
|
|
180
|
+
export declare const defaultNotificationRuntimeStorageKey = "mpgd:notifications-runtime";
|
|
181
|
+
/**
|
|
182
|
+
* A `NotificationRuntimeStorage` over the platform storage adapter
|
|
183
|
+
* (`gateway.storage`), one JSON value under `key`. The host owns its
|
|
184
|
+
* durability; a failed load or save surfaces from the runtime call that
|
|
185
|
+
* needed it as `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE`.
|
|
186
|
+
*/
|
|
187
|
+
export declare function createNotificationRuntimeStorage(input: {
|
|
188
|
+
readonly storage: Pick<StorageAdapter, 'load' | 'save'>;
|
|
189
|
+
readonly key?: string;
|
|
190
|
+
}): NotificationRuntimeStorage;
|
|
191
|
+
/** A `NotificationRuntimeStorage` that lives in memory: nothing survives a restart. */
|
|
192
|
+
export declare function createMemoryNotificationRuntimeStorage(): NotificationRuntimeStorage;
|
|
193
|
+
/** A stored value as a runtime state; anything else reads as an empty state. */
|
|
194
|
+
export declare function notificationRuntimeStateOf(value: unknown): NotificationRuntimeState;
|
|
195
|
+
/**
|
|
196
|
+
* Creates the notifications runtime (RFC "Runtime"): the provider composed
|
|
197
|
+
* with the backend for the authenticated session. The conformance runner's
|
|
198
|
+
* reference runtime is this one, so every vector exercises it.
|
|
199
|
+
*
|
|
200
|
+
* - Reads return the provider snapshot. A topic the host reads `unknown`
|
|
201
|
+
* takes the latest record, `sources` says which answered, and a
|
|
202
|
+
* game-rendered consent that is not opted in reads `prompt` until a
|
|
203
|
+
* decision is recorded and `denied` after it. A topic whose mapping needs no
|
|
204
|
+
* agreement reads `granted`. A read that observes a changed decided state
|
|
205
|
+
* records it, and every read still answers when the backend cannot.
|
|
206
|
+
* - Reads, requests, and revocations take each topic once, however often it
|
|
207
|
+
* is listed.
|
|
208
|
+
* - Requests throw `NOTIFICATIONS_USER_GESTURE_REQUIRED` without a gesture on
|
|
209
|
+
* a gesture-only provider. An unsupported provider answers `unavailable`, a
|
|
210
|
+
* provider in `action-required` answers that with its `requiredAction`, an
|
|
211
|
+
* undeclared or unmapped topic answers `unavailable`, a topic that needs no
|
|
212
|
+
* agreement answers `already-granted`, and a topic the host reads `blocked`
|
|
213
|
+
* answers `blocked`, all without reaching the host; every other topic
|
|
214
|
+
* reaches the prompt.
|
|
215
|
+
* - Records: decided states that differ from the latest record of their kind
|
|
216
|
+
* and scope, never `dismissed` or `action-required` answers. Device
|
|
217
|
+
* providers write none (the endpoint holds the permission), other app-scope
|
|
218
|
+
* providers write `delivery-permission`, and topic-scope providers write
|
|
219
|
+
* `host-agreement` with the host evidence, plus a `topic-preference` of
|
|
220
|
+
* `granted` when the player accepts a `requested` topic's agreement.
|
|
221
|
+
* Per-send acceptances are appended every time. A record belongs to the
|
|
222
|
+
* player signed in when the call or event began: one observed before
|
|
223
|
+
* authentication waits for it (only the latest per kind and scope), and one
|
|
224
|
+
* observed for a player waits for that player's session. A player action
|
|
225
|
+
* (`setTopicPreference`, `setAdvertisingConsent`) needs an authenticated
|
|
226
|
+
* player and throws `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE` without one, as
|
|
227
|
+
* does every other backend call (a server reminder, `reminders.cancel`'s
|
|
228
|
+
* server part, and `notices`). A backend call belongs to the player signed
|
|
229
|
+
* in when it began and is refused when another player signed in since. A
|
|
230
|
+
* record the backend would refuse is dropped and its error surfaces.
|
|
231
|
+
* - Registrations: the provider's registration goes to the backend at
|
|
232
|
+
* authentication, on `register()`, on `endpoint-changed` (a rotated device
|
|
233
|
+
* address, or a platform user's changed locale or zone), and when a read
|
|
234
|
+
* or request, or a revocation, changes the device's decided permission; and
|
|
235
|
+
* at start and on reads once the last one the backend acknowledged is older
|
|
236
|
+
* than 7 days, belongs to another session, or was never acknowledged (a
|
|
237
|
+
* first registration that failed). An identical one for the same player and session is skipped
|
|
238
|
+
* for 7 days, except on `register()`. `unregister()` unbinds the
|
|
239
|
+
* installation from the player signed in when it was called and cancels the
|
|
240
|
+
* pending local reminders, even when the unbind fails, and nothing binds it
|
|
241
|
+
* again, across restarts, until `register()` or the next authentication. A
|
|
242
|
+
* session found at start or on a read other than the one `unregister()`
|
|
243
|
+
* left (any session, when none was signed in then) is such an
|
|
244
|
+
* authentication even when the runtime did not observe it. While bound, a
|
|
245
|
+
* registration acknowledged for another session is sent again for this
|
|
246
|
+
* one, and so is, at start, one for a session with nothing acknowledged.
|
|
247
|
+
* - Reminders: one key across both routes, and `cancel` removes it from each
|
|
248
|
+
* route that has it. Local ones pass the client gate steps (permission,
|
|
249
|
+
* then the topic preference from the backend, then quiet hours, which move
|
|
250
|
+
* them or refuse a topic that is not deferrable, then the topic's daily
|
|
251
|
+
* cap); server ones are checked and stored by the backend. Turning a topic off cancels its local
|
|
252
|
+
* reminders, a known permission other than granted cancels them, and start,
|
|
253
|
+
* authentication, and every backend read reconcile them with the catalog
|
|
254
|
+
* (a topic that no longer takes the local route loses its reminders), the
|
|
255
|
+
* latest preference, and permission.
|
|
256
|
+
* - Game listeners run after the runtime's own handling of an event; a
|
|
257
|
+
* listener that throws stops neither the others nor the runtime, and its
|
|
258
|
+
* error surfaces from `runUntilIdle`, as do failures of background work:
|
|
259
|
+
* the first one, or an `AggregateError` that counts them all. `stop()` and
|
|
260
|
+
* a later `start()` buffer events for the next first subscriber again.
|
|
261
|
+
*/
|
|
262
|
+
export declare function createNotificationRuntime(input: CreateNotificationRuntimeInput): NotificationRuntimeController;
|