@ar.io/wayfinder-core 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 +122 -0
- package/dist/{fetch.d.ts → fetch/base-fetch.d.ts} +1 -1
- package/dist/fetch/base-fetch.d.ts.map +1 -0
- package/dist/fetch/wayfinder-fetch.d.ts +45 -0
- package/dist/fetch/wayfinder-fetch.d.ts.map +1 -0
- package/dist/fetch/wayfinder-fetch.js +192 -0
- package/dist/retrieval/chunk.d.ts +36 -0
- package/dist/retrieval/chunk.d.ts.map +1 -0
- package/dist/retrieval/chunk.js +178 -0
- package/dist/retrieval/contiguous.d.ts +35 -0
- package/dist/retrieval/contiguous.d.ts.map +1 -0
- package/dist/retrieval/contiguous.js +41 -0
- package/dist/retrieval/index.d.ts +19 -0
- package/dist/retrieval/index.d.ts.map +1 -0
- package/dist/retrieval/index.js +18 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/verification-url.d.ts +39 -0
- package/dist/utils/verification-url.d.ts.map +1 -0
- package/dist/utils/verification-url.js +45 -0
- package/dist/verification/data-root-verification.d.ts.map +1 -1
- package/dist/verification/data-root-verification.js +2 -1
- package/dist/verification/hash-verification.d.ts.map +1 -1
- package/dist/verification/hash-verification.js +2 -3
- package/dist/verification/signature-verification.d.ts.map +1 -1
- package/dist/verification/signature-verification.js +7 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wayfinder.d.ts +11 -22
- package/dist/wayfinder.d.ts.map +1 -1
- package/dist/wayfinder.js +49 -224
- package/package.json +1 -1
- package/dist/fetch.d.ts.map +0 -1
- /package/dist/{fetch.js → fetch/base-fetch.js} +0 -0
package/README.md
CHANGED
|
@@ -345,6 +345,128 @@ const wayfinder = createWayfinderClient({
|
|
|
345
345
|
});
|
|
346
346
|
```
|
|
347
347
|
|
|
348
|
+
## Data Retrieval Strategies
|
|
349
|
+
|
|
350
|
+
Wayfinder supports multiple data retrieval strategies to fetch transaction data from AR.IO gateways. These strategies determine how data is requested and assembled from the underlying storage layer.
|
|
351
|
+
|
|
352
|
+
| Strategy | Default | Performance | Use Case | Requirements |
|
|
353
|
+
| --------------------------------- | ------- | ----------- | ------------------------------------------- | -------------------------------------- |
|
|
354
|
+
| `ContiguousDataRetrievalStrategy` | ✅ | High | Standard data fetching via direct GET | Gateway has the data available |
|
|
355
|
+
| `ChunkDataRetrievalStrategy` | ❌ | Medium | Chunk-based data assembly for large files | Gateway supports `/chunk/<offset>/data` endpoint and has requested transaction indexed as offsets are needed |
|
|
356
|
+
|
|
357
|
+
#### ContiguousDataRetrievalStrategy
|
|
358
|
+
|
|
359
|
+
The default strategy that fetches data using a direct GET request to the gateway. This is the most straightforward approach and works for most use cases.
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
import { Wayfinder, ContiguousDataRetrievalStrategy } from '@ar.io/wayfinder-core';
|
|
363
|
+
|
|
364
|
+
const wayfinder = new Wayfinder({
|
|
365
|
+
dataRetrievalStrategy: new ContiguousDataRetrievalStrategy(),
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### ChunkDataRetrievalStrategy
|
|
370
|
+
|
|
371
|
+
An advanced strategy that provides the easiest way to load chunks stored on Arweave nodes via the robust chunk API provided by AR.IO gateways. This approach is particularly useful for:
|
|
372
|
+
|
|
373
|
+
- **Direct chunk access**: Efficiently retrieves data directly from the underlying chunk storage layer
|
|
374
|
+
- **Bundled data items**: Seamlessly fetches data items from within ANS-104 bundles using calculated offsets
|
|
375
|
+
- **x402 payment compatibility**: Both strategies support custom fetch clients for payment-enabled requests
|
|
376
|
+
- **Large file handling**: More reliable for large transactions that may time out with direct requests
|
|
377
|
+
|
|
378
|
+
**Requirements:**
|
|
379
|
+
|
|
380
|
+
- Gateway must support the `/chunk/<offset>/data` endpoint (added in [r58](https://github.com/ar-io/ar-io-node/releases/tag/r58))
|
|
381
|
+
- Gateway must have the requested transaction indexed (offsets are needed to fetch directly from chunks)
|
|
382
|
+
|
|
383
|
+
```javascript
|
|
384
|
+
import { Wayfinder, ChunkDataRetrievalStrategy } from '@ar.io/wayfinder-core';
|
|
385
|
+
|
|
386
|
+
const wayfinder = new Wayfinder({
|
|
387
|
+
dataRetrievalStrategy: new ChunkDataRetrievalStrategy(),
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**How it works:**
|
|
392
|
+
|
|
393
|
+
1. Makes a HEAD request to get transaction metadata (root transaction ID, data offset, content length)
|
|
394
|
+
2. Queries `/tx/{root-tx-id}/offset` to get the root transaction's absolute offset in the weave
|
|
395
|
+
3. Calculates the absolute offset for the requested data item
|
|
396
|
+
4. Fetches data in chunks using `/chunk/<offset>/data` and assembles the complete data stream
|
|
397
|
+
5. Validates that chunks belong to the expected root transaction for security
|
|
398
|
+
|
|
399
|
+
**Sequence Diagram:**
|
|
400
|
+
|
|
401
|
+
```mermaid
|
|
402
|
+
sequenceDiagram
|
|
403
|
+
participant Client
|
|
404
|
+
participant Wayfinder
|
|
405
|
+
participant Gateway as AR.IO Gateway
|
|
406
|
+
participant Arweave as Arweave Nodes
|
|
407
|
+
|
|
408
|
+
Client->>Wayfinder: request('ar://data-item-id')
|
|
409
|
+
activate Wayfinder
|
|
410
|
+
|
|
411
|
+
Wayfinder->>Gateway: HEAD /tx/{data-item-id}
|
|
412
|
+
Note over Gateway: Lookup data item metadata<br/>from indexed bundles
|
|
413
|
+
Gateway-->>Wayfinder: Headers:<br/>- x-root-tx-id (bundle ID)<br/>- x-data-offset<br/>- content-length
|
|
414
|
+
|
|
415
|
+
Wayfinder->>Gateway: GET /tx/{root-tx-id}/offset
|
|
416
|
+
Gateway->>Arweave: GET /tx/{root-tx-id}/offset
|
|
417
|
+
Note over Arweave: Lookup transaction offset<br/>in the weave
|
|
418
|
+
Arweave-->>Gateway: Root transaction offset
|
|
419
|
+
Gateway-->>Wayfinder: Root transaction offset in weave
|
|
420
|
+
|
|
421
|
+
Note over Wayfinder: Calculate absolute offset:<br/>absolute = root_offset + data_offset
|
|
422
|
+
|
|
423
|
+
loop For each chunk needed
|
|
424
|
+
Wayfinder->>Gateway: GET /chunk/{absolute-offset}/data
|
|
425
|
+
|
|
426
|
+
Note over Gateway: Serve chunk data from<br/>indexed storage using<br/>root transaction ID
|
|
427
|
+
|
|
428
|
+
Gateway-->>Wayfinder: Chunk data + validation headers<br/>(x-root-tx-id for security)
|
|
429
|
+
|
|
430
|
+
Note over Wayfinder: Validate chunk belongs<br/>to expected root TX
|
|
431
|
+
|
|
432
|
+
Wayfinder-->>Client: Stream chunk data
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
Wayfinder-->>Client: Complete response
|
|
436
|
+
deactivate Wayfinder
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Example with createWayfinderClient:**
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
import { createWayfinderClient, ChunkDataRetrievalStrategy } from '@ar.io/wayfinder-core';
|
|
443
|
+
|
|
444
|
+
const wayfinder = createWayfinderClient({
|
|
445
|
+
dataRetrievalStrategy: new ChunkDataRetrievalStrategy(),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Fetch a data item from within an ANS-104 bundle
|
|
449
|
+
const response = await wayfinder.request('ar://data-item-id');
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### x402 Support
|
|
453
|
+
|
|
454
|
+
Both data retrieval strategies support custom fetch implementations, allowing you to use x402-enabled fetch clients for paid gateway requests.
|
|
455
|
+
|
|
456
|
+
```javascript
|
|
457
|
+
import { createWayfinderClient, ChunkDataRetrievalStrategy } from '@ar.io/wayfinder-core';
|
|
458
|
+
import { createX402Fetch } from '@ar.io/wayfinder-x402-fetch';
|
|
459
|
+
|
|
460
|
+
const x402Fetch = createX402Fetch({ /* payment config */ });
|
|
461
|
+
|
|
462
|
+
const wayfinder = createWayfinderClient({
|
|
463
|
+
fetch: x402Fetch,
|
|
464
|
+
dataRetrievalStrategy: new ChunkDataRetrievalStrategy({
|
|
465
|
+
fetch: x402Fetch,
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
348
470
|
## Verification Strategies
|
|
349
471
|
|
|
350
472
|
Wayfinder includes verification mechanisms to ensure the integrity of retrieved data. Verification strategies offer different trade-offs between complexity, performance, and security.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-fetch.d.ts","sourceRoot":"","sources":["../../src/fetch/base-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAO,OAAO,UAAU,CAAC,KAEpD,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type Tracer } from '@opentelemetry/api';
|
|
18
|
+
import { WayfinderEmitter } from '../emitter.js';
|
|
19
|
+
import type { DataRetrievalStrategy, Logger, RoutingStrategy, VerificationStrategy, WayfinderEvents, WayfinderRequestInit } from '../types.js';
|
|
20
|
+
/**
|
|
21
|
+
* Creates a wrapped fetch function that supports ar:// protocol
|
|
22
|
+
|
|
23
|
+
* @param logger - Optional logger for logging fetch operations
|
|
24
|
+
* @param strict - Whether to enforce strict verification
|
|
25
|
+
* @param fetch - Base fetch function to use for HTTP requests
|
|
26
|
+
* @param routingStrategy - Strategy for selecting gateways
|
|
27
|
+
* @param dataRetrievalStrategy - Strategy for retrieving data
|
|
28
|
+
* @param verificationStrategy - Strategy for verifying data integrity
|
|
29
|
+
* @param emitter - Optional event emitter for wayfinder events
|
|
30
|
+
* @param tracer - Optional OpenTelemetry tracer for tracing fetch operations
|
|
31
|
+
* @param events - Optional event handlers for wayfinder events
|
|
32
|
+
* @returns a wrapped fetch function that supports ar:// protocol and always returns Response
|
|
33
|
+
*/
|
|
34
|
+
export declare const createWayfinderFetch: ({ logger, strict, fetch, routingStrategy, dataRetrievalStrategy, verificationStrategy, emitter, tracer, events, }: {
|
|
35
|
+
logger?: Logger;
|
|
36
|
+
verificationStrategy?: VerificationStrategy;
|
|
37
|
+
strict?: boolean;
|
|
38
|
+
routingStrategy?: RoutingStrategy;
|
|
39
|
+
dataRetrievalStrategy?: DataRetrievalStrategy;
|
|
40
|
+
emitter?: WayfinderEmitter;
|
|
41
|
+
tracer?: Tracer;
|
|
42
|
+
fetch?: typeof globalThis.fetch;
|
|
43
|
+
events?: WayfinderEvents;
|
|
44
|
+
}) => ((input: URL | RequestInfo, init?: WayfinderRequestInit) => Promise<Response>);
|
|
45
|
+
//# sourceMappingURL=wayfinder-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wayfinder-fetch.d.ts","sourceRoot":"","sources":["../../src/fetch/wayfinder-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAkB,MAAM,oBAAoB,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAIjD,OAAO,KAAK,EACV,qBAAqB,EACrB,MAAM,EACN,eAAe,EACf,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACrB,MAAM,aAAa,CAAC;AASrB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAAI,mHAYlC;IACD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAC9C,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B,KAAG,CAAC,CACH,KAAK,EAAE,GAAG,GAAG,WAAW,EACxB,IAAI,CAAC,EAAE,oBAAoB,KACxB,OAAO,CAAC,QAAQ,CAAC,CA0LrB,CAAC"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { context, trace } from '@opentelemetry/api';
|
|
18
|
+
import { arioHeaderNames } from '../constants.js';
|
|
19
|
+
import { WayfinderEmitter } from '../emitter.js';
|
|
20
|
+
import { defaultLogger } from '../logger.js';
|
|
21
|
+
import { ContiguousDataRetrievalStrategy } from '../retrieval/contiguous.js';
|
|
22
|
+
import { RandomRoutingStrategy } from '../routing/random.js';
|
|
23
|
+
import { tapAndVerifyReadableStream } from '../utils/verify-stream.js';
|
|
24
|
+
import { constructGatewayUrl, createWayfinderRequestHeaders, extractRoutingInfo, } from '../wayfinder.js';
|
|
25
|
+
import { createBaseFetch } from './base-fetch.js';
|
|
26
|
+
/**
|
|
27
|
+
* Creates a wrapped fetch function that supports ar:// protocol
|
|
28
|
+
|
|
29
|
+
* @param logger - Optional logger for logging fetch operations
|
|
30
|
+
* @param strict - Whether to enforce strict verification
|
|
31
|
+
* @param fetch - Base fetch function to use for HTTP requests
|
|
32
|
+
* @param routingStrategy - Strategy for selecting gateways
|
|
33
|
+
* @param dataRetrievalStrategy - Strategy for retrieving data
|
|
34
|
+
* @param verificationStrategy - Strategy for verifying data integrity
|
|
35
|
+
* @param emitter - Optional event emitter for wayfinder events
|
|
36
|
+
* @param tracer - Optional OpenTelemetry tracer for tracing fetch operations
|
|
37
|
+
* @param events - Optional event handlers for wayfinder events
|
|
38
|
+
* @returns a wrapped fetch function that supports ar:// protocol and always returns Response
|
|
39
|
+
*/
|
|
40
|
+
export const createWayfinderFetch = ({ logger = defaultLogger, strict = false, fetch = createBaseFetch(), routingStrategy = new RandomRoutingStrategy(), dataRetrievalStrategy = new ContiguousDataRetrievalStrategy({
|
|
41
|
+
fetch,
|
|
42
|
+
}), verificationStrategy, emitter, tracer, events, }) => {
|
|
43
|
+
return async (input, init) => {
|
|
44
|
+
/**
|
|
45
|
+
* Summary:
|
|
46
|
+
*
|
|
47
|
+
* 1. Check if URL is ar:// - if not, call fetch directly
|
|
48
|
+
* 2. Extract routing info (subdomain, path, txId, arnsName)
|
|
49
|
+
* 3. Use routing strategy to select gateway
|
|
50
|
+
* 4. Construct gateway URL given the requested resource
|
|
51
|
+
* 5. If no txId or arnsName, perform direct fetch to gateway URL
|
|
52
|
+
* 6. If txId or arnsName present, use data retrieval strategy to fetch data
|
|
53
|
+
* 7. If verification strategy present, verify data stream
|
|
54
|
+
* 8. Return a Response object with the (optionally verified) data stream
|
|
55
|
+
*/
|
|
56
|
+
const requestUri = input instanceof URL ? input.toString() : input.toString();
|
|
57
|
+
if (!requestUri.startsWith('ar://')) {
|
|
58
|
+
logger?.debug('URL is not a wayfinder url, skipping routing', {
|
|
59
|
+
input,
|
|
60
|
+
});
|
|
61
|
+
emitter?.emit('routing-skipped', {
|
|
62
|
+
originalUrl: JSON.stringify(input),
|
|
63
|
+
});
|
|
64
|
+
return fetch(input, init);
|
|
65
|
+
}
|
|
66
|
+
const { subdomain, path, txId, arnsName } = extractRoutingInfo(requestUri);
|
|
67
|
+
// Create request-specific emitter
|
|
68
|
+
const requestEmitter = new WayfinderEmitter({
|
|
69
|
+
verification: {
|
|
70
|
+
...events,
|
|
71
|
+
...init?.verificationSettings?.events,
|
|
72
|
+
},
|
|
73
|
+
routing: {
|
|
74
|
+
...events,
|
|
75
|
+
...init?.routingSettings?.events,
|
|
76
|
+
},
|
|
77
|
+
parentEmitter: emitter,
|
|
78
|
+
});
|
|
79
|
+
// Create parent span for the entire fetch operation
|
|
80
|
+
const parentSpan = tracer?.startSpan('wayfinder.fetch');
|
|
81
|
+
// Create request span
|
|
82
|
+
const requestSpan = parentSpan
|
|
83
|
+
? tracer?.startSpan('wayfinder.fetch.wayfinderDataFetcher', undefined, trace.setSpan(context.active(), parentSpan))
|
|
84
|
+
: undefined;
|
|
85
|
+
// Add request attributes to span
|
|
86
|
+
requestSpan?.setAttribute('request.url', requestUri);
|
|
87
|
+
requestSpan?.setAttribute('request.method', 'GET');
|
|
88
|
+
// Emit routing started event
|
|
89
|
+
requestEmitter.emit('routing-started', {
|
|
90
|
+
originalUrl: requestUri,
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
logger.debug('Fetching data', {
|
|
94
|
+
uri: requestUri,
|
|
95
|
+
subdomain,
|
|
96
|
+
path,
|
|
97
|
+
});
|
|
98
|
+
// Select gateway using routing strategy
|
|
99
|
+
const selectedGateway = await routingStrategy.selectGateway({
|
|
100
|
+
path,
|
|
101
|
+
subdomain,
|
|
102
|
+
});
|
|
103
|
+
// it's just a non data specific request, construct the gateway URL and fetch directly
|
|
104
|
+
const redirectUrl = constructGatewayUrl({
|
|
105
|
+
selectedGateway,
|
|
106
|
+
subdomain,
|
|
107
|
+
path,
|
|
108
|
+
});
|
|
109
|
+
// Emit routing succeeded event
|
|
110
|
+
requestEmitter.emit('routing-succeeded', {
|
|
111
|
+
originalUrl: requestUri,
|
|
112
|
+
selectedGateway: selectedGateway.toString(),
|
|
113
|
+
redirectUrl: redirectUrl.toString(),
|
|
114
|
+
});
|
|
115
|
+
// if its a txId or arnsName use the dataRetrievalStrategy to fetch the data; otherwise just call internal fetch
|
|
116
|
+
if (!txId && !arnsName) {
|
|
117
|
+
logger.debug('No transaction ID or ARNS name found, performing direct fetch', {
|
|
118
|
+
uri: requestUri,
|
|
119
|
+
});
|
|
120
|
+
return fetch(redirectUrl.toString(), init);
|
|
121
|
+
}
|
|
122
|
+
const requestHeaders = {
|
|
123
|
+
...Object.fromEntries(new Headers(init?.headers || {})),
|
|
124
|
+
...createWayfinderRequestHeaders({
|
|
125
|
+
traceId: requestSpan?.spanContext().traceId,
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
// Use data retrieval strategy to fetch the actual data
|
|
129
|
+
const dataResponse = await dataRetrievalStrategy.getData({
|
|
130
|
+
gateway: selectedGateway,
|
|
131
|
+
requestUrl: redirectUrl,
|
|
132
|
+
headers: requestHeaders,
|
|
133
|
+
});
|
|
134
|
+
// If the response is not successful (e.g., 404, 500), return it directly
|
|
135
|
+
if (!dataResponse.ok) {
|
|
136
|
+
logger.debug('Gateway returned error response', {
|
|
137
|
+
uri: requestUri,
|
|
138
|
+
status: dataResponse.status,
|
|
139
|
+
statusText: dataResponse.statusText,
|
|
140
|
+
});
|
|
141
|
+
return dataResponse;
|
|
142
|
+
}
|
|
143
|
+
logger.debug('Successfully fetched data', {
|
|
144
|
+
uri: requestUri,
|
|
145
|
+
});
|
|
146
|
+
// Extract data ID from headers for verification
|
|
147
|
+
const resolvedDataId = dataResponse.headers.get(arioHeaderNames.dataId.toLowerCase()) || txId;
|
|
148
|
+
const contentLength = dataResponse.headers.has('content-length')
|
|
149
|
+
? parseInt(dataResponse.headers.get('content-length'), 10)
|
|
150
|
+
: 0;
|
|
151
|
+
const finalVerificationStrategy = init?.verificationSettings?.enabled
|
|
152
|
+
? init.verificationSettings.strategy
|
|
153
|
+
: verificationStrategy;
|
|
154
|
+
let finalStream = dataResponse.body;
|
|
155
|
+
// Apply verification if strategy is provided
|
|
156
|
+
if (resolvedDataId && dataResponse.body && finalVerificationStrategy) {
|
|
157
|
+
logger.debug('Applying verification to data stream', {
|
|
158
|
+
dataId: resolvedDataId,
|
|
159
|
+
});
|
|
160
|
+
// Determine strict mode - check init first, then fall back to instance settings
|
|
161
|
+
const isStrictMode = init?.verificationSettings?.strict ?? strict;
|
|
162
|
+
finalStream = tapAndVerifyReadableStream({
|
|
163
|
+
originalStream: dataResponse.body,
|
|
164
|
+
contentLength: contentLength,
|
|
165
|
+
verifyData: finalVerificationStrategy.verifyData.bind(finalVerificationStrategy),
|
|
166
|
+
txId: resolvedDataId,
|
|
167
|
+
headers: Object.fromEntries(dataResponse.headers),
|
|
168
|
+
emitter: requestEmitter,
|
|
169
|
+
strict: isStrictMode,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return new Response(finalStream, {
|
|
173
|
+
headers: dataResponse.headers,
|
|
174
|
+
status: dataResponse.status,
|
|
175
|
+
statusText: dataResponse.statusText,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
requestEmitter.emit('routing-failed', error);
|
|
180
|
+
logger.error('Failed to fetch data', {
|
|
181
|
+
error: error.message,
|
|
182
|
+
stack: error.stack,
|
|
183
|
+
uri: requestUri,
|
|
184
|
+
});
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
requestSpan?.end();
|
|
189
|
+
parentSpan?.end();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { DataRetrievalStrategy, Logger } from '../types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Chunk data retrieval strategy that fetches transaction data in chunks
|
|
20
|
+
* by first getting metadata from HEAD request, then streaming individual
|
|
21
|
+
* chunks from /chunk/{offset}/data endpoint.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ChunkDataRetrievalStrategy implements DataRetrievalStrategy {
|
|
24
|
+
private logger;
|
|
25
|
+
private fetch;
|
|
26
|
+
constructor({ logger, fetch, }?: {
|
|
27
|
+
logger?: Logger;
|
|
28
|
+
fetch?: typeof globalThis.fetch;
|
|
29
|
+
});
|
|
30
|
+
getData({ gateway, requestUrl, headers, }: {
|
|
31
|
+
gateway: URL;
|
|
32
|
+
requestUrl: URL;
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
}): Promise<Response>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=chunk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk.d.ts","sourceRoot":"","sources":["../../src/retrieval/chunk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEjE;;;;GAIG;AACH,qBAAa,0BAA2B,YAAW,qBAAqB;IACtE,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAA0B;gBAE3B,EACV,MAAsB,EACtB,KAAwB,GACzB,GAAE;QACD,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;KAC5B;IAKA,OAAO,CAAC,EACZ,OAAO,EACP,UAAU,EACV,OAAO,GACR,EAAE;QACD,OAAO,EAAE,GAAG,CAAC;QACb,UAAU,EAAE,GAAG,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAClC,GAAG,OAAO,CAAC,QAAQ,CAAC;CAwOtB"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { arioHeaderNames } from '../constants.js';
|
|
18
|
+
import { defaultLogger } from '../logger.js';
|
|
19
|
+
/**
|
|
20
|
+
* Chunk data retrieval strategy that fetches transaction data in chunks
|
|
21
|
+
* by first getting metadata from HEAD request, then streaming individual
|
|
22
|
+
* chunks from /chunk/{offset}/data endpoint.
|
|
23
|
+
*/
|
|
24
|
+
export class ChunkDataRetrievalStrategy {
|
|
25
|
+
logger;
|
|
26
|
+
fetch;
|
|
27
|
+
constructor({ logger = defaultLogger, fetch = globalThis.fetch, } = {}) {
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
this.fetch = fetch;
|
|
30
|
+
}
|
|
31
|
+
async getData({ gateway, requestUrl, headers, }) {
|
|
32
|
+
this.logger.debug('Fetching data via ChunkDataRetrievalStrategy from gateway', {
|
|
33
|
+
gateway: gateway.toString(),
|
|
34
|
+
requestUrl: requestUrl.toString(),
|
|
35
|
+
});
|
|
36
|
+
const headResponse = await this.fetch(requestUrl.toString(), {
|
|
37
|
+
method: 'HEAD',
|
|
38
|
+
headers,
|
|
39
|
+
});
|
|
40
|
+
if (!headResponse.ok) {
|
|
41
|
+
throw new Error(`HEAD request failed: ${headResponse.status}`);
|
|
42
|
+
}
|
|
43
|
+
const rootTransactionId = headResponse.headers.get(arioHeaderNames.rootTransactionId);
|
|
44
|
+
if (!rootTransactionId) {
|
|
45
|
+
this.logger.warn('Missing root transaction ID header, cannot use chunk API');
|
|
46
|
+
throw new Error('No root transaction ID header present - cannot use chunk API');
|
|
47
|
+
}
|
|
48
|
+
const relativeRootOffsetHeader = headResponse.headers.get(arioHeaderNames.rootDataOffset);
|
|
49
|
+
if (!relativeRootOffsetHeader) {
|
|
50
|
+
this.logger.warn('Missing root data offset header, cannot use chunk API');
|
|
51
|
+
throw new Error('No root data offset header present - cannot use chunk API');
|
|
52
|
+
}
|
|
53
|
+
const relativeRootOffset = parseInt(relativeRootOffsetHeader, 10);
|
|
54
|
+
// get the absolute offset of the root transaction id from the gateway via /offset path
|
|
55
|
+
const offsetForRootTransactionIdUrl = new URL(`/tx/${rootTransactionId}/offset`, gateway);
|
|
56
|
+
const offsetResponse = await this.fetch(offsetForRootTransactionIdUrl.toString(), {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
redirect: 'follow',
|
|
59
|
+
headers,
|
|
60
|
+
});
|
|
61
|
+
if (!offsetResponse.ok) {
|
|
62
|
+
throw new Error(`Failed to fetch offset for root transaction ID: ${offsetResponse.status}`);
|
|
63
|
+
}
|
|
64
|
+
const { offset: offsetForRootTransactionIdString, size: rootTransactionSizeString, } = (await offsetResponse.json());
|
|
65
|
+
const rootTransactionEndOffset = parseInt(offsetForRootTransactionIdString, 10);
|
|
66
|
+
const rootTransactionSize = parseInt(rootTransactionSizeString, 10);
|
|
67
|
+
// The /tx/{id}/offset endpoint returns the END offset of the transaction
|
|
68
|
+
// We need to calculate the START offset: endOffset - size + 1
|
|
69
|
+
const absoluteOffsetForRootTransaction = rootTransactionEndOffset - rootTransactionSize + 1;
|
|
70
|
+
const absoluteOffsetForDataItem = absoluteOffsetForRootTransaction + relativeRootOffset;
|
|
71
|
+
const contentLength = headResponse.headers.get('content-length');
|
|
72
|
+
if (!contentLength) {
|
|
73
|
+
throw new Error('Missing content-length header from HEAD response');
|
|
74
|
+
}
|
|
75
|
+
const totalSize = parseInt(contentLength, 10);
|
|
76
|
+
this.logger.debug('Successfully retrieved necessary offset information', {
|
|
77
|
+
rootTransactionId,
|
|
78
|
+
relativeRootOffset,
|
|
79
|
+
rootTransactionEndOffset,
|
|
80
|
+
rootTransactionSize,
|
|
81
|
+
absoluteOffsetForRootTransaction,
|
|
82
|
+
absoluteOffsetForDataItem,
|
|
83
|
+
totalSize,
|
|
84
|
+
});
|
|
85
|
+
// Store references for use inside the stream
|
|
86
|
+
const logger = this.logger;
|
|
87
|
+
const fetchFn = this.fetch;
|
|
88
|
+
const chunkGateway = gateway;
|
|
89
|
+
// Create a readable stream that fetches chunks on demand
|
|
90
|
+
const stream = new ReadableStream({
|
|
91
|
+
async start(controller) {
|
|
92
|
+
let currentOffset = absoluteOffsetForDataItem; // Start from where our data item actually is
|
|
93
|
+
let bytesRead = 0;
|
|
94
|
+
while (bytesRead < totalSize) {
|
|
95
|
+
try {
|
|
96
|
+
const chunkUrl = new URL(`/chunk/${currentOffset}/data`, chunkGateway);
|
|
97
|
+
logger.debug('Fetching chunk', {
|
|
98
|
+
url: chunkUrl.toString(),
|
|
99
|
+
currentOffset,
|
|
100
|
+
bytesRead,
|
|
101
|
+
totalSize,
|
|
102
|
+
});
|
|
103
|
+
const chunkResponse = await fetchFn(chunkUrl.toString(), {
|
|
104
|
+
method: 'GET',
|
|
105
|
+
redirect: 'follow',
|
|
106
|
+
headers,
|
|
107
|
+
});
|
|
108
|
+
if (!chunkResponse.ok) {
|
|
109
|
+
throw new Error(`Chunk request failed at offset ${currentOffset}: ${chunkResponse.status} ${chunkResponse.statusText}`);
|
|
110
|
+
}
|
|
111
|
+
// Get chunk metadata headers
|
|
112
|
+
const chunkReadOffsetHeader = chunkResponse.headers.get(arioHeaderNames.chunkReadOffset);
|
|
113
|
+
const chunkStartOffsetHeader = chunkResponse.headers.get(arioHeaderNames.chunkStartOffset);
|
|
114
|
+
const chunkTxId = chunkResponse.headers.get(arioHeaderNames.chunkTxId);
|
|
115
|
+
if (!chunkReadOffsetHeader) {
|
|
116
|
+
throw new Error('Missing chunk read offset header from chunk response');
|
|
117
|
+
}
|
|
118
|
+
// Assert that the chunk belongs to our root transaction
|
|
119
|
+
if (chunkTxId !== rootTransactionId) {
|
|
120
|
+
logger.error('Chunk belongs to wrong transaction', {
|
|
121
|
+
currentOffset,
|
|
122
|
+
expectedTxId: rootTransactionId,
|
|
123
|
+
actualTxId: chunkTxId,
|
|
124
|
+
chunkStartOffset: chunkStartOffsetHeader,
|
|
125
|
+
chunkReadOffset: chunkReadOffsetHeader,
|
|
126
|
+
});
|
|
127
|
+
throw new Error(`Chunk transaction ID mismatch at offset ${currentOffset}. Expected: ${rootTransactionId}, Got: ${chunkTxId}`);
|
|
128
|
+
}
|
|
129
|
+
logger.debug('Chunk belongs to correct root transaction', {
|
|
130
|
+
chunkTxId,
|
|
131
|
+
rootTransactionId,
|
|
132
|
+
offset: currentOffset,
|
|
133
|
+
});
|
|
134
|
+
const chunkData = await chunkResponse.arrayBuffer();
|
|
135
|
+
const fullChunkArray = new Uint8Array(chunkData);
|
|
136
|
+
const chunkReadOffset = parseInt(chunkReadOffsetHeader, 10);
|
|
137
|
+
// Extract data starting from chunk read offset
|
|
138
|
+
let dataToEnqueue = fullChunkArray.slice(chunkReadOffset);
|
|
139
|
+
// Limit data to only what we need (don't exceed totalSize)
|
|
140
|
+
const remainingBytes = totalSize - bytesRead;
|
|
141
|
+
if (dataToEnqueue.length > remainingBytes) {
|
|
142
|
+
dataToEnqueue = dataToEnqueue.slice(0, remainingBytes);
|
|
143
|
+
}
|
|
144
|
+
// Enqueue the extracted data
|
|
145
|
+
controller.enqueue(dataToEnqueue);
|
|
146
|
+
// Update counters
|
|
147
|
+
bytesRead += dataToEnqueue.length;
|
|
148
|
+
// Calculate next offset for multi-chunk files
|
|
149
|
+
const chunkStartOffset = parseInt(chunkStartOffsetHeader || currentOffset.toString(), 10);
|
|
150
|
+
currentOffset = chunkStartOffset + fullChunkArray.length;
|
|
151
|
+
// If we've read all the data, close the stream
|
|
152
|
+
if (bytesRead >= totalSize) {
|
|
153
|
+
logger.info('Successfully retrieved all data', {
|
|
154
|
+
totalBytesRead: bytesRead,
|
|
155
|
+
totalSize,
|
|
156
|
+
});
|
|
157
|
+
controller.close();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
controller.error(error);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const response = new Response(stream, {
|
|
169
|
+
status: 200,
|
|
170
|
+
headers: {
|
|
171
|
+
// all the original ario headers from the HEAD request
|
|
172
|
+
...Object.fromEntries(headResponse.headers),
|
|
173
|
+
'x-wayfinder-data-retrieval-strategy': 'chunk',
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WayFinder
|
|
3
|
+
* Copyright (C) 2022-2025 Permanent Data Solutions, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { DataRetrievalStrategy, Logger } from '../types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Contiguous data retrieval strategy that fetches the entire transaction
|
|
20
|
+
* data in a single streaming request
|
|
21
|
+
*/
|
|
22
|
+
export declare class ContiguousDataRetrievalStrategy implements DataRetrievalStrategy {
|
|
23
|
+
private logger;
|
|
24
|
+
private fetch;
|
|
25
|
+
constructor({ logger, fetch, }?: {
|
|
26
|
+
logger?: Logger;
|
|
27
|
+
fetch?: typeof globalThis.fetch;
|
|
28
|
+
});
|
|
29
|
+
getData({ requestUrl, headers, }: {
|
|
30
|
+
gateway: URL;
|
|
31
|
+
requestUrl: URL;
|
|
32
|
+
headers?: Record<string, string>;
|
|
33
|
+
}): Promise<Response>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=contiguous.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contiguous.d.ts","sourceRoot":"","sources":["../../src/retrieval/contiguous.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEjE;;;GAGG;AACH,qBAAa,+BAAgC,YAAW,qBAAqB;IAC3E,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAA0B;gBAE3B,EACV,MAAsB,EACtB,KAAwB,GACzB,GAAE;QACD,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;KAC5B;IAKA,OAAO,CAAC,EACZ,UAAU,EACV,OAAO,GACR,EAAE;QACD,OAAO,EAAE,GAAG,CAAC;QACb,UAAU,EAAE,GAAG,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAClC,GAAG,OAAO,CAAC,QAAQ,CAAC;CAatB"}
|