@mpgd/game-services 0.8.1 → 0.8.2

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.
@@ -124,7 +124,7 @@ export class InMemoryVerifiedLeaderboardService {
124
124
  }
125
125
  createRankedEntries(definition) {
126
126
  return [...this.getRetainedAttempts(definition.leaderboardId).values()]
127
- .sort((left, right) => compareAttempts(definition.scoreOrder, left, right))
127
+ .sort((left, right) => compareLeaderboardOrder(definition.scoreOrder, left, right))
128
128
  .map((attempt, index) => toRankedEntry(attempt, index + 1));
129
129
  }
130
130
  }
@@ -238,8 +238,10 @@ export function assertVerifiedLeaderboardSnapshot(input) {
238
238
  if (!Array.isArray(input.entries)) {
239
239
  throw new Error('entries must be an array.');
240
240
  }
241
+ const entries = [];
241
242
  for (const entry of input.entries) {
242
243
  assertLeaderboardRankedEntry(entry);
244
+ entries.push(entry);
243
245
  }
244
246
  if (input.participantEntry !== undefined) {
245
247
  assertLeaderboardRankedEntry(input.participantEntry);
@@ -249,8 +251,87 @@ export function assertVerifiedLeaderboardSnapshot(input) {
249
251
  || input.totalParticipants < 0) {
250
252
  throw new Error('totalParticipants must be a non-negative integer.');
251
253
  }
254
+ assertVerifiedLeaderboardSnapshotEntries(input.definition, entries, input.participantEntry, input.totalParticipants);
252
255
  assertTimestamp(input.generatedAt, 'generatedAt');
253
- assertOptionalCursor(input.nextCursor);
256
+ assertVerifiedLeaderboardSnapshotCursor(input.definition, entries, input.totalParticipants, input.nextCursor);
257
+ }
258
+ function assertVerifiedLeaderboardSnapshotEntries(definition, entries, participantEntry, totalParticipants) {
259
+ const participantIds = new Set();
260
+ const attemptIds = new Set();
261
+ for (const [index, entry] of entries.entries()) {
262
+ if (entry.rank > totalParticipants) {
263
+ throw new Error('entry ranks must not exceed totalParticipants.');
264
+ }
265
+ if (participantIds.has(entry.participantId)) {
266
+ throw new Error('entries must not repeat a participantId.');
267
+ }
268
+ if (attemptIds.has(entry.attemptId)) {
269
+ throw new Error('entries must not repeat an attemptId.');
270
+ }
271
+ participantIds.add(entry.participantId);
272
+ attemptIds.add(entry.attemptId);
273
+ const previous = entries[index - 1];
274
+ if (previous === undefined) {
275
+ continue;
276
+ }
277
+ if (entry.rank !== previous.rank + 1) {
278
+ throw new Error('entry ranks must be contiguous within a snapshot page.');
279
+ }
280
+ if (compareLeaderboardOrder(definition.scoreOrder, previous, entry) >= 0) {
281
+ throw new Error('entries must follow the leaderboard ranking order.');
282
+ }
283
+ }
284
+ if (participantEntry !== undefined) {
285
+ if (participantEntry.rank > totalParticipants) {
286
+ throw new Error('participantEntry rank must not exceed totalParticipants.');
287
+ }
288
+ const overlappingEntry = entries.find((entry) => entry.rank === participantEntry.rank
289
+ || entry.participantId === participantEntry.participantId
290
+ || entry.attemptId === participantEntry.attemptId);
291
+ if (overlappingEntry !== undefined
292
+ && !areLeaderboardRankedEntriesEqual(overlappingEntry, participantEntry)) {
293
+ throw new Error('participantEntry must match overlapping snapshot entries.');
294
+ }
295
+ for (const entry of entries) {
296
+ if (entry.rank === participantEntry.rank) {
297
+ continue;
298
+ }
299
+ const order = compareLeaderboardOrder(definition.scoreOrder, participantEntry, entry);
300
+ const participantRanksBeforeEntry = participantEntry.rank < entry.rank;
301
+ if ((participantRanksBeforeEntry && order >= 0)
302
+ || (!participantRanksBeforeEntry && order <= 0)) {
303
+ throw new Error('participantEntry must follow the leaderboard ranking order.');
304
+ }
305
+ }
306
+ }
307
+ }
308
+ function areLeaderboardRankedEntriesEqual(left, right) {
309
+ return left.rank === right.rank
310
+ && left.participantId === right.participantId
311
+ && left.participantLabel === right.participantLabel
312
+ && left.attemptId === right.attemptId
313
+ && left.score === right.score
314
+ && parseTimestamp(left.completedAt) === parseTimestamp(right.completedAt)
315
+ && areVerifiedLeaderboardMetricsEqual(left.metrics, right.metrics);
316
+ }
317
+ function assertVerifiedLeaderboardSnapshotCursor(definition, entries, totalParticipants, nextCursor) {
318
+ assertOptionalCursor(nextCursor);
319
+ const lastEntry = entries.at(-1);
320
+ if (nextCursor === undefined) {
321
+ if (lastEntry !== undefined && lastEntry.rank < totalParticipants) {
322
+ throw new Error('nextCursor must be present when snapshot entries remain.');
323
+ }
324
+ return;
325
+ }
326
+ if (lastEntry === undefined) {
327
+ throw new Error('nextCursor requires at least one snapshot entry.');
328
+ }
329
+ if (lastEntry.rank >= totalParticipants) {
330
+ throw new Error('nextCursor must be omitted after the final snapshot entry.');
331
+ }
332
+ if (nextCursor !== createVerifiedLeaderboardCursor(definition, lastEntry)) {
333
+ throw new Error('nextCursor must continue after the final snapshot entry.');
334
+ }
254
335
  }
255
336
  export function createVerifiedLeaderboardCursor(definition, entry) {
256
337
  assertVerifiedLeaderboardDefinition(definition);
@@ -306,7 +387,7 @@ function shouldReplaceRetainedAttempt(definition, retained, candidate) {
306
387
  if (definition.attemptSelection === 'first') {
307
388
  return compareAttemptChronology(candidate, retained) < 0;
308
389
  }
309
- return compareAttempts(definition.scoreOrder, candidate, retained) < 0;
390
+ return compareLeaderboardOrder(definition.scoreOrder, candidate, retained) < 0;
310
391
  }
311
392
  function compareAttemptChronology(left, right) {
312
393
  const completedAtComparison = parseTimestamp(left.completedAt) - parseTimestamp(right.completedAt);
@@ -315,7 +396,7 @@ function compareAttemptChronology(left, right) {
315
396
  }
316
397
  return compareOrdinal(left.attemptId, right.attemptId);
317
398
  }
318
- function compareAttempts(scoreOrder, left, right) {
399
+ function compareLeaderboardOrder(scoreOrder, left, right) {
319
400
  if (left.score !== right.score) {
320
401
  return scoreOrder === 'ascending' ? left.score - right.score : right.score - left.score;
321
402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-services",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {