@meith/runtime 0.16.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/LICENSE.md +165 -0
- package/package.json +42 -0
- package/src/event-handlers.ts +98 -0
- package/src/groups.ts +9 -0
- package/src/index.ts +14 -0
- package/src/mail-brand.ts +83 -0
- package/src/plugin-context.ts +56 -0
- package/src/plugin-lifecycle.ts +41 -0
- package/src/plugin-rendering.ts +60 -0
- package/src/plugin-tasks.ts +52 -0
- package/src/scheduler-bundle.ts +373 -0
- package/src/task-workers.ts +293 -0
- package/src/visible-forums.ts +23 -0
- package/src/webhook-delivery.ts +117 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { deliveryHeaders, nextRetryDelaySeconds, verdictFor, type WebhookTopic } from '@meith/api'
|
|
2
|
+
import { logger } from '@meith/core'
|
|
3
|
+
|
|
4
|
+
export interface ClaimedDelivery {
|
|
5
|
+
readonly id: number
|
|
6
|
+
readonly webhookId: number
|
|
7
|
+
readonly deliveryId: string
|
|
8
|
+
readonly topic: string
|
|
9
|
+
readonly payload: Record<string, unknown>
|
|
10
|
+
readonly attempts: number
|
|
11
|
+
readonly url: string
|
|
12
|
+
readonly secret: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WebhookDeliveryStore {
|
|
16
|
+
claimDue(now: Date, limit: number): Promise<readonly ClaimedDelivery[]>
|
|
17
|
+
markDelivered(id: number, status: number, at: Date): Promise<void>
|
|
18
|
+
scheduleRetry(id: number, at: Date, status: number | null, error: string): Promise<void>
|
|
19
|
+
markDead(id: number, status: number | null, error: string, at: Date): Promise<void>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const REQUEST_TIMEOUT_MS = 10_000
|
|
23
|
+
|
|
24
|
+
export interface DeliverWebhooksResult {
|
|
25
|
+
readonly attempted: number
|
|
26
|
+
readonly delivered: number
|
|
27
|
+
readonly retried: number
|
|
28
|
+
readonly dead: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DeliverWebhooksOptions {
|
|
32
|
+
readonly now?: Date
|
|
33
|
+
readonly random?: () => number
|
|
34
|
+
readonly fetchImpl?: typeof fetch
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function deliverWebhooks(
|
|
38
|
+
store: WebhookDeliveryStore,
|
|
39
|
+
limit: number,
|
|
40
|
+
options: DeliverWebhooksOptions = {},
|
|
41
|
+
): Promise<DeliverWebhooksResult> {
|
|
42
|
+
const now = options.now ?? new Date()
|
|
43
|
+
const doFetch = options.fetchImpl ?? fetch
|
|
44
|
+
const log = () => logger({ module: 'webhooks' })
|
|
45
|
+
|
|
46
|
+
const claimed = await store.claimDue(now, limit)
|
|
47
|
+
let delivered = 0
|
|
48
|
+
let retried = 0
|
|
49
|
+
let dead = 0
|
|
50
|
+
|
|
51
|
+
for (const row of claimed) {
|
|
52
|
+
const body = JSON.stringify(row.payload)
|
|
53
|
+
const timestampSeconds = Math.floor(now.getTime() / 1000)
|
|
54
|
+
|
|
55
|
+
const headers = deliveryHeaders(
|
|
56
|
+
{ id: row.webhookId, url: row.url, secret: row.secret, topics: [], active: true },
|
|
57
|
+
{
|
|
58
|
+
subscriptionId: row.webhookId,
|
|
59
|
+
topic: row.topic as WebhookTopic,
|
|
60
|
+
deliveryId: row.deliveryId,
|
|
61
|
+
payload: row.payload,
|
|
62
|
+
},
|
|
63
|
+
timestampSeconds,
|
|
64
|
+
body,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
let status: number | null = null
|
|
68
|
+
let failure = ''
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const response = await doFetch(row.url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers,
|
|
74
|
+
body,
|
|
75
|
+
redirect: 'manual',
|
|
76
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
77
|
+
})
|
|
78
|
+
status = response.status
|
|
79
|
+
} catch (err) {
|
|
80
|
+
failure = err instanceof Error ? err.message : String(err)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const verdict =
|
|
84
|
+
status === null
|
|
85
|
+
? nextRetryDelaySeconds(row.attempts, options.random) === null
|
|
86
|
+
? 'dead'
|
|
87
|
+
: 'retry'
|
|
88
|
+
: verdictFor(status, row.attempts)
|
|
89
|
+
|
|
90
|
+
if (verdict === 'delivered') {
|
|
91
|
+
await store.markDelivered(row.id, status!, now)
|
|
92
|
+
delivered++
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (verdict === 'dead') {
|
|
97
|
+
await store.markDead(row.id, status, failure || `HTTP ${status}`, now)
|
|
98
|
+
dead++
|
|
99
|
+
log().warn(
|
|
100
|
+
{ webhookId: row.webhookId, deliveryId: row.deliveryId, status, attempts: row.attempts },
|
|
101
|
+
'webhook delivery dead-lettered',
|
|
102
|
+
)
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const delay = nextRetryDelaySeconds(row.attempts, options.random) ?? 3600
|
|
107
|
+
await store.scheduleRetry(
|
|
108
|
+
row.id,
|
|
109
|
+
new Date(now.getTime() + delay * 1000),
|
|
110
|
+
status,
|
|
111
|
+
failure || `HTTP ${status}`,
|
|
112
|
+
)
|
|
113
|
+
retried++
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { attempted: claimed.length, delivered, retried, dead }
|
|
117
|
+
}
|