@mpgd/adapter-devvit 0.13.3 → 0.13.4
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.
|
@@ -20,6 +20,12 @@ export declare const devvitCommerceV2MethodUnsupportedBridgeErrorCode: 'COMMERCE
|
|
|
20
20
|
export declare const devvitCommerceCheckoutClientOnlyBridgeErrorCode: 'DEVVIT_COMMERCE_CHECKOUT_CLIENT_ONLY';
|
|
21
21
|
/** Bridge error when the payments plugin fails; retryable. */
|
|
22
22
|
export declare const devvitPaymentsUnavailableBridgeErrorCode: 'DEVVIT_PAYMENTS_UNAVAILABLE';
|
|
23
|
+
/**
|
|
24
|
+
* Bridge error when the paid-order scan hit its page cap with pages left.
|
|
25
|
+
* Retryable. A caller must not treat orders absent from this scan as delivered:
|
|
26
|
+
* the list was not finished.
|
|
27
|
+
*/
|
|
28
|
+
export declare const devvitOrderScanIncompleteBridgeErrorCode: 'DEVVIT_ORDER_SCAN_INCOMPLETE';
|
|
23
29
|
/** `Order.status` as `@devvit/protos` encodes it (`OrderStatus` enum) or as the handler types spell it. */
|
|
24
30
|
export type DevvitPaymentsOrderStatusLike = number | string;
|
|
25
31
|
/** The fields of a `@devvit/payments` `Order` the handler reads. */
|
|
@@ -81,6 +87,17 @@ export interface DevvitCommerceV2BridgeHandler {
|
|
|
81
87
|
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
82
88
|
}
|
|
83
89
|
/**
|
|
90
|
+
* Hard cap on the pages scanned per listing. `@devvit/payments` 0.14 ignores
|
|
91
|
+
* `status` and `buyer` (`GetOrdersRequest` marks both "currently does nothing";
|
|
92
|
+
* an empty filter means every order), and `PaymentsClient` forwards the
|
|
93
|
+
* request unchanged, so every status is scanned. Devvit serves this handler
|
|
94
|
+
* inside a time-bounded HTTP request. Thirty-two serial plugin calls stay
|
|
95
|
+
* inside that budget (the kit treats about 20 seconds as the practical limit
|
|
96
|
+
* for a Devvit task). Hitting the cap with a next page fails the request
|
|
97
|
+
* instead of returning a partial list: unseen orders are not delivered.
|
|
98
|
+
*/
|
|
99
|
+
export declare const maximumDevvitOrderScanPages = 32;
|
|
100
|
+
/**
|
|
84
101
|
* Serves `commerce.v2.*` from the payments plugin:
|
|
85
102
|
*
|
|
86
103
|
* - `getCapabilities` answers the Devvit profile (`createDevvitCommerceCapabilities`).
|
|
@@ -92,13 +109,10 @@ export interface DevvitCommerceV2BridgeHandler {
|
|
|
92
109
|
* `purchased` (the order id as `transactionId` and `token`,
|
|
93
110
|
* `devvit.order.v1` evidence whose `status` says which), bounded by
|
|
94
111
|
* `maximumDevvitAvailableOrders`. Reddit marks an order `DELIVERED` as soon
|
|
95
|
-
* as the fulfillment handler answered
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* (newest first, as the Devvit docs describe) and stops after two
|
|
100
|
-
* consecutive pages without a paid or delivered order or after
|
|
101
|
-
* `maximumOrderPages`, warning when pages are left.
|
|
112
|
+
* as the fulfillment handler answered. The scan reads pages of 64 until the
|
|
113
|
+
* cursor ends, or until `maximumDevvitOrderScanPages`. A scan that still has
|
|
114
|
+
* a next page at that cap fails with `DEVVIT_ORDER_SCAN_INCOMPLETE` rather
|
|
115
|
+
* than returning a partial list: an order that was not read is not delivered.
|
|
102
116
|
* - `requestPurchase` is refused with `DEVVIT_COMMERCE_CHECKOUT_CLIENT_ONLY`,
|
|
103
117
|
* `finishTransaction` with `COMMERCE_V2_METHOD_UNSUPPORTED`, and
|
|
104
118
|
* `restorePurchases` answers `{ restored: false }`: the webview provider
|
|
@@ -22,6 +22,12 @@ export const devvitCommerceV2MethodUnsupportedBridgeErrorCode = 'COMMERCE_V2_MET
|
|
|
22
22
|
export const devvitCommerceCheckoutClientOnlyBridgeErrorCode = 'DEVVIT_COMMERCE_CHECKOUT_CLIENT_ONLY';
|
|
23
23
|
/** Bridge error when the payments plugin fails; retryable. */
|
|
24
24
|
export const devvitPaymentsUnavailableBridgeErrorCode = 'DEVVIT_PAYMENTS_UNAVAILABLE';
|
|
25
|
+
/**
|
|
26
|
+
* Bridge error when the paid-order scan hit its page cap with pages left.
|
|
27
|
+
* Retryable. A caller must not treat orders absent from this scan as delivered:
|
|
28
|
+
* the list was not finished.
|
|
29
|
+
*/
|
|
30
|
+
export const devvitOrderScanIncompleteBridgeErrorCode = 'DEVVIT_ORDER_SCAN_INCOMPLETE';
|
|
25
31
|
/** `OrderStatus.ORDER_STATUS_PAID` of `@devvit/protos`, and its handler-type spelling. */
|
|
26
32
|
const paidOrderStatuses = new Set([
|
|
27
33
|
3,
|
|
@@ -61,17 +67,16 @@ const durableAccountingTypes = new Set([
|
|
|
61
67
|
]);
|
|
62
68
|
const orderPageSize = maximumDevvitAvailableOrders;
|
|
63
69
|
/**
|
|
64
|
-
* Hard cap on the pages scanned per listing.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
70
|
+
* Hard cap on the pages scanned per listing. `@devvit/payments` 0.14 ignores
|
|
71
|
+
* `status` and `buyer` (`GetOrdersRequest` marks both "currently does nothing";
|
|
72
|
+
* an empty filter means every order), and `PaymentsClient` forwards the
|
|
73
|
+
* request unchanged, so every status is scanned. Devvit serves this handler
|
|
74
|
+
* inside a time-bounded HTTP request. Thirty-two serial plugin calls stay
|
|
75
|
+
* inside that budget (the kit treats about 20 seconds as the practical limit
|
|
76
|
+
* for a Devvit task). Hitting the cap with a next page fails the request
|
|
77
|
+
* instead of returning a partial list: unseen orders are not delivered.
|
|
71
78
|
*/
|
|
72
|
-
const
|
|
73
|
-
/** Consecutive pages without a paid or delivered order after which the scan stops early. */
|
|
74
|
-
const settledPagesBeforeStop = 2;
|
|
79
|
+
export const maximumDevvitOrderScanPages = 32;
|
|
75
80
|
/**
|
|
76
81
|
* Serves `commerce.v2.*` from the payments plugin:
|
|
77
82
|
*
|
|
@@ -84,13 +89,10 @@ const settledPagesBeforeStop = 2;
|
|
|
84
89
|
* `purchased` (the order id as `transactionId` and `token`,
|
|
85
90
|
* `devvit.order.v1` evidence whose `status` says which), bounded by
|
|
86
91
|
* `maximumDevvitAvailableOrders`. Reddit marks an order `DELIVERED` as soon
|
|
87
|
-
* as the fulfillment handler answered
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* (newest first, as the Devvit docs describe) and stops after two
|
|
92
|
-
* consecutive pages without a paid or delivered order or after
|
|
93
|
-
* `maximumOrderPages`, warning when pages are left.
|
|
92
|
+
* as the fulfillment handler answered. The scan reads pages of 64 until the
|
|
93
|
+
* cursor ends, or until `maximumDevvitOrderScanPages`. A scan that still has
|
|
94
|
+
* a next page at that cap fails with `DEVVIT_ORDER_SCAN_INCOMPLETE` rather
|
|
95
|
+
* than returning a partial list: an order that was not read is not delivered.
|
|
94
96
|
* - `requestPurchase` is refused with `DEVVIT_COMMERCE_CHECKOUT_CLIENT_ONLY`,
|
|
95
97
|
* `finishTransaction` with `COMMERCE_V2_METHOD_UNSUPPORTED`, and
|
|
96
98
|
* `restorePurchases` answers `{ restored: false }`: the webview provider
|
|
@@ -113,19 +115,14 @@ export function createDevvitCommerceV2BridgeHandler(input) {
|
|
|
113
115
|
const listPaidOrders = async () => {
|
|
114
116
|
const orders = [];
|
|
115
117
|
let cursor;
|
|
116
|
-
let
|
|
117
|
-
for (let page = 0; page < maximumOrderPages; page += 1) {
|
|
118
|
+
for (let page = 0; page < maximumDevvitOrderScanPages; page += 1) {
|
|
118
119
|
const response = await input.payments.getOrders({
|
|
119
120
|
limit: orderPageSize,
|
|
120
121
|
...(cursor === undefined ? {} : { cursor }),
|
|
121
122
|
});
|
|
122
123
|
const next = response.pageInfo;
|
|
123
124
|
const hasNextPage = next !== undefined && next.hasNextPage && next.endCursor.length > 0;
|
|
124
|
-
let listableOnPage = 0;
|
|
125
125
|
for (const [index, order] of response.orders.entries()) {
|
|
126
|
-
if (paidOrderStatuses.has(order.status) || deliveredOrderStatuses.has(order.status)) {
|
|
127
|
-
listableOnPage += 1;
|
|
128
|
-
}
|
|
129
126
|
const paid = readListedOrder(order, productsBySku);
|
|
130
127
|
if (paid !== undefined) {
|
|
131
128
|
orders.push(paid);
|
|
@@ -138,21 +135,15 @@ export function createDevvitCommerceV2BridgeHandler(input) {
|
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
137
|
if (!hasNextPage || next === undefined) {
|
|
141
|
-
|
|
138
|
+
return orders;
|
|
142
139
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
warn(`Devvit order scan stopped after ${String(settledPagesBeforeStop)} pages without a `
|
|
147
|
-
+ 'paid or delivered order; older pages were not read.');
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
if (page + 1 === maximumOrderPages) {
|
|
151
|
-
warn('Devvit order scan reached its page cap; older pages were not read.');
|
|
140
|
+
if (page + 1 === maximumDevvitOrderScanPages) {
|
|
141
|
+
// A partial list would look complete. Unseen orders are not delivered.
|
|
142
|
+
throw incompleteOrderScan();
|
|
152
143
|
}
|
|
153
144
|
cursor = next.endCursor;
|
|
154
145
|
}
|
|
155
|
-
|
|
146
|
+
throw incompleteOrderScan();
|
|
156
147
|
};
|
|
157
148
|
const serve = async (request) => {
|
|
158
149
|
const { payload } = request;
|
|
@@ -343,6 +334,14 @@ function normalizeBridgeProducts(input) {
|
|
|
343
334
|
return { productId: product.productId, sku: product.sku };
|
|
344
335
|
});
|
|
345
336
|
}
|
|
337
|
+
function incompleteOrderScan() {
|
|
338
|
+
return new PlatformOperationError({
|
|
339
|
+
code: devvitOrderScanIncompleteBridgeErrorCode,
|
|
340
|
+
message: `Devvit order scan is incomplete: it reached ${String(maximumDevvitOrderScanPages)} `
|
|
341
|
+
+ 'pages with pages left. Unseen orders were not read and are not delivered.',
|
|
342
|
+
retryable: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
346
345
|
async function paymentsCall(call) {
|
|
347
346
|
try {
|
|
348
347
|
return await call();
|
|
@@ -4,6 +4,8 @@ const defaultSnapshotLimit = 10;
|
|
|
4
4
|
const defaultTransactionAttempts = 3;
|
|
5
5
|
const maximumTransactionAttempts = 32;
|
|
6
6
|
const keyPrefixPattern = /^[A-Za-z0-9:_-]{1,128}$/;
|
|
7
|
+
/** The commit marker's value; its SET replies `OK`, which `@devvit/redis` always maps. */
|
|
8
|
+
const commitMarkerValue = '1';
|
|
7
9
|
/**
|
|
8
10
|
* Durable Devvit Redis provider for modest verified leaderboards. Writes are
|
|
9
11
|
* fenced with WATCH/MULTI/EXEC and keep the original idempotency response.
|
|
@@ -66,13 +68,22 @@ export function createDevvitRedisVerifiedLeaderboardService(redis, options = {})
|
|
|
66
68
|
await transaction.hSet(keys.attempts, {
|
|
67
69
|
[attemptMember]: serializeAttemptDecision(request.attempt, response),
|
|
68
70
|
});
|
|
71
|
+
// `@devvit/redis` `TxClient.exec()` resolves an aborted WATCH as `[]`
|
|
72
|
+
// and drops zero or nil replies. The marker's `OK` always maps, so a
|
|
73
|
+
// committed EXEC answers at least that reply even when the array is
|
|
74
|
+
// shorter than the queued commands. Only an empty, `null`, or all-nil
|
|
75
|
+
// result is an abort. Requiring `results.length === queued` would
|
|
76
|
+
// retry a landed score whose zero replies Devvit dropped.
|
|
77
|
+
await transaction.set(keys.commitMarker, commitMarkerValue);
|
|
69
78
|
const results = await transaction.exec();
|
|
70
|
-
if (results
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (!Array.isArray(results)) {
|
|
79
|
+
if (results !== null && !Array.isArray(results)) {
|
|
74
80
|
throw new Error('Devvit Redis transaction returned an unsupported response.');
|
|
75
81
|
}
|
|
82
|
+
if (results === null
|
|
83
|
+
|| results.length === 0
|
|
84
|
+
|| results.every((reply) => reply === null || reply === undefined)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
76
87
|
return response;
|
|
77
88
|
}
|
|
78
89
|
catch (error) {
|
|
@@ -136,6 +147,7 @@ async function createBoardKeys(keyPrefix, leaderboardId) {
|
|
|
136
147
|
attempts: `${base}:attempts`,
|
|
137
148
|
entries: `${base}:entries`,
|
|
138
149
|
ranking: `${base}:ranking`,
|
|
150
|
+
commitMarker: `${keyPrefix}:commit`,
|
|
139
151
|
};
|
|
140
152
|
return {
|
|
141
153
|
...keys,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-devvit",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
4
4
|
"description": "Reddit Devvit Web platform adapter for mpgd games.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
],
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@devvit/web": "0.14.1",
|
|
58
|
-
"@mpgd/bridge": "0.14.
|
|
59
|
-
"@mpgd/platform": "0.27.
|
|
60
|
-
"@mpgd/game-services": "0.27.
|
|
58
|
+
"@mpgd/bridge": "0.14.3",
|
|
59
|
+
"@mpgd/platform": "0.27.1",
|
|
60
|
+
"@mpgd/game-services": "0.27.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^24.0.0",
|