@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.
Files changed (34) hide show
  1. package/README.md +122 -0
  2. package/dist/{fetch.d.ts → fetch/base-fetch.d.ts} +1 -1
  3. package/dist/fetch/base-fetch.d.ts.map +1 -0
  4. package/dist/fetch/wayfinder-fetch.d.ts +45 -0
  5. package/dist/fetch/wayfinder-fetch.d.ts.map +1 -0
  6. package/dist/fetch/wayfinder-fetch.js +192 -0
  7. package/dist/retrieval/chunk.d.ts +36 -0
  8. package/dist/retrieval/chunk.d.ts.map +1 -0
  9. package/dist/retrieval/chunk.js +178 -0
  10. package/dist/retrieval/contiguous.d.ts +35 -0
  11. package/dist/retrieval/contiguous.d.ts.map +1 -0
  12. package/dist/retrieval/contiguous.js +41 -0
  13. package/dist/retrieval/index.d.ts +19 -0
  14. package/dist/retrieval/index.d.ts.map +1 -0
  15. package/dist/retrieval/index.js +18 -0
  16. package/dist/types.d.ts +19 -0
  17. package/dist/types.d.ts.map +1 -1
  18. package/dist/utils/verification-url.d.ts +39 -0
  19. package/dist/utils/verification-url.d.ts.map +1 -0
  20. package/dist/utils/verification-url.js +45 -0
  21. package/dist/verification/data-root-verification.d.ts.map +1 -1
  22. package/dist/verification/data-root-verification.js +2 -1
  23. package/dist/verification/hash-verification.d.ts.map +1 -1
  24. package/dist/verification/hash-verification.js +2 -3
  25. package/dist/verification/signature-verification.d.ts.map +1 -1
  26. package/dist/verification/signature-verification.js +7 -5
  27. package/dist/version.d.ts +1 -1
  28. package/dist/version.js +1 -1
  29. package/dist/wayfinder.d.ts +11 -22
  30. package/dist/wayfinder.d.ts.map +1 -1
  31. package/dist/wayfinder.js +49 -224
  32. package/package.json +1 -1
  33. package/dist/fetch.d.ts.map +0 -1
  34. /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.
@@ -20,4 +20,4 @@
20
20
  * @returns The native fetch function
21
21
  */
22
22
  export declare const createBaseFetch: () => typeof globalThis.fetch;
23
- //# sourceMappingURL=fetch.d.ts.map
23
+ //# sourceMappingURL=base-fetch.d.ts.map
@@ -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"}