tapimo 0.11.0 → 0.12.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/App.d.ts +2296 -1010
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +2 -2
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +15 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +7 -0
- package/dist/Assets.js.map +1 -1
- package/dist/admin/App.d.ts +53 -4
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +4 -4
- package/dist/admin/apps/earn-vaults.d.ts +114 -0
- package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
- package/dist/admin/apps/earn-vaults.js +59 -6
- package/dist/admin/apps/earn-vaults.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +4 -2
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +9 -9
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +151 -151
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +1699 -749
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +56 -7
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/ZoneSelection.d.ts +62 -0
- package/dist/apps/data/ZoneSelection.d.ts.map +1 -0
- package/dist/apps/data/ZoneSelection.js +44 -0
- package/dist/apps/data/ZoneSelection.js.map +1 -0
- package/dist/apps/data/routes/activities.d.ts +2367 -4
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +368 -87
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +471 -340
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +28 -28
- package/dist/apps/data/routes/balances.js +1 -1
- 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/blocks.js +1 -1
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/earn.d.ts +131 -6
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +157 -25
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +825 -119
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +16 -13
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +9 -11
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +34 -34
- package/dist/apps/data/routes/receipts.d.ts +230 -230
- package/dist/apps/data/routes/receipts.js +1 -1
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.js +24 -10
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
- package/dist/apps/data/routes/tokenlist.js +16 -15
- package/dist/apps/data/routes/tokenlist.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +14 -0
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +34 -15
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.js +1 -1
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +7 -9
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +4 -4
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +2 -2
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +30 -30
- package/dist/apps/funding/App.d.ts +27 -27
- package/dist/apps/funding/routes/chains.d.ts.map +1 -1
- package/dist/apps/funding/routes/chains.js +5 -4
- package/dist/apps/funding/routes/chains.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +16 -16
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +8 -8
- 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/providers.d.ts.map +1 -1
- package/dist/apps/funding/routes/providers.js +3 -2
- package/dist/apps/funding/routes/providers.js.map +1 -1
- package/dist/apps/funding/routes/quotes.d.ts.map +1 -1
- package/dist/apps/funding/routes/quotes.js +2 -2
- package/dist/apps/funding/routes/quotes.js.map +1 -1
- package/dist/apps/management/App.d.ts +627 -293
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +19 -6
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +114 -54
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +76 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +103 -30
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +91 -32
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +33 -33
- package/dist/apps/management/routes/faucet.d.ts.map +1 -1
- package/dist/apps/management/routes/faucet.js +23 -12
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +46 -13
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +79 -29
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +86 -41
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
- package/dist/apps/management/routes/invite-links.js +100 -41
- package/dist/apps/management/routes/invite-links.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +5 -5
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +14 -10
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +93 -72
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +37 -18
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +48 -8
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +48 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +90 -28
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +72 -21
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +8 -5
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +78 -78
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +41 -31
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/management/routes/verified-token-requests.d.ts +34 -6
- package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -1
- package/dist/apps/management/routes/verified-token-requests.js +36 -17
- package/dist/apps/management/routes/verified-token-requests.js.map +1 -1
- package/dist/apps/management/routes/webhooks.d.ts +147 -130
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/management/routes/webhooks.js +80 -35
- package/dist/apps/management/routes/webhooks.js.map +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
- package/dist/apps/mpp/internal/Adapter.js +12 -0
- package/dist/apps/mpp/internal/Adapter.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 +40 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +23 -1
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +19 -3
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/earnVaults.d.ts +5 -1
- package/dist/db/tables/earnVaults.d.ts.map +1 -1
- package/dist/db/tables/earnVaults.js +5 -0
- package/dist/db/tables/earnVaults.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +10 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/db/tables/webhookQueueCompletions.d.ts +15 -0
- package/dist/db/tables/webhookQueueCompletions.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueCompletions.js +32 -0
- package/dist/db/tables/webhookQueueCompletions.js.map +1 -0
- package/dist/db/tables/webhookQueueEvents.d.ts +14 -20
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
- package/dist/db/tables/webhookQueueEvents.js +68 -47
- package/dist/db/tables/webhookQueueEvents.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +16 -5
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +29 -5
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +8 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +13 -3
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +11 -7
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EarnVaults.d.ts +41 -2
- package/dist/internal/EarnVaults.d.ts.map +1 -1
- package/dist/internal/EarnVaults.js +167 -0
- package/dist/internal/EarnVaults.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +26 -22
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +128 -39
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Response.d.ts +13 -8
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Response.js +16 -5
- package/dist/internal/Response.js.map +1 -1
- package/dist/internal/Schema.d.ts +5 -7
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +6 -7
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +15 -6
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +45 -15
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts +2 -0
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +16 -4
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +29 -12
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +37 -20
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/Deposit.d.ts +2 -2
- package/dist/internal/funding/Deposit.d.ts.map +1 -1
- package/dist/internal/funding/Deposit.js +9 -8
- package/dist/internal/funding/Deposit.js.map +1 -1
- package/dist/internal/funding/DepositAddress.d.ts +4 -4
- package/dist/internal/funding/DepositAddress.d.ts.map +1 -1
- package/dist/internal/funding/DepositAddress.js +9 -8
- package/dist/internal/funding/DepositAddress.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +13 -12
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/providers/stargate.d.ts +19 -0
- package/dist/internal/funding/providers/stargate.d.ts.map +1 -0
- package/dist/internal/funding/providers/stargate.js +143 -0
- package/dist/internal/funding/providers/stargate.js.map +1 -0
- package/dist/providers/index.d.ts +7 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -0
- package/dist/providers/index.js.map +1 -1
- package/package.json +2 -2
- package/src/App.test.ts +48 -0
- package/src/App.ts +4 -2
- package/src/Assets.test.ts +18 -0
- package/src/Assets.ts +22 -0
- package/src/Client.test-d.ts +265 -16
- package/src/admin/apps/earn-vaults.test.ts +143 -0
- package/src/admin/apps/earn-vaults.ts +83 -6
- package/src/admin/apps/organizations.ts +6 -2
- package/src/admin/ui/src/routes/api-keys.tsx +36 -0
- package/src/admin/ui.generated.ts +151 -189
- package/src/apps/data/App.test.ts +31 -0
- package/src/apps/data/App.ts +56 -8
- package/src/apps/data/ZoneSelection.test.ts +196 -0
- package/src/apps/data/ZoneSelection.ts +108 -0
- package/src/apps/data/routes/activities.test.ts +328 -1
- package/src/apps/data/routes/activities.ts +498 -93
- package/src/apps/data/routes/balances.ts +1 -1
- package/src/apps/data/routes/blocks.ts +1 -1
- package/src/apps/data/routes/earn.test.ts +183 -23
- package/src/apps/data/routes/earn.ts +225 -29
- package/src/apps/data/routes/earn.zone-access.test.ts +46 -0
- package/src/apps/data/routes/exchanges.test.ts +31 -1
- package/src/apps/data/routes/exchanges.ts +19 -15
- package/src/apps/data/routes/fee-amm.ts +18 -11
- package/src/apps/data/routes/receipts.ts +1 -1
- package/src/apps/data/routes/rpc.test.ts +75 -0
- package/src/apps/data/routes/rpc.ts +23 -9
- package/src/apps/data/routes/tokenlist.ts +21 -18
- package/src/apps/data/routes/tokens.test.ts +29 -0
- package/src/apps/data/routes/tokens.ts +38 -15
- package/src/apps/data/routes/transactions.ts +1 -1
- package/src/apps/data/routes/transfers.test.ts +3 -3
- package/src/apps/data/routes/transfers.ts +12 -9
- package/src/apps/data/routes/webhooks.ts +6 -2
- package/src/apps/data/routes/zones.test.ts +19 -13
- package/src/apps/funding/routes/chains.test.ts +211 -7
- package/src/apps/funding/routes/chains.ts +19 -12
- package/src/apps/funding/routes/deposit-addresses.test.ts +394 -0
- package/src/apps/funding/routes/deposit-addresses.ts +84 -72
- package/src/apps/funding/routes/providers.ts +9 -5
- package/src/apps/funding/routes/quotes.test.ts +46 -0
- package/src/apps/funding/routes/quotes.ts +8 -5
- package/src/apps/funding/routes/transfers.test.ts +35 -32
- package/src/apps/management/Billing.test.ts +26 -0
- package/src/apps/management/Billing.ts +24 -7
- package/src/apps/management/routes/api-keys.test.ts +178 -0
- package/src/apps/management/routes/api-keys.ts +203 -144
- package/src/apps/management/routes/billing.test.ts +209 -0
- package/src/apps/management/routes/billing.ts +254 -177
- package/src/apps/management/routes/faucet.test.ts +39 -0
- package/src/apps/management/routes/faucet.ts +75 -41
- package/src/apps/management/routes/invitations.test.ts +216 -0
- package/src/apps/management/routes/invitations.ts +186 -109
- package/src/apps/management/routes/invite-links.test.ts +263 -1
- package/src/apps/management/routes/invite-links.ts +295 -214
- package/src/apps/management/routes/me.test.ts +49 -0
- package/src/apps/management/routes/me.ts +42 -32
- package/src/apps/management/routes/members.test.ts +124 -1
- package/src/apps/management/routes/members.ts +81 -51
- package/src/apps/management/routes/orgs.test.ts +176 -0
- package/src/apps/management/routes/orgs.ts +101 -59
- package/src/apps/management/routes/projects.test.ts +183 -0
- package/src/apps/management/routes/projects.ts +124 -60
- package/src/apps/management/routes/scopes.test.ts +44 -0
- package/src/apps/management/routes/scopes.ts +37 -28
- package/src/apps/management/routes/usage.test.ts +92 -0
- package/src/apps/management/routes/usage.ts +186 -151
- package/src/apps/management/routes/verified-token-requests.test.ts +114 -0
- package/src/apps/management/routes/verified-token-requests.ts +166 -133
- package/src/apps/management/routes/webhooks.test.ts +234 -0
- package/src/apps/management/routes/webhooks.ts +191 -78
- package/src/apps/mpp/internal/Adapter.ts +13 -1
- package/src/db/Db.test.ts +41 -0
- package/src/db/Db.ts +62 -0
- package/src/db/Schema.ts +28 -3
- package/src/db/tables/earnVaults.test.ts +22 -0
- package/src/db/tables/earnVaults.ts +11 -1
- package/src/db/tables/webhookDeliveries.ts +13 -0
- package/src/db/tables/webhookQueueCompletions.ts +55 -0
- package/src/db/tables/webhookQueueEvents.ts +95 -61
- package/src/db/tables/webhookSubscriptions.ts +45 -6
- package/src/internal/Auth.ts +23 -5
- package/src/internal/Cache.ts +14 -7
- package/src/internal/EarnVaults.capabilities.test.ts +33 -0
- package/src/internal/EarnVaults.test.ts +79 -0
- package/src/internal/EarnVaults.ts +277 -0
- package/src/internal/OpenApi.ts +165 -49
- package/src/internal/Response.test.ts +95 -0
- package/src/internal/Response.ts +40 -2
- package/src/internal/Schema.test.ts +16 -0
- package/src/internal/Schema.ts +7 -7
- package/src/internal/Store.test.ts +186 -0
- package/src/internal/Store.ts +57 -17
- package/src/internal/Tidx.test.ts +19 -0
- package/src/internal/Tidx.ts +16 -4
- package/src/internal/Webhooks.test.ts +122 -8
- package/src/internal/Webhooks.ts +59 -29
- package/src/internal/funding/Deposit.ts +71 -64
- package/src/internal/funding/DepositAddress.ts +50 -43
- package/src/internal/funding/Provider.ts +164 -142
- package/src/internal/funding/providers/index.test.ts +185 -0
- package/src/internal/funding/providers/stargate.ts +184 -0
- package/src/providers/index.test.ts +1 -0
- package/src/providers/index.ts +9 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono'
|
|
2
|
+
import { Tidx } from 'tidx.ts'
|
|
2
3
|
|
|
3
4
|
import * as Response from './Response.js'
|
|
4
5
|
|
|
@@ -25,4 +26,98 @@ describe('upstream', () => {
|
|
|
25
26
|
`)
|
|
26
27
|
expect(captured).toBe(cause)
|
|
27
28
|
})
|
|
29
|
+
|
|
30
|
+
test('returns a query error when a positional page times out', async () => {
|
|
31
|
+
const app = new Hono()
|
|
32
|
+
const cause = new Tidx.FetchRequestError(
|
|
33
|
+
'Request timeout',
|
|
34
|
+
new globalThis.Response(null, { status: 408 }),
|
|
35
|
+
)
|
|
36
|
+
let captured: Error | undefined
|
|
37
|
+
|
|
38
|
+
app.use(async (c, next) => {
|
|
39
|
+
await next()
|
|
40
|
+
captured = c.error
|
|
41
|
+
})
|
|
42
|
+
app.get('/', (c) => Response.upstream(c, cause, { page: 2 }))
|
|
43
|
+
|
|
44
|
+
const response = await app.request('/')
|
|
45
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
46
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
47
|
+
{
|
|
48
|
+
"error": {
|
|
49
|
+
"code": "query_invalid",
|
|
50
|
+
"message": "The requested page timed out. Use cursor pagination for deep traversal.",
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
`)
|
|
54
|
+
expect(captured).toBe(cause)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('returns a query error when a positional page is rejected', async () => {
|
|
58
|
+
const app = new Hono()
|
|
59
|
+
const cause = new Tidx.FetchRequestError(
|
|
60
|
+
'Query rejected',
|
|
61
|
+
new globalThis.Response(null, { status: 422 }),
|
|
62
|
+
)
|
|
63
|
+
let captured: Error | undefined
|
|
64
|
+
|
|
65
|
+
app.use(async (c, next) => {
|
|
66
|
+
await next()
|
|
67
|
+
captured = c.error
|
|
68
|
+
})
|
|
69
|
+
app.get('/', (c) => Response.upstream(c, cause, { page: 584 }))
|
|
70
|
+
|
|
71
|
+
const response = await app.request('/')
|
|
72
|
+
expect(response.status).toMatchInlineSnapshot(`400`)
|
|
73
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
74
|
+
{
|
|
75
|
+
"error": {
|
|
76
|
+
"code": "query_invalid",
|
|
77
|
+
"message": "The requested page was rejected. Use cursor pagination for deep traversal.",
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
`)
|
|
81
|
+
expect(captured).toBe(cause)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('keeps timeouts without a deep positional page as upstream errors', async () => {
|
|
85
|
+
const app = new Hono()
|
|
86
|
+
const cause = new Tidx.FetchRequestError(
|
|
87
|
+
'Request timeout',
|
|
88
|
+
new globalThis.Response(null, { status: 408 }),
|
|
89
|
+
)
|
|
90
|
+
app.get('/', (c) => Response.upstream(c, cause, { page: undefined }))
|
|
91
|
+
|
|
92
|
+
const response = await app.request('/')
|
|
93
|
+
expect(response.status).toMatchInlineSnapshot(`502`)
|
|
94
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
95
|
+
{
|
|
96
|
+
"error": {
|
|
97
|
+
"code": "upstream_error",
|
|
98
|
+
"message": "Upstream service could not complete the request",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
`)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('keeps query rejections without a deep positional page as upstream errors', async () => {
|
|
105
|
+
const app = new Hono()
|
|
106
|
+
const cause = new Tidx.FetchRequestError(
|
|
107
|
+
'Query rejected',
|
|
108
|
+
new globalThis.Response(null, { status: 422 }),
|
|
109
|
+
)
|
|
110
|
+
app.get('/', (c) => Response.upstream(c, cause, { page: undefined }))
|
|
111
|
+
|
|
112
|
+
const response = await app.request('/')
|
|
113
|
+
expect(response.status).toMatchInlineSnapshot(`502`)
|
|
114
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
115
|
+
{
|
|
116
|
+
"error": {
|
|
117
|
+
"code": "upstream_error",
|
|
118
|
+
"message": "Upstream service could not complete the request",
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
`)
|
|
122
|
+
})
|
|
28
123
|
})
|
package/src/internal/Response.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Context } from 'hono'
|
|
2
2
|
import type * as z from 'zod/mini'
|
|
3
3
|
|
|
4
|
+
import * as Tidx from './Tidx.js'
|
|
5
|
+
|
|
4
6
|
/** Creates a JSON error response. */
|
|
5
7
|
export function error<const status extends error.Status, const code extends string>(
|
|
6
8
|
c: Context,
|
|
@@ -39,14 +41,34 @@ export function unsupportedChainId(c: Context, chainId: number, supported: Itera
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
* Maps
|
|
44
|
+
* Maps upstream failures to 502, except TIDX query failures after the first positional page return 400.
|
|
43
45
|
* Preserves `Error` causes for reporting and rethrows non-`Error` causes.
|
|
44
46
|
*/
|
|
45
|
-
export function upstream(c: Context, cause: unknown)
|
|
47
|
+
export function upstream(c: Context, cause: unknown): upstream.Response
|
|
48
|
+
export function upstream(
|
|
49
|
+
c: Context,
|
|
50
|
+
cause: unknown,
|
|
51
|
+
options: upstream.Options,
|
|
52
|
+
): upstream.PageResponse
|
|
53
|
+
export function upstream(c: Context, cause: unknown, options?: upstream.Options) {
|
|
46
54
|
if (cause instanceof Error) {
|
|
47
55
|
// Preserve the original error for error-reporting middleware without
|
|
48
56
|
// exposing provider URLs, payloads, or credentials in the response.
|
|
49
57
|
c.error = cause
|
|
58
|
+
if (options?.page !== undefined && options.page > 1) {
|
|
59
|
+
if (Tidx.isQueryTimeout(cause))
|
|
60
|
+
return error(c, {
|
|
61
|
+
code: 'query_invalid',
|
|
62
|
+
message: 'The requested page timed out. Use cursor pagination for deep traversal.',
|
|
63
|
+
status: 400,
|
|
64
|
+
})
|
|
65
|
+
if (Tidx.isQueryRejection(cause))
|
|
66
|
+
return error(c, {
|
|
67
|
+
code: 'query_invalid',
|
|
68
|
+
message: 'The requested page was rejected. Use cursor pagination for deep traversal.',
|
|
69
|
+
status: 400,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
50
72
|
return error(c, {
|
|
51
73
|
code: 'upstream_error',
|
|
52
74
|
message: 'Upstream service could not complete the request',
|
|
@@ -56,6 +78,22 @@ export function upstream(c: Context, cause: unknown) {
|
|
|
56
78
|
throw cause
|
|
57
79
|
}
|
|
58
80
|
|
|
81
|
+
export declare namespace upstream {
|
|
82
|
+
/** Pagination context for classifying a deep-page query failure. */
|
|
83
|
+
type Options = {
|
|
84
|
+
/** Validated positional page number, when requested. */
|
|
85
|
+
page: number | undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Upstream response for routes that support positional pagination. */
|
|
89
|
+
type PageResponse =
|
|
90
|
+
| ReturnType<typeof error<400, 'query_invalid'>>
|
|
91
|
+
| ReturnType<typeof error<502, 'upstream_error'>>
|
|
92
|
+
|
|
93
|
+
/** Default upstream error response. */
|
|
94
|
+
type Response = ReturnType<typeof error<502, 'upstream_error'>>
|
|
95
|
+
}
|
|
96
|
+
|
|
59
97
|
/**
|
|
60
98
|
* Validates `value` against `schema`. Returns the parsed value on success or
|
|
61
99
|
* throws a generic upstream error so the public response body never leaks Zod
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as z from 'zod/mini'
|
|
2
|
+
|
|
1
3
|
import * as Schema from './Schema.js'
|
|
2
4
|
|
|
3
5
|
describe('ChainId', () => {
|
|
@@ -51,6 +53,20 @@ describe('Limit', () => {
|
|
|
51
53
|
})
|
|
52
54
|
})
|
|
53
55
|
|
|
56
|
+
describe('pageChecks', () => {
|
|
57
|
+
const Query = z
|
|
58
|
+
.object({ cursor: Schema.Cursor, limit: Schema.Limit, page: Schema.Page })
|
|
59
|
+
.check(...Schema.pageChecks())
|
|
60
|
+
|
|
61
|
+
test('accepts positional pages through the 500-row window', () => {
|
|
62
|
+
expect(Query.safeParse({ limit: '10', page: '50' }).success).toMatchInlineSnapshot(`true`)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('rejects deeper positional pages', () => {
|
|
66
|
+
expect(Query.safeParse({ limit: '10', page: '51' }).success).toMatchInlineSnapshot(`false`)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
54
70
|
describe('timestampBound', () => {
|
|
55
71
|
test('normalizes RFC 3339 offsets to UTC', () => {
|
|
56
72
|
const bound = Schema.timestampBound('swaps', 'from')
|
package/src/internal/Schema.ts
CHANGED
|
@@ -191,6 +191,8 @@ export const ErrorDetail = z
|
|
|
191
191
|
})
|
|
192
192
|
.check(z.describe('A single problem with the request.'))
|
|
193
193
|
|
|
194
|
+
z.globalRegistry.add(ErrorDetail, { ...z.globalRegistry.get(ErrorDetail), ref: 'ErrorDetail' })
|
|
195
|
+
|
|
194
196
|
/**
|
|
195
197
|
* Builds the global error envelope, optionally enumerating `error.code` to a
|
|
196
198
|
* fixed set of machine-readable codes. Pass the codes a given status can return
|
|
@@ -384,12 +386,10 @@ export const Cursor = z
|
|
|
384
386
|
|
|
385
387
|
/**
|
|
386
388
|
* Maximum page window (`page × limit` rows) accepted by positional pagination.
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* cap): shallow page-numbered access is served positionally, deep traversal
|
|
390
|
-
* must use cursors.
|
|
389
|
+
* Stays below the indexer's own 10k row ceiling so large offsets do not reach
|
|
390
|
+
* the indexer. Deeper traversal must use cursors.
|
|
391
391
|
*/
|
|
392
|
-
export const maxPageWindow =
|
|
392
|
+
export const maxPageWindow = 500
|
|
393
393
|
|
|
394
394
|
/**
|
|
395
395
|
* Page-number query parameter (1-indexed) shared by list endpoints that
|
|
@@ -443,8 +443,8 @@ export const NextCursor = z
|
|
|
443
443
|
* count is computed by a capped subquery (`… LIMIT countCap`) so it can't blow
|
|
444
444
|
* the indexer's execution budget on the 10⁹-plus row tables; when the bound is
|
|
445
445
|
* hit the count is a lower bound, signalled by `Meta.totalCountCapped`. Mirrors the
|
|
446
|
-
* indexer's own 10k row ceiling
|
|
447
|
-
*
|
|
446
|
+
* indexer's own 10k row ceiling and the capped-count conventions of comparable
|
|
447
|
+
* APIs (Stripe Search's 10k `total_count`,
|
|
448
448
|
* Elasticsearch `track_total_hits`, Shopify's `Count.precision`).
|
|
449
449
|
*/
|
|
450
450
|
export const countCap = 10_000
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HonoRequest } from 'hono/request'
|
|
1
2
|
import { Store } from 'tapimo'
|
|
2
3
|
import * as TestStore from '../../test/Store.js'
|
|
3
4
|
|
|
@@ -754,6 +755,18 @@ describe('durableObject', () => {
|
|
|
754
755
|
})
|
|
755
756
|
})
|
|
756
757
|
|
|
758
|
+
describe('withRequest', () => {
|
|
759
|
+
test('preserves the source store contract', async () => {
|
|
760
|
+
const source = new ReceiverStore()
|
|
761
|
+
const store = Store.withRequest(source, new HonoRequest(new Request('https://api.tempo.xyz')))
|
|
762
|
+
|
|
763
|
+
await store.put('value', 'stored')
|
|
764
|
+
|
|
765
|
+
expect(store.type).toMatchInlineSnapshot(`"cache"`)
|
|
766
|
+
expect(await source.get('value')).toMatchInlineSnapshot(`"stored"`)
|
|
767
|
+
})
|
|
768
|
+
})
|
|
769
|
+
|
|
757
770
|
describe('memoize', () => {
|
|
758
771
|
test('coalesces concurrent cache misses for the same key', async () => {
|
|
759
772
|
const store = Store.memory()
|
|
@@ -836,6 +849,126 @@ describe('memoize', () => {
|
|
|
836
849
|
`)
|
|
837
850
|
})
|
|
838
851
|
|
|
852
|
+
test('refreshes the entry when request headers send no-cache', async () => {
|
|
853
|
+
const store = Store.memory()
|
|
854
|
+
let calls = 0
|
|
855
|
+
const fetch = async () => ({ value: ++calls })
|
|
856
|
+
|
|
857
|
+
await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
|
|
858
|
+
const refreshed = await Store.memoize(fetch, {
|
|
859
|
+
key: 'memo:test',
|
|
860
|
+
store: Store.withRequest(
|
|
861
|
+
store,
|
|
862
|
+
new HonoRequest(
|
|
863
|
+
new Request('https://api.tempo.xyz', {
|
|
864
|
+
headers: { 'cache-control': 'no-cache' },
|
|
865
|
+
}),
|
|
866
|
+
),
|
|
867
|
+
),
|
|
868
|
+
ttl: 60_000,
|
|
869
|
+
})
|
|
870
|
+
const after = await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
|
|
871
|
+
|
|
872
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
873
|
+
expect(refreshed).toMatchInlineSnapshot(`
|
|
874
|
+
{
|
|
875
|
+
"value": 2,
|
|
876
|
+
}
|
|
877
|
+
`)
|
|
878
|
+
expect(after).toEqual(refreshed)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
test('propagates no-cache through nested memoizers', async () => {
|
|
882
|
+
const source = Store.memory()
|
|
883
|
+
let calls = 0
|
|
884
|
+
const fetch = async () => ({ value: ++calls })
|
|
885
|
+
|
|
886
|
+
await Store.memoize(fetch, { key: 'memo:inner', store: source, ttl: 60_000 })
|
|
887
|
+
const store = Store.withRequest(
|
|
888
|
+
source,
|
|
889
|
+
new HonoRequest(
|
|
890
|
+
new Request('https://api.tempo.xyz', {
|
|
891
|
+
headers: { 'cache-control': 'no-cache' },
|
|
892
|
+
}),
|
|
893
|
+
),
|
|
894
|
+
)
|
|
895
|
+
const value = await Store.memoize(
|
|
896
|
+
() => Store.memoize(fetch, { key: 'memo:inner', store, ttl: 60_000 }),
|
|
897
|
+
{ key: 'memo:outer', store, ttl: 60_000 },
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
901
|
+
expect(value).toMatchInlineSnapshot(`
|
|
902
|
+
{
|
|
903
|
+
"value": 2,
|
|
904
|
+
}
|
|
905
|
+
`)
|
|
906
|
+
})
|
|
907
|
+
|
|
908
|
+
test('bypasses without replacing the entry when request headers send no-store', async () => {
|
|
909
|
+
const store = Store.memory()
|
|
910
|
+
let calls = 0
|
|
911
|
+
const fetch = async () => ({ value: ++calls })
|
|
912
|
+
|
|
913
|
+
const first = await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
|
|
914
|
+
const bypassed = await Store.memoize(fetch, {
|
|
915
|
+
key: 'memo:test',
|
|
916
|
+
store: Store.withRequest(
|
|
917
|
+
store,
|
|
918
|
+
new HonoRequest(
|
|
919
|
+
new Request('https://api.tempo.xyz', {
|
|
920
|
+
headers: { 'cache-control': 'no-store' },
|
|
921
|
+
}),
|
|
922
|
+
),
|
|
923
|
+
),
|
|
924
|
+
ttl: 60_000,
|
|
925
|
+
})
|
|
926
|
+
const after = await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
|
|
927
|
+
|
|
928
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
929
|
+
expect(bypassed).toMatchInlineSnapshot(`
|
|
930
|
+
{
|
|
931
|
+
"value": 2,
|
|
932
|
+
}
|
|
933
|
+
`)
|
|
934
|
+
expect(after).toEqual(first)
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
test('honors Pragma no-cache only without Cache-Control', async () => {
|
|
938
|
+
const store = Store.memory()
|
|
939
|
+
let calls = 0
|
|
940
|
+
const fetch = async () => ({ value: ++calls })
|
|
941
|
+
|
|
942
|
+
await Store.memoize(fetch, { key: 'memo:test', store, ttl: 60_000 })
|
|
943
|
+
await Store.memoize(fetch, {
|
|
944
|
+
key: 'memo:test',
|
|
945
|
+
store: Store.withRequest(
|
|
946
|
+
store,
|
|
947
|
+
new HonoRequest(new Request('https://api.tempo.xyz', { headers: { pragma: 'no-cache' } })),
|
|
948
|
+
),
|
|
949
|
+
ttl: 60_000,
|
|
950
|
+
})
|
|
951
|
+
const cached = await Store.memoize(fetch, {
|
|
952
|
+
key: 'memo:test',
|
|
953
|
+
store: Store.withRequest(
|
|
954
|
+
store,
|
|
955
|
+
new HonoRequest(
|
|
956
|
+
new Request('https://api.tempo.xyz', {
|
|
957
|
+
headers: { 'cache-control': 'max-age=0', pragma: 'no-cache' },
|
|
958
|
+
}),
|
|
959
|
+
),
|
|
960
|
+
),
|
|
961
|
+
ttl: 60_000,
|
|
962
|
+
})
|
|
963
|
+
|
|
964
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
965
|
+
expect(cached).toMatchInlineSnapshot(`
|
|
966
|
+
{
|
|
967
|
+
"value": 2,
|
|
968
|
+
}
|
|
969
|
+
`)
|
|
970
|
+
})
|
|
971
|
+
|
|
839
972
|
test('preserves bigint values and collision-shaped strings on cache hits', async () => {
|
|
840
973
|
const store = Store.memory()
|
|
841
974
|
let calls = 0
|
|
@@ -877,6 +1010,34 @@ describe('memoize', () => {
|
|
|
877
1010
|
expect(calls).toBe(2)
|
|
878
1011
|
})
|
|
879
1012
|
|
|
1013
|
+
test('coalesces values excluded by the cache predicate without storing them', async () => {
|
|
1014
|
+
const store = Store.memory()
|
|
1015
|
+
const pending = deferred<void>()
|
|
1016
|
+
let calls = 0
|
|
1017
|
+
const fetch = async () => {
|
|
1018
|
+
calls += 1
|
|
1019
|
+
await pending.promise
|
|
1020
|
+
return { cacheable: false, value: 'fresh' }
|
|
1021
|
+
}
|
|
1022
|
+
const options = {
|
|
1023
|
+
key: 'memo:test',
|
|
1024
|
+
shouldCache: (value: Awaited<ReturnType<typeof fetch>>) => value.cacheable,
|
|
1025
|
+
store,
|
|
1026
|
+
ttl: 60_000,
|
|
1027
|
+
}
|
|
1028
|
+
const requests = [Store.memoize(fetch, options), Store.memoize(fetch, options)]
|
|
1029
|
+
|
|
1030
|
+
await Promise.resolve()
|
|
1031
|
+
pending.resolve()
|
|
1032
|
+
|
|
1033
|
+
expect(await Promise.all(requests)).toStrictEqual([
|
|
1034
|
+
{ cacheable: false, value: 'fresh' },
|
|
1035
|
+
{ cacheable: false, value: 'fresh' },
|
|
1036
|
+
])
|
|
1037
|
+
expect(await store.get('memo:test')).toBeNull()
|
|
1038
|
+
expect(calls).toBe(1)
|
|
1039
|
+
})
|
|
1040
|
+
|
|
880
1041
|
test('refreshes legacy untagged cache entries', async () => {
|
|
881
1042
|
const store = Store.memory({ entries: [['memo:test', '{"value":"123#__bigint"}']] })
|
|
882
1043
|
let calls = 0
|
|
@@ -945,3 +1106,28 @@ function durableObjectStorage(): Store.durableObject.Storage & { cells: Map<stri
|
|
|
945
1106
|
},
|
|
946
1107
|
}
|
|
947
1108
|
}
|
|
1109
|
+
|
|
1110
|
+
class ReceiverStore implements Store.Store {
|
|
1111
|
+
#cells = new Map<string, string>()
|
|
1112
|
+
type = 'cache' as const
|
|
1113
|
+
|
|
1114
|
+
async delete(key: string) {
|
|
1115
|
+
this.#cells.delete(key)
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async get(key: string) {
|
|
1119
|
+
return this.#cells.get(key) ?? null
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async list(options: { prefix?: string | undefined } = {}) {
|
|
1123
|
+
return {
|
|
1124
|
+
keys: [...this.#cells.keys()]
|
|
1125
|
+
.filter((key) => options.prefix === undefined || key.startsWith(options.prefix))
|
|
1126
|
+
.map((name) => ({ name })),
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async put(key: string, value: string) {
|
|
1131
|
+
this.#cells.set(key, value)
|
|
1132
|
+
}
|
|
1133
|
+
}
|
package/src/internal/Store.ts
CHANGED
|
@@ -575,26 +575,62 @@ export declare namespace durableObject {
|
|
|
575
575
|
}
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
/** Associates request cache directives with a store used by request handlers. */
|
|
579
|
+
export function withRequest(store: Store, request: withRequest.Request): Store {
|
|
580
|
+
const source = requestStores.get(store)?.store ?? store
|
|
581
|
+
const scoped = {
|
|
582
|
+
delete: source.delete.bind(source),
|
|
583
|
+
...(source.deleteIf ? { deleteIf: source.deleteIf.bind(source) } : {}),
|
|
584
|
+
get: source.get.bind(source),
|
|
585
|
+
...(source.increment ? { increment: source.increment.bind(source) } : {}),
|
|
586
|
+
list: source.list.bind(source),
|
|
587
|
+
put: source.put.bind(source),
|
|
588
|
+
...(source.swap ? { swap: source.swap.bind(source) } : {}),
|
|
589
|
+
type: source.type,
|
|
590
|
+
}
|
|
591
|
+
requestStores.set(scoped, { request, store: source })
|
|
592
|
+
return scoped
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export declare namespace withRequest {
|
|
596
|
+
/** Request surface needed to read cache directives. */
|
|
597
|
+
type Request = {
|
|
598
|
+
/** Reads one request header. */
|
|
599
|
+
header(name: string): string | undefined
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
578
603
|
/**
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
* under `key` with `ttl` milliseconds, and returns it. Undefined results are
|
|
582
|
-
* returned without being cached.
|
|
604
|
+
* Returns a cached value when present. Otherwise, runs `fetch` and stores its
|
|
605
|
+
* result unless undefined or rejected by `shouldCache`.
|
|
583
606
|
*/
|
|
584
607
|
export async function memoize<value>(
|
|
585
608
|
fetch: () => Promise<value>,
|
|
586
|
-
options: memoize.Options
|
|
609
|
+
options: memoize.Options<value>,
|
|
587
610
|
): Promise<value> {
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
611
|
+
const scoped = requestStores.get(options.store)
|
|
612
|
+
const { key, ttl } = options
|
|
613
|
+
const request = scoped?.request
|
|
614
|
+
const store = scoped?.store ?? options.store
|
|
615
|
+
|
|
616
|
+
// Request cache directives follow the same semantics as the response cache:
|
|
617
|
+
// no-cache refreshes the entry, while no-store bypasses it entirely.
|
|
618
|
+
const cacheControl = request?.header('cache-control')
|
|
619
|
+
const directives = (cacheControl ?? request?.header('pragma') ?? '').toLowerCase()
|
|
620
|
+
const noStore = directives.includes('no-store')
|
|
621
|
+
const noCache = noStore || directives.includes('no-cache')
|
|
622
|
+
if (noStore) return fetch()
|
|
623
|
+
|
|
624
|
+
if (!noCache) {
|
|
625
|
+
const raw = await store.get(key)
|
|
626
|
+
// Old cache entries used ambiguous untagged bigint strings. Refresh them
|
|
627
|
+
// instead of guessing whether a matching string was originally a bigint.
|
|
628
|
+
if (raw?.startsWith(jsonCodecPrefix)) {
|
|
629
|
+
try {
|
|
630
|
+
return jsonDecode(raw) as value
|
|
631
|
+
} catch {
|
|
632
|
+
// Corrupt entries are treated as misses so the next call refreshes.
|
|
633
|
+
}
|
|
598
634
|
}
|
|
599
635
|
}
|
|
600
636
|
|
|
@@ -606,8 +642,9 @@ export async function memoize<value>(
|
|
|
606
642
|
// boundary; this only prevents a local cold-key stampede while the first
|
|
607
643
|
// fetch is still in flight.
|
|
608
644
|
const flight = (async () => {
|
|
645
|
+
if (noCache) await store.delete(key)
|
|
609
646
|
const value = await fetch()
|
|
610
|
-
if (value === undefined) return value
|
|
647
|
+
if (value === undefined || options.shouldCache?.(value) === false) return value
|
|
611
648
|
await store.put(key, jsonEncode(value), { ttl })
|
|
612
649
|
return value
|
|
613
650
|
})()
|
|
@@ -622,9 +659,11 @@ export async function memoize<value>(
|
|
|
622
659
|
|
|
623
660
|
export declare namespace memoize {
|
|
624
661
|
/** Options for {@link memoize}. */
|
|
625
|
-
type Options = {
|
|
662
|
+
type Options<value = unknown> = {
|
|
626
663
|
/** Cache key. Callers are responsible for namespacing/versioning. */
|
|
627
664
|
key: string
|
|
665
|
+
/** Returns whether a fetched value should be stored. Single-flight callers still share it. */
|
|
666
|
+
shouldCache?: ((value: value) => boolean) | undefined
|
|
628
667
|
/** Store used to persist cache entries. */
|
|
629
668
|
store: Store
|
|
630
669
|
/** Time-to-live in milliseconds. */
|
|
@@ -632,6 +671,7 @@ export declare namespace memoize {
|
|
|
632
671
|
}
|
|
633
672
|
}
|
|
634
673
|
|
|
674
|
+
const requestStores = new WeakMap<Store, { request: withRequest.Request; store: Store }>()
|
|
635
675
|
const flights = new WeakMap<Store, Map<string, Promise<unknown>>>()
|
|
636
676
|
|
|
637
677
|
function flightsFor(store: Store) {
|
|
@@ -59,10 +59,16 @@ describe('providerFailure', () => {
|
|
|
59
59
|
|
|
60
60
|
test('classifies timeout and rate-limit statuses', () => {
|
|
61
61
|
expect([
|
|
62
|
+
Tidx.providerFailure(appError('query timed out')),
|
|
62
63
|
Tidx.providerFailure(appError('secret upstream detail', 408)),
|
|
63
64
|
Tidx.providerFailure(appError('secret upstream detail', 429)),
|
|
64
65
|
]).toMatchInlineSnapshot(`
|
|
65
66
|
[
|
|
67
|
+
{
|
|
68
|
+
"failure": "timeout",
|
|
69
|
+
"id": "tidx",
|
|
70
|
+
"operation": "query",
|
|
71
|
+
},
|
|
66
72
|
{
|
|
67
73
|
"failure": "timeout",
|
|
68
74
|
"id": "tidx",
|
|
@@ -135,6 +141,19 @@ describe('isQueryRejection', () => {
|
|
|
135
141
|
})
|
|
136
142
|
})
|
|
137
143
|
|
|
144
|
+
describe('isQueryTimeout', () => {
|
|
145
|
+
test('matches explicit indexer query timeout failures only', () => {
|
|
146
|
+
expect([
|
|
147
|
+
Tidx.isQueryTimeout(appError('Request timeout', 408)),
|
|
148
|
+
Tidx.isQueryTimeout(appError('query timed out')),
|
|
149
|
+
Tidx.isQueryTimeout(appError('error sending request for url (...)')),
|
|
150
|
+
Tidx.isQueryTimeout(appError('db error')),
|
|
151
|
+
Tidx.isQueryTimeout(appError('Request timeout', 429)),
|
|
152
|
+
Tidx.isQueryTimeout(new DOMException('query timed out', 'TimeoutError')),
|
|
153
|
+
]).toEqual([true, true, true, false, false, false])
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
138
157
|
describe('escape', () => {
|
|
139
158
|
test('doubles quotes for ANSI string literals, leaving backslashes alone', () => {
|
|
140
159
|
expect(Tidx.escape('USD')).toMatchInlineSnapshot(`"USD"`)
|
package/src/internal/Tidx.ts
CHANGED
|
@@ -142,8 +142,10 @@ const transientRetryDelay = 250
|
|
|
142
142
|
* dropped at the server's execution cap — typically a cold object-storage
|
|
143
143
|
* cache read that warms across attempts, so retrying genuinely helps.
|
|
144
144
|
*/
|
|
145
|
-
const transientErrorPattern =
|
|
146
|
-
|
|
145
|
+
const transientErrorPattern = /db error|connection reset|connection refused/i
|
|
146
|
+
|
|
147
|
+
/** Indexer failures caused by the query execution deadline. */
|
|
148
|
+
const queryTimeoutPattern = /timeout|timed out|error sending request/i
|
|
147
149
|
|
|
148
150
|
/**
|
|
149
151
|
* Escapes a free-form string for safe embedding inside a single-quoted SQL
|
|
@@ -239,8 +241,11 @@ export function providerFailure(cause: unknown): Log.ProviderFailure {
|
|
|
239
241
|
return { ...base, failure: 'timeout' }
|
|
240
242
|
|
|
241
243
|
const status = Errors.getStatus(cause)
|
|
244
|
+
if (isQueryTimeout(cause))
|
|
245
|
+
return status === 408
|
|
246
|
+
? { ...base, failure: 'timeout', status }
|
|
247
|
+
: { ...base, failure: 'timeout' }
|
|
242
248
|
if (status !== undefined && status >= 200 && status < 300) return { ...base, failure: 'payload' }
|
|
243
|
-
if (status === 408) return { ...base, failure: 'timeout', status }
|
|
244
249
|
if (status === 422) return { ...base, failure: 'query', status }
|
|
245
250
|
if (status === 429) return { ...base, failure: 'rate_limit', status }
|
|
246
251
|
if (status !== undefined) return { ...base, failure: 'http', status }
|
|
@@ -270,6 +275,13 @@ export function isQueryRejection(cause: unknown): boolean {
|
|
|
270
275
|
return status === 400 || status === 422
|
|
271
276
|
}
|
|
272
277
|
|
|
278
|
+
/** Whether a failed TIDX request exhausted its query execution deadline. */
|
|
279
|
+
export function isQueryTimeout(cause: unknown): boolean {
|
|
280
|
+
if (!(cause instanceof Tidx.FetchRequestError)) return false
|
|
281
|
+
if (cause.status === 408) return true
|
|
282
|
+
return cause.status >= 200 && cause.status < 300 && queryTimeoutPattern.test(cause.message)
|
|
283
|
+
}
|
|
284
|
+
|
|
273
285
|
/**
|
|
274
286
|
* True for deterministic indexer failures a retry cannot fix: a
|
|
275
287
|
* `FetchRequestError` outside {@link isTransientError}'s transient classes,
|
|
@@ -294,7 +306,7 @@ export function isDeterministicError(error: unknown): boolean {
|
|
|
294
306
|
*/
|
|
295
307
|
function isTransientError(error: unknown): boolean {
|
|
296
308
|
if (!(error instanceof Tidx.FetchRequestError)) return false
|
|
297
|
-
if (error
|
|
309
|
+
if (isQueryTimeout(error) || error.status === 429 || error.status >= 500) return true
|
|
298
310
|
return error.status >= 200 && error.status < 300 && transientErrorPattern.test(error.message)
|
|
299
311
|
}
|
|
300
312
|
|