@mpgd/cli 0.50.0 → 0.51.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/index.js +2 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +70 -0
- package/package.json +17 -17
- package/templates/phaser-game/apps/target-devvit/devvit.json +12 -0
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +34 -4
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +38 -11
- package/templates/phaser-game/apps/target-devvit/src/server/notificationTasks.ts +64 -0
package/dist/index.js
CHANGED
|
@@ -3131,7 +3131,8 @@ function resolvePublishedTemplateDependencyVersion(packageName, packageJson) {
|
|
|
3131
3131
|
if (packageName === '@mpgd/cli') {
|
|
3132
3132
|
return `^${cliVersion}`;
|
|
3133
3133
|
}
|
|
3134
|
-
const version = packageJson?.
|
|
3134
|
+
const version = packageJson?.dependencies?.[packageName]
|
|
3135
|
+
?? packageJson?.devDependencies?.[packageName];
|
|
3135
3136
|
return version !== undefined && version !== 'workspace:*' && isValidDependencyVersion(version)
|
|
3136
3137
|
? version
|
|
3137
3138
|
: undefined;
|
|
@@ -6895,6 +6895,76 @@ var maximumLeaseTtlMs = 60 * 60 * 1e3;
|
|
|
6895
6895
|
// node_modules/.cache/mpgd-native-build/packages/game-services/src/verified-leaderboard.js
|
|
6896
6896
|
var timestampPattern = new RegExp("^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,3}))?(Z|([+-])(\\d{2}):(\\d{2}))$");
|
|
6897
6897
|
|
|
6898
|
+
// node_modules/.cache/mpgd-native-build/packages/game-services/src/notifications/fcm.js
|
|
6899
|
+
var fcmAddressKinds = Object.freeze(["fcm-token"]);
|
|
6900
|
+
var fcmReservedDataKeys = Object.freeze(["topic", "route"]);
|
|
6901
|
+
var tokenRefreshMarginMs = 5 * 60 * 1e3;
|
|
6902
|
+
|
|
6903
|
+
// node_modules/.cache/mpgd-native-build/packages/game-services/src/notifications/registry.js
|
|
6904
|
+
var notificationHistoryRetentionDays = 30;
|
|
6905
|
+
var maxHistoryWindowSeconds = notificationHistoryRetentionDays * 86400;
|
|
6906
|
+
|
|
6907
|
+
// node_modules/.cache/mpgd-native-build/packages/game-services/src/notifications/store.js
|
|
6908
|
+
var notificationAmbiguousReplayHours = 24;
|
|
6909
|
+
var ambiguousReplayMs = notificationAmbiguousReplayHours * 36e5;
|
|
6910
|
+
|
|
6911
|
+
// node_modules/.cache/mpgd-native-build/packages/game-services/src/notifications/service.js
|
|
6912
|
+
var secondMs = 1e3;
|
|
6913
|
+
var dayMs2 = 86400 * secondMs;
|
|
6914
|
+
var keyRetentionMs = 30 * dayMs2;
|
|
6915
|
+
var endpointStaleMs = 60 * dayMs2;
|
|
6916
|
+
var endpointPurgeDelayMs = 30 * dayMs2;
|
|
6917
|
+
var historyRetentionMs = notificationHistoryRetentionDays * dayMs2;
|
|
6918
|
+
var ambiguousReplayMs2 = notificationAmbiguousReplayHours * 36e5;
|
|
6919
|
+
var maxTextLength = 2048;
|
|
6920
|
+
var maxRowKeyLength = maxTextLength + 1 + 9 * maxTextLength;
|
|
6921
|
+
var schedulerRetryMs = 60 * secondMs;
|
|
6922
|
+
var releaseBackoffMs = 60 * secondMs;
|
|
6923
|
+
|
|
6924
|
+
// node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/redis-notification-store.js
|
|
6925
|
+
var defaultPurgeLimit = Number.POSITIVE_INFINITY;
|
|
6926
|
+
|
|
6927
|
+
// node_modules/.cache/mpgd-native-build/adapters/devvit/src/notifications-contract.js
|
|
6928
|
+
var devvitHostCodeCharacters = "[A-Za-z0-9_.:-]{1,128}";
|
|
6929
|
+
|
|
6930
|
+
// node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/notifications-delivery.js
|
|
6931
|
+
var devvitNotificationProviderCodes = Object.freeze({
|
|
6932
|
+
deviceRecipient: "DEVVIT_DEVICE_RECIPIENT",
|
|
6933
|
+
recipientInvalid: "DEVVIT_RECIPIENT_INVALID",
|
|
6934
|
+
linkInvalid: "DEVVIT_LINK_INVALID",
|
|
6935
|
+
textMissing: "DEVVIT_TEXT_MISSING",
|
|
6936
|
+
titleTooLong: "DEVVIT_TITLE_TOO_LONG",
|
|
6937
|
+
bodyTooLong: "DEVVIT_BODY_TOO_LONG",
|
|
6938
|
+
templateSyntax: "DEVVIT_TEMPLATE_SYNTAX",
|
|
6939
|
+
enqueueFailed: "DEVVIT_ENQUEUE_FAILED",
|
|
6940
|
+
enqueueRejected: "DEVVIT_ENQUEUE_REJECTED",
|
|
6941
|
+
enqueueUnanswered: "DEVVIT_ENQUEUE_UNANSWERED",
|
|
6942
|
+
enqueueUnclassified: "DEVVIT_ENQUEUE_UNCLASSIFIED"
|
|
6943
|
+
});
|
|
6944
|
+
var dailyPeriodSource = String.raw`daily|per[- ]?(?:day|user|app)|today'?s?`;
|
|
6945
|
+
var dailyLimitWords = new RegExp(String.raw`\b(?:${dailyPeriodSource})\b`, "iu");
|
|
6946
|
+
var adjacentCountedLimit = new RegExp(String.raw`\b(?:${dailyPeriodSource}|send(?:ing)?|notifications?) limits?\b`, "iu");
|
|
6947
|
+
var devvitNotificationSchedulerTasks = Object.freeze({
|
|
6948
|
+
dispatch: Object.freeze({
|
|
6949
|
+
name: "mpgd-notifications-dispatch",
|
|
6950
|
+
endpoint: "/internal/scheduler/mpgd-notifications-dispatch",
|
|
6951
|
+
cron: "* * * * *"
|
|
6952
|
+
}),
|
|
6953
|
+
purge: Object.freeze({
|
|
6954
|
+
name: "mpgd-notifications-purge",
|
|
6955
|
+
endpoint: "/internal/scheduler/mpgd-notifications-purge",
|
|
6956
|
+
cron: "*/5 * * * *"
|
|
6957
|
+
})
|
|
6958
|
+
});
|
|
6959
|
+
|
|
6960
|
+
// node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/notifications-bridge.js
|
|
6961
|
+
var devvitNotificationHostCodes = Object.freeze({
|
|
6962
|
+
optInRefused: "DEVVIT_OPT_IN_REFUSED",
|
|
6963
|
+
optOutRefused: "DEVVIT_OPT_OUT_REFUSED",
|
|
6964
|
+
pluginFailed: "DEVVIT_NOTIFICATIONS_PLUGIN_FAILED"
|
|
6965
|
+
});
|
|
6966
|
+
var hostCodeValue = new RegExp(`^${devvitHostCodeCharacters}$`, "u");
|
|
6967
|
+
|
|
6898
6968
|
// node_modules/.cache/mpgd-native-build/packages/cli/src/target-name.js
|
|
6899
6969
|
var deploymentTargetNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
6900
6970
|
var reservedDeploymentTargetNames2 = /* @__PURE__ */ new Set([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"parse5": "7.3.0",
|
|
71
71
|
"postcss": "8.5.28",
|
|
72
72
|
"postcss-value-parser": "4.2.0",
|
|
73
|
-
"@mpgd/catalog": "0.10.
|
|
73
|
+
"@mpgd/catalog": "0.10.2",
|
|
74
74
|
"@mpgd/phaser-assets": "0.9.1",
|
|
75
|
-
"@mpgd/platform": "0.
|
|
76
|
-
"@mpgd/target-config": "0.23.
|
|
75
|
+
"@mpgd/platform": "0.24.0",
|
|
76
|
+
"@mpgd/target-config": "0.23.2"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@types/node": "^24.0.0",
|
|
@@ -81,19 +81,19 @@
|
|
|
81
81
|
"@types/yauzl": "3.4.0",
|
|
82
82
|
"ttsc": "0.32.1",
|
|
83
83
|
"typescript": "7.0.2",
|
|
84
|
-
"@mpgd/adapter-ait": "0.
|
|
85
|
-
"@mpgd/adapter-
|
|
86
|
-
"@mpgd/adapter-
|
|
87
|
-
"@mpgd/adapter-
|
|
88
|
-
"@mpgd/adapter-verse8": "0.7.
|
|
89
|
-
"@mpgd/
|
|
90
|
-
"@mpgd/
|
|
91
|
-
"@mpgd/capacitor-storekit": "0.3.
|
|
92
|
-
"@mpgd/bridge": "0.13.
|
|
93
|
-
"@mpgd/game-services": "0.
|
|
94
|
-
"@mpgd/game-runtime": "0.7.
|
|
95
|
-
"@mpgd/
|
|
96
|
-
"@mpgd/
|
|
84
|
+
"@mpgd/adapter-ait": "0.18.0",
|
|
85
|
+
"@mpgd/adapter-capacitor": "0.11.0",
|
|
86
|
+
"@mpgd/adapter-devvit": "0.13.0",
|
|
87
|
+
"@mpgd/adapter-browser": "0.11.2",
|
|
88
|
+
"@mpgd/adapter-verse8": "0.7.2",
|
|
89
|
+
"@mpgd/capacitor-play-billing": "0.3.2",
|
|
90
|
+
"@mpgd/analytics": "0.4.11",
|
|
91
|
+
"@mpgd/capacitor-storekit": "0.3.2",
|
|
92
|
+
"@mpgd/bridge": "0.13.2",
|
|
93
|
+
"@mpgd/game-services": "0.25.0",
|
|
94
|
+
"@mpgd/game-runtime": "0.7.2",
|
|
95
|
+
"@mpgd/i18n": "0.6.16",
|
|
96
|
+
"@mpgd/lite-shell": "0.1.0"
|
|
97
97
|
},
|
|
98
98
|
"main": "./dist/index.js",
|
|
99
99
|
"types": "./dist/index.d.ts",
|
|
@@ -43,5 +43,17 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"dev": "vite build --mode staging --watch",
|
|
45
45
|
"build": "pnpm build"
|
|
46
|
+
},
|
|
47
|
+
"scheduler": {
|
|
48
|
+
"tasks": {
|
|
49
|
+
"mpgd-notifications-dispatch": {
|
|
50
|
+
"endpoint": "/internal/scheduler/mpgd-notifications-dispatch",
|
|
51
|
+
"cron": "* * * * *"
|
|
52
|
+
},
|
|
53
|
+
"mpgd-notifications-purge": {
|
|
54
|
+
"endpoint": "/internal/scheduler/mpgd-notifications-purge",
|
|
55
|
+
"cron": "*/5 * * * *"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
59
|
}
|
|
@@ -59,6 +59,15 @@ export interface DevvitBridgeCommerceHandler {
|
|
|
59
59
|
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Serves the nine `notifications.v2.*` bridge methods:
|
|
64
|
+
* `createDevvitNotificationsV2BridgeHandler` from `@mpgd/adapter-devvit/server`
|
|
65
|
+
* over the Devvit `notifications` plugin and the request's logged-in user.
|
|
66
|
+
*/
|
|
67
|
+
export interface DevvitBridgeNotificationsHandler {
|
|
68
|
+
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
export interface DevvitBridgeHandlerDependencies {
|
|
63
72
|
readonly redis: DevvitBridgeRedisLike;
|
|
64
73
|
/** Authenticated Reddit user ID from the Devvit request context, never from the payload. */
|
|
@@ -75,6 +84,13 @@ export interface DevvitBridgeHandlerDependencies {
|
|
|
75
84
|
* plugin wired never lists orders or products.
|
|
76
85
|
*/
|
|
77
86
|
readonly commerce?: DevvitBridgeCommerceHandler | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Notifications v2: the game-rendered opt-in and opt-out. Absent, every
|
|
89
|
+
* `notifications.v2.*` method answers `NOTIFICATIONS_V2_UNAVAILABLE`, and
|
|
90
|
+
* the v1 `notifications.*` answers stay `approval-required` and
|
|
91
|
+
* `unavailable`.
|
|
92
|
+
*/
|
|
93
|
+
readonly notifications?: DevvitBridgeNotificationsHandler | undefined;
|
|
78
94
|
readonly warn?: ((message: string) => void) | undefined;
|
|
79
95
|
}
|
|
80
96
|
|
|
@@ -93,11 +109,25 @@ export function createDevvitBridgeHandler(
|
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
return async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse> {
|
|
96
|
-
// Notifications v2
|
|
97
|
-
//
|
|
98
|
-
// answers
|
|
112
|
+
// Notifications v2: the configured handler serves the game-rendered
|
|
113
|
+
// opt-in; without one every v2 method answers the dedicated code. The v1
|
|
114
|
+
// `notifications.*` answers below stay; a gateway that exposes v2 serves
|
|
115
|
+
// v1 from it instead.
|
|
99
116
|
if (isBridgeNotificationsV2Method(input.method)) {
|
|
100
|
-
|
|
117
|
+
if (dependencies.notifications === undefined) {
|
|
118
|
+
return createNotificationsV2UnavailableBridgeError(input, 'Devvit server bridge');
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return await dependencies.notifications.request(input);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
warn(`devvit notifications ${input.method} failed: ${errorMessage(error)}`);
|
|
124
|
+
return createBridgeError(
|
|
125
|
+
input.id,
|
|
126
|
+
'NOTIFICATIONS_TEMPORARILY_UNAVAILABLE',
|
|
127
|
+
`${input.method} failed on the Devvit server.`,
|
|
128
|
+
true,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
101
131
|
}
|
|
102
132
|
switch (input.method) {
|
|
103
133
|
case 'runtime.getCapabilities':
|
|
@@ -13,6 +13,7 @@ import productCatalogJson from '@mpgd/catalog/catalog.json';
|
|
|
13
13
|
|
|
14
14
|
import devvitManifest from '../../devvit.json';
|
|
15
15
|
import { createDevvitBridgeHandler } from './bridge.js';
|
|
16
|
+
import { handleNotificationTask, notificationTaskForPath } from './notificationTasks.js';
|
|
16
17
|
|
|
17
18
|
const maxRequestBodySize = 1_048_576;
|
|
18
19
|
const gameName = '__GAME_NAME__';
|
|
@@ -45,6 +46,12 @@ const handleBridgeRequest = createDevvitBridgeHandler({
|
|
|
45
46
|
storageKeyNamespace: gameName,
|
|
46
47
|
...(commerce === undefined ? {} : { commerce }),
|
|
47
48
|
});
|
|
49
|
+
/**
|
|
50
|
+
* The notifications v2 service (`createDevvitNotificationService` from
|
|
51
|
+
* `@mpgd/adapter-devvit/server`). None until the game declares a topic
|
|
52
|
+
* catalog, so the scheduler tasks below do no work.
|
|
53
|
+
*/
|
|
54
|
+
const notificationService: Parameters<typeof handleNotificationTask>[0]['notifications'] = undefined;
|
|
48
55
|
const bridgeRpcHandler = createBridgeRpcNodeHandler(createBridgeRpcRouter(handleBridgeRequest), {
|
|
49
56
|
maxBodySize: maxRequestBodySize,
|
|
50
57
|
prefix: defaultBridgeRpcEndpoint,
|
|
@@ -57,17 +64,7 @@ async function handleHttpRequest(
|
|
|
57
64
|
setResponseSecurityHeaders(response);
|
|
58
65
|
|
|
59
66
|
if (request.method === 'POST' && requestPathname(request) === '/internal/menu/create-post') {
|
|
60
|
-
|
|
61
|
-
await drainRequestBody(request, maxRequestBodySize);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
if (!(error instanceof RequestBodyTooLargeError)) {
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
discardOversizedRequestBody(request, response);
|
|
68
|
-
sendJson(response, 413, {
|
|
69
|
-
error: 'REQUEST_BODY_TOO_LARGE',
|
|
70
|
-
});
|
|
67
|
+
if (!(await drainOrRejectBody(request, response))) {
|
|
71
68
|
return;
|
|
72
69
|
}
|
|
73
70
|
|
|
@@ -75,6 +72,21 @@ async function handleHttpRequest(
|
|
|
75
72
|
return;
|
|
76
73
|
}
|
|
77
74
|
|
|
75
|
+
const notificationTask = request.method === 'POST'
|
|
76
|
+
? notificationTaskForPath(requestPathname(request))
|
|
77
|
+
: undefined;
|
|
78
|
+
if (notificationTask !== undefined) {
|
|
79
|
+
if (!(await drainOrRejectBody(request, response))) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const answer = await handleNotificationTask({
|
|
83
|
+
task: notificationTask,
|
|
84
|
+
notifications: notificationService,
|
|
85
|
+
});
|
|
86
|
+
sendJson(response, answer.status, answer.body);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
if (await bridgeRpcHandler(request, response)) {
|
|
79
91
|
return;
|
|
80
92
|
}
|
|
@@ -220,6 +232,21 @@ async function* readRequestBodyChunks(
|
|
|
220
232
|
}
|
|
221
233
|
}
|
|
222
234
|
|
|
235
|
+
/** Drains a bounded internal request body; answers 413 and false when it is too large. */
|
|
236
|
+
async function drainOrRejectBody(request: IncomingMessage, response: ServerResponse): Promise<boolean> {
|
|
237
|
+
try {
|
|
238
|
+
await drainRequestBody(request, maxRequestBodySize);
|
|
239
|
+
return true;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (!(error instanceof RequestBodyTooLargeError)) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
discardOversizedRequestBody(request, response);
|
|
245
|
+
sendJson(response, 413, { error: 'REQUEST_BODY_TOO_LARGE' });
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
223
250
|
async function drainRequestBody(
|
|
224
251
|
request: IncomingMessage,
|
|
225
252
|
maxBodySize: number,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
devvitNotificationTaskForEndpoint,
|
|
3
|
+
runDevvitNotificationTask,
|
|
4
|
+
type DevvitNotificationTask,
|
|
5
|
+
type DevvitNotificationTaskResult,
|
|
6
|
+
} from '@mpgd/adapter-devvit/server';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The notifications scheduler tasks of `devvit.json` (`scheduler.tasks`):
|
|
10
|
+
* `mpgd-notifications-dispatch` runs the notifications scheduler every
|
|
11
|
+
* minute (due server reminders and due sends, starting no round after 20
|
|
12
|
+
* seconds) and `mpgd-notifications-purge` purges old rows in bounded
|
|
13
|
+
* batches every five minutes (`runDevvitNotificationTask` from
|
|
14
|
+
* `@mpgd/adapter-devvit/server`).
|
|
15
|
+
* A server without a notifications service (no topic catalog yet) answers
|
|
16
|
+
* the tasks without work.
|
|
17
|
+
*/
|
|
18
|
+
export interface NotificationTaskAnswer {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
/** A `TaskResponse` (`{}`) from `@devvit/web/server`. */
|
|
21
|
+
readonly body: Record<string, never>;
|
|
22
|
+
readonly task: DevvitNotificationTask;
|
|
23
|
+
readonly result?: DevvitNotificationTaskResult;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The task an internal scheduler path names, or `undefined` for any other path. */
|
|
27
|
+
export function notificationTaskForPath(pathname: string): DevvitNotificationTask | undefined {
|
|
28
|
+
return devvitNotificationTaskForEndpoint(pathname);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Runs `task`. A failure answers 500 (so the Devvit
|
|
33
|
+
* logs show it) and is logged; the next run tries again.
|
|
34
|
+
*/
|
|
35
|
+
export async function handleNotificationTask(input: {
|
|
36
|
+
readonly task: DevvitNotificationTask;
|
|
37
|
+
/** The service's `runScheduler` and `purge` (`createDevvitNotificationService(...).service`). */
|
|
38
|
+
readonly notifications: Parameters<typeof runDevvitNotificationTask>[0] | undefined;
|
|
39
|
+
readonly log?: (message: string, error?: unknown) => void;
|
|
40
|
+
}): Promise<NotificationTaskAnswer> {
|
|
41
|
+
const log = input.log ?? ((message: string, error?: unknown) => {
|
|
42
|
+
if (error === undefined) {
|
|
43
|
+
console.log(message);
|
|
44
|
+
} else {
|
|
45
|
+
console.error(message, error);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (input.notifications === undefined) {
|
|
49
|
+
return { status: 200, body: {}, task: input.task };
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const result = await runDevvitNotificationTask(input.notifications, input.task);
|
|
53
|
+
if (
|
|
54
|
+
(result.task === 'dispatch' && (result.touched > 0 || result.failed > 0))
|
|
55
|
+
|| (result.task === 'purge' && result.sends + result.history + result.endpoints > 0)
|
|
56
|
+
) {
|
|
57
|
+
log(`devvit notifications ${result.task}: ${JSON.stringify(result)}`);
|
|
58
|
+
}
|
|
59
|
+
return { status: 200, body: {}, task: input.task, result };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
log(`devvit notifications ${input.task} task failed`, error);
|
|
62
|
+
return { status: 500, body: {}, task: input.task };
|
|
63
|
+
}
|
|
64
|
+
}
|