playlist-data-engine 1.7.2 → 1.8.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/README.md +20 -3
- package/bin/cli.cjs +85 -0
- package/dist/core/parser/TrackExtras.d.ts +150 -1
- package/dist/core/parser/TrackExtras.d.ts.map +1 -1
- package/dist/gateway-CDMPqFEH.js +1320 -0
- package/dist/gateway-DKa45Uz6.cjs +6 -0
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +1 -1
- package/dist/gateway.mjs +22 -14
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/playlist-data-engine.js +34 -34
- package/dist/playlist-data-engine.mjs +964 -1240
- package/dist/utils/engineDocs.d.ts +33 -0
- package/dist/utils/engineDocs.d.ts.map +1 -0
- package/dist/utils/playlistUtils.d.ts +37 -0
- package/dist/utils/playlistUtils.d.ts.map +1 -1
- package/dist/utils/validators.d.ts +27 -0
- package/dist/utils/validators.d.ts.map +1 -1
- package/docs/DATA_ENGINE_REFERENCE.md +6660 -0
- package/docs/USAGE_IN_OTHER_PROJECTS.md +587 -0
- package/docs/features/AUDIO_ANALYSIS.md +610 -0
- package/docs/features/BEAT_DETECTION.md +5250 -0
- package/docs/features/COMBAT_SYSTEM.md +1632 -0
- package/docs/features/CONTENT_PACKS.md +464 -0
- package/docs/features/CUSTOM_CONTENT.md +603 -0
- package/docs/features/ENEMY_GENERATION.md +1711 -0
- package/docs/features/EQUIPMENT_SYSTEM.md +2279 -0
- package/docs/features/EXTENSIBILITY_GUIDE.md +1106 -0
- package/docs/features/GATEWAY_RESOLUTION.md +725 -0
- package/docs/features/IRL_SENSORS.md +360 -0
- package/docs/features/PLAYLIST_PARSING.md +446 -0
- package/docs/features/PREREQUISITES.md +571 -0
- package/docs/features/ROLLS_AND_SEEDS.md +687 -0
- package/docs/features/XP_AND_STATS.md +1221 -0
- package/llms.txt +33 -0
- package/package.json +9 -2
- package/skills/playlist-data-engine/SKILL.md +69 -0
- package/dist/gateway-DUk4nCao.cjs +0 -1
- package/dist/gateway-DyR4M-uH.js +0 -681
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
# Arweave Gateway Resolution & the AR.IO Wayfinder
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The playlist-data-engine loads ML models, playlist metadata, artwork, and WASM modules from the Arweave permaweb. Since any single gateway can be slow, down, or return errors, the engine uses a custom **`ArweaveGatewayManager`** combined with **@ar.io/wayfinder-core** (v2.0.1) to provide resilient, multi-tier gateway routing.
|
|
6
|
+
|
|
7
|
+
Playlists, ML models, audio, and images are all stored on Arweave. Every Arweave URL that the engine encounters passes through this resolution pipeline.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
resolveUrl(arweaveUrl, { bypassArweaveNet?, bypassWayfinder? })
|
|
15
|
+
│
|
|
16
|
+
├─ 0. Cache hit? ─────────────────────────────────── return immediately
|
|
17
|
+
│
|
|
18
|
+
├─ 1. Persisted gateway (from localStorage — known-working gateway from previous session)
|
|
19
|
+
│
|
|
20
|
+
├─ 2. arweave.net (official gateway — reliable fallback)
|
|
21
|
+
│ └─ Skipped when bypassArweaveNet: true
|
|
22
|
+
│
|
|
23
|
+
├─ 3. Static fallback gateways (ardrive.net, turbo-gateway.com)
|
|
24
|
+
│ └─ Filtered by health data (>70% failure rate skipped after 3+ checks)
|
|
25
|
+
│ └─ All tried in parallel via Promise.any()
|
|
26
|
+
│
|
|
27
|
+
└─ 4. AR.IO Wayfinder (wider pool via composite routing, up to 3 retries)
|
|
28
|
+
└─ Skipped when bypassWayfinder: true
|
|
29
|
+
├─ FastestPingRoutingStrategy (top 10 by operator stake, 3s timeout)
|
|
30
|
+
└─ RandomRoutingStrategy (top 20 by operator stake, fallback)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Resolution is **sequential between tiers** but **parallel within the fallback tier** — each tier is tried in order, and within the static fallback tier (step 3), all gateways are checked in parallel via `Promise.any()`. The first gateway that passes a real HEAD check wins; remaining in-flight checks are aborted. Non-Arweave URLs pass through unchanged.
|
|
34
|
+
|
|
35
|
+
If the URL returned by `resolveUrl()` actually fails during data transfer, call [`reportGatewayFailure()`](#reportgatewayfailureurl-options) to trigger a fresh resolution that **excludes the failed gateway**:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
reportGatewayFailure() retry order:
|
|
39
|
+
1. arweave.net (if not the failed host)
|
|
40
|
+
2. Wayfinder
|
|
41
|
+
3. Remaining fallback gateways (excluding the failed host)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## The Resolution Pipeline
|
|
47
|
+
|
|
48
|
+
When [`resolveUrl()`](#resolveurlurl-signal) is called with an Arweave URL, the manager follows this fallback chain:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
0. Cache lookup (per-txId, 24-hour TTL) — return immediately if hit
|
|
52
|
+
1. Persisted gateway from localStorage (known-working gateway from previous session)
|
|
53
|
+
2. arweave.net (official gateway — reliable fallback)
|
|
54
|
+
3. Remaining static fallback gateways (ardrive.net → turbo-gateway.com, parallel)
|
|
55
|
+
4. AR.IO Wayfinder (dynamic routing via NetworkGatewaysProvider, up to 3 retries)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Each step runs a real `HEAD` request to verify the gateway can serve the specific transaction before returning it. If the signal is already aborted when called, the method returns immediately without doing any work.
|
|
59
|
+
|
|
60
|
+
**Request deduplication**: If multiple callers request the same uncached `txId` concurrently, only one runs the full fallback chain — others share its result via an in-flight promise map. The promise is cleaned up once it resolves or rejects.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## AR.IO Wayfinder Integration
|
|
65
|
+
|
|
66
|
+
### What Wayfinder Is
|
|
67
|
+
|
|
68
|
+
AR.IO Wayfinder is a dynamic gateway routing system for Arweave. Instead of relying on a hardcoded list of gateways, it selects the best gateway from a network of community-operated Arweave gateways ranked by operator stake.
|
|
69
|
+
|
|
70
|
+
The engine uses `@ar.io/wayfinder-core` (v2.0.1) and `@ar.io/sdk` (v4.0.3). Both ship as **regular dependencies** — they install with the engine rather than being peer dependencies the consumer must provide — and are **externalized** in the Vite build, so the library imports them at runtime instead of bundling them.
|
|
71
|
+
|
|
72
|
+
### Lazy Dynamic Import
|
|
73
|
+
|
|
74
|
+
Wayfinder and the AR.IO SDK are loaded asynchronously via separate dynamic `import()` calls to avoid pulling Node.js `crypto` polyfills into the main bundle:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// src/utils/arweaveGatewayManager.ts:38-49
|
|
78
|
+
async function loadWayfinder(): Promise<typeof import('@ar.io/wayfinder-core') | null> {
|
|
79
|
+
try {
|
|
80
|
+
return await import('@ar.io/wayfinder-core');
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function loadArioSdk(): Promise<typeof import('@ar.io/sdk') | null> {
|
|
87
|
+
try {
|
|
88
|
+
return await import('@ar.io/sdk');
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If either package fails to load (missing dependency, browser environment without support, etc.), the manager degrades gracefully — static gateway fallback continues to work.
|
|
96
|
+
|
|
97
|
+
### Three Initialization Tiers
|
|
98
|
+
|
|
99
|
+
Both packages must load successfully for the full composite routing strategy. The manager handles three initialization tiers:
|
|
100
|
+
|
|
101
|
+
1. **Full mode**: Both `@ar.io/wayfinder-core` and `@ar.io/sdk` available → custom CompositeRoutingStrategy (FastestPing + Random)
|
|
102
|
+
2. **Basic mode**: Only `@ar.io/wayfinder-core` available → default Wayfinder routing (no custom strategy)
|
|
103
|
+
3. **Offline mode**: Neither available → static gateways only
|
|
104
|
+
|
|
105
|
+
### Composite Routing Strategy
|
|
106
|
+
|
|
107
|
+
When both packages load, the Wayfinder client is initialized with a two-layer routing strategy in [arweaveGatewayManager.ts:338-383](../../src/utils/arweaveGatewayManager.ts#L338-L383):
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const rpc = solanaKit.createSolanaRpc(this.solanaRpcUrl);
|
|
111
|
+
const arioClient = ARIO.init({ rpc });
|
|
112
|
+
|
|
113
|
+
const primaryProvider = new NetworkGatewaysProvider({
|
|
114
|
+
ario: arioClient,
|
|
115
|
+
sortBy: 'weights.compositeWeight',
|
|
116
|
+
limit: 50,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const fallbackProvider = new NetworkGatewaysProvider({
|
|
120
|
+
ario: arioClient,
|
|
121
|
+
sortBy: 'weights.compositeWeight',
|
|
122
|
+
limit: 100,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
this.wayfinder = wfMod.createWayfinderClient({
|
|
126
|
+
routingStrategy: new CompositeRoutingStrategy({
|
|
127
|
+
strategies: [
|
|
128
|
+
new FastestPingRoutingStrategy({ timeoutMs: 3000, gatewaysProvider: primaryProvider }),
|
|
129
|
+
new RandomRoutingStrategy({ gatewaysProvider: fallbackProvider }),
|
|
130
|
+
],
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| Strategy | Gateway Pool | Timeout | Purpose |
|
|
136
|
+
|----------|-------------|---------|---------|
|
|
137
|
+
| `FastestPingRoutingStrategy` | Top 50 by composite weight | 3000ms | Primary: ping all 50, pick the fastest |
|
|
138
|
+
| `RandomRoutingStrategy` | Top 100 by composite weight | — | Fallback: if all pings fail, pick randomly from wider pool |
|
|
139
|
+
|
|
140
|
+
If either `@ar.io/sdk` is unavailable or the custom strategy throws, it falls back to `createWayfinderClient()` with no arguments (default routing).
|
|
141
|
+
|
|
142
|
+
### Tuning the Gateway Pool
|
|
143
|
+
|
|
144
|
+
The `NetworkGatewaysProvider` is what decides *which* gateways Wayfinder considers. Two knobs matter: **`sortBy`** (how to rank the network's hundreds of gateways) and **`limit`** (how many to keep). Both are exposed as config on `ArweaveGatewayManager` — see [Configuration Options](#configuration-options) below.
|
|
145
|
+
|
|
146
|
+
**`wayfinderSortBy` options** (defined in `@ar.io/wayfinder-core`'s `SortBy` type):
|
|
147
|
+
|
|
148
|
+
| Value | Ranks by | Use when |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `weights.compositeWeight` *(default)* | AR.IO's combined performance + tenure + stake + observer score | You want the protocol's own "best overall" ranking. Recommended general default. |
|
|
151
|
+
| `weights.normalizedCompositeWeight` | Composite weight normalized across the network | Similar to compositeWeight but scale-adjusted. |
|
|
152
|
+
| `weights.gatewayPerformanceRatio` | Recent uptime / response performance | You care most about speed and reliability, not network reputation. |
|
|
153
|
+
| `weights.tenureWeight` | Time in good standing | Older = battle-tested, but stale gateways may have decayed. |
|
|
154
|
+
| `weights.stakeWeight` | Normalized operator stake | Same problem as raw stake — surfaces whales. |
|
|
155
|
+
| `stats.passedConsecutiveEpochs` | Consecutive epochs the gateway passed observation | Reliability proxy — gateways that consistently meet network checks. |
|
|
156
|
+
| `totalDelegatedStake` | Stake delegated *by other users* | Weak community-trust signal. |
|
|
157
|
+
| `startTimestamp` | When the gateway joined the network | Roughly correlates with tenure. |
|
|
158
|
+
| `operatorStake` | Raw tokens locked by the operator | **Not recommended** — a single operator can spam subdomains (e.g. `*.noddex.com`) and dominate the top N. |
|
|
159
|
+
|
|
160
|
+
**`limit`** controls how many gateways the provider returns. The two providers use different limits on purpose:
|
|
161
|
+
- **`wayfinderPrimaryLimit`** (default 50): pool used by `FastestPingRoutingStrategy`. Keep small enough that pinging all of them stays under the 3s timeout.
|
|
162
|
+
- **`wayfinderFallbackLimit`** (default 100): pool used by `RandomRoutingStrategy`. Wider pools cost nothing because `Random` just picks one without pinging — they just give more variety across sessions.
|
|
163
|
+
|
|
164
|
+
**Why one operator can dominate**: AR.IO ranks per *gateway record*, not per operator. A single operator running `sol01.example.com` through `sol10.example.com` will fill 10 slots if they rank high on the chosen `sortBy`. `compositeWeight` mitigates this somewhat (subdomains generally have similar weights so they cluster together), but the only real fix is custom base-domain deduplication wrapping the provider — not currently implemented.
|
|
165
|
+
|
|
166
|
+
### Wayfinder Strategy Presets
|
|
167
|
+
|
|
168
|
+
The `wayfinderStrategy` config option picks the routing approach. Presets map to combinations of `@ar.io/wayfinder-core` strategy classes so consumers don't have to import the SDK themselves.
|
|
169
|
+
|
|
170
|
+
| Preset | Behavior | Use when |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `'composite-ping-random'` *(default)* | `FastestPing` over primary pool → falls back to `Random` over fallback pool | Balanced speed + resilience. Recommended default. |
|
|
173
|
+
| `'random-only'` | Pick uniformly at random from fallback pool, no pinging | You want max variety across sessions and don't care about ping-optimal selection. Spreads load across the network. |
|
|
174
|
+
| `'ping-only'` | Ping primary pool, pick fastest, no random fallback | You want strict "fastest available" behavior. If all pings fail, Wayfinder returns nothing and static fallbacks take over. |
|
|
175
|
+
| `'round-robin'` | Cycle through primary pool in order | Predictable distribution. No health awareness — a dead gateway in the rotation will be picked. |
|
|
176
|
+
|
|
177
|
+
Strategy classes Wayfinder also exposes but aren't covered by presets (would require forking): `StaticRoutingStrategy` (always one hardcoded gateway), `PreferredWithFallbackRoutingStrategy` (try a pinned gateway first), and raw `CompositeRoutingStrategy` with custom chain order. Add a new preset value to [arweaveGatewayManager.ts](../../src/utils/arweaveGatewayManager.ts) if you need one of these.
|
|
178
|
+
|
|
179
|
+
### Configuration Options
|
|
180
|
+
|
|
181
|
+
All Wayfinder knobs are passable to the `ArweaveGatewayManager` constructor:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { ArweaveGatewayManager } from 'playlist-data-engine';
|
|
185
|
+
|
|
186
|
+
const manager = new ArweaveGatewayManager({
|
|
187
|
+
// Solana RPC for ar.io SDK registry reads
|
|
188
|
+
solanaRpcUrl: 'https://mainnet.helius-rpc.com/?api-key=YOUR_KEY',
|
|
189
|
+
|
|
190
|
+
// How AR.IO ranks gateways before building the candidate pool
|
|
191
|
+
wayfinderSortBy: 'weights.gatewayPerformanceRatio',
|
|
192
|
+
|
|
193
|
+
// Pool sizes
|
|
194
|
+
wayfinderPrimaryLimit: 30, // FastestPing pool (default 50)
|
|
195
|
+
wayfinderFallbackLimit: 200, // Random pool (default 100)
|
|
196
|
+
|
|
197
|
+
// Routing strategy preset
|
|
198
|
+
wayfinderStrategy: 'random-only',
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
| Option | Type | Default | Description |
|
|
203
|
+
|---|---|---|---|
|
|
204
|
+
| `solanaRpcUrl` | `string` | `https://solana-rpc.publicnode.com` | Solana RPC for `@ar.io/sdk` registry reads. |
|
|
205
|
+
| `wayfinderSortBy` | `WayfinderSortBy` | `'weights.compositeWeight'` | Gateway ranking field — see table above. |
|
|
206
|
+
| `wayfinderPrimaryLimit` | `number` | `50` | Primary (FastestPing) pool size. |
|
|
207
|
+
| `wayfinderFallbackLimit` | `number` | `100` | Fallback (Random) pool size. |
|
|
208
|
+
| `wayfinderStrategy` | `WayfinderStrategy` | `'composite-ping-random'` | Routing strategy preset — see table above. |
|
|
209
|
+
|
|
210
|
+
### Solana RPC Dependency
|
|
211
|
+
|
|
212
|
+
As of `@ar.io/sdk` v4, the AR.IO gateway registry lives on Solana — `ARIO.init({ rpc })` requires a Solana RPC client to read the gateway list that powers `NetworkGatewaysProvider`. Without a working Solana RPC, `NetworkGatewaysProvider` returns no gateways and `CompositeRoutingStrategy` fails with `all strategies failed`, killing the Wayfinder fallback entirely (static gateways still work).
|
|
213
|
+
|
|
214
|
+
The RPC URL is configurable via `ArweaveGatewayManagerConfig.solanaRpcUrl`:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
new ArweaveGatewayManager({
|
|
218
|
+
solanaRpcUrl: 'https://mainnet.helius-rpc.com/?api-key=YOUR_KEY',
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Default:** `https://solana-rpc.publicnode.com` — public, CORS-enabled, no signup required. Works out of the box but is rate-limited; override for production.
|
|
223
|
+
|
|
224
|
+
**Do not use `https://api.mainnet-beta.solana.com`** — Solana Labs blocks browser-origin requests with HTTP 403, which guarantees Wayfinder will fail in any browser context.
|
|
225
|
+
|
|
226
|
+
**Recommended providers for production:** Helius, QuickNode, Triton, Alchemy — all have free tiers with CORS enabled and much higher rate limits than the public RPC.
|
|
227
|
+
|
|
228
|
+
**Default behavior changes based on whether you provide a custom RPC:** When `solanaRpcUrl` is not set, the manager treats Wayfinder as opt-in — `resolveUrl(url)` defaults to `bypassWayfinder: true`. To use Wayfinder with the default RPC, you must explicitly pass `resolveUrl(url, { bypassWayfinder: false })`. When you do provide a custom `solanaRpcUrl`, Wayfinder is opt-out — it runs unless you pass `bypassWayfinder: true`. This avoids silently degrading resolution when the rate-limited public RPC fails.
|
|
229
|
+
|
|
230
|
+
### How Wayfinder Is Used
|
|
231
|
+
|
|
232
|
+
Wayfinder is **not the primary resolution path**. It sits after static fallbacks as a last resort in [arweaveGatewayManager.ts:599-610](../../src/utils/arweaveGatewayManager.ts#L599-L610):
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// Step 4: Try Wayfinder as last resort (wider pool, but slower)
|
|
236
|
+
if (this.wayfinder) {
|
|
237
|
+
this.logger.debug('Static gateways failed, trying Wayfinder', { txId });
|
|
238
|
+
const wayfinderResult = await this.tryWayfinder(url, txId, pathSuffix, signal);
|
|
239
|
+
if (wayfinderResult) return wayfinderResult;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The `tryWayfinder()` method uses a **first-pick + walk-the-list** strategy:
|
|
244
|
+
|
|
245
|
+
1. **First pick** — calls `this.wayfinder.resolveUrl({ originalUrl: url })` with a 7-second timeout. This lets Wayfinder's configured routing strategy (e.g. `FastestPingRoutingStrategy`) pick the best candidate, benefiting from its internal ping caching.
|
|
246
|
+
2. Verifies the first pick with a real HEAD check against the txId. If it passes, sets it as the active gateway and returns.
|
|
247
|
+
3. **Walk the ranked list** — if the first pick fails verification, instead of asking Wayfinder for a new pick (which re-runs the whole ping race), the engine fetches the ranked gateway list directly from `NetworkGatewaysProvider` and walks it in rank order. Up to `MAX_WALK_DEPTH = 10` candidates are HEAD-checked; the first that has the file wins.
|
|
248
|
+
4. Already-failed hosts (from the first pick and any prior chain steps) are skipped during the walk.
|
|
249
|
+
|
|
250
|
+
Key design choices:
|
|
251
|
+
- **Wayfinder picks aren't trusted blindly** — even though Wayfinder has its own health checks, the engine verifies each candidate can serve the specific transaction. Prevents false positives where Wayfinder returns a gateway that works for the network but doesn't have the specific file.
|
|
252
|
+
- **No redundant ping races** — the original retry loop re-ran Wayfinder's full ping selection on each attempt, which often returned the same fastest-but-broken gateway. Walking the ranked list is faster and explores actually different candidates.
|
|
253
|
+
- **Ranked list is cached** — see [Ranked Gateway List Caching](#ranked-gateway-list-caching).
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Custom Extensions (Beyond Stock Wayfinder)
|
|
258
|
+
|
|
259
|
+
### Persisted Gateway State
|
|
260
|
+
|
|
261
|
+
The active gateway is persisted to `localStorage` under the key `arweave_active_gateway` with a **2-hour TTL**. On the next session or page load, the persisted gateway is restored and tried first (before arweave.net). If it's expired or fails, it's cleared immediately.
|
|
262
|
+
|
|
263
|
+
This is a custom addition — Wayfinder itself has no persistence. It means the engine remembers which gateway was working across sessions, avoiding the latency of Wayfinder's ping-based routing on every startup.
|
|
264
|
+
|
|
265
|
+
### Ranked Gateway List Caching
|
|
266
|
+
|
|
267
|
+
The ranked gateway list returned by `NetworkGatewaysProvider.getGateways()` is cached in two layers to minimize Solana RPC traffic:
|
|
268
|
+
|
|
269
|
+
1. **In-memory cache** — held on the manager instance after the first fetch. Used for the walk step of `tryWayfinder()` so retries within a single session never re-hit Solana.
|
|
270
|
+
2. **localStorage persistence** — written under the key `arweave_ranked_gateways` whenever the in-memory cache refreshes. On the next page load, the persisted list hydrates the in-memory cache before any Wayfinder call.
|
|
271
|
+
|
|
272
|
+
Both layers share the same **2-hour TTL** (`RANKED_GATEWAYS_TTL_MS`). The TTL is intentionally long: the AR.IO registry is slow-moving (gateways stake AR tokens to join, rankings shift on epoch-length performance metrics), so the top 50 by `compositeWeight` is essentially stable across hours. When the TTL expires, the next walk triggers a single refresh from the provider, which writes back to both layers. If the refresh fails (e.g. RPC outage), the stale cache is used as a fallback rather than returning nothing.
|
|
273
|
+
|
|
274
|
+
**Net effect on Solana RPC usage:**
|
|
275
|
+
- Cold start (no cache): 1 RPC call to fetch the list
|
|
276
|
+
- Returning visit within 2 hours: 0 RPC calls (loaded from localStorage)
|
|
277
|
+
- After 2 hours: 1 RPC call to refresh
|
|
278
|
+
- Per file resolve within the cache window: 0 additional RPC calls
|
|
279
|
+
|
|
280
|
+
This is especially important when using the default public RPC (`solana-rpc.publicnode.com`), since it's rate-limited. Persisting the list across sessions means even heavy users typically only make a handful of RPC calls per hour.
|
|
281
|
+
|
|
282
|
+
### Slow Gateway Detection & Tracking
|
|
283
|
+
|
|
284
|
+
The engine tracks real fetch timing (not just HEAD checks) to detect degrading gateways *before* they fail completely:
|
|
285
|
+
|
|
286
|
+
- **[`reportFetchSuccess(timingMs)`](#reportfetchsuccesstimingms)**: Call after a successful fetch. If the timing exceeds `slowResponseThreshold` (default: 8000ms), a `consecutiveSlowResponses` counter increments. Fast responses reset it.
|
|
287
|
+
- **[`reportFetchTiming(host, timingMs, success)`](#reportfetchtiminghost-timingms-success)**: Lower-level method that records timing data for any gateway (not just the active one), feeding into health tracking.
|
|
288
|
+
|
|
289
|
+
When `consecutiveSlowResponses` reaches `maxSlowResponses` (default: 3), the **next** `resolveUrl()` call proactively rotates away from the active gateway: it clears the active gateway, the persisted gateway, and the per-txId cache, then re-runs the full fallback chain to find a faster gateway.
|
|
290
|
+
|
|
291
|
+
### Health-Aware Fallback Filtering
|
|
292
|
+
|
|
293
|
+
When trying fallback gateways (step 3), the engine filters out gateways with a **>70% failure rate** (and at least 3 recorded checks). This prevents repeatedly trying known-dead gateways. If all gateways would be filtered, it uses the unfiltered list instead (graceful degradation).
|
|
294
|
+
|
|
295
|
+
This is a custom health tracking system built on top of `ResponseTimeRecord[]` — not a Wayfinder feature. It tracks per gateway:
|
|
296
|
+
- Success/failure counts
|
|
297
|
+
- Average response times
|
|
298
|
+
- Last success/failure timestamps
|
|
299
|
+
- Success rate (healthy = >= 50%)
|
|
300
|
+
|
|
301
|
+
### AbortSignal Propagation
|
|
302
|
+
|
|
303
|
+
Every resolution path supports `AbortSignal` for cancellation:
|
|
304
|
+
- `resolveUrl(url, signal?)` — cancel in-flight gateway checks
|
|
305
|
+
- `reportGatewayFailure(url, { signal? })` — cancel retry resolution
|
|
306
|
+
|
|
307
|
+
If the signal is already aborted when called, the method returns the original URL immediately without doing any work.
|
|
308
|
+
|
|
309
|
+
### Failure Reason Differentiation
|
|
310
|
+
|
|
311
|
+
[`reportGatewayFailure()`](#reportgatewayfailureurl-options) accepts a `reason` parameter to distinguish failure types:
|
|
312
|
+
|
|
313
|
+
| Reason | Behavior |
|
|
314
|
+
|--------|----------|
|
|
315
|
+
| `'load-error'` | Gateway failed — clear active gateway, find new one |
|
|
316
|
+
| `'user-cancel-slow'` | User cancelled after waiting — gateway is suspect, rotate |
|
|
317
|
+
| `'user-cancel-fast'` | User cancelled quickly — gateway is probably fine, **keep it** |
|
|
318
|
+
| _(omitted)_ | Same as `'load-error'` |
|
|
319
|
+
|
|
320
|
+
The `excludeHost` option lets callers skip a specific gateway during retry without affecting the active gateway state.
|
|
321
|
+
|
|
322
|
+
### Bypassing arweave.net
|
|
323
|
+
|
|
324
|
+
arweave.net reliability can be spotty — the gateway may be up but unable to serve specific files. The `bypassArweaveNet` flag on `resolveUrl()` removes arweave.net from the resolution chain entirely. The chain becomes: persisted → fallback gateways → Wayfinder. Useful when arweave.net is unreliable but you don't want to remove it from the static gateway list permanently.
|
|
325
|
+
|
|
326
|
+
To monitor arweave.net availability independently, use `checkGateway()` directly.
|
|
327
|
+
|
|
328
|
+
### Bypassing Wayfinder
|
|
329
|
+
|
|
330
|
+
The `bypassWayfinder` flag skips AR.IO Wayfinder resolution entirely. The chain becomes: persisted → arweave.net → static fallback gateways. Useful when you want to stick to known static gateways and avoid the latency of Wayfinder's composite routing (ping checks, network provider calls).
|
|
331
|
+
|
|
332
|
+
### Gateway Prefetching
|
|
333
|
+
|
|
334
|
+
[`prefetchUrls(urls, options?)`](#prefetchurlsurls-options) resolves multiple Arweave URLs in parallel with configurable concurrency (default: 5). Useful for warming the cache at startup with known model URLs or playlist images.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Public API
|
|
339
|
+
|
|
340
|
+
### Exports
|
|
341
|
+
|
|
342
|
+
Everything is exported from `'playlist-data-engine'`:
|
|
343
|
+
|
|
344
|
+
| Export | Type | Description |
|
|
345
|
+
|--------|------|-------------|
|
|
346
|
+
| `arweaveGatewayManager` | `ArweaveGatewayManager` | Pre-configured singleton instance |
|
|
347
|
+
| `ArweaveGatewayManager` | Class | Create custom instances |
|
|
348
|
+
| `GatewayConfig` | Interface | `{ host, protocol, priority }` |
|
|
349
|
+
| `ArweaveGatewayManagerConfig` | Interface | Constructor options |
|
|
350
|
+
| `ResolveUrlOptions` | Interface | Options for `resolveUrl()` (signal, bypassArweaveNet, bypassWayfinder) |
|
|
351
|
+
| `GatewayCache` | Interface | Cache entry shape |
|
|
352
|
+
| `GatewayCheckResult` | Interface | Resolution result with gateway info |
|
|
353
|
+
| `PrefetchOptions` | Interface | Prefetch concurrency/behavior |
|
|
354
|
+
| `PrefetchResult` | Interface | Prefetch outcome summary |
|
|
355
|
+
| `PrefetchResultEntry` | Interface | Per-URL prefetch result |
|
|
356
|
+
| `CacheStats` | Interface | Cache statistics |
|
|
357
|
+
| `GatewayHealthStats` | Interface | Per-gateway health data |
|
|
358
|
+
| `HealthCheckResult` | Interface | Health check outcome |
|
|
359
|
+
| `HealthCheckOptions` | Interface | Health check configuration |
|
|
360
|
+
| `isArweaveUrl` | Function | Check if a URL is an Arweave transaction |
|
|
361
|
+
| `parseArweaveUrl` | Function | Extract txId and pathSuffix |
|
|
362
|
+
| `constructGatewayUrl` | Function | Build gateway URL from components |
|
|
363
|
+
| `getAllGatewayUrls` | Function | Get all gateway URLs for a txId in priority order |
|
|
364
|
+
| `DEFAULT_GATEWAYS` | `GatewayConfig[]` | Default gateway list |
|
|
365
|
+
| `KNOWN_GATEWAY_HOSTS` | `string[]` | Known gateway hostnames |
|
|
366
|
+
| `ArweaveUrlInfo` | Type | Return type of `parseArweaveUrl()` |
|
|
367
|
+
|
|
368
|
+
### Constructor: `ArweaveGatewayManagerConfig`
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { ArweaveGatewayManager } from 'playlist-data-engine';
|
|
372
|
+
|
|
373
|
+
const manager = new ArweaveGatewayManager({
|
|
374
|
+
gateways: [
|
|
375
|
+
{ host: 'arweave.net', protocol: 'https', priority: 1 },
|
|
376
|
+
{ host: 'ardrive.net', protocol: 'https', priority: 2 },
|
|
377
|
+
],
|
|
378
|
+
timeout: 5000,
|
|
379
|
+
cacheTTL: 86400000,
|
|
380
|
+
slowResponseThreshold: 8000,
|
|
381
|
+
maxSlowResponses: 3,
|
|
382
|
+
solanaRpcUrl: 'https://solana-rpc.publicnode.com',
|
|
383
|
+
wayfinderSortBy: 'weights.compositeWeight',
|
|
384
|
+
wayfinderPrimaryLimit: 50,
|
|
385
|
+
wayfinderFallbackLimit: 100,
|
|
386
|
+
wayfinderStrategy: 'composite-ping-random',
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
| Option | Type | Default | Description |
|
|
391
|
+
|--------|------|---------|-------------|
|
|
392
|
+
| `gateways` | `GatewayConfig[]` | 3 defaults | Custom gateway list (deep-cloned, sorted by priority) |
|
|
393
|
+
| `timeout` | `number` | `5000` | Per-gateway HEAD check timeout (ms) |
|
|
394
|
+
| `cacheTTL` | `number` | `86400000` | Per-txId cache TTL (24 hours) |
|
|
395
|
+
| `slowResponseThreshold` | `number` | `8000` | Threshold above which a fetch is "slow" (ms) |
|
|
396
|
+
| `maxSlowResponses` | `number` | `3` | Consecutive slow fetches before proactive gateway rotation |
|
|
397
|
+
| `solanaRpcUrl` | `string` | `https://solana-rpc.publicnode.com` | Solana RPC URL for `@ar.io/sdk` gateway registry reads. See [Solana RPC Dependency](#solana-rpc-dependency). |
|
|
398
|
+
| `wayfinderSortBy` | `WayfinderSortBy` | `weights.compositeWeight` | How AR.IO ranks gateways before building the pool. See [Tuning the Gateway Pool](#tuning-the-gateway-pool). |
|
|
399
|
+
| `wayfinderPrimaryLimit` | `number` | `50` | Primary (FastestPing) pool size. |
|
|
400
|
+
| `wayfinderFallbackLimit` | `number` | `100` | Fallback (Random) pool size. |
|
|
401
|
+
| `wayfinderStrategy` | `WayfinderStrategy` | `composite-ping-random` | Routing strategy preset. See [Wayfinder Strategy Presets](#wayfinder-strategy-presets). |
|
|
402
|
+
|
|
403
|
+
### Methods
|
|
404
|
+
|
|
405
|
+
#### `resolveUrl(url, options?)`
|
|
406
|
+
|
|
407
|
+
Main entry point. Resolves an Arweave URL to a working gateway URL using the sequential fallback system (persisted → arweave.net → static fallbacks → Wayfinder with retries). Non-Arweave URLs pass through unchanged.
|
|
408
|
+
|
|
409
|
+
**Options (`ResolveUrlOptions`):**
|
|
410
|
+
|
|
411
|
+
| Option | Type | Default | Description |
|
|
412
|
+
|--------|------|---------|-------------|
|
|
413
|
+
| `signal` | `AbortSignal` | — | Cancel in-flight gateway checks |
|
|
414
|
+
| `bypassArweaveNet` | `boolean` | `false` | Skip arweave.net in the resolution chain — go persisted → fallbacks → Wayfinder |
|
|
415
|
+
| `bypassWayfinder` | `boolean` | `false` | Skip Wayfinder in the resolution chain — only use static gateways |
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// Default behavior — arweave.net is in the chain
|
|
419
|
+
const workingUrl = await arweaveGatewayManager.resolveUrl(
|
|
420
|
+
'https://arweave.net/abc123.../model.json',
|
|
421
|
+
{ signal: abortController.signal }
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Bypass arweave.net — skip straight to fallbacks when arweave.net is unreliable
|
|
425
|
+
const workingUrl = await arweaveGatewayManager.resolveUrl(
|
|
426
|
+
'https://arweave.net/abc123.../model.json',
|
|
427
|
+
{ bypassArweaveNet: true }
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// Bypass Wayfinder — stick to static gateways only
|
|
431
|
+
const workingUrl = await arweaveGatewayManager.resolveUrl(
|
|
432
|
+
'https://arweave.net/abc123.../model.json',
|
|
433
|
+
{ bypassWayfinder: true }
|
|
434
|
+
);
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### `resolveUrlSimple(url)`
|
|
438
|
+
|
|
439
|
+
Synchronous fast path — returns a gateway URL instantly by reusing the current active gateway (or arweave.net if none is set), with no network checks. Use this on the hot path where latency matters. When real fetches fail, call [`reportGatewayFailure()`](#reportgatewayfailureurl-options) to trigger gateway rotation.
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const url = arweaveGatewayManager.resolveUrlSimple(
|
|
443
|
+
'https://arweave.net/abc123.../model.json'
|
|
444
|
+
);
|
|
445
|
+
// → 'https://ardrive.net/abc123.../model.json' (immediate, no network)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### `reportGatewayFailure(url, options?)`
|
|
449
|
+
|
|
450
|
+
Reports that a real data fetch failed on the gateway that was returned by `resolveUrl()`. Clears the active gateway, persists the clearing to localStorage, and resolves a new gateway.
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
const start = performance.now();
|
|
454
|
+
try {
|
|
455
|
+
const response = await fetch(workingUrl, { signal });
|
|
456
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
const elapsed = performance.now() - start;
|
|
459
|
+
const reason = err.name === 'AbortError'
|
|
460
|
+
? (elapsed < 5000 ? 'user-cancel-fast' : 'user-cancel-slow')
|
|
461
|
+
: 'load-error';
|
|
462
|
+
newUrl = await arweaveGatewayManager.reportGatewayFailure(workingUrl, {
|
|
463
|
+
signal: abortController.signal,
|
|
464
|
+
reason,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
#### `reportFetchSuccess(timingMs)`
|
|
470
|
+
|
|
471
|
+
Feeds timing data back from a successful fetch. If the fetch was above `slowResponseThreshold`, the consecutive slow counter increments. If fast, the counter resets.
|
|
472
|
+
|
|
473
|
+
```typescript
|
|
474
|
+
const start = performance.now();
|
|
475
|
+
const response = await fetch(workingUrl);
|
|
476
|
+
arweaveGatewayManager.reportFetchSuccess(performance.now() - start);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### `reportFetchTiming(host, timingMs, success)`
|
|
480
|
+
|
|
481
|
+
Records real data-transfer timing for a specific gateway host. This feeds into the health tracking system and affects priority adjustment and health-aware filtering.
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
arweaveGatewayManager.reportFetchTiming('ar-io.net', 1250, true);
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
#### `prefetchUrls(urls, options?)`
|
|
488
|
+
|
|
489
|
+
Warms up the gateway cache by resolving multiple URLs in parallel.
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const result = await arweaveGatewayManager.prefetchUrls([
|
|
493
|
+
'https://arweave.net/abc.../model.json',
|
|
494
|
+
'https://arweave.net/def.../model.json',
|
|
495
|
+
], {
|
|
496
|
+
concurrency: 5,
|
|
497
|
+
continueOnError: true,
|
|
498
|
+
});
|
|
499
|
+
// result.succeeded, result.failed, result.skipped, result.entries
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
#### `getCacheStats()` / `clearCache()` / `getCachedGateway(txId)` / `setCache(txId, gateway)`
|
|
503
|
+
|
|
504
|
+
Cache management. `getCacheStats()` returns size, hit/miss counts, and cached txIds.
|
|
505
|
+
|
|
506
|
+
#### `getGatewayHealth(host)` / `getAllGatewayHealth()`
|
|
507
|
+
|
|
508
|
+
Returns health statistics for one or all gateways based on recorded HEAD-check and fetch timing data.
|
|
509
|
+
|
|
510
|
+
#### `adjustGatewayPriorities(options?)` / `resetGatewayPriorities()`
|
|
511
|
+
|
|
512
|
+
Reorders gateways by health data (healthy first, then by speed) or resets to original configured priorities.
|
|
513
|
+
|
|
514
|
+
#### `runHealthCheck(options?)`
|
|
515
|
+
|
|
516
|
+
Runs a parallel HEAD check against all static gateways using a known transaction ID. Optionally adjusts gateway priorities based on results.
|
|
517
|
+
|
|
518
|
+
#### `checkGateway(txId, gateway, pathSuffix?, signal?)`
|
|
519
|
+
|
|
520
|
+
Verifies a single gateway can serve a transaction. Returns `true`, `false`, or `'maybe'` when the check is inconclusive.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Internal Consumers
|
|
525
|
+
|
|
526
|
+
These engine components use the gateway manager automatically — no configuration needed:
|
|
527
|
+
|
|
528
|
+
| Component | What It Resolves | How |
|
|
529
|
+
|-----------|-----------------|-----|
|
|
530
|
+
| `PlaylistParser` | Playlist image URLs (`image_url`, `image_thumb_url`) | `arweaveGatewayManager.resolveUrl()` when `resolveImageUrls: true` |
|
|
531
|
+
| `MusicClassifier` | TensorFlow model URLs | `arweaveGatewayManager.resolveUrl` as default `resolveUrl` option |
|
|
532
|
+
| `EssentiaPitchDetector` | CREPE model URLs | `arweaveGatewayManager.resolveUrl` as default `resolveUrl` option |
|
|
533
|
+
| `PitchAnalyzer` | CREPE model URLs | `arweaveGatewayManager.resolveUrl` as default `resolveUrl` option |
|
|
534
|
+
| `ColorExtractor` | Track artwork image URLs | `arweaveGatewayManager.resolveUrl` directly |
|
|
535
|
+
|
|
536
|
+
Each component also accepts an optional `resolveUrl` callback override if a consumer wants to provide custom resolution logic.
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Resolving Mix and Audio URIs
|
|
541
|
+
|
|
542
|
+
The parser resolves image URLs only, and only when `resolveImageUrls: true` (see `PlaylistParser` above). Audio and mix URIs pass through parsing exactly as written — `ar://` identifiers, `ipfs://` paths, plain gateway URLs — and are not playable until something resolves them.
|
|
543
|
+
|
|
544
|
+
Before playback, hand the URI to the gateway manager:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { arweaveGatewayManager } from 'playlist-data-engine';
|
|
548
|
+
|
|
549
|
+
const url = await arweaveGatewayManager.resolveUrl(track.audio_url);
|
|
550
|
+
|
|
551
|
+
// Play `url`. When the real fetch fails, rotate gateways and get a new URL:
|
|
552
|
+
const retryUrl = await arweaveGatewayManager.reportGatewayFailure(url);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Mix URIs have one extra wrinkle: a mix can point at a metadata JSON file (`mime_type: 'application/json'`, or a `.json` path) rather than at audio, with the actual song named in that file's audio fields. [`resolveMixUrl()`](./PLAYLIST_PARSING.md#choosing-a-mix) wraps the same gateway resolution for mixes and follows that indirection one level — see [PLAYLIST_PARSING.md — Choosing a Mix](./PLAYLIST_PARSING.md#choosing-a-mix).
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## Caching Layers
|
|
560
|
+
|
|
561
|
+
The manager uses three separate caching mechanisms:
|
|
562
|
+
|
|
563
|
+
1. **Per-txId in-memory cache** — Maps a 43-character transaction ID to a working `GatewayConfig`. TTL is configurable (default 24 hours). Checked first on every `resolveUrl()` call.
|
|
564
|
+
2. **Active gateway persistence (localStorage)** — The single active gateway is persisted under key `arweave_active_gateway` with a 2-hour TTL. On page reload, the manager restores this gateway and tries it first (before arweave.net).
|
|
565
|
+
3. **Wayfinder internal caching** — The `NetworkGatewaysProvider` caches the AR.IO gateway list internally. The `FastestPingRoutingStrategy` caches ping results. These are managed by the wayfinder library itself.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Default Static Gateways
|
|
570
|
+
|
|
571
|
+
| Priority | Host | Protocol |
|
|
572
|
+
|----------|------|----------|
|
|
573
|
+
| 1 | `arweave.net` | https |
|
|
574
|
+
| 2 | `ardrive.net` | https |
|
|
575
|
+
| 3 | `turbo-gateway.com` | https |
|
|
576
|
+
|
|
577
|
+
These are tried in priority order after the persisted gateway. The persisted gateway from localStorage is tried first (known-working from previous session), then arweave.net, then the remaining static gateways in parallel. Wayfinder is the last resort with up to 3 retries.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## Externalized Dependencies
|
|
582
|
+
|
|
583
|
+
The gateway manager requires `@ar.io/wayfinder-core` and `@ar.io/sdk` for Wayfinder functionality. Both are listed as regular dependencies in `package.json` (`@ar.io/wayfinder-core` 2.0.1, `@ar.io/sdk` 4.0.3) and externalized in the [vite.config.ts](../../vite.config.ts) rollup config:
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
// vite.config.ts:19-20
|
|
587
|
+
external: [
|
|
588
|
+
// ...
|
|
589
|
+
'@ar.io/wayfinder-core',
|
|
590
|
+
'@ar.io/sdk',
|
|
591
|
+
],
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
The engine degrades gracefully if these packages are not available — static gateway fallback continues to work.
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Architecture Decisions
|
|
599
|
+
|
|
600
|
+
**Why Wayfinder isn't the primary path** — Wayfinder requires pinging multiple gateways (up to 10), which takes 1-3 seconds. The engine tries the persisted gateway and arweave.net first because they're instant (single HEAD check, or cached hit). Static fallbacks come next. Wayfinder is only consulted as a last resort when all faster options fail.
|
|
601
|
+
|
|
602
|
+
**Why Wayfinder results are verified** — Wayfinder's `resolveUrl()` returns a gateway that should work, but it can't guarantee the specific transaction is available on that gateway. The engine runs its own HEAD check against the resolved gateway to confirm the file exists before returning it. This prevents false positives.
|
|
603
|
+
|
|
604
|
+
**Why dynamic imports** — `@ar.io/wayfinder-core` depends on Node.js `crypto` APIs. In browser environments, bundling it directly would require polyfills. The dynamic `import()` means it's loaded separately at runtime — if it fails, the manager falls back to static gateways.
|
|
605
|
+
|
|
606
|
+
**Why localStorage persistence** — Without persistence, every page load would start from scratch: try arweave.net, fail, try static fallbacks, try Wayfinder (slow), etc. By remembering the last working gateway, subsequent page loads skip directly to a known-good gateway as the first step.
|
|
607
|
+
|
|
608
|
+
**Why sequential fallback, not parallel** — Each step in the pipeline has different cost. A cache hit is free, arweave.net is usually fast, and Wayfinder is slow (1-3s of pinging). Running all in parallel would waste bandwidth and hit rate limits on gateways that will never be needed. Sequential fallback stops at the first success.
|
|
609
|
+
|
|
610
|
+
**Why real HEAD checks, not pings** — The manager verifies each gateway can serve the *specific transaction*, not just that the gateway is up. A gateway that is healthy but doesn't have the data is correctly identified and skipped.
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Source Files
|
|
615
|
+
|
|
616
|
+
| File | Purpose |
|
|
617
|
+
|------|---------|
|
|
618
|
+
| [src/utils/arweaveGatewayManager.ts](../../src/utils/arweaveGatewayManager.ts) | Gateway manager class with Wayfinder integration |
|
|
619
|
+
| [src/utils/arweaveUtils.ts](../../src/utils/arweaveUtils.ts) | `GatewayConfig` interface, URL parsing, `DEFAULT_GATEWAYS`, utility functions |
|
|
620
|
+
| [src/utils/ipfsUtils.ts](../../src/utils/ipfsUtils.ts) | IPFS URL detection, extraction, and resolution |
|
|
621
|
+
| [src/index.ts](../../src/index.ts) | Public API exports |
|
|
622
|
+
| [vite.config.ts](../../vite.config.ts) | Externalizes `@ar.io/wayfinder-core` and `@ar.io/sdk` from bundle |
|
|
623
|
+
| [tests/unit/arweaveGatewayManager.test.ts](../../tests/unit/arweaveGatewayManager.test.ts) | Unit tests with mocked Wayfinder |
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## URL Utilities
|
|
628
|
+
|
|
629
|
+
The companion [arweaveUtils.ts](../../src/utils/arweaveUtils.ts) provides lower-level URL handling:
|
|
630
|
+
|
|
631
|
+
| Export | Description |
|
|
632
|
+
|--------|-------------|
|
|
633
|
+
| `isArweaveUrl(url)` | Returns `true` for `ar://` protocol URLs or URLs containing known gateway hosts |
|
|
634
|
+
| `parseArweaveUrl(url)` | Extracts txId and path suffix from `ar://` and `https://` Arweave URLs |
|
|
635
|
+
| `constructGatewayUrl(txId, gateway, pathSuffix?)` | Builds a gateway URL from components |
|
|
636
|
+
| `getAllGatewayUrls(txId, gateways?, pathSuffix?)` | Returns all gateway URLs for a txId in priority order |
|
|
637
|
+
| `DEFAULT_GATEWAYS` | The three default gateway configs |
|
|
638
|
+
| `KNOWN_GATEWAY_HOSTS` | Array of known gateway hostnames |
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## IPFS URL Utilities
|
|
643
|
+
|
|
644
|
+
The engine can detect and resolve IPFS URLs across multiple formats. NFT platforms like Sound.xyz, Catalog, and Spinamp frequently reference content (audio, images, artwork) via IPFS.
|
|
645
|
+
|
|
646
|
+
### Supported URL Formats
|
|
647
|
+
|
|
648
|
+
| Format | Example |
|
|
649
|
+
|--------|---------|
|
|
650
|
+
| Native scheme (double ipfs) | `ipfs://ipfs/QmXxx/path` |
|
|
651
|
+
| Native scheme | `ipfs://QmXxx/path` |
|
|
652
|
+
| Gateway URL | `https://ipfs.io/ipfs/QmXxx/path` |
|
|
653
|
+
| Subdomain | `https://QmXxx.ipfs.dweb.link/path` |
|
|
654
|
+
|
|
655
|
+
### Functions
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
import { isIPFS, extractIPFSPath, resolveIPFSLink } from 'playlist-data-engine';
|
|
659
|
+
|
|
660
|
+
// Detection
|
|
661
|
+
isIPFS('https://soundxyz.mypinata.cloud/ipfs/QmXxx/file.jpg'); // true
|
|
662
|
+
isIPFS('https://example.com/image.png'); // false
|
|
663
|
+
|
|
664
|
+
// Extract CID + path
|
|
665
|
+
extractIPFSPath('ipfs://QmXxx/file.jpg'); // 'QmXxx/file.jpg'
|
|
666
|
+
extractIPFSPath('https://QmXxx.ipfs.dweb.link/file.jpg'); // 'QmXxx/file.jpg'
|
|
667
|
+
|
|
668
|
+
// Resolve to a specific gateway (string param, backward compatible)
|
|
669
|
+
resolveIPFSLink('ipfs://QmXxx'); // 'https://ipfs.io/ipfs/QmXxx'
|
|
670
|
+
resolveIPFSLink('https://soundxyz.mypinata.cloud/ipfs/QmXxx', 'dweb.link'); // 'https://dweb.link/ipfs/QmXxx'
|
|
671
|
+
|
|
672
|
+
// Preserve the original gateway when it's a recognized IPFS host
|
|
673
|
+
resolveIPFSLink('https://soundxyz.mypinata.cloud/ipfs/QmXxx', { preserveOriginalGateway: true });
|
|
674
|
+
// => 'https://soundxyz.mypinata.cloud/ipfs/QmXxx'
|
|
675
|
+
|
|
676
|
+
// Force swap even with preserveOriginalGateway: false (same as default)
|
|
677
|
+
resolveIPFSLink('https://soundxyz.mypinata.cloud/ipfs/QmXxx', {
|
|
678
|
+
gatewayHost: 'dweb.link',
|
|
679
|
+
preserveOriginalGateway: false,
|
|
680
|
+
});
|
|
681
|
+
// => 'https://dweb.link/ipfs/QmXxx'
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Non-IPFS URLs pass through `resolveIPFSLink` unchanged.
|
|
685
|
+
|
|
686
|
+
### Known Gateway Hosts
|
|
687
|
+
|
|
688
|
+
The `isIPFS` and `extractIPFSPath` functions recognize these gateway hosts (exact match or subdomain):
|
|
689
|
+
|
|
690
|
+
`ipfs.io`, `cloudflare-ipfs.com`, `dweb.link`, `gateway.pinata.cloud`, `ipfs.infura.io`, `nftstorage.link`, plus platform-specific hosts (Sound.xyz, Catalog, Spinamp, Bonfire, Web3 Music Pipeline).
|
|
691
|
+
|
|
692
|
+
For the full list, see `KNOWN_IPFS_GATEWAY_HOSTS` in [src/utils/ipfsUtils.ts](../../src/utils/ipfsUtils.ts).
|
|
693
|
+
|
|
694
|
+
### Exports
|
|
695
|
+
|
|
696
|
+
| Export | Type | Description |
|
|
697
|
+
|--------|------|-------------|
|
|
698
|
+
| `isIPFS` | Function | Check if a URL is an IPFS URL (native scheme, gateway, or subdomain) |
|
|
699
|
+
| `extractIPFSPath` | Function | Extract CID + path from any recognized IPFS URL format |
|
|
700
|
+
| `resolveIPFSLink` | Function | Resolve IPFS URL to specified gateway; accepts string gateway host or `ResolveIPFSOptions` with `preserveOriginalGateway` |
|
|
701
|
+
| `ResolveIPFSOptions` | Interface | Options for `resolveIPFSLink`: `gatewayHost?` and `preserveOriginalGateway?` |
|
|
702
|
+
| `KNOWN_IPFS_GATEWAY_HOSTS` | `readonly string[]` | Recognized IPFS gateway hostnames |
|
|
703
|
+
| `DEFAULT_IPFS_GATEWAY` | `string` | Gateway host used by `resolveIPFSLink` when none specified |
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## Known Shortcomings & Future Improvements
|
|
708
|
+
|
|
709
|
+
These are issues and improvement opportunities identified in the current implementation. None are bugs — the system works correctly as-is — but each represents an area where the implementation could be improved in a future iteration.
|
|
710
|
+
|
|
711
|
+
### Hardcoded Health Filtering Thresholds
|
|
712
|
+
|
|
713
|
+
The health filtering thresholds `HEALTHY_FAILURE_THRESHOLD` (0.70) and `MIN_CHECKS_FOR_FILTER` (3) are magic numbers inside `tryFallbackGateways()` ([arweaveGatewayManager.ts:650-651](../../src/utils/arweaveGatewayManager.ts#L650-L651)). These are not exposed in the constructor config. Making them configurable would allow consumers to tune the trade-off between resilience and gateway pool size.
|
|
714
|
+
|
|
715
|
+
### No Circuit Breaker Pattern
|
|
716
|
+
|
|
717
|
+
There is no mechanism to fully disable a failing gateway for a cooldown period. The health-aware filtering helps avoid known-dead gateways, but it only activates after 3+ checks and doesn't implement a time-based cooldown. A proper circuit breaker (e.g., open for 60s after N failures) would prevent repeatedly hitting a gateway that's temporarily down.
|
|
718
|
+
|
|
719
|
+
### Tight Coupling to Browser APIs
|
|
720
|
+
|
|
721
|
+
`localStorage` is accessed directly in `persistGateway()` and `loadPersistedGateway()`. This prevents the manager from being used in non-browser environments (Node.js, workers) without modification. Dependency injection for the persistence layer would improve testability and portability.
|
|
722
|
+
|
|
723
|
+
### Unbounded Health Data Per Gateway
|
|
724
|
+
|
|
725
|
+
`MAX_HEALTH_RECORDS` caps at 50 records per gateway, but there is no bound on the number of unique gateways that can accumulate health data. If `reportFetchTiming()` is called for arbitrary hosts (e.g., Wayfinder-returned gateways), the `healthData` map grows without bound over a long session.
|