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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,14 @@ page-owned authenticated reader; it does not connect to Autonomi nodes itself.
The page and client must therefore remain open while the media URL is in use.
Multiple clients can serve independent media sources on the same page.

A media source's reader streams: from wherever playback starts or seeks to, the
shared core fetches up to four records of the next 32 MiB in parallel. Playback
therefore does not wait for each record's discovery. It also fetches the file's
last record when playback starts, because MP4 and WebM usually keep their index
there. It keeps at most about 24 of the file's records cached, releasing those
behind playback. Readers from `openFile()` read ahead only once a read
continues a previous read, so a single header read does not fetch the window.

If the application already has a root-scoped service worker, merge the
`autonomi-file-range` fetch and message-handling logic from the packaged worker
into it and pass that worker's URL:
Expand Down
108 changes: 108 additions & 0 deletions docs/audits/2026-10-01-ant-core-0.11-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# ant-core 0.11.0 sync and streaming media readers — 2026-10-01

Validates SDK 0.1.1: the bundled WASM rebuilt from ant-client main after
[ant-client#209](https://github.com/WithAutonomi/ant-client/pull/209) (read-ahead
for sequential and streaming range reads) and #211 (six peers per browser lookup
round) merged, with media sources opening streaming readers.

## Changes

- `npm run sync:wasm` rebuilt `src/wasm` from a clean checkout of ant-client main.
The bundled core gains read-ahead, the wider lookup round, reads from holders
discovered after the early allowance, and pointer bindings. The SDK does not
expose pointers.
- ant-client#209 merged with `openPublicFile`/`openPrivateFile` taking an options
object, `{ streaming }`, deserialized as `BrowserFileReaderOptions`. The SDK
passed a bare boolean, written against an earlier draft. Against the new
core that is rejected before any request, so every `openFile()` and
`createMediaSource()` call would have failed:

```text
invalid type: boolean `true`, expected struct BrowserFileReaderOptions
```

The SDK now passes `{ streaming: false }` from `openFile()` and
`{ streaming: true }` from `createMediaSource()`. A new boundary test runs both
shapes against the packaged WASM. The mocked client tests could not catch the
mismatch.

## Provenance

- SDK: `e724655` (branch `fix/media-streaming-read-ahead`), version 0.1.1.
- Production WASM source: ant-client `db85c72515f9c1061ef78c9663e79621e2816170`
(main, ant-core 0.11.0), clean checkout, as recorded in `src/wasm/source.json`.
- WASM SHA-256: `8fa35e58927fd3f5190ec43b1989313de3cc8968cac1bfa373247d4b3842fcd1`.
- Cargo.lock SHA-256: `51412fd67f073a306f12b766d6c5a35bc8a32c7a32f339b3b04ed987cbefab9c`.
- Devnet node: ant-node `c092f225877b3058eccbc47e2082fc44750ed2b5`, the
`ant-core/browser-tests/node-revision` pin at that ant-client revision, clean
clone, `cargo build --locked --bin ant-devnet`.
- Baseline: the published `@withautonomi/ant-browser-sdk@0.1.0` (WASM ant-client
`a04b9fc`, SHA-256 `7685faa4…`).
- Tools: Node v22.23.1, Rust 1.96.1, wasm-pack 0.15.0, Anvil 1.7.1, Playwright
1.62.1 (headless Chromium).

## Validation

- `npm run check`: build, types and **188 tests passed**, including the new
reader-options boundary test.
- `npm run verify:wasm`: ant-client `db85c72` is on main and the WASM matches its
recorded hash. `npm run build:examples`: all five built. `npm pack --dry-run`:
89 files, 2.2 MB. ADR governance passed.
- Real Chromium, production WASM, seven isolated WebRTC nodes and local Anvil,
on a fresh devnet, with the retained probe adapted from the
[bootstrap review audit](2026-09-25-bootstrap-review-fixes.md):
- Core paid recovery: one payment, an injected post-payment interruption,
recovery without repayment, 4 replicas, **12,800 matching bytes**.
- SDK all-in-one demo, small public file: uploaded and downloaded **13,056
matching bytes**.
- Demo, large public file: uploaded **16,789,561 bytes** (9 records, one
payment transaction) and downloaded matching bytes. Its media stream answered
five byte ranges with 206 and matching bytes. The ranges covered the start,
the middle, the last 64 KiB, a 3 MiB span and the last 1,000 bytes.
- Demo, private file: uploaded **12,583,689 bytes**, saved the 323-byte DataMap,
loaded it back, downloaded matching bytes, and streamed the same five ranges.
- SDK API on both files: `openFile()` reads at the start, middle, end, past the
end and across records, a whole-file sequential read in 1 MiB ranges,
`stream()`, and `createMediaSource()` byte ranges all matched.
- No page or console errors.
- Mainnet, Chromium, bundled seeds. File:
`134e4537ad1b2e29f0dc48f8e025a560989e91055ebf1c66bca2208ca8bba889` (611,984,394
bytes, a 16-minute video). The same probe ran against 0.1.0 and the candidate:

| | 0.1.0 | 0.1.1 candidate |
| --- | ---: | ---: |
| `openFile()`: 64 KiB at start / middle / end | 8.0 / 60.6 / 1.4 s | 14.1 / 9.6 / 7.9 s |
| Media: first frame | 55.5 s | 7.3 s |
| Media: position after 60 s of playback | froze at 8.1 s | 57.2 s, one 2.8 s stall |
| Seek to 8:00, then 45 s | 6.2 s seek, ~15 s stall | 16.7 s seek, no stall |

The startup probe (`sdk-startup.mjs`, three content chunks, two alternating
runs each) connected in 2.1–2.4 s for both builds. It reached chunk 3/147
after 12.3 and 20.1 s for 0.1.0, and after 14.5 and 18.3 s for the candidate.

Commands, probes, logs and SHA-256 checksums are in
[the evidence directory](2026-10-01-ant-core-0.11-sync/).

## Findings outside this change

- On the seven-node devnet, a second large upload in the same page fails before
payment with "insufficient peers: Witnessed close group lookup failed before
payment … need 7". The published 0.1.0 fails the same way: 0/4 for both
builds, and 0/3 for both with a five-second pause. A first large upload in a
fresh session succeeded 3/3 with the candidate and 2/3 with 0.1.0. A
likely, unconfirmed cause is ant-node's per-IP WebRTC limits: every lookup on
a seven-node network needs all seven nodes, and every session comes from one
address. It needs its own investigation.
- When the probe closed the 0.1.0 media source and client at the end, 0.1.0
threw `attempted to take ownership of Rust value while it was borrowed`. The
candidate closed cleanly in its run. This single observation does not show
that the core fixed the problem.

## Limits

Mainnet figures are single live samples; they show behaviour, not a controlled
benchmark. The devnet checks verify range-read and stream bytes, but not
read-ahead's network-level behaviour. Media on the devnet was exercised through
byte ranges, not playback, because the test files are random bytes. Only
Chromium ran the devnet and mainnet probes. The pointer bindings in the new core
were not exercised, because the SDK does not expose them.
62 changes: 62 additions & 0 deletions docs/audits/2026-10-01-ant-core-0.11-sync/checksums.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"devnet-ab-large-upload.mjs.gz": {
"uncompressedSha256": "579d4a26fe6f862ebe8acfa4ab78384d1c8e82edfaa53e4f9b3acba60221eb8f"
},
"devnet-ab1.log.gz": {
"uncompressedSha256": "ddfbdb1cdd1ee47897b0164d8162d17d8e499c0ffe3e02cb4a2a7b3a7050269f"
},
"devnet-ab2warm.log.gz": {
"uncompressedSha256": "603f810924323955bf6b418b1878b86e683e9faffb53014c206ac844c5b9bbb5"
},
"devnet-ab3warm5s.log.gz": {
"uncompressedSha256": "0b4c6971ecd5b0f94ccfc799e0c40b9a4bcaca912c69f13aefd5293857346556"
},
"devnet-ant-node-build.log.gz": {
"uncompressedSha256": "6b22f9a5c00bff76113d2a86c243e7ff80ae063d1fa361b44132217296784534"
},
"devnet-browser-check.mjs.gz": {
"uncompressedSha256": "3ca2a7429a339d4b48a8dd59fa7e56ef330a793748df7d5782f498e1d98f6491"
},
"devnet-browser-check.run2-large-upload-insufficient-peers.log.gz": {
"uncompressedSha256": "45f9318ff5a4aef44927d33fffdf17000ac6965c89ac5e940ae7a01921efec50"
},
"devnet-browser-check.run3-passed.log.gz": {
"uncompressedSha256": "55719bc0b477d7e979a7ee0bcec108270e90c2abc6d1d520da9219268cbd1e9b"
},
"devnet-commands.txt.gz": {
"uncompressedSha256": "2454abdfc64ad6b4a070196442903e78cf866ad73cda41c6ab4f646ac7c2a928"
},
"devnet-revisions.txt.gz": {
"uncompressedSha256": "20b3cd034c6c466a90fabb2d0147ed218287193e3f04d417ebc256d97a929700"
},
"devnet-summary.run3.json.gz": {
"uncompressedSha256": "200b6c8f62b9f78ca86b539d9c870f0b05783ae6f02d915908b066843dbee86b"
},
"mainnet-media-baseline.jsonl.gz": {
"uncompressedSha256": "b27790247a35e1c8c878108c8e244451c8cece6b672bf00c268254729bbe94a8"
},
"mainnet-media-candidate.jsonl.gz": {
"uncompressedSha256": "d0eb490668b08ea9eaea6c7783232676229cfe74aa3a87edf70bc7212d5a97b1"
},
"mainnet-media-probe.mjs.gz": {
"uncompressedSha256": "50a0c58e1862074743141f24ecd364f5c1a2367a1d08b4f29bd43cf7d712b255"
},
"mainnet-startup.jsonl.gz": {
"uncompressedSha256": "c8abd4647ef6ca41a6f46ebe8e28fa6e25139565244d52afd1460d1b4b9a9531"
},
"sdk-adr.log.gz": {
"uncompressedSha256": "33a5a770b31525815ada5577561626a3de5b1833fd6faaff852356cdf2500a34"
},
"sdk-check.log.gz": {
"uncompressedSha256": "7b99838f9408501622df1687630bbcb90f00a1e1c3c76f54a1d03cb11c173b77"
},
"sdk-examples.log.gz": {
"uncompressedSha256": "e2cd663d71343d09074af47fad9f7381012926f17e0196ad28b523010b5332f8"
},
"sdk-pack.log.gz": {
"uncompressedSha256": "7c4ecd36d7e3f4d0256855757072b5cb679a84ad90debfeff19ad88979fb20fa"
},
"sdk-verify-wasm.log.gz": {
"uncompressedSha256": "428e81fbf6311ccf89714ce6fe742a2c48a5ece165d05968ebb0cc6a04fbde3f"
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@withautonomi/ant-browser-sdk",
"version": "0.1.0",
"version": "0.1.1",
"description": "Build browser applications that connect directly to the Autonomi Network over WebRTC.",
"type": "module",
"license": "MIT OR Apache-2.0",
Expand Down
20 changes: 15 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,9 +679,18 @@ export class AutonomiClient {
}

/** Open a bounded random-access reader without reconstructing the whole file. */
async openFile(
openFile(
file: string | PublicFile | PrivateFileReference,
options: OperationOptions = {},
): Promise<PublicFileReader> {
return this.#openReader(file, options, false);
}

/** A streaming reader treats every read as sequential and fetches ahead of it. */
async #openReader(
file: string | PublicFile | PrivateFileReference,
options: OperationOptions,
streaming: boolean,
): Promise<PublicFileReader> {
const operation = this.#startOperation(options);
const report = this.#reporter("open-file", options.onProgress, operation);
Expand All @@ -691,8 +700,8 @@ export class AutonomiClient {
throwIfAborted(operation.signal);
raw = await abortable(
isPrivateFile(file)
? this.#network.openPrivateFile(corePrivateFile(file), report)
: this.#network.openPublicFile(typeof file === "string" ? file : coreFileReference(file), report),
? this.#network.openPrivateFile(corePrivateFile(file), report, { streaming })
: this.#network.openPublicFile(typeof file === "string" ? file : coreFileReference(file), report, { streaming }),
operation.signal,
undefined,
closeReader,
Expand Down Expand Up @@ -730,11 +739,12 @@ export class AutonomiClient {
try {
this.#assertOpen();
report("Opening an Autonomi random-access media reader");
reader = await this.openFile(file, {
// Playback reads sequentially from wherever it starts or seeks to.
reader = await this.#openReader(file, {
...(options.onProgress ? { onProgress: options.onProgress } : {}),
parentOperationId: operation.id,
signal: operation.signal,
});
}, true);
this.#media ??= new MediaBridge();
source = await this.#media.attach(reader, {
...options,
Expand Down
8 changes: 8 additions & 0 deletions src/internal/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export interface RawFileReader {
free(): void;
}

/** The core's `BrowserFileReaderOptions`; omitted fields take the core's defaults. */
export interface RawFileReaderOptions {
/** Treat every read as sequential and fetch ahead of it, as media playback needs. */
streaming?: boolean;
}

export interface RawNetworkClient {
connect(expectedPayment?: unknown): Promise<unknown>;
reconcileFailedUploadPayment(
Expand All @@ -33,6 +39,7 @@ export interface RawNetworkClient {
openPublicFile(
file: unknown,
onProgress?: (message: string) => void,
options?: RawFileReaderOptions,
): Promise<RawFileReader>;
downloadPrivateFile(
file: unknown,
Expand All @@ -42,6 +49,7 @@ export interface RawNetworkClient {
openPrivateFile(
file: unknown,
onProgress?: (message: string) => void,
options?: RawFileReaderOptions,
): Promise<RawFileReader>;
/** Quote, pay for, and store one batch; `loadRecord` receives the batch-local index. */
uploadRecords(
Expand Down
Loading
Loading