@mpgd/game-services 0.8.2 → 0.10.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.
@@ -0,0 +1,193 @@
1
+ import { assertRecordVerifiedLeaderboardAttemptResponse, assertVerifiedLeaderboardSnapshot, } from './verified-leaderboard';
2
+ export const verifiedLeaderboardDurabilityConformanceScenarios = [
3
+ 'replacement-after-interrupted-retained-write',
4
+ 'concurrent-retry-after-interrupted-retained-write',
5
+ 'snapshot-after-interrupted-retained-write',
6
+ ];
7
+ const durabilityScenarioRunners = {
8
+ 'replacement-after-interrupted-retained-write': runReplacementAfterInterruptedRetainedWriteScenario,
9
+ 'concurrent-retry-after-interrupted-retained-write': runConcurrentRetryAfterInterruptedRetainedWriteScenario,
10
+ 'snapshot-after-interrupted-retained-write': runSnapshotAfterInterruptedRetainedWriteScenario,
11
+ };
12
+ /**
13
+ * Runs provider-neutral recovery checks that require a provider-specific fault
14
+ * boundary. It complements the semantic conformance suite and deliberately
15
+ * does not prescribe a shared persistence implementation.
16
+ */
17
+ export async function runVerifiedLeaderboardDurabilityConformance(input) {
18
+ const now = input.now ?? '2030-01-02T03:04:05.000Z';
19
+ const passedScenarios = [];
20
+ for (const scenario of verifiedLeaderboardDurabilityConformanceScenarios) {
21
+ const runScenario = durabilityScenarioRunners[scenario];
22
+ let fixture;
23
+ let scenarioError;
24
+ let scenarioFailed = false;
25
+ try {
26
+ fixture = await input.createFixture({ scenario, now });
27
+ await runScenario(fixture);
28
+ }
29
+ catch (error) {
30
+ scenarioError = error;
31
+ scenarioFailed = true;
32
+ }
33
+ let cleanupError;
34
+ let cleanupFailed = false;
35
+ try {
36
+ await fixture?.dispose?.();
37
+ }
38
+ catch (error) {
39
+ cleanupError = error;
40
+ cleanupFailed = true;
41
+ }
42
+ if (scenarioFailed) {
43
+ throw new Error(`Verified leaderboard durability conformance failed: ${scenario}.`, {
44
+ cause: cleanupFailed
45
+ ? new AggregateError([scenarioError, cleanupError], `Scenario and fixture cleanup both failed: ${scenario}.`)
46
+ : scenarioError,
47
+ });
48
+ }
49
+ if (cleanupFailed) {
50
+ const message = `Verified leaderboard durability conformance cleanup failed: ${scenario}.`;
51
+ throw new Error(message, { cause: cleanupError });
52
+ }
53
+ passedScenarios.push(scenario);
54
+ }
55
+ return { passedScenarios };
56
+ }
57
+ /**
58
+ * Wraps an atomic provider so the next armed record commits successfully but
59
+ * its caller observes an interruption. Use a provider-specific fixture when
60
+ * the implementation has a recoverable boundary inside a multi-write flow.
61
+ */
62
+ export function createAmbiguousCommitVerifiedLeaderboardDurabilityFixture(provider, dispose) {
63
+ let interruptNextRecord = false;
64
+ return {
65
+ service: {
66
+ async recordVerifiedAttempt(input) {
67
+ const response = await provider.recordVerifiedAttempt(input);
68
+ if (interruptNextRecord) {
69
+ interruptNextRecord = false;
70
+ throw new Error('Simulated verified leaderboard response interruption.');
71
+ }
72
+ return response;
73
+ },
74
+ getSnapshot: (input) => provider.getSnapshot(input),
75
+ },
76
+ interruptNextRetainedRecord() {
77
+ if (interruptNextRecord) {
78
+ throw new Error('A verified leaderboard interruption is already armed.');
79
+ }
80
+ interruptNextRecord = true;
81
+ },
82
+ ...(dispose === undefined ? {} : { dispose }),
83
+ };
84
+ }
85
+ async function runReplacementAfterInterruptedRetainedWriteScenario(fixture) {
86
+ const interrupted = createAttempt('replacement-after-interrupted-retained-write', 'interrupted', 10, '2030-01-02T03:00:00.000Z');
87
+ const later = createAttempt('replacement-after-interrupted-retained-write', 'later-better', 20, '2030-01-02T03:01:00.000Z');
88
+ fixture.interruptNextRetainedRecord();
89
+ await assertRejects(() => fixture.service.recordVerifiedAttempt(interrupted), 'the armed retained record must surface an interruption');
90
+ const laterResponse = await fixture.service.recordVerifiedAttempt(later);
91
+ assertRecordVerifiedLeaderboardAttemptResponse(laterResponse);
92
+ assertEqual(laterResponse.retained, true, 'the later better attempt must be retained');
93
+ assertEqual(laterResponse.entry.attemptId, later.attempt.attemptId, 'the later better attempt must replace the interrupted retained entry');
94
+ const retry = await fixture.service.recordVerifiedAttempt(interrupted);
95
+ assertOriginalInterruptedDecision(retry, interrupted);
96
+ const snapshot = await fixture.service.getSnapshot({
97
+ leaderboardId: interrupted.definition.leaderboardId,
98
+ participantId: interrupted.attempt.participantId,
99
+ });
100
+ assert(snapshot !== undefined, 'the recovered board must return a snapshot');
101
+ assertVerifiedLeaderboardSnapshot(snapshot);
102
+ assertEqual(snapshot.totalParticipants, 1, 'the replacement must keep one participant');
103
+ assertEqual(snapshot.entries.length, 1, 'the replacement must keep one ranked entry');
104
+ assertEqual(snapshot.entries[0]?.participantId, later.attempt.participantId, 'the ranked snapshot must contain the replacement participant');
105
+ assertEqual(snapshot.entries[0]?.attemptId, later.attempt.attemptId, 'the ranked snapshot must contain the later better attempt');
106
+ assertEqual(snapshot.entries[0]?.score, later.attempt.score, 'the ranked snapshot must contain the later better score');
107
+ assertEqual(snapshot.participantEntry?.attemptId, later.attempt.attemptId, 'the final snapshot must retain the later better attempt');
108
+ }
109
+ async function runConcurrentRetryAfterInterruptedRetainedWriteScenario(fixture) {
110
+ const interrupted = createAttempt('concurrent-retry-after-interrupted-retained-write', 'interrupted', 10, '2030-01-02T03:00:00.000Z');
111
+ fixture.interruptNextRetainedRecord();
112
+ await assertRejects(() => fixture.service.recordVerifiedAttempt(interrupted), 'the armed retained record must surface an interruption');
113
+ const retries = await Promise.all([
114
+ fixture.service.recordVerifiedAttempt(interrupted),
115
+ fixture.service.recordVerifiedAttempt(interrupted),
116
+ ]);
117
+ for (const retry of retries) {
118
+ assertOriginalInterruptedDecision(retry, interrupted);
119
+ }
120
+ const snapshot = await fixture.service.getSnapshot({
121
+ leaderboardId: interrupted.definition.leaderboardId,
122
+ });
123
+ assert(snapshot !== undefined, 'the concurrently recovered board must return a snapshot');
124
+ assertVerifiedLeaderboardSnapshot(snapshot);
125
+ assertEqual(snapshot.totalParticipants, 1, 'concurrent retries must not duplicate participants');
126
+ assertEqual(snapshot.entries.length, 1, 'concurrent retries must not duplicate retained rows');
127
+ }
128
+ async function runSnapshotAfterInterruptedRetainedWriteScenario(fixture) {
129
+ const interrupted = createAttempt('snapshot-after-interrupted-retained-write', 'interrupted', 10, '2030-01-02T03:00:00.000Z');
130
+ fixture.interruptNextRetainedRecord();
131
+ await assertRejects(() => fixture.service.recordVerifiedAttempt(interrupted), 'the armed retained record must surface an interruption');
132
+ const snapshot = await fixture.service.getSnapshot({
133
+ leaderboardId: interrupted.definition.leaderboardId,
134
+ participantId: interrupted.attempt.participantId,
135
+ });
136
+ assert(snapshot !== undefined, 'reads must recover an interrupted retained record');
137
+ assertVerifiedLeaderboardSnapshot(snapshot);
138
+ assertEqual(snapshot.totalParticipants, 1, 'recovery must restore one participant');
139
+ assertEqual(snapshot.entries.length, 1, 'recovery must restore one ranked entry');
140
+ assertEqual(snapshot.entries[0]?.participantId, interrupted.attempt.participantId, 'reads must expose the recovered participant in ranked entries');
141
+ assertEqual(snapshot.entries[0]?.attemptId, interrupted.attempt.attemptId, 'reads must expose the recovered attempt in ranked entries');
142
+ assertEqual(snapshot.entries[0]?.score, interrupted.attempt.score, 'reads must expose the recovered score in ranked entries');
143
+ assertEqual(snapshot.participantEntry?.attemptId, interrupted.attempt.attemptId, 'reads must expose the recovered retained entry');
144
+ const retry = await fixture.service.recordVerifiedAttempt(interrupted);
145
+ assertOriginalInterruptedDecision(retry, interrupted);
146
+ }
147
+ function assertOriginalInterruptedDecision(response, interrupted) {
148
+ assertRecordVerifiedLeaderboardAttemptResponse(response);
149
+ assertEqual(response.alreadyProcessed, true, 'retries must detect the interrupted write');
150
+ assertEqual(response.retained, true, 'retries must preserve the original retained decision');
151
+ assertEqual(response.entry.attemptId, interrupted.attempt.attemptId, 'retries must preserve the original response entry');
152
+ assertEqual(response.entry.score, interrupted.attempt.score, 'retries must preserve the original response score');
153
+ }
154
+ function createAttempt(scenario, suffix, score, completedAt) {
155
+ return {
156
+ definition: {
157
+ leaderboardId: `durability:${scenario}`,
158
+ scoreOrder: 'descending',
159
+ attemptSelection: 'best',
160
+ },
161
+ attempt: {
162
+ participantId: `participant:${scenario}`,
163
+ attemptId: `attempt:${scenario}:${suffix}`,
164
+ score,
165
+ completedAt,
166
+ verification: {
167
+ authorityId: 'verified-leaderboard-durability-conformance',
168
+ evidenceId: `evidence:${scenario}:${suffix}`,
169
+ verifiedAt: completedAt,
170
+ },
171
+ },
172
+ };
173
+ }
174
+ async function assertRejects(callback, message) {
175
+ let rejected = false;
176
+ try {
177
+ await callback();
178
+ }
179
+ catch {
180
+ rejected = true;
181
+ }
182
+ assert(rejected, message);
183
+ }
184
+ function assert(condition, message) {
185
+ if (!condition) {
186
+ throw new Error(message);
187
+ }
188
+ }
189
+ function assertEqual(actual, expected, message) {
190
+ if (actual !== expected) {
191
+ throw new Error(`${message}: expected ${String(expected)}, received ${String(actual)}.`);
192
+ }
193
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-services",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,6 +29,30 @@
29
29
  "types": "./dist/index.d.ts",
30
30
  "default": "./dist/index.js"
31
31
  },
32
+ "./admob-ssv": {
33
+ "types": "./dist/admob-ssv.d.ts",
34
+ "default": "./dist/admob-ssv.js"
35
+ },
36
+ "./admob-ssv-conformance": {
37
+ "types": "./dist/admob-ssv-conformance.d.ts",
38
+ "default": "./dist/admob-ssv-conformance.js"
39
+ },
40
+ "./app-store-verifier": {
41
+ "types": "./dist/app-store-verifier.d.ts",
42
+ "default": "./dist/app-store-verifier.js"
43
+ },
44
+ "./app-store-verifier-conformance": {
45
+ "types": "./dist/app-store-verifier-conformance.d.ts",
46
+ "default": "./dist/app-store-verifier-conformance.js"
47
+ },
48
+ "./apps-in-toss-evidence-verification": {
49
+ "types": "./dist/apps-in-toss-evidence-verification.d.ts",
50
+ "default": "./dist/apps-in-toss-evidence-verification.js"
51
+ },
52
+ "./apps-in-toss-evidence-verification-conformance": {
53
+ "types": "./dist/apps-in-toss-evidence-verification-conformance.d.ts",
54
+ "default": "./dist/apps-in-toss-evidence-verification-conformance.js"
55
+ },
32
56
  "./client": {
33
57
  "types": "./dist/client.d.ts",
34
58
  "default": "./dist/client.js"
@@ -41,6 +65,14 @@
41
65
  "types": "./dist/evidence-verification.d.ts",
42
66
  "default": "./dist/evidence-verification.js"
43
67
  },
68
+ "./google-play-purchase": {
69
+ "types": "./dist/google-play-purchase.d.ts",
70
+ "default": "./dist/google-play-purchase.js"
71
+ },
72
+ "./google-play-purchase-conformance": {
73
+ "types": "./dist/google-play-purchase-conformance.d.ts",
74
+ "default": "./dist/google-play-purchase-conformance.js"
75
+ },
44
76
  "./notification-delivery": {
45
77
  "types": "./dist/notification-delivery.d.ts",
46
78
  "default": "./dist/notification-delivery.js"
@@ -76,18 +108,23 @@
76
108
  "./verified-leaderboard-conformance": {
77
109
  "types": "./dist/verified-leaderboard-conformance.d.ts",
78
110
  "default": "./dist/verified-leaderboard-conformance.js"
111
+ },
112
+ "./verified-leaderboard-durability-conformance": {
113
+ "types": "./dist/verified-leaderboard-durability-conformance.d.ts",
114
+ "default": "./dist/verified-leaderboard-durability-conformance.js"
79
115
  }
80
116
  },
81
117
  "files": [
82
118
  "dist"
83
119
  ],
84
120
  "dependencies": {
121
+ "@noble/secp256k1": "3.1.0",
85
122
  "@orpc/client": "2.0.0-beta.14",
86
123
  "@orpc/contract": "2.0.0-beta.14",
87
124
  "@orpc/server": "2.0.0-beta.14",
88
- "@mpgd/analytics": "0.3.6",
89
- "@mpgd/catalog": "0.4.0",
90
- "@mpgd/platform": "0.6.0"
125
+ "@mpgd/catalog": "0.5.0",
126
+ "@mpgd/platform": "0.7.0",
127
+ "@mpgd/analytics": "0.3.7"
91
128
  },
92
129
  "devDependencies": {
93
130
  "ttsc": "0.18.4",
@@ -103,6 +140,6 @@
103
140
  "lint": "ttsc --noEmit",
104
141
  "format": "ttsc format",
105
142
  "fix": "ttsc fix",
106
- "test": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.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/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-transport.test.ts"
143
+ "test": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.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/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/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"
107
144
  }
108
145
  }