tapimo 0.10.0 → 0.10.1
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/ApiKey.d.ts +2 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKeys.d.ts +4 -2
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +26 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +3346 -2575
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +5 -5
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +91 -84
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +26 -17
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +4 -2
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +26 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/Zones.d.ts +4266 -82
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +27 -8
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +3467 -2696
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +2999 -100
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +340 -2
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +850 -336
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +26 -26
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +11 -3
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +4 -4
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/earn.d.ts +75 -75
- package/dist/apps/data/routes/earn.js +2 -2
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +120 -120
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/indexer.d.ts +35 -34
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +3 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +5 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/transactions.d.ts +281 -281
- package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
- package/dist/apps/data/routes/webhooks.d.ts +418 -364
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +161 -14
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +31 -31
- package/dist/apps/data/routes/zones.js +1 -1
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/App.d.ts +96 -96
- package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
- package/dist/apps/funding/routes/deposit-addresses.js +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/providers.d.ts +13 -13
- package/dist/apps/funding/routes/transfers.d.ts +32 -32
- package/dist/apps/funding/routes/transfers.js +1 -1
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +418 -418
- package/dist/apps/management/routes/api-keys.d.ts +50 -50
- package/dist/apps/management/routes/billing.d.ts +72 -72
- package/dist/apps/management/routes/billing.js +1 -1
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +37 -37
- package/dist/apps/management/routes/faucet.js +2 -2
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +75 -75
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +76 -76
- package/dist/apps/management/routes/webhooks.d.ts +49 -49
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +4 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +58 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +19 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +24 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueEvents.js +160 -0
- package/dist/db/tables/webhookQueueEvents.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +10 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -4
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +27 -0
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +29 -2
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +4 -6
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +15 -14
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Store.d.ts +2 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +57 -18
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Value.d.ts +2 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +25 -0
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +20 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -1
- package/dist/internal/WebhookTransfer.js +7 -4
- package/dist/internal/WebhookTransfer.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +139 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +129 -7
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +3 -4
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +59 -8
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKeys.test.ts +20 -0
- package/src/ApiKeys.ts +28 -1
- package/src/App.test.ts +10 -8
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +1 -0
- package/src/Webhooks.ts +19 -17
- package/src/admin/apps/api-keys.test.ts +9 -0
- package/src/admin/apps/api-keys.ts +15 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -2
- package/src/admin/ui/src/routes/api-keys.tsx +31 -9
- package/src/admin/ui.generated.ts +69 -69
- package/src/analytics/Analytics.test.ts +8 -0
- package/src/analytics/Analytics.ts +26 -0
- package/src/apps/Zones.test.ts +20 -3
- package/src/apps/Zones.ts +28 -8
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.test.ts +2 -2
- package/src/apps/data/routes/activities.bench.ts +41 -0
- package/src/apps/data/routes/activities.test.ts +103 -2
- package/src/apps/data/routes/activities.ts +401 -2
- package/src/apps/data/routes/balances.bench.ts +21 -0
- package/src/apps/data/routes/balances.ts +11 -3
- package/src/apps/data/routes/blocks.bench.ts +34 -0
- package/src/apps/data/routes/coingecko.bench.ts +53 -0
- package/src/apps/data/routes/earn.bench.ts +71 -0
- package/src/apps/data/routes/earn.ts +2 -2
- package/src/apps/data/routes/exchanges.bench.ts +85 -0
- package/src/apps/data/routes/fee-amm.bench.ts +13 -0
- package/src/apps/data/routes/indexer.bench.ts +8 -0
- package/src/apps/data/routes/indexer.test.ts +2 -1
- package/src/apps/data/routes/indexer.ts +3 -2
- package/src/apps/data/routes/receipts.bench.ts +39 -0
- package/src/apps/data/routes/rpc.bench.ts +14 -0
- package/src/apps/data/routes/rpc.test.ts +27 -0
- package/src/apps/data/routes/rpc.ts +5 -1
- package/src/apps/data/routes/tokenlist.bench.ts +3 -0
- package/src/apps/data/routes/tokens.bench.ts +76 -0
- package/src/apps/data/routes/transactions.bench.ts +40 -0
- package/src/apps/data/routes/transfers.bench.ts +28 -0
- package/src/apps/data/routes/valuation.bench.ts +14 -0
- package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
- package/src/apps/data/routes/webhooks.bench.ts +26 -0
- package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
- package/src/apps/data/routes/webhooks.test.ts +53 -6
- package/src/apps/data/routes/webhooks.ts +221 -14
- package/src/apps/data/routes/zones.bench.ts +32 -0
- package/src/apps/data/routes/zones.test.ts +1 -1
- package/src/apps/data/routes/zones.ts +1 -1
- package/src/apps/funding/routes/chains.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.ts +1 -1
- package/src/apps/funding/routes/providers.test.ts +1 -1
- package/src/apps/funding/routes/quotes.test.ts +1 -1
- package/src/apps/funding/routes/transfers.test.ts +3 -6
- package/src/apps/funding/routes/transfers.ts +1 -1
- package/src/apps/management/routes/billing.ts +1 -1
- package/src/apps/management/routes/faucet.ts +2 -2
- package/src/apps/management/routes/me.test.ts +31 -0
- package/src/apps/mcp.ts +4 -1
- package/src/db/Db.ts +81 -0
- package/src/db/Schema.ts +32 -0
- package/src/db/tables/webhookQueueEvents.ts +285 -0
- package/src/db/tables/webhookSubscriptions.ts +15 -0
- package/src/internal/Auth.test.ts +61 -50
- package/src/internal/Auth.ts +9 -4
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +58 -1
- package/src/internal/MetricSink.ts +60 -2
- package/src/internal/RateLimit.test.ts +23 -18
- package/src/internal/RateLimit.ts +23 -15
- package/src/internal/Store.test.ts +99 -13
- package/src/internal/Store.ts +52 -17
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Value.test.ts +25 -0
- package/src/internal/Value.ts +29 -0
- package/src/internal/WebhookTransfer.test.ts +89 -0
- package/src/internal/WebhookTransfer.ts +11 -7
- package/src/internal/Webhooks.test.ts +479 -0
- package/src/internal/Webhooks.ts +281 -7
- package/src/internal/funding/Provider.test.ts +1 -1
- package/src/internal/funding/providers/index.test.ts +73 -3
- package/src/internal/funding/providers/relay.test.ts +1 -5
- package/src/internal/funding/providers/relay.ts +56 -12
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Hex } from 'ox'
|
|
2
|
+
|
|
3
|
+
import * as WebhookTransfer from './WebhookTransfer.js'
|
|
4
|
+
|
|
5
|
+
const token: Hex.Hex = '0x20c0000000000000000000000000000000000001'
|
|
6
|
+
const sender: Hex.Hex = `0x${'aa'.repeat(20)}`
|
|
7
|
+
const recipient: Hex.Hex = `0x${'bb'.repeat(20)}`
|
|
8
|
+
const other: Hex.Hex = `0x${'cc'.repeat(20)}`
|
|
9
|
+
const transactionHash = `0x${'ef'.repeat(32)}`
|
|
10
|
+
const transferTopic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
|
|
11
|
+
const timestamp = '2026-01-01T00:00:00.000Z'
|
|
12
|
+
|
|
13
|
+
const addressTopic = (address: string) => `0x${'0'.repeat(24)}${address.slice(2)}`
|
|
14
|
+
|
|
15
|
+
function transferLog(overrides: Record<string, unknown> = {}) {
|
|
16
|
+
return {
|
|
17
|
+
address: token,
|
|
18
|
+
blockNumber: '0x7b',
|
|
19
|
+
data: `0x${(10n ** 18n).toString(16).padStart(64, '0')}`,
|
|
20
|
+
logIndex: '0x4',
|
|
21
|
+
topics: [transferTopic, addressTopic(sender), addressTopic(recipient)],
|
|
22
|
+
transactionHash,
|
|
23
|
+
transactionIndex: '0x0',
|
|
24
|
+
...overrides,
|
|
25
|
+
} as never
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('decode', () => {
|
|
29
|
+
test('decodes a strict TIP-20 Transfer log', () => {
|
|
30
|
+
expect(WebhookTransfer.decode(transferLog(), timestamp)).toMatchInlineSnapshot(`
|
|
31
|
+
{
|
|
32
|
+
"address": "0x20c0000000000000000000000000000000000001",
|
|
33
|
+
"amount": "1000000000000000000",
|
|
34
|
+
"blockNumber": 123,
|
|
35
|
+
"logIndex": 4,
|
|
36
|
+
"recipient": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
37
|
+
"sender": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
38
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
39
|
+
"transactionHash": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef",
|
|
40
|
+
}
|
|
41
|
+
`)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('rejects non-TIP-20 addresses and malformed payloads', () => {
|
|
45
|
+
const cases: readonly Record<string, unknown>[] = [
|
|
46
|
+
// Outside the deterministic TIP-20 address range.
|
|
47
|
+
{ address: other },
|
|
48
|
+
// Missing recipient topic.
|
|
49
|
+
{ topics: [transferTopic, addressTopic(sender)] },
|
|
50
|
+
// Wrong event selector.
|
|
51
|
+
{ topics: [`0x${'12'.repeat(32)}`, addressTopic(sender), addressTopic(recipient)] },
|
|
52
|
+
// Sender topic without the address left-padding.
|
|
53
|
+
{ topics: [transferTopic, `0x${'ff'.repeat(32)}`, addressTopic(recipient)] },
|
|
54
|
+
// Amount payload is not one word.
|
|
55
|
+
{ data: '0x01' },
|
|
56
|
+
// Pending-log positions cannot form a cursor.
|
|
57
|
+
{ blockNumber: null },
|
|
58
|
+
{ logIndex: null },
|
|
59
|
+
]
|
|
60
|
+
for (const overrides of cases)
|
|
61
|
+
expect(
|
|
62
|
+
WebhookTransfer.decode(transferLog(overrides), timestamp),
|
|
63
|
+
JSON.stringify(overrides),
|
|
64
|
+
).toBeUndefined()
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('matches', () => {
|
|
69
|
+
const data = WebhookTransfer.decode(transferLog(), timestamp)!
|
|
70
|
+
|
|
71
|
+
test('AND-combines side, either-side address, and token filters', () => {
|
|
72
|
+
const cases: readonly [WebhookTransfer.Filters, boolean][] = [
|
|
73
|
+
[{}, true],
|
|
74
|
+
[{ sender }, true],
|
|
75
|
+
[{ sender: recipient }, false],
|
|
76
|
+
[{ recipient }, true],
|
|
77
|
+
[{ recipient: sender }, false],
|
|
78
|
+
[{ address: sender }, true],
|
|
79
|
+
[{ address: recipient }, true],
|
|
80
|
+
[{ address: other }, false],
|
|
81
|
+
[{ token }, true],
|
|
82
|
+
[{ token: '0x20c0000000000000000000000000000000000002' }, false],
|
|
83
|
+
[{ recipient, sender, token }, true],
|
|
84
|
+
[{ recipient: sender, sender, token }, false],
|
|
85
|
+
]
|
|
86
|
+
for (const [filters, expected] of cases)
|
|
87
|
+
expect(WebhookTransfer.matches(data, filters), JSON.stringify(filters)).toBe(expected)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -18,9 +18,11 @@ const scanChunkBlocks = 8
|
|
|
18
18
|
const event = parseAbiItem(Transfers.signature)
|
|
19
19
|
const topic = encodeEventTopics({ abi: [event] })[0]
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** Parsed transfer filter predicates. */
|
|
22
|
+
export type Filters = z.output<typeof Transfers.schema.Predicates>
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
/** One decoded TIP-20 Transfer row, as the read endpoint returns it. */
|
|
25
|
+
export type EventData = {
|
|
24
26
|
address: string
|
|
25
27
|
amount: string
|
|
26
28
|
blockNumber: number
|
|
@@ -303,7 +305,7 @@ function isTip20Address(address: string): boolean {
|
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
/** Decodes one strict TIP-20 Transfer wire payload. */
|
|
306
|
-
function decode(log: RpcLog, timestamp: string): EventData | undefined {
|
|
308
|
+
export function decode(log: RpcLog, timestamp: string): EventData | undefined {
|
|
307
309
|
if (!isTip20Address(log.address)) return undefined
|
|
308
310
|
if (
|
|
309
311
|
log.topics.length !== 3 ||
|
|
@@ -343,7 +345,7 @@ function decode(log: RpcLog, timestamp: string): EventData | undefined {
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
/** Re-checks every transfer filter after RPC topic pushdown. */
|
|
346
|
-
function matches(data: EventData, filters: Filters): boolean {
|
|
348
|
+
export function matches(data: EventData, filters: Filters): boolean {
|
|
347
349
|
if (filters.address && data.sender !== filters.address && data.recipient !== filters.address)
|
|
348
350
|
return false
|
|
349
351
|
if (filters.recipient && data.recipient !== filters.recipient) return false
|
|
@@ -389,13 +391,13 @@ async function scanCohort(options: {
|
|
|
389
391
|
items: [],
|
|
390
392
|
state,
|
|
391
393
|
}))
|
|
392
|
-
const timestamps = new Map<number, Promise<string>>()
|
|
394
|
+
const timestamps = new Map<number, Promise<string | undefined>>()
|
|
393
395
|
const timestamp = (blockNumber: number) => {
|
|
394
396
|
const existing = timestamps.get(blockNumber)
|
|
395
397
|
if (existing) return existing
|
|
396
398
|
const pending = client
|
|
397
399
|
.getBlock({ blockNumber: BigInt(blockNumber), includeTransactions: false })
|
|
398
|
-
.then((block) =>
|
|
400
|
+
.then((block) => Value.blockToIso(block))
|
|
399
401
|
timestamps.set(blockNumber, pending)
|
|
400
402
|
return pending
|
|
401
403
|
}
|
|
@@ -412,7 +414,9 @@ async function scanCohort(options: {
|
|
|
412
414
|
const blockNumber = Value.hexToNumber(log.blockNumber)
|
|
413
415
|
const logIndex = Value.hexToNumber(log.logIndex)
|
|
414
416
|
if (blockNumber === undefined || logIndex === undefined) continue
|
|
415
|
-
const
|
|
417
|
+
const blockTimestamp = await timestamp(blockNumber)
|
|
418
|
+
if (blockTimestamp === undefined) continue
|
|
419
|
+
const data = decode(log, blockTimestamp)
|
|
416
420
|
if (!data) continue
|
|
417
421
|
for (const accumulator of accumulators) {
|
|
418
422
|
if (!canMatch(accumulator, blockNumber, logIndex)) continue
|
|
@@ -804,6 +804,25 @@ describe('delivery log', () => {
|
|
|
804
804
|
`)
|
|
805
805
|
})
|
|
806
806
|
|
|
807
|
+
test('deliverAndRecord records envelope serialization failures', async () => {
|
|
808
|
+
const db = TestApp.database()
|
|
809
|
+
const created = await Webhooks.createSubscription(db, createInput())
|
|
810
|
+
const envelope = {
|
|
811
|
+
chainId: 4217,
|
|
812
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
813
|
+
data: { amount: 1n },
|
|
814
|
+
id: 'evt_invalid',
|
|
815
|
+
subscriptionId: created.id,
|
|
816
|
+
type: 'token:transfer',
|
|
817
|
+
} satisfies Webhooks.Envelope
|
|
818
|
+
|
|
819
|
+
const result = await Webhooks.deliverAndRecord(db, created, envelope, { now: fixedNow })
|
|
820
|
+
|
|
821
|
+
expect(result).toEqual({ error: 'delivery failed before request', ok: false })
|
|
822
|
+
const after = await Webhooks.getSubscription(db, apiKeyOwner, created.id)
|
|
823
|
+
expect(after?.failureCount).toBe(1)
|
|
824
|
+
})
|
|
825
|
+
|
|
807
826
|
test('deliverCurrentAndRecord skips paused and deleted queued subscriptions', async () => {
|
|
808
827
|
const db = TestApp.database()
|
|
809
828
|
const paused = await Webhooks.createSubscription(db, createInput())
|
|
@@ -909,6 +928,466 @@ describe('delivery log', () => {
|
|
|
909
928
|
})
|
|
910
929
|
})
|
|
911
930
|
|
|
931
|
+
async function stagedObligation(db: ReturnType<typeof TestApp.database>, logIndex = 4) {
|
|
932
|
+
const subscription = await Webhooks.createSubscription(db, createInput())
|
|
933
|
+
const envelope = Webhooks.buildEnvelope({
|
|
934
|
+
blockNumber: 123,
|
|
935
|
+
createdAt: new Date(),
|
|
936
|
+
data: { amount: '1' },
|
|
937
|
+
logIndex,
|
|
938
|
+
subscription,
|
|
939
|
+
})
|
|
940
|
+
const { created, references } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
|
|
941
|
+
return { created, envelope, reference: references[0]!, subscription }
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
describe('ensureQueueEvents', () => {
|
|
945
|
+
test('keeps oversized envelopes behind compact idempotent Queue references', async () => {
|
|
946
|
+
const db = TestApp.database()
|
|
947
|
+
const subscription = await Webhooks.createSubscription(
|
|
948
|
+
db,
|
|
949
|
+
createInput({ eventType: 'transaction:included' }),
|
|
950
|
+
)
|
|
951
|
+
const input = `0x${'ab'.repeat(70_000)}`
|
|
952
|
+
const envelope = Webhooks.buildEnvelope({
|
|
953
|
+
blockNumber: 123,
|
|
954
|
+
createdAt: fixedNow(),
|
|
955
|
+
data: { input, meta: { rpc: { input } } },
|
|
956
|
+
logIndex: 4,
|
|
957
|
+
subscription,
|
|
958
|
+
})
|
|
959
|
+
const dispatchable = { envelope, subscription }
|
|
960
|
+
|
|
961
|
+
const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [dispatchable]) // prettier-ignore
|
|
962
|
+
await Webhooks.ensureQueueEvents(db, [dispatchable])
|
|
963
|
+
|
|
964
|
+
expect(
|
|
965
|
+
new TextEncoder().encode(JSON.stringify({ body: dispatchable })).byteLength,
|
|
966
|
+
).toBeGreaterThan(128_000)
|
|
967
|
+
expect(new TextEncoder().encode(JSON.stringify({ body: reference })).byteLength).toBeLessThan(
|
|
968
|
+
1_000,
|
|
969
|
+
)
|
|
970
|
+
expect((await Webhooks.getQueueEvent(db, reference!))?.envelope).toEqual(envelope)
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
test('delivers a staged oversized envelope from its compact reference', async () => {
|
|
974
|
+
const db = TestApp.database()
|
|
975
|
+
const subscription = await Webhooks.createSubscription(
|
|
976
|
+
db,
|
|
977
|
+
createInput({ eventType: 'transaction:included' }),
|
|
978
|
+
)
|
|
979
|
+
const input = `0x${'ab'.repeat(70_000)}`
|
|
980
|
+
const envelope = Webhooks.buildEnvelope({
|
|
981
|
+
blockNumber: 123,
|
|
982
|
+
data: { input, meta: { rpc: { input } } },
|
|
983
|
+
logIndex: 4,
|
|
984
|
+
subscription,
|
|
985
|
+
})
|
|
986
|
+
const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }]) // prettier-ignore
|
|
987
|
+
const histograms = new Map<string, number>()
|
|
988
|
+
const metrics = Metrics.from({
|
|
989
|
+
count() {},
|
|
990
|
+
flush() {},
|
|
991
|
+
gauge() {},
|
|
992
|
+
histogram(name, value) {
|
|
993
|
+
histograms.set(name, value)
|
|
994
|
+
},
|
|
995
|
+
})
|
|
996
|
+
let deliveredBytes = 0
|
|
997
|
+
const fetch = (async (_url, init) => {
|
|
998
|
+
deliveredBytes = new TextEncoder().encode(
|
|
999
|
+
typeof init?.body === 'string' ? init.body : '',
|
|
1000
|
+
).byteLength
|
|
1001
|
+
return new Response(null, { status: 200 })
|
|
1002
|
+
}) satisfies typeof globalThis.fetch
|
|
1003
|
+
|
|
1004
|
+
const result = await Webhooks.deliverQueueEventAndRecord(db, reference!, {
|
|
1005
|
+
fetch,
|
|
1006
|
+
metrics,
|
|
1007
|
+
queuedAt: Date.now(),
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
expect(result.status).toBe('delivered')
|
|
1011
|
+
expect(deliveredBytes).toBeGreaterThan(128_000)
|
|
1012
|
+
expect(histograms.has('webhook_delivery_processing_ms')).toBe(true)
|
|
1013
|
+
expect(histograms.has('webhook_delivery_queue_wait_ms')).toBe(true)
|
|
1014
|
+
expect(histograms.has('webhook_delivery_subscription_read_ms')).toBe(true)
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
test('skips a staged event after its subscription expires', async () => {
|
|
1018
|
+
vi.useFakeTimers({ now: fixedNow(), toFake: ['Date'] })
|
|
1019
|
+
try {
|
|
1020
|
+
const db = TestApp.database()
|
|
1021
|
+
const subscription = await Webhooks.createSubscription(
|
|
1022
|
+
db,
|
|
1023
|
+
createInput({ eventType: 'transaction:included', owner: mppOwner, ttl: 60_000 }),
|
|
1024
|
+
{ now: fixedNow },
|
|
1025
|
+
)
|
|
1026
|
+
const envelope = Webhooks.buildEnvelope({
|
|
1027
|
+
blockNumber: 123,
|
|
1028
|
+
data: {},
|
|
1029
|
+
logIndex: 4,
|
|
1030
|
+
subscription,
|
|
1031
|
+
})
|
|
1032
|
+
const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }]) // prettier-ignore
|
|
1033
|
+
|
|
1034
|
+
vi.setSystemTime(new Date('2026-01-01T00:01:00.000Z'))
|
|
1035
|
+
|
|
1036
|
+
await expect(Webhooks.deliverQueueEventAndRecord(db, reference!)).resolves
|
|
1037
|
+
.toMatchInlineSnapshot(`
|
|
1038
|
+
{
|
|
1039
|
+
"status": "skipped",
|
|
1040
|
+
}
|
|
1041
|
+
`)
|
|
1042
|
+
} finally {
|
|
1043
|
+
vi.useRealTimers()
|
|
1044
|
+
}
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
test('stages once and keeps the first envelope', async () => {
|
|
1048
|
+
const db = TestApp.database()
|
|
1049
|
+
const { created, envelope, reference, subscription } = await stagedObligation(db)
|
|
1050
|
+
const replacement = { ...envelope, data: { amount: '2' } }
|
|
1051
|
+
|
|
1052
|
+
const second = await Webhooks.ensureQueueEvents(db, [{ envelope: replacement, subscription }])
|
|
1053
|
+
|
|
1054
|
+
expect(created).toBe(1)
|
|
1055
|
+
expect(second.created).toBe(0)
|
|
1056
|
+
expect((await Webhooks.getQueueEvent(db, reference))?.envelope).toEqual(envelope)
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
test('never resurrects a terminal obligation', async () => {
|
|
1060
|
+
const db = TestApp.database()
|
|
1061
|
+
const { envelope, reference, subscription } = await stagedObligation(db)
|
|
1062
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1063
|
+
expect(claim.type).toBe('claimed')
|
|
1064
|
+
if (claim.type !== 'claimed') return
|
|
1065
|
+
expect(
|
|
1066
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1067
|
+
claimedAt: claim.record.attemptingAt!,
|
|
1068
|
+
status: 'succeeded',
|
|
1069
|
+
}),
|
|
1070
|
+
).toBe(true)
|
|
1071
|
+
|
|
1072
|
+
const again = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
|
|
1073
|
+
expect(again.created).toBe(0)
|
|
1074
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
|
|
1075
|
+
status: 'succeeded',
|
|
1076
|
+
type: 'terminal',
|
|
1077
|
+
})
|
|
1078
|
+
// Terminal rows read as absent, so no consumer path can redeliver them.
|
|
1079
|
+
expect(await Webhooks.getQueueEvent(db, reference)).toBeNull()
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
test('replaces an expired terminal row so replays stay deliverable', async () => {
|
|
1083
|
+
vi.useFakeTimers({ toFake: ['Date'] })
|
|
1084
|
+
vi.setSystemTime(fixedNow())
|
|
1085
|
+
try {
|
|
1086
|
+
const db = TestApp.database()
|
|
1087
|
+
const { envelope, reference, subscription } = await stagedObligation(db)
|
|
1088
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1089
|
+
expect(claim.type).toBe('claimed')
|
|
1090
|
+
if (claim.type !== 'claimed') return
|
|
1091
|
+
expect(
|
|
1092
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1093
|
+
claimedAt: claim.record.attemptingAt!,
|
|
1094
|
+
status: 'succeeded',
|
|
1095
|
+
}),
|
|
1096
|
+
).toBe(true)
|
|
1097
|
+
|
|
1098
|
+
// Past the dedupe window an unpruned terminal row must not suppress a
|
|
1099
|
+
// replay.
|
|
1100
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
|
|
1101
|
+
const replay = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
|
|
1102
|
+
expect(replay.created).toBe(1)
|
|
1103
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
|
|
1104
|
+
type: 'claimed',
|
|
1105
|
+
})
|
|
1106
|
+
} finally {
|
|
1107
|
+
vi.useRealTimers()
|
|
1108
|
+
}
|
|
1109
|
+
})
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
describe('claimQueueEvent', () => {
|
|
1113
|
+
beforeEach(() => {
|
|
1114
|
+
vi.useFakeTimers({ toFake: ['Date'] })
|
|
1115
|
+
vi.setSystemTime(fixedNow())
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
afterEach(() => {
|
|
1119
|
+
vi.useRealTimers()
|
|
1120
|
+
})
|
|
1121
|
+
|
|
1122
|
+
test('claims a due obligation once and reclaims after staleness', async () => {
|
|
1123
|
+
const db = TestApp.database()
|
|
1124
|
+
const { reference } = await stagedObligation(db)
|
|
1125
|
+
|
|
1126
|
+
const first = await Webhooks.claimQueueEvent(db, reference)
|
|
1127
|
+
expect(first.type).toBe('claimed')
|
|
1128
|
+
if (first.type !== 'claimed') return
|
|
1129
|
+
expect(first.record.attemptCount).toBe(1)
|
|
1130
|
+
|
|
1131
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
|
|
1132
|
+
type: 'attempting',
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 1))
|
|
1136
|
+
const reclaimed = await Webhooks.claimQueueEvent(db, reference)
|
|
1137
|
+
expect(reclaimed.type).toBe('claimed')
|
|
1138
|
+
if (reclaimed.type !== 'claimed') return
|
|
1139
|
+
expect(reclaimed.record.attemptCount).toBe(2)
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
test('classifies missing, scheduled, and terminal obligations', async () => {
|
|
1143
|
+
const db = TestApp.database()
|
|
1144
|
+
const { reference } = await stagedObligation(db)
|
|
1145
|
+
|
|
1146
|
+
await expect(
|
|
1147
|
+
Webhooks.claimQueueEvent(db, { eventId: 'evt_absent', subscriptionId: 'wh_absent' }),
|
|
1148
|
+
).resolves.toEqual({ type: 'missing' })
|
|
1149
|
+
|
|
1150
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1151
|
+
expect(claim.type).toBe('claimed')
|
|
1152
|
+
if (claim.type !== 'claimed') return
|
|
1153
|
+
expect(
|
|
1154
|
+
await Webhooks.scheduleQueueEventRetry(db, reference, {
|
|
1155
|
+
attemptCount: 1,
|
|
1156
|
+
claimedAt: claim.record.attemptingAt!,
|
|
1157
|
+
}),
|
|
1158
|
+
).toBe(true)
|
|
1159
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
|
|
1160
|
+
nextAttemptAt: new Date(fixedNow().getTime() + Webhooks.retryDelayMs(1)).toISOString(),
|
|
1161
|
+
type: 'scheduled',
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.retryDelayMs(1) + 1))
|
|
1165
|
+
const retried = await Webhooks.claimQueueEvent(db, reference)
|
|
1166
|
+
expect(retried.type).toBe('claimed')
|
|
1167
|
+
if (retried.type !== 'claimed') return
|
|
1168
|
+
expect(
|
|
1169
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1170
|
+
claimedAt: retried.record.attemptingAt!,
|
|
1171
|
+
status: 'failed',
|
|
1172
|
+
}),
|
|
1173
|
+
).toBe(true)
|
|
1174
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
|
|
1175
|
+
status: 'failed',
|
|
1176
|
+
type: 'terminal',
|
|
1177
|
+
})
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
test('keeps owed work claimable indefinitely', async () => {
|
|
1181
|
+
const db = TestApp.database()
|
|
1182
|
+
const { reference } = await stagedObligation(db)
|
|
1183
|
+
|
|
1184
|
+
// Pending obligations never expire; only terminal transitions stamp a
|
|
1185
|
+
// dedupe deadline.
|
|
1186
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs * 3))
|
|
1187
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
|
|
1188
|
+
type: 'claimed',
|
|
1189
|
+
})
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
test('never prunes pending work', async () => {
|
|
1193
|
+
const db = TestApp.database()
|
|
1194
|
+
const { reference } = await stagedObligation(db)
|
|
1195
|
+
|
|
1196
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
|
|
1197
|
+
await Webhooks.pruneExpired(db)
|
|
1198
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1199
|
+
expect(claim.type).toBe('claimed')
|
|
1200
|
+
if (claim.type !== 'claimed') return
|
|
1201
|
+
expect(
|
|
1202
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1203
|
+
claimedAt: claim.record.attemptingAt!,
|
|
1204
|
+
status: 'succeeded',
|
|
1205
|
+
}),
|
|
1206
|
+
).toBe(true)
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
test('fences a stale claimant after a reclaim', async () => {
|
|
1210
|
+
const db = TestApp.database()
|
|
1211
|
+
const { reference } = await stagedObligation(db)
|
|
1212
|
+
const stale = await Webhooks.claimQueueEvent(db, reference)
|
|
1213
|
+
expect(stale.type).toBe('claimed')
|
|
1214
|
+
if (stale.type !== 'claimed') return
|
|
1215
|
+
|
|
1216
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 1))
|
|
1217
|
+
const current = await Webhooks.claimQueueEvent(db, reference)
|
|
1218
|
+
expect(current.type).toBe('claimed')
|
|
1219
|
+
if (current.type !== 'claimed') return
|
|
1220
|
+
|
|
1221
|
+
const staleClaimedAt = stale.record.attemptingAt!
|
|
1222
|
+
expect(
|
|
1223
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1224
|
+
claimedAt: staleClaimedAt,
|
|
1225
|
+
status: 'failed',
|
|
1226
|
+
}),
|
|
1227
|
+
).toBe(false)
|
|
1228
|
+
expect(
|
|
1229
|
+
await Webhooks.scheduleQueueEventRetry(db, reference, {
|
|
1230
|
+
attemptCount: 1,
|
|
1231
|
+
claimedAt: staleClaimedAt,
|
|
1232
|
+
}),
|
|
1233
|
+
).toBe(false)
|
|
1234
|
+
expect(
|
|
1235
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1236
|
+
claimedAt: current.record.attemptingAt!,
|
|
1237
|
+
status: 'succeeded',
|
|
1238
|
+
}),
|
|
1239
|
+
).toBe(true)
|
|
1240
|
+
})
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
describe('retryDelayMs', () => {
|
|
1244
|
+
test('backs off exponentially and caps hourly', () => {
|
|
1245
|
+
expect([1, 2, 3, 4, 5, 6, 10].map(Webhooks.retryDelayMs)).toMatchInlineSnapshot(`
|
|
1246
|
+
[
|
|
1247
|
+
5000,
|
|
1248
|
+
25000,
|
|
1249
|
+
125000,
|
|
1250
|
+
625000,
|
|
1251
|
+
3125000,
|
|
1252
|
+
3600000,
|
|
1253
|
+
3600000,
|
|
1254
|
+
]
|
|
1255
|
+
`)
|
|
1256
|
+
})
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
describe('dueQueueEvents', () => {
|
|
1260
|
+
beforeEach(() => {
|
|
1261
|
+
vi.useFakeTimers({ toFake: ['Date'] })
|
|
1262
|
+
vi.setSystemTime(fixedNow())
|
|
1263
|
+
})
|
|
1264
|
+
|
|
1265
|
+
afterEach(() => {
|
|
1266
|
+
vi.useRealTimers()
|
|
1267
|
+
})
|
|
1268
|
+
|
|
1269
|
+
test('lists due pending work and skips in-flight, scheduled, and terminal rows', async () => {
|
|
1270
|
+
const db = TestApp.database()
|
|
1271
|
+
// Distinct staging times so the due order (next_attempt_at) is deterministic.
|
|
1272
|
+
const due = await stagedObligation(db, 1)
|
|
1273
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + 1))
|
|
1274
|
+
const inFlight = await stagedObligation(db, 2)
|
|
1275
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + 2))
|
|
1276
|
+
const scheduled = await stagedObligation(db, 3)
|
|
1277
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + 3))
|
|
1278
|
+
const terminal = await stagedObligation(db, 4)
|
|
1279
|
+
|
|
1280
|
+
expect((await Webhooks.claimQueueEvent(db, inFlight.reference)).type).toBe('claimed')
|
|
1281
|
+
const scheduledClaim = await Webhooks.claimQueueEvent(db, scheduled.reference)
|
|
1282
|
+
expect(scheduledClaim.type).toBe('claimed')
|
|
1283
|
+
if (scheduledClaim.type !== 'claimed') return
|
|
1284
|
+
expect(
|
|
1285
|
+
await Webhooks.scheduleQueueEventRetry(db, scheduled.reference, {
|
|
1286
|
+
attemptCount: 3,
|
|
1287
|
+
claimedAt: scheduledClaim.record.attemptingAt!,
|
|
1288
|
+
}),
|
|
1289
|
+
).toBe(true)
|
|
1290
|
+
const terminalClaim = await Webhooks.claimQueueEvent(db, terminal.reference)
|
|
1291
|
+
expect(terminalClaim.type).toBe('claimed')
|
|
1292
|
+
if (terminalClaim.type !== 'claimed') return
|
|
1293
|
+
expect(
|
|
1294
|
+
await Webhooks.completeQueueEvent(db, terminal.reference, {
|
|
1295
|
+
claimedAt: terminalClaim.record.attemptingAt!,
|
|
1296
|
+
status: 'succeeded',
|
|
1297
|
+
}),
|
|
1298
|
+
).toBe(true)
|
|
1299
|
+
|
|
1300
|
+
await expect(Webhooks.dueQueueEvents(db, 10)).resolves.toEqual([due.reference])
|
|
1301
|
+
|
|
1302
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 10))
|
|
1303
|
+
await expect(Webhooks.dueQueueEvents(db, 10)).resolves.toEqual([
|
|
1304
|
+
due.reference,
|
|
1305
|
+
inFlight.reference,
|
|
1306
|
+
])
|
|
1307
|
+
})
|
|
1308
|
+
})
|
|
1309
|
+
|
|
1310
|
+
describe('pendingQueueEvents', () => {
|
|
1311
|
+
beforeEach(() => {
|
|
1312
|
+
vi.useFakeTimers({ toFake: ['Date'] })
|
|
1313
|
+
vi.setSystemTime(fixedNow())
|
|
1314
|
+
})
|
|
1315
|
+
|
|
1316
|
+
afterEach(() => {
|
|
1317
|
+
vi.useRealTimers()
|
|
1318
|
+
})
|
|
1319
|
+
|
|
1320
|
+
test('counts pending obligations and reports the oldest staging time', async () => {
|
|
1321
|
+
const db = TestApp.database()
|
|
1322
|
+
await stagedObligation(db, 1)
|
|
1323
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + 1_000))
|
|
1324
|
+
const done = await stagedObligation(db, 2)
|
|
1325
|
+
const doneClaim = await Webhooks.claimQueueEvent(db, done.reference)
|
|
1326
|
+
expect(doneClaim.type).toBe('claimed')
|
|
1327
|
+
if (doneClaim.type !== 'claimed') return
|
|
1328
|
+
expect(
|
|
1329
|
+
await Webhooks.completeQueueEvent(db, done.reference, {
|
|
1330
|
+
claimedAt: doneClaim.record.attemptingAt!,
|
|
1331
|
+
status: 'succeeded',
|
|
1332
|
+
}),
|
|
1333
|
+
).toBe(true)
|
|
1334
|
+
|
|
1335
|
+
await expect(Webhooks.pendingQueueEvents(db)).resolves.toEqual({
|
|
1336
|
+
count: 1,
|
|
1337
|
+
dueCount: 1,
|
|
1338
|
+
oldestAt: fixedNow().toISOString(),
|
|
1339
|
+
oldestDueAt: fixedNow().toISOString(),
|
|
1340
|
+
})
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
test('excludes in-flight claims and scheduled retries from due work', async () => {
|
|
1344
|
+
const db = TestApp.database()
|
|
1345
|
+
const inFlight = await stagedObligation(db, 1)
|
|
1346
|
+
const claim = await Webhooks.claimQueueEvent(db, inFlight.reference)
|
|
1347
|
+
expect(claim.type).toBe('claimed')
|
|
1348
|
+
|
|
1349
|
+
await expect(Webhooks.pendingQueueEvents(db)).resolves.toMatchObject({
|
|
1350
|
+
count: 1,
|
|
1351
|
+
dueCount: 0,
|
|
1352
|
+
oldestDueAt: null,
|
|
1353
|
+
})
|
|
1354
|
+
})
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
describe('deliverClaimedAndRecord', () => {
|
|
1358
|
+
test('delivers a claimed envelope and skips an inactive subscription', async () => {
|
|
1359
|
+
const db = TestApp.database()
|
|
1360
|
+
const { envelope, reference, subscription } = await stagedObligation(db)
|
|
1361
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1362
|
+
expect(claim.type).toBe('claimed')
|
|
1363
|
+
if (claim.type !== 'claimed') return
|
|
1364
|
+
|
|
1365
|
+
let delivered: unknown
|
|
1366
|
+
const fetch = (async (_url, init) => {
|
|
1367
|
+
delivered = JSON.parse(typeof init?.body === 'string' ? init.body : '{}')
|
|
1368
|
+
return new Response(null, { status: 200 })
|
|
1369
|
+
}) satisfies typeof globalThis.fetch
|
|
1370
|
+
|
|
1371
|
+
const delivery = await Webhooks.deliverClaimedAndRecord(
|
|
1372
|
+
db,
|
|
1373
|
+
{ envelope: claim.record.envelope, subscriptionId: reference.subscriptionId },
|
|
1374
|
+
{ fetch },
|
|
1375
|
+
)
|
|
1376
|
+
expect(delivery.status).toBe('delivered')
|
|
1377
|
+
if (delivery.status !== 'delivered') return
|
|
1378
|
+
expect(delivery.result.ok).toBe(true)
|
|
1379
|
+
expect(delivered).toEqual(envelope)
|
|
1380
|
+
|
|
1381
|
+
await Webhooks.deleteSubscription(db, apiKeyOwner, subscription.id)
|
|
1382
|
+
await expect(
|
|
1383
|
+
Webhooks.deliverClaimedAndRecord(db, {
|
|
1384
|
+
envelope: claim.record.envelope,
|
|
1385
|
+
subscriptionId: reference.subscriptionId,
|
|
1386
|
+
}),
|
|
1387
|
+
).resolves.toEqual({ status: 'skipped' })
|
|
1388
|
+
})
|
|
1389
|
+
})
|
|
1390
|
+
|
|
912
1391
|
describe('buildPingEnvelope', () => {
|
|
913
1392
|
test('builds a synthetic ping envelope with a deterministic id from the nonce', async () => {
|
|
914
1393
|
const db = TestApp.database()
|