@mpgd/game-services 0.24.1 → 0.25.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/dist/notifications/ait-functional-delivery.d.ts +165 -0
- package/dist/notifications/ait-functional-delivery.js +445 -0
- package/dist/notifications/delivery-provider.d.ts +121 -0
- package/dist/notifications/delivery-provider.js +23 -0
- package/dist/notifications/fcm.d.ts +138 -0
- package/dist/notifications/fcm.js +602 -0
- package/dist/notifications/index.d.ts +12 -0
- package/dist/notifications/index.js +12 -0
- package/dist/notifications/registry.d.ts +86 -0
- package/dist/notifications/registry.js +131 -0
- package/dist/notifications/service.d.ts +222 -0
- package/dist/notifications/service.js +2609 -0
- package/dist/notifications/store-conformance.d.ts +11 -0
- package/dist/notifications/store-conformance.js +795 -0
- package/dist/notifications/store.d.ts +423 -0
- package/dist/notifications/store.js +443 -0
- package/dist/notifications/text.d.ts +2 -0
- package/dist/notifications/text.js +4 -0
- package/package.json +17 -5
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { normalizeNotificationTimestamp, } from '@mpgd/platform/notifications';
|
|
2
|
+
export function notificationRowOutcome(row) {
|
|
3
|
+
switch (row.state.status) {
|
|
4
|
+
case 'delivered':
|
|
5
|
+
return { status: 'delivered', alreadyProcessed: false };
|
|
6
|
+
case 'in-flight':
|
|
7
|
+
return { status: 'in-flight' };
|
|
8
|
+
case 'deferred':
|
|
9
|
+
return { status: 'deferred', reason: row.state.reason, until: row.state.until };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* How far a recipient's answer got, for a send's result across recipients:
|
|
14
|
+
* delivered, in flight, waiting, a provider's refusal, a policy refusal, and
|
|
15
|
+
* last a recipient that did not resolve.
|
|
16
|
+
*/
|
|
17
|
+
function outcomeRank(outcome) {
|
|
18
|
+
switch (outcome.status) {
|
|
19
|
+
case 'delivered':
|
|
20
|
+
return 5;
|
|
21
|
+
case 'in-flight':
|
|
22
|
+
return 4;
|
|
23
|
+
case 'deferred':
|
|
24
|
+
return 3;
|
|
25
|
+
case 'unavailable':
|
|
26
|
+
return outcome.reason === 'no-endpoint' || outcome.reason === 'link-missing' ? 0 : 2;
|
|
27
|
+
case 'suppressed':
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The highest-ranked outcome, the earliest among deferred ones, the first among equals. */
|
|
32
|
+
export function bestNotificationOutcome(outcomes) {
|
|
33
|
+
let best;
|
|
34
|
+
for (const outcome of outcomes) {
|
|
35
|
+
if (best === undefined
|
|
36
|
+
|| outcomeRank(outcome) > outcomeRank(best)
|
|
37
|
+
|| (outcome.status === 'deferred'
|
|
38
|
+
&& best.status === 'deferred'
|
|
39
|
+
&& Date.parse(outcome.until) < Date.parse(best.until))) {
|
|
40
|
+
best = outcome;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return best;
|
|
44
|
+
}
|
|
45
|
+
/** A send's stored outcome: its best row, or `in-flight` while its first evaluation holds the claim. */
|
|
46
|
+
export function notificationSendOutcome(send) {
|
|
47
|
+
const stored = bestNotificationOutcome(send.rows.map(notificationRowOutcome));
|
|
48
|
+
return stored ?? (send.claim === undefined ? undefined : { status: 'in-flight' });
|
|
49
|
+
}
|
|
50
|
+
/** How long an ambiguous delivery is replayed before it is left in flight. */
|
|
51
|
+
export const notificationAmbiguousReplayHours = 24;
|
|
52
|
+
const ambiguousReplayMs = notificationAmbiguousReplayHours * 3_600_000;
|
|
53
|
+
/** When the scheduler may first take the row, or `undefined` when it never will. */
|
|
54
|
+
function rowDueAt(row) {
|
|
55
|
+
if (row.state.status === 'deferred') {
|
|
56
|
+
return Date.parse(row.state.until);
|
|
57
|
+
}
|
|
58
|
+
if (row.state.status !== 'in-flight') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const lease = Date.parse(row.state.leaseUntil);
|
|
62
|
+
if (!row.state.attempted || row.firstAttemptedAt === undefined) {
|
|
63
|
+
return lease;
|
|
64
|
+
}
|
|
65
|
+
// The provider may have sent it every time: replays stop after a day, and
|
|
66
|
+
// the row stays in flight until its key's retention ends.
|
|
67
|
+
return isNotificationRowAbandoned(row, lease) ? undefined : lease;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether an attempted row is past its replays: its lease would end a day
|
|
71
|
+
* or more after its first attempt.
|
|
72
|
+
*/
|
|
73
|
+
export function isNotificationRowAbandoned(row, leaseUntil) {
|
|
74
|
+
return row.firstAttemptedAt !== undefined
|
|
75
|
+
&& leaseUntil - Date.parse(row.firstAttemptedAt) >= ambiguousReplayMs;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether the scheduler may take a row at `at`: a deferred row once `until`
|
|
79
|
+
* passed, and an in-flight row once its lease expired. That covers a queued
|
|
80
|
+
* delivery whose message was lost, a claim whose holder failed before the
|
|
81
|
+
* provider call, and an ambiguous answer, for a device endpoint, a scheduled
|
|
82
|
+
* send, or a direct platform send whose caller never retries. Ambiguous
|
|
83
|
+
* replays stop 24 hours after the first attempt.
|
|
84
|
+
*/
|
|
85
|
+
export function isNotificationRowDue(row, at) {
|
|
86
|
+
const due = rowDueAt(row);
|
|
87
|
+
return due !== undefined && due <= at;
|
|
88
|
+
}
|
|
89
|
+
/** When the scheduler may first take one of the send's rows, or `undefined` when none waits for it. */
|
|
90
|
+
export function notificationSendDueAt(send) {
|
|
91
|
+
let due;
|
|
92
|
+
for (const row of send.rows) {
|
|
93
|
+
const at = rowDueAt(row);
|
|
94
|
+
if (at !== undefined && (due === undefined || at < due)) {
|
|
95
|
+
due = at;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return due === undefined ? undefined : formatNotificationInstant(due);
|
|
99
|
+
}
|
|
100
|
+
/** Epoch milliseconds of a stored time; one that does not parse is a caller error, as in the D1 store. */
|
|
101
|
+
function instantOf(at) {
|
|
102
|
+
const millis = Date.parse(at);
|
|
103
|
+
if (!Number.isFinite(millis)) {
|
|
104
|
+
throw new TypeError(`Not a timestamp: ${JSON.stringify(at)}`);
|
|
105
|
+
}
|
|
106
|
+
return millis;
|
|
107
|
+
}
|
|
108
|
+
function dueOf(entry) {
|
|
109
|
+
return entry.dueAt === undefined ? undefined : instantOf(entry.dueAt);
|
|
110
|
+
}
|
|
111
|
+
/** The entry as a JSON store keeps it: under its key's player and id, without an undefined `dueAt`. */
|
|
112
|
+
function storedEntry(entry, player, id) {
|
|
113
|
+
const { dueAt, ...rest } = structuredClone(entry);
|
|
114
|
+
return { ...rest, player, id, ...(dueAt === undefined ? {} : { dueAt }) };
|
|
115
|
+
}
|
|
116
|
+
/** Refuses an entry whose `dueAt` does not parse before anything is written. */
|
|
117
|
+
function assertDue(entry) {
|
|
118
|
+
dueOf(entry);
|
|
119
|
+
}
|
|
120
|
+
/** Code-point order, which is UTF-8 byte order: the order of SQLite's BINARY collation. */
|
|
121
|
+
function compareCodePoints(left, right) {
|
|
122
|
+
if (left === right) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const a = Array.from(left);
|
|
126
|
+
const b = Array.from(right);
|
|
127
|
+
for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
|
|
128
|
+
const difference = (a[index]?.codePointAt(0) ?? 0) - (b[index]?.codePointAt(0) ?? 0);
|
|
129
|
+
if (difference !== 0) {
|
|
130
|
+
return difference;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return a.length - b.length;
|
|
134
|
+
}
|
|
135
|
+
export function formatNotificationInstant(millis) {
|
|
136
|
+
return normalizeNotificationTimestamp(new Date(millis).toISOString());
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A process-local store for tests, conformance runs, and local development.
|
|
140
|
+
* Every method finishes its change before its first await, so concurrent
|
|
141
|
+
* callers see each change whole. Production deployments use a durable store.
|
|
142
|
+
*/
|
|
143
|
+
export function createInMemoryNotificationStore() {
|
|
144
|
+
const consent = [];
|
|
145
|
+
const endpoints = [];
|
|
146
|
+
const platformRegistrations = new Map();
|
|
147
|
+
const noticesGiven = [];
|
|
148
|
+
const sends = new Map();
|
|
149
|
+
const history = [];
|
|
150
|
+
const schedule = new Map();
|
|
151
|
+
const scheduleKey = (player, id) => JSON.stringify([player, id]);
|
|
152
|
+
let versions = 0;
|
|
153
|
+
const nextVersion = () => {
|
|
154
|
+
versions += 1;
|
|
155
|
+
return `v${versions}`;
|
|
156
|
+
};
|
|
157
|
+
const copy = (value) => structuredClone(value);
|
|
158
|
+
const noticeName = (notice) => JSON.stringify([
|
|
159
|
+
notice.player,
|
|
160
|
+
notice.kind,
|
|
161
|
+
notice.record.kind,
|
|
162
|
+
Date.parse(notice.record.at),
|
|
163
|
+
notice.dueBy,
|
|
164
|
+
]);
|
|
165
|
+
return {
|
|
166
|
+
inspect() {
|
|
167
|
+
return copy({
|
|
168
|
+
consent: consent.map((row) => ({ ...row.record, player: row.player })),
|
|
169
|
+
endpoints,
|
|
170
|
+
platformRegistrations: [...platformRegistrations.values()],
|
|
171
|
+
noticesGiven,
|
|
172
|
+
sends: [...sends.values()].map((entry) => entry.send),
|
|
173
|
+
history,
|
|
174
|
+
schedule: [...schedule.values()].map((held) => held.entry),
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
async appendConsent({ player, record, at }) {
|
|
178
|
+
let millis = Date.parse(at);
|
|
179
|
+
for (let index = consent.length - 1; index >= 0; index -= 1) {
|
|
180
|
+
const row = consent[index];
|
|
181
|
+
if (row !== undefined && row.player === player) {
|
|
182
|
+
millis = Math.max(millis, Date.parse(row.record.at) + 1);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const stored = { ...copy(record), at: formatNotificationInstant(millis) };
|
|
187
|
+
consent.push({ player, record: stored });
|
|
188
|
+
return copy(stored);
|
|
189
|
+
},
|
|
190
|
+
async listConsent(player) {
|
|
191
|
+
return copy(consent.filter((row) => row.player === player).map((row) => row.record));
|
|
192
|
+
},
|
|
193
|
+
async pruneConsent({ player, at }) {
|
|
194
|
+
const instants = new Set(at.map(instantOf));
|
|
195
|
+
let removed = 0;
|
|
196
|
+
for (let index = consent.length - 1; index >= 0; index -= 1) {
|
|
197
|
+
const row = consent[index];
|
|
198
|
+
if (row !== undefined && row.player === player && instants.has(Date.parse(row.record.at))) {
|
|
199
|
+
consent.splice(index, 1);
|
|
200
|
+
removed += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (let index = noticesGiven.length - 1; index >= 0; index -= 1) {
|
|
204
|
+
const given = noticesGiven[index];
|
|
205
|
+
if (given !== undefined && given.player === player && instants.has(Date.parse(given.record.at))) {
|
|
206
|
+
noticesGiven.splice(index, 1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return removed;
|
|
210
|
+
},
|
|
211
|
+
async putEndpoint(write) {
|
|
212
|
+
for (let index = endpoints.length - 1; index >= 0; index -= 1) {
|
|
213
|
+
const other = endpoints[index];
|
|
214
|
+
if (other !== undefined
|
|
215
|
+
&& other.provider === write.provider
|
|
216
|
+
&& other.address.value === write.address.value
|
|
217
|
+
&& other.installationId !== write.installationId) {
|
|
218
|
+
endpoints.splice(index, 1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const index = endpoints.findIndex((endpoint) => endpoint.provider === write.provider && endpoint.installationId === write.installationId);
|
|
222
|
+
const existing = index === -1 ? undefined : endpoints[index];
|
|
223
|
+
const dead = existing?.status === 'invalid' && existing.address.value === write.address.value;
|
|
224
|
+
const stored = {
|
|
225
|
+
...copy(write),
|
|
226
|
+
status: dead ? 'invalid' : 'valid',
|
|
227
|
+
...(dead && existing.invalidatedAt !== undefined ? { invalidatedAt: existing.invalidatedAt } : {}),
|
|
228
|
+
};
|
|
229
|
+
if (index === -1) {
|
|
230
|
+
endpoints.push(stored);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
endpoints[index] = stored;
|
|
234
|
+
}
|
|
235
|
+
return copy(stored);
|
|
236
|
+
},
|
|
237
|
+
async listEndpoints({ player, provider }) {
|
|
238
|
+
return copy(endpoints.filter((endpoint) => endpoint.player === player && endpoint.provider === provider));
|
|
239
|
+
},
|
|
240
|
+
async getEndpoint({ provider, installationId }) {
|
|
241
|
+
const endpoint = endpoints.find((candidate) => candidate.provider === provider && candidate.installationId === installationId);
|
|
242
|
+
return endpoint === undefined ? undefined : copy(endpoint);
|
|
243
|
+
},
|
|
244
|
+
async invalidateEndpoint({ provider, installationId, value, at }) {
|
|
245
|
+
const index = endpoints.findIndex((endpoint) => endpoint.provider === provider
|
|
246
|
+
&& endpoint.installationId === installationId
|
|
247
|
+
&& endpoint.address.value === value);
|
|
248
|
+
const endpoint = endpoints[index];
|
|
249
|
+
if (endpoint !== undefined && endpoint.status === 'valid') {
|
|
250
|
+
endpoints[index] = { ...endpoint, status: 'invalid', invalidatedAt: at };
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
async removeEndpoints({ installationId, player, provider }) {
|
|
254
|
+
for (let index = endpoints.length - 1; index >= 0; index -= 1) {
|
|
255
|
+
const endpoint = endpoints[index];
|
|
256
|
+
if (endpoint !== undefined
|
|
257
|
+
&& endpoint.installationId === installationId
|
|
258
|
+
&& endpoint.player === player
|
|
259
|
+
&& (provider === undefined || endpoint.provider === provider)) {
|
|
260
|
+
endpoints.splice(index, 1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
async removeSessionEndpoints({ sessionId }) {
|
|
265
|
+
let removed = 0;
|
|
266
|
+
for (let index = endpoints.length - 1; index >= 0; index -= 1) {
|
|
267
|
+
if (endpoints[index]?.sessionId === sessionId) {
|
|
268
|
+
endpoints.splice(index, 1);
|
|
269
|
+
removed += 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return removed;
|
|
273
|
+
},
|
|
274
|
+
async extendSessionEndpoints({ sessionId, expiresAt }) {
|
|
275
|
+
let changed = 0;
|
|
276
|
+
for (let index = 0; index < endpoints.length; index += 1) {
|
|
277
|
+
const endpoint = endpoints[index];
|
|
278
|
+
if (endpoint === undefined
|
|
279
|
+
|| endpoint.sessionId !== sessionId
|
|
280
|
+
|| endpoint.sessionExpiresAt === undefined
|
|
281
|
+
|| (expiresAt !== undefined && Date.parse(endpoint.sessionExpiresAt) >= Date.parse(expiresAt))) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const { sessionExpiresAt: _earlier, ...kept } = endpoint;
|
|
285
|
+
endpoints[index] = expiresAt === undefined ? kept : { ...kept, sessionExpiresAt: expiresAt };
|
|
286
|
+
changed += 1;
|
|
287
|
+
}
|
|
288
|
+
return changed;
|
|
289
|
+
},
|
|
290
|
+
async putPlatformRegistration(registration) {
|
|
291
|
+
platformRegistrations.set(JSON.stringify([registration.player, registration.provider]), copy(registration));
|
|
292
|
+
},
|
|
293
|
+
async getPlatformRegistration({ player, provider }) {
|
|
294
|
+
const registration = platformRegistrations.get(JSON.stringify([player, provider]));
|
|
295
|
+
return registration === undefined ? undefined : copy(registration);
|
|
296
|
+
},
|
|
297
|
+
async addNoticeGiven(notice) {
|
|
298
|
+
const name = noticeName(notice);
|
|
299
|
+
if (!noticesGiven.some((given) => noticeName(given) === name)) {
|
|
300
|
+
noticesGiven.push(copy(notice));
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
async listNoticesGiven(player) {
|
|
304
|
+
return copy(noticesGiven.filter((given) => given.player === player));
|
|
305
|
+
},
|
|
306
|
+
async getSend(key) {
|
|
307
|
+
const entry = sends.get(key);
|
|
308
|
+
return entry === undefined ? undefined : copy(entry);
|
|
309
|
+
},
|
|
310
|
+
async createSend(send) {
|
|
311
|
+
if (sends.has(send.key)) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const version = nextVersion();
|
|
315
|
+
sends.set(send.key, { send: copy(send), version });
|
|
316
|
+
return version;
|
|
317
|
+
},
|
|
318
|
+
async updateSend({ key, version, send }) {
|
|
319
|
+
const entry = sends.get(key);
|
|
320
|
+
if (entry === undefined || entry.version !== version) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (send === undefined) {
|
|
324
|
+
sends.delete(key);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
sends.set(key, { send: copy(send), version: nextVersion() });
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
},
|
|
331
|
+
async listInFlightSends(player) {
|
|
332
|
+
return copy([...sends.values()]
|
|
333
|
+
.map((entry) => entry.send)
|
|
334
|
+
.filter((send) => send.request.player === player && send.rows.some((row) => row.state.status === 'in-flight')));
|
|
335
|
+
},
|
|
336
|
+
async listDueSends({ at, limit }) {
|
|
337
|
+
const now = instantOf(at);
|
|
338
|
+
return [...sends.values()]
|
|
339
|
+
.flatMap(({ send }) => {
|
|
340
|
+
const due = notificationSendDueAt(send);
|
|
341
|
+
return due !== undefined && Date.parse(due) <= now ? [{ key: send.key, due: Date.parse(due) }] : [];
|
|
342
|
+
})
|
|
343
|
+
.sort((left, right) => left.due - right.due)
|
|
344
|
+
.slice(0, Math.max(0, limit))
|
|
345
|
+
.map((entry) => entry.key);
|
|
346
|
+
},
|
|
347
|
+
async addHistory(entry) {
|
|
348
|
+
instantOf(entry.at);
|
|
349
|
+
if (!history.some((sent) => sent.sendKey === entry.sendKey && sent.provider === entry.provider)) {
|
|
350
|
+
history.push(copy(entry));
|
|
351
|
+
history.sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
async listHistory({ player, since }) {
|
|
355
|
+
const from = instantOf(since);
|
|
356
|
+
return copy(history.filter((sent) => sent.player === player && Date.parse(sent.at) >= from));
|
|
357
|
+
},
|
|
358
|
+
async countHistory({ provider, since, until }) {
|
|
359
|
+
const from = instantOf(since);
|
|
360
|
+
const to = instantOf(until);
|
|
361
|
+
return history.filter((sent) => sent.provider === provider && Date.parse(sent.at) >= from && Date.parse(sent.at) <= to).length;
|
|
362
|
+
},
|
|
363
|
+
async getScheduleEntry({ player, id }) {
|
|
364
|
+
const held = schedule.get(scheduleKey(player, id));
|
|
365
|
+
return held === undefined ? undefined : copy(held);
|
|
366
|
+
},
|
|
367
|
+
async createScheduleEntry(entry) {
|
|
368
|
+
assertDue(entry);
|
|
369
|
+
const key = scheduleKey(entry.player, entry.id);
|
|
370
|
+
if (schedule.has(key)) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
const version = nextVersion();
|
|
374
|
+
schedule.set(key, { entry: storedEntry(entry, entry.player, entry.id), version });
|
|
375
|
+
return version;
|
|
376
|
+
},
|
|
377
|
+
async updateScheduleEntry({ player, id, version, entry }) {
|
|
378
|
+
if (entry !== undefined) {
|
|
379
|
+
assertDue(entry);
|
|
380
|
+
}
|
|
381
|
+
const key = scheduleKey(player, id);
|
|
382
|
+
const held = schedule.get(key);
|
|
383
|
+
if (held === undefined || held.version !== version) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
if (entry === undefined) {
|
|
387
|
+
schedule.delete(key);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
schedule.set(key, { entry: storedEntry(entry, player, id), version: nextVersion() });
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
},
|
|
394
|
+
async countPendingReminders(player) {
|
|
395
|
+
return [...schedule.values()].filter(({ entry }) => entry.player === player
|
|
396
|
+
&& entry.id.startsWith('reminder:')
|
|
397
|
+
&& entry.item.kind === 'reminder'
|
|
398
|
+
&& entry.item.firedAt === undefined).length;
|
|
399
|
+
},
|
|
400
|
+
async listDueScheduleEntries({ at, limit }) {
|
|
401
|
+
const now = instantOf(at);
|
|
402
|
+
return [...schedule.values()]
|
|
403
|
+
.flatMap(({ entry }) => {
|
|
404
|
+
const due = dueOf(entry);
|
|
405
|
+
return due !== undefined && due <= now ? [{ player: entry.player, id: entry.id, due }] : [];
|
|
406
|
+
})
|
|
407
|
+
.sort((left, right) => left.due - right.due || compareCodePoints(left.player, right.player) || compareCodePoints(left.id, right.id))
|
|
408
|
+
.slice(0, Math.max(0, limit))
|
|
409
|
+
.map(({ player, id }) => ({ player, id }));
|
|
410
|
+
},
|
|
411
|
+
async purge(cutoffs) {
|
|
412
|
+
for (const cutoff of Object.values(cutoffs)) {
|
|
413
|
+
instantOf(cutoff);
|
|
414
|
+
}
|
|
415
|
+
const before = (at, cutoff) => at !== undefined && Date.parse(at) < Date.parse(cutoff);
|
|
416
|
+
let sendCount = 0;
|
|
417
|
+
for (const [key, entry] of [...sends]) {
|
|
418
|
+
if (before(entry.send.changedAt, cutoffs.sendsChangedBefore)) {
|
|
419
|
+
sends.delete(key);
|
|
420
|
+
sendCount += 1;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const historyBefore = history.length;
|
|
424
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
425
|
+
if (before(history[index]?.at, cutoffs.historyBefore)) {
|
|
426
|
+
history.splice(index, 1);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
let endpointCount = 0;
|
|
430
|
+
for (let index = endpoints.length - 1; index >= 0; index -= 1) {
|
|
431
|
+
const endpoint = endpoints[index];
|
|
432
|
+
if (endpoint !== undefined
|
|
433
|
+
&& ((endpoint.status === 'invalid' && before(endpoint.invalidatedAt, cutoffs.invalidEndpointsBefore))
|
|
434
|
+
|| before(endpoint.lastRegisteredAt, cutoffs.staleEndpointsBefore)
|
|
435
|
+
|| before(endpoint.sessionExpiresAt, cutoffs.sessionEndpointsBefore))) {
|
|
436
|
+
endpoints.splice(index, 1);
|
|
437
|
+
endpointCount += 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return { sends: sendCount, history: historyBefore - history.length, endpoints: endpointCount };
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/game-services",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -129,6 +129,18 @@
|
|
|
129
129
|
"types": "./dist/notification-delivery.d.ts",
|
|
130
130
|
"default": "./dist/notification-delivery.js"
|
|
131
131
|
},
|
|
132
|
+
"./notifications": {
|
|
133
|
+
"types": "./dist/notifications/index.d.ts",
|
|
134
|
+
"default": "./dist/notifications/index.js"
|
|
135
|
+
},
|
|
136
|
+
"./notifications/store-conformance": {
|
|
137
|
+
"types": "./dist/notifications/store-conformance.d.ts",
|
|
138
|
+
"default": "./dist/notifications/store-conformance.js"
|
|
139
|
+
},
|
|
140
|
+
"./notifications/ait-functional-delivery": {
|
|
141
|
+
"types": "./dist/notifications/ait-functional-delivery.d.ts",
|
|
142
|
+
"default": "./dist/notifications/ait-functional-delivery.js"
|
|
143
|
+
},
|
|
132
144
|
"./platform-order": {
|
|
133
145
|
"types": "./dist/platform-order.d.ts",
|
|
134
146
|
"default": "./dist/platform-order.js"
|
|
@@ -203,9 +215,9 @@
|
|
|
203
215
|
"@orpc/client": "2.0.0-beta.14",
|
|
204
216
|
"@orpc/contract": "2.0.0-beta.14",
|
|
205
217
|
"@orpc/server": "2.0.0-beta.14",
|
|
206
|
-
"@mpgd/
|
|
207
|
-
"@mpgd/
|
|
208
|
-
"@mpgd/platform": "0.
|
|
218
|
+
"@mpgd/analytics": "0.4.11",
|
|
219
|
+
"@mpgd/catalog": "0.10.2",
|
|
220
|
+
"@mpgd/platform": "0.24.0"
|
|
209
221
|
},
|
|
210
222
|
"peerDependencies": {
|
|
211
223
|
"@apple/app-store-server-library": "^3.1.0"
|
|
@@ -234,7 +246,7 @@
|
|
|
234
246
|
"fix": "ttsc fix",
|
|
235
247
|
"build": "cd ../.. && node tools/run-ttsx.mjs tools/package/build-packages.ts @mpgd/game-services",
|
|
236
248
|
"test:packed-transport": "pnpm build && node test/runtime-transport-package-import.mjs && node test/reward-evidence-import.mjs",
|
|
237
|
-
"test:unit": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-progress.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/types.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-publisher-client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-identity.test.ts && node tools/run-ttsx.mjs packages/game-services/src/platform-order.test.ts && node tools/run-ttsx.mjs packages/game-services/src/progress-link.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notification-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/admob-ssv.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-verifier.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-evidence-verification-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-partner-api.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-durability-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-transport.test.ts && node tools/run-ttsx.mjs packages/game-services/src/guest-session.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-reward-evidence.test.ts && node tools/run-ttsx.mjs packages/game-services/src/ad-reward-verifier-registry.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-reward.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-routing.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-ledger-revocation.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-anonymous-auth.test.ts",
|
|
249
|
+
"test:unit": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-progress.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/types.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-publisher-client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-identity.test.ts && node tools/run-ttsx.mjs packages/game-services/src/platform-order.test.ts && node tools/run-ttsx.mjs packages/game-services/src/progress-link.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notification-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notifications/service.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notifications/service-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notifications/scheduler.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notifications/fcm.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notifications/store-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notifications/ait-functional-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/admob-ssv.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-verifier.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-evidence-verification-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-partner-api.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-durability-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-transport.test.ts && node tools/run-ttsx.mjs packages/game-services/src/guest-session.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-reward-evidence.test.ts && node tools/run-ttsx.mjs packages/game-services/src/ad-reward-verifier-registry.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-reward.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-routing.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-ledger-revocation.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-anonymous-auth.test.ts",
|
|
238
250
|
"test:recovery": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/monetization-recovery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-recovery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-reconciler.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-reconciler-conformance.test.ts",
|
|
239
251
|
"test": "pnpm test:unit && pnpm test:app-store-server && pnpm test:recovery && pnpm test:packed-transport",
|
|
240
252
|
"test:app-store-server": "vitest run"
|