Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/developer-guide/concepts/javascript-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,30 @@ The preferred way to provide random-access data to loaders.gl is through `Readab

`ReadableFile` classes replace the deprecated `FileProvider` utilities; new code should use the `ReadableFile` wrappers exported from `@loaders.gl/loader-utils` (and `DataViewReadableFile` from `@loaders.gl/zip`) to keep loader interactions consistent across platforms.

### Validated HTTP ranges

`HttpFile.open()` pins the remote object's byte length and available `ETag`/`Last-Modified`
validators. Supplying identity from a trusted manifest avoids the opening one-byte probe:

```ts
import {HttpFile} from '@loaders.gl/loader-utils';

const file = await HttpFile.open('https://example.com/data.parquet', {
byteLength: manifest.byteLength,
etag: manifest.etag,
consistency: 'strict'
});

const bytes = await file.read(offset, length, abortController.signal);
console.log(file.getIdentitySnapshot(), file.getTelemetry());
```

Every read requires an exact `206` response and validates `Content-Range`, response length, and the
pinned object identity before returning bytes. `strict` consistency requires validators to remain
visible; the default `best-effort` mode still rejects changed validators but permits servers whose
CORS policy does not expose them. A shared `RangeRequestScheduler` can coalesce nearby reads while
keeping different authentication and validator contexts isolated.

## Saving data

Saving data from a browser is either done by POST requests to a server, or via local downloads.
Expand Down
1 change: 1 addition & 0 deletions docs/docs-sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
"modules/loader-utils/README",
"modules/loader-utils/api-reference/data-source-manager",
"modules/loader-utils/api-reference/readable-file",
"modules/loader-utils/api-reference/http-file",
"modules/loader-utils/api-reference/request-scheduler",
"modules/loader-utils/api-reference/range-request-scheduler",
"modules/loader-utils/api-reference/parse-with-context"
Expand Down
7 changes: 7 additions & 0 deletions docs/modules/loader-utils/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Overview

The `@loaders.gl/loader-utils` contains utilities for creating loaders.

## API reference

- [`ReadableFile`](/docs/modules/loader-utils/api-reference/readable-file) provides the common random-access file contract.
- [`HttpFile`](/docs/modules/loader-utils/api-reference/http-file) validates random-access HTTP reads and remote object identity.
- [`RequestScheduler`](/docs/modules/loader-utils/api-reference/request-scheduler) limits asynchronous request concurrency.
- [`RangeRequestScheduler`](/docs/modules/loader-utils/api-reference/range-request-scheduler) coalesces compatible byte ranges.
163 changes: 163 additions & 0 deletions docs/modules/loader-utils/api-reference/http-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# HttpFile

<p class="badges">
<img src="https://img.shields.io/badge/From-v5.0-blue.svg?style=flat-square" alt="From-v5.0" />
<img src="https://img.shields.io/badge/experimental-yellow.svg?style=flat-square" alt="experimental" />
</p>

`HttpFile` provides validated random access to a remote object in browsers and Node.js. It sends
exact HTTP range requests, pins the object's length and available validators, and rejects responses
that no longer describe the same object.

```typescript
import {HttpFile} from '@loaders.gl/loader-utils';

const file = await HttpFile.open('https://example.com/data.parquet', {
consistency: 'strict'
});

const footer = await file.read(file.size - 8, 8);
console.log(new Uint8Array(footer), file.getTelemetry());
await file.close();
```

The server must support byte ranges. A normal request must return `206 Partial Content` with an
exact `Content-Range`; `HttpFile` deliberately rejects a `200 OK` full-object fallback. A valid
zero-byte object may respond to the opening probe with `416 Range Not Satisfiable` and
`Content-Range: bytes */0`.

## Opening a file

### `HttpFile.open(url, options?, signal?): Promise<HttpFile>`

Creates a file and immediately pins its identity. Unless a complete identity is supplied, opening
sends `Range: bytes=0-0` to discover the object length, `ETag`, and `Last-Modified` value.

Supplying a known length together with either validator avoids that opening request:

```typescript
const file = await HttpFile.open(url, {
byteLength: manifest.byteLength,
etag: manifest.etag,
consistency: 'strict'
});
```

### `new HttpFile(url, options?)`

Creates a lazy file. The first call to `open()`, `stat()`, or `read()` discovers and pins the
identity. Concurrent callers share one discovery request, while each caller can independently
cancel its own wait.

Prefer the static `HttpFile.open()` form when code needs to use `size` synchronously.

## Options

| Option | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `fetch` | `(url, init?) => Promise<Response>` | `globalThis.fetch` | Custom fetch implementation. |
| `fetchOptions` | `RequestInit` | none | Headers, credentials, and other options copied to every request. `HttpFile` supplies the `GET` method, `Range` header, and combined signal. |
| `byteLength` | `number` | discovered | Trusted non-negative object length. |
| `etag` | `string` | discovered | Trusted object ETag. |
| `lastModified` | `string` | discovered | Trusted Last-Modified value. |
| `consistency` | `'best-effort' \| 'strict'` | `'best-effort'` | Controls how missing validators are handled. Changed visible validators are always rejected. |
| `rangeScheduler` | `RangeRequestScheduler` | private scheduler | Shared scheduler used to coalesce compatible reads. Each `HttpFile` remains an isolated request context. |
| `rangeSchedulerProps` | `RangeRequestSchedulerProps` | `{batchDelayMs: 0}` | Configuration for the private scheduler. Ignored when `rangeScheduler` is supplied. |

`fetchOptions.headers` are preserved except for `Range`, which is set for each read. A signal in
`fetchOptions` applies to every request; a signal passed to `open()` or `read()` applies only to that
operation.

## Consistency modes

### `best-effort`

Rejects changed validators whenever the server exposes them. It permits a response that omits a
previously visible validator, which is useful when a server or CORS policy does not expose headers
consistently. If an ETag disappears but both responses expose `Last-Modified`, that fallback must
still match.

### `strict`

Requires every response to expose the pinned validator. If no validator was supplied, the opening
response must expose either `ETag` or `Last-Modified`.

For cross-origin URLs, expose `Content-Range`, `ETag`, and `Last-Modified` through the server's CORS
configuration when strict validation is required.

## Properties

### `size: number`

Pinned object length. A lazy file reports a supplied `byteLength`, or zero until identity discovery
has completed.

### `bigsize: bigint`

The same object length represented as a bigint.

### `url: string`

Remote object URL. `handle` contains the same value for the `ReadableFile` interface.

## Methods

### `open(signal?): Promise<this>`

Pins the identity of a lazily constructed file. Repeated calls reuse the cached identity.

### `read(offset?, length?, signal?): Promise<ArrayBuffer>`

Reads exactly `length` bytes starting at `offset`. Both values must be non-negative safe integers,
and the range must not extend past `size`. A zero-length read returns an empty buffer without an
HTTP request after identity is known.

Each non-empty response is accepted only when all of the following are true:

- the status is `206`;
- `Content-Range` exactly matches the requested offsets and pinned object length;
- visible validators are consistent with the pinned identity; and
- the consumed response body contains exactly the requested number of bytes.

### `stat(): Promise<Stat>`

Returns the pinned `size`, `bigsize`, and `isDirectory: false`. It discovers identity first when the
file is lazy.

### `getIdentitySnapshot(): HttpFileIdentity | null`

Returns the frozen pinned `{byteLength, etag, lastModified}` object, or `null` before a lazy file has
opened.

### `getTelemetry(): HttpFileTelemetry`

Returns a frozen point-in-time snapshot:

```typescript
type HttpFileTelemetry = {
requestedBytes: number;
downloadedBytes: number;
requestCount: number;
networkTimeMs: number;
abortCount: number;
errorCount: number;
};
```

The counters belong to this `HttpFile`, even when its scheduler is shared with other files.

### `fetchRange(offset, length, signal?): Promise<Response>`

Compatibility method for `ReadableFile` consumers that expect a `Response`. New code should prefer
`read()`.

### `close(): Promise<void>`

Prevents new operations. Active requests remain controlled by their per-operation or persistent
abort signals.

## Sharing a range scheduler

Several files may share a [`RangeRequestScheduler`](./range-request-scheduler) to centralize queue
configuration and stats. `HttpFile` assigns every instance a private isolation key, so requests
with different credentials, validators, or fetch implementations are never coalesced together.
20 changes: 20 additions & 0 deletions docs/modules/loader-utils/api-reference/range-request-scheduler.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ const scheduler = new RangeRequestScheduler({
stats
});

// Reuse one key only for requests with the same URL, credentials, and fetch behavior.
const transportContext = {};

const arrayBuffer = await scheduler.fetch({
url,
offset: 1_000_000,
length: 4096,
isolationKey: transportContext,
fetchOptions: {
headers: {Authorization: 'Bearer token'}
}
Expand Down Expand Up @@ -56,13 +60,28 @@ creates the `Range` header, preserves caller headers from `fetchOptions`, aborts
`200 OK` full-object responses, handles `416` size probes for offset `0`, and records
transport diagnostics in `stats`.

HTTP fetch calls are isolated by default because separate calls may use different credentials,
headers, or fetch implementations. To coalesce compatible calls, pass the same stable
`isolationKey` object to each call. Keys are compared by identity (`===`), so creating a new object
for every request does not enable coalescing. Never reuse a key across different authentication or
validator contexts.

### `scheduleRequest(request): Promise<ArrayBuffer>`

Enqueues one exact range using a caller-supplied transport callback. The returned promise
resolves to the exact requested byte slice, not the merged transport response.

`request.fetchRange` must return the bytes for the offset and length it receives. Those may be
larger than the original request when several child requests are merged.
If a server legitimately clamps the final range at end of file, return a transport result with
`arrayBuffer` and the authoritative `sourceByteLength`. The scheduler accepts a short response only
when its end offset exactly matches that declared length; unmarked and mismatched short responses
are rejected.

`scheduleRequest()` coalesces requests with the same `sourceId` by default. Pass distinct
`isolationKey` values when one source identifier can refer to different transport, credential, or
validator contexts. Conversely, pass the same stable key to state explicitly that those contexts
are compatible.

Use `scheduleRequest` for non-HTTP transports or sources that need custom response handling.

Expand Down Expand Up @@ -94,6 +113,7 @@ type RangeStats = {
requestedBytes: number;
transportBytes: number;
responseBytes: number;
networkTimeMs: number;
overfetchBytes: number;
failedTransportRanges: number;
abortedLogicalRanges: number;
Expand Down
15 changes: 9 additions & 6 deletions docs/modules/loader-utils/api-reference/readable-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

## Available classes

- `HttpFile` (browser & Node.js) – wraps a URL and downloads byte ranges with HTTP range requests when supported.
- [`HttpFile`](./http-file) (browser & Node.js) – validates HTTP byte-range reads and pins remote object identity.
- `BlobFile` (browser & Node.js) – provides random access reads on `Blob` or `File` instances via the standard slicing APIs.
- `NodeFile` (Node.js) – exposes random access reads backed by the local file system without importing `fs` directly in application code.
- `DataViewReadableFile` (browser & Node.js) – adapts an in-memory `ArrayBuffer`/`DataView` into the `ReadableFile` interface for archive parsing or other buffer-first workflows.

All implementations satisfy the `ReadableFile` interface exported from `@loaders.gl/loader-utils` and support `slice`/`read` helpers for incremental processing of large files.
All implementations satisfy the `ReadableFile` interface exported from `@loaders.gl/loader-utils` and support exact `read` operations for incremental processing of large files.

:::info
Legacy `FileProvider` classes have been removed from the default `@loaders.gl/loader-utils` exports. Use the `ReadableFile` implementations above instead.
Expand All @@ -22,8 +22,8 @@ Legacy `FileProvider` classes have been removed from the default `@loaders.gl/lo
```typescript
import {HttpFile} from '@loaders.gl/loader-utils';

const file = new HttpFile('https://example.com/archive.3tz');
const header = await file.slice(0, 1024).arrayBuffer();
const file = await HttpFile.open('https://example.com/archive.3tz');
const header = await file.read(0, 1024);
```

### Reading browser `File` drops
Expand All @@ -33,7 +33,7 @@ import {BlobFile} from '@loaders.gl/loader-utils';

async function inspectUpload(fileInput: File) {
const blobFile = new BlobFile(fileInput);
const signature = await blobFile.slice(0, 8).arrayBuffer();
const signature = await blobFile.read(0, 8);
return new Uint8Array(signature);
}
```
Expand All @@ -44,7 +44,9 @@ async function inspectUpload(fileInput: File) {
import {NodeFile} from '@loaders.gl/loader-utils';

const nodeFile = new NodeFile('/data/tileset.slpk');
const footerBytes = await nodeFile.slice(-4096).arrayBuffer();
const {size} = await nodeFile.stat();
const footerLength = Math.min(size, 4096);
const footerBytes = await nodeFile.read(size - footerLength, footerLength);
```

### Adapting an `ArrayBuffer`
Expand All @@ -54,6 +56,7 @@ import {DataViewReadableFile} from '@loaders.gl/zip';

const archiveBuffer = await fetch(url).then((response) => response.arrayBuffer());
const archiveFile = new DataViewReadableFile(new DataView(archiveBuffer));
const header = await archiveFile.read(0, 8);
```

These adapters can be passed anywhere a loader expects a `ReadableFile`, ensuring consistent random access across browser and Node.js environments.
18 changes: 17 additions & 1 deletion docs/modules/parquet/api-reference/parquet-source-loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,19 @@ await Array.fromAsync(source.read());
console.log(source.getTelemetry());
```

The snapshot reports exact transport counts and bytes, range-cache hits, cumulative
The frozen snapshot reports exact transport counts and bytes, range-cache hits, cumulative
network/decode/Arrow durations, candidate/pruned/decoded row groups, emitted batches and rows,
retries, cancellations, and failures. `retryCount` remains zero while the source uses its fail-fast
range policy.

### `capabilities: ParquetSourceCapabilities`

The source exposes the frozen `PARQUET_SOURCE_CAPABILITIES` descriptor synchronously, before any
network or decoding work starts. It reports support for cached immutable metadata, row-group and
column selection, provenance, cancellation, custom range transport, object-version validation,
statistics, transport/decode telemetry, and package-local WASM delivery. Source worker decoding is
the remaining deferred capability.

### `close(): Promise<void>`

Aborts active requests, closes the range-backed file, and permanently closes the source. Calling
Expand Down Expand Up @@ -177,6 +185,14 @@ individual read.
| `rangeRequests.stats` | `Stats` | scheduler default | probe.gl range-request counters. |
| `rangeRequests.onEvent` | `(event) => void` | `undefined` | Range scheduling diagnostic callback. |

## Package-local WASM

`@loaders.gl/parquet/wasm` exports `PARQUET_WASM_URL`, a bundler-resolvable URL for the packaged
`parquet_wasm_bg.wasm` asset. The raw file is also exported as
`@loaders.gl/parquet/parquet_wasm_bg.wasm` for explicit copy or self-hosting workflows. The current
`ParquetSource` uses the TypeScript range decoder and does not initialize WASM; these entry points
serve the package's WASM loader and writer paths.

## Current limitations

- Decoding runs on the caller thread; worker-backed decoding and transferable Arrow buffers are not
Expand Down
3 changes: 3 additions & 0 deletions docs/whats-new.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,11 @@ Release Date: 2026

- [`ParquetSourceLoader`](/docs/modules/parquet/api-reference/parquet-source-loader) NEW - adds reusable range-backed Parquet metadata and selective row-group/column reads as cancellable Arrow batches with source provenance and object-version validation. Its lightweight root export dynamically preloads the runtime implementation.
- `ParquetSourceLoader` exposes normalized column-chunk statistics, predicate-based row-group pruning, and cumulative transport/decode/Arrow telemetry with exact request and byte counts.
- `ParquetSourceLoader` now publishes an immutable capability descriptor, deep-freezes cached schema metadata and batch provenance, and preserves caller abort reasons.
- `ParquetSourceLoader` now materializes selected columns directly into Arrow batches without an intermediate object-row table.
- The wasm-backed `ParquetLoader` can decode in a cancellable worker, transfers Arrow output through Arrow IPC, and resolves its packaged worker and WASM assets without an implicit CDN dependency.
- The Parquet WASM backend is updated to 0.7.2, and `@loaders.gl/parquet/wasm` exposes its package-local binary URL for explicit asset workflows.
- `HttpFile` now supports pinned object identity, strict or best-effort validator consistency, cancellable exact range reads, shared scheduling, and immutable request/byte/time telemetry.
- The Parquet TypeScript backend now reads Data Page V2, `DELTA_BINARY_PACKED`, `DELTA_LENGTH_BYTE_ARRAY`, `DELTA_BYTE_ARRAY`, and legacy Hadoop-framed LZ4 data.
- [`ParquetJSLoader`](/docs/modules/parquet/api-reference/parquet-js-loader) and [`ParquetJSWriter`](/docs/modules/parquet/api-reference/parquet-js-writer) NEW - add the experimental parquetjs plain-row and plain-table APIs.
- `parquet.shape` on [`ParquetLoader`](/docs/modules/parquet/api-reference/parquet-loader) is now documented for selecting object-row or Arrow output from the canonical wasm-backed loader.
Expand Down
Loading
Loading