@freqhole/api-client 0.2.15 → 0.3.1
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/package.json +1 -1
- package/src/CharnelLocalTransport.ts +70 -35
- package/src/CharnelTransport.ts +391 -106
- package/src/FreqholeClient.ts +46 -83
- package/src/WasmTransport.ts +265 -219
- package/src/blobCachePolicy.test.ts +199 -0
- package/src/codegen/routes.ts +75 -13
- package/src/codegen/schema.ts +1436 -225
- package/src/domains/entities.ts +304 -0
- package/src/domains/favorites.types.ts +13 -3
- package/src/domains/music.ts +74 -126
- package/src/domains/upload.ts +54 -53
- package/src/domains/video.ts +463 -0
- package/src/errors.ts +142 -0
- package/src/index.ts +47 -10
- package/src/permissions.ts +18 -36
- package/src/tauriRuntime.ts +22 -0
- package/src/test/fixtures.ts +13 -23
- package/src/test/integration.ts +15 -28
- package/src/test/stateful.ts +11 -29
- package/src/transport.ts +72 -39
package/src/WasmTransport.ts
CHANGED
|
@@ -3,14 +3,9 @@
|
|
|
3
3
|
// uses midden's MiddenNode to make API requests to peer nodes.
|
|
4
4
|
// blobs are cached in Cache API for audio playback.
|
|
5
5
|
|
|
6
|
-
import type { BlobData, Transport, TransportResponse } from "./transport.js";
|
|
6
|
+
import type { BlobData, BlobFetchOptions, Transport, TransportResponse } from "./transport.js";
|
|
7
7
|
import { snapshotJobEventsViaRequest } from "./transport.js";
|
|
8
|
-
import type {
|
|
9
|
-
CloseReason,
|
|
10
|
-
EventFilter,
|
|
11
|
-
JobEvent,
|
|
12
|
-
JobStateSnapshot,
|
|
13
|
-
} from "./codegen/schema.js";
|
|
8
|
+
import type { CloseReason, EventFilter, JobEvent, JobStateSnapshot } from "./codegen/schema.js";
|
|
14
9
|
import { JobEventsStreamClosed } from "./CharnelLocalTransport.js";
|
|
15
10
|
|
|
16
11
|
/**
|
|
@@ -34,7 +29,11 @@ export interface BiStreamLike {
|
|
|
34
29
|
// length-prefixed framing (unused here, but matches the wasm class)
|
|
35
30
|
write_message?(data: Uint8Array): Promise<void>;
|
|
36
31
|
read_message?(): Promise<Uint8Array | null>;
|
|
37
|
-
// raw byte primitives
|
|
32
|
+
// raw byte primitives - used by cenotaph's freqhole/1 api router and
|
|
33
|
+
// the freqhole-player/1 pairing/control handler's write_raw_and_finish
|
|
34
|
+
// response framing (see @freqhole/cenotaph's control/apiRouter.ts).
|
|
35
|
+
read_to_end(max_size: number): Promise<Uint8Array | null>;
|
|
36
|
+
write_raw_and_finish(data: Uint8Array): Promise<void>;
|
|
38
37
|
close(): void;
|
|
39
38
|
}
|
|
40
39
|
|
|
@@ -44,6 +43,12 @@ export interface BiStreamLike {
|
|
|
44
43
|
*/
|
|
45
44
|
export interface MiddenNodeLike {
|
|
46
45
|
node_id(): string;
|
|
46
|
+
// full endpoint address JSON (node_id + relay url + any direct addrs) -
|
|
47
|
+
// lets a dialer skip pkarr/DNS discovery, which otherwise races on a
|
|
48
|
+
// freshly-booted identity (the connecting peer may 404 the pkarr lookup
|
|
49
|
+
// before it's ever published). optional: not every MiddenNodeLike impl
|
|
50
|
+
// (e.g. spume's charnel/tauri stub) has a real iroh endpoint to report.
|
|
51
|
+
node_addr?(): string;
|
|
47
52
|
secret_key(): Uint8Array;
|
|
48
53
|
api_request(
|
|
49
54
|
peer_addr: string,
|
|
@@ -73,15 +78,9 @@ export interface MiddenNodeLike {
|
|
|
73
78
|
// must be called once for remote peers to pull blobs from this node
|
|
74
79
|
start_blob_server?(): void;
|
|
75
80
|
// download blob with iroh-blobs verified streaming - optional
|
|
76
|
-
download_verified?(
|
|
77
|
-
peer_addr: string,
|
|
78
|
-
blake3_hash: string,
|
|
79
|
-
): Promise<Uint8Array>;
|
|
81
|
+
download_verified?(peer_addr: string, blake3_hash: string): Promise<Uint8Array>;
|
|
80
82
|
// download blob with automatic ensure + retry - optional
|
|
81
|
-
download_verified_with_ensure?(
|
|
82
|
-
peer_addr: string,
|
|
83
|
-
blake3_hash: string,
|
|
84
|
-
): Promise<Uint8Array>;
|
|
83
|
+
download_verified_with_ensure?(peer_addr: string, blake3_hash: string): Promise<Uint8Array>;
|
|
85
84
|
// download blob by ID with on-demand blake3 computation - optional
|
|
86
85
|
// returns [Uint8Array, string] but typed as any[] for wasm-bindgen compatibility
|
|
87
86
|
download_verified_by_id?(peer_addr: string, blob_id: string): Promise<any[]>;
|
|
@@ -106,11 +105,7 @@ export interface MiddenNodeLike {
|
|
|
106
105
|
// dispatch a freqhole-admin/1 ALPN command to a peer.
|
|
107
106
|
// returns the grimoire response envelope `{ success, message, data, errors }`.
|
|
108
107
|
// `args` is a json-encoded string (use "null" for commands with no payload).
|
|
109
|
-
proxy_admin?(
|
|
110
|
-
peer_addr: string,
|
|
111
|
-
command: string,
|
|
112
|
-
args: string,
|
|
113
|
-
): Promise<unknown>;
|
|
108
|
+
proxy_admin?(peer_addr: string, command: string, args: string): Promise<unknown>;
|
|
114
109
|
// tune into a freqhole radio broadcaster (freqhole-radio/1 ALPN).
|
|
115
110
|
// callbacks:
|
|
116
111
|
// on_hello(json: string) — HelloMessage as JSON, fires once on connect
|
|
@@ -127,6 +122,13 @@ export interface MiddenNodeLike {
|
|
|
127
122
|
// open a raw bi-directional stream on an arbitrary ALPN. used for
|
|
128
123
|
// job events (freqhole-events/1) and other custom protocols.
|
|
129
124
|
open_bi?(peer_addr: string, alpn: string): Promise<BiStreamLike>;
|
|
125
|
+
// accept the next incoming connection on any registered ALPN (blocks
|
|
126
|
+
// until one arrives; resolves null once the endpoint is closed). used
|
|
127
|
+
// by @freqhole/cenotaph's startAcceptLoop to drive spume's own
|
|
128
|
+
// remote-playback accept mode - see docs/cenotaph-migration-plan.md
|
|
129
|
+
// phase 1. optional because charnel's CharnelTransport has no wasm
|
|
130
|
+
// accept-loop equivalent (yet).
|
|
131
|
+
accept?(): Promise<BiStreamLike | null>;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
/**
|
|
@@ -173,6 +175,110 @@ function cacheKey(blobId: string): string {
|
|
|
173
175
|
return `https://blob.local/${blobId}`;
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
/**
|
|
179
|
+
* error thrown by request()/upload methods when a midden/wasm call
|
|
180
|
+
* fails. mirrors sendToRemote.ts's `EnvelopeError` shape (and
|
|
181
|
+
* CharnelTransport.ts's `TransportError`) so callers can consistently
|
|
182
|
+
* check `.errorType` regardless of which transport produced the failure.
|
|
183
|
+
*/
|
|
184
|
+
export class TransportError extends Error {
|
|
185
|
+
readonly errorType?: string;
|
|
186
|
+
/** the original error this was derived from, if any (see uploadViaIrohBlobs). */
|
|
187
|
+
readonly cause?: unknown;
|
|
188
|
+
/**
|
|
189
|
+
* per-step breakdown for a multi-step fallback chain (e.g. `fetchBlob`'s
|
|
190
|
+
* verified-download -> api-request -> on-demand-blake3 -> legacy
|
|
191
|
+
* fetch_blob sequence). populated only by call sites that actually fall
|
|
192
|
+
* through multiple attempts before failing.
|
|
193
|
+
*/
|
|
194
|
+
readonly attempts?: Array<{ step: string; reason: string }>;
|
|
195
|
+
constructor(
|
|
196
|
+
message: string,
|
|
197
|
+
opts?: {
|
|
198
|
+
errorType?: string;
|
|
199
|
+
cause?: unknown;
|
|
200
|
+
attempts?: Array<{ step: string; reason: string }>;
|
|
201
|
+
},
|
|
202
|
+
) {
|
|
203
|
+
super(message);
|
|
204
|
+
this.name = "TransportError";
|
|
205
|
+
this.errorType = opts?.errorType;
|
|
206
|
+
this.cause = opts?.cause;
|
|
207
|
+
this.attempts = opts?.attempts;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* extract a best-effort `error_type` out of a caught midden/wasm
|
|
213
|
+
* rejection. these are sometimes a plain string, sometimes an `Error`,
|
|
214
|
+
* and (once the rust side lands the convention - see
|
|
215
|
+
* docs/error-handling-tasks.md track P0-D) sometimes a short
|
|
216
|
+
* machine-parseable prefix like `"file_not_found: could not open file"`,
|
|
217
|
+
* or a JSON-encoded structured error body forwarded from a remote peer's
|
|
218
|
+
* response. this parses either shape, falling back to the raw message
|
|
219
|
+
* when neither is present - never matches on natural-language message
|
|
220
|
+
* text (the prefix regex only matches a single snake_case token
|
|
221
|
+
* immediately before the colon, so an ordinary sentence like "failed to
|
|
222
|
+
* open file: permission denied" is left alone).
|
|
223
|
+
*/
|
|
224
|
+
// P2P `ApiRequest` messages are read in one shot on the remote peer and
|
|
225
|
+
// capped by that peer's configured `federation.max_message_size_mb`
|
|
226
|
+
// (10 MB by default - see grimoire/src/config.rs). the base64 fallback
|
|
227
|
+
// (used here when a blob-pull upload path isn't available for this
|
|
228
|
+
// route, e.g. video uploads today) embeds the whole file directly in
|
|
229
|
+
// that message, inflated ~4/3 by base64 plus a small JSON envelope. we
|
|
230
|
+
// don't know the actual remote's configured cap, so this is a
|
|
231
|
+
// conservative pre-flight check against the documented default with
|
|
232
|
+
// headroom for that overhead - it exists purely to fail fast with a
|
|
233
|
+
// clear message instead of a raw mid-transfer "stream too long"/generic
|
|
234
|
+
// "network error".
|
|
235
|
+
const MAX_BASE64_UPLOAD_BYTES = 7 * 1024 * 1024;
|
|
236
|
+
|
|
237
|
+
function uploadTooLargeError(file: File, path: string): TransportError {
|
|
238
|
+
const mb = (file.size / (1024 * 1024)).toFixed(1);
|
|
239
|
+
const limitMb = (MAX_BASE64_UPLOAD_BYTES / (1024 * 1024)).toFixed(0);
|
|
240
|
+
const kind = path.includes("/video") ? "video uploads" : "uploads of this size";
|
|
241
|
+
return new TransportError(
|
|
242
|
+
`this file is ${mb} MB, too large to upload over this P2P connection (roughly ${limitMb} MB limit) - large ${kind} aren't fully supported over P2P yet`,
|
|
243
|
+
{ errorType: "upload_too_large_for_transport" },
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function extractErrorType(err: unknown): { message: string; errorType?: string } {
|
|
248
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
249
|
+
const trimmed = message.trim();
|
|
250
|
+
|
|
251
|
+
if (trimmed.startsWith("{")) {
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
|
254
|
+
const firstError =
|
|
255
|
+
Array.isArray(parsed.errors) && parsed.errors.length > 0
|
|
256
|
+
? (parsed.errors[0] as Record<string, unknown>)
|
|
257
|
+
: undefined;
|
|
258
|
+
const errorType =
|
|
259
|
+
(typeof firstError?.error_type === "string" && firstError.error_type) ||
|
|
260
|
+
(typeof parsed.error_type === "string" && parsed.error_type) ||
|
|
261
|
+
undefined;
|
|
262
|
+
if (errorType) {
|
|
263
|
+
const detail =
|
|
264
|
+
(typeof firstError?.detail === "string" && firstError.detail) ||
|
|
265
|
+
(typeof parsed.message === "string" && parsed.message) ||
|
|
266
|
+
message;
|
|
267
|
+
return { message: detail, errorType };
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// not valid json, fall through to prefix-token parsing below
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const prefixMatch = /^([a-z][a-z0-9_]*):\s*(.+)$/s.exec(message);
|
|
275
|
+
if (prefixMatch) {
|
|
276
|
+
return { message: prefixMatch[2], errorType: prefixMatch[1] };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { message };
|
|
280
|
+
}
|
|
281
|
+
|
|
176
282
|
/**
|
|
177
283
|
* WASM transport - uses midden for P2P connections
|
|
178
284
|
*
|
|
@@ -212,31 +318,41 @@ export class WasmTransport implements Transport {
|
|
|
212
318
|
return this.peerAddr;
|
|
213
319
|
}
|
|
214
320
|
|
|
215
|
-
async request(
|
|
216
|
-
method: string,
|
|
217
|
-
path: string,
|
|
218
|
-
body?: string,
|
|
219
|
-
): Promise<TransportResponse> {
|
|
321
|
+
async request(method: string, path: string, body?: string): Promise<TransportResponse> {
|
|
220
322
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
323
|
+
// TEMP DEBUG - remove once the first-pair-attempt-fails bug is found
|
|
324
|
+
console.log(`[debug/WasmTransport] request start ${method} ${path} -> ${this.peerAddr}`);
|
|
325
|
+
const result = await this.node.api_request(this.peerAddr, method, path, body ?? null);
|
|
326
|
+
// TEMP DEBUG - remove once the first-pair-attempt-fails bug is found
|
|
327
|
+
console.log(`[debug/WasmTransport] request ok ${method} ${path} status=${result.status}`);
|
|
328
|
+
if (result.status < 200 || result.status >= 300) {
|
|
329
|
+
// TEMP DEBUG - remove once sync-to-local wiring bug is found
|
|
330
|
+
console.log(`[debug/WasmTransport] ${method} ${path} non-2xx body:`, result.body);
|
|
331
|
+
}
|
|
227
332
|
return {
|
|
228
333
|
status: result.status,
|
|
229
334
|
body: result.body,
|
|
230
335
|
};
|
|
231
336
|
} catch (e) {
|
|
232
337
|
// P2P connection errors - rethrow with message that isNetworkError will catch
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
338
|
+
const { message, errorType } = extractErrorType(e);
|
|
339
|
+
// TEMP DEBUG - remove once sync-to-local wiring bug is found
|
|
340
|
+
console.log(`[debug/WasmTransport] ${method} ${path} threw:`, e);
|
|
341
|
+
console.warn(`[WasmTransport] P2P request failed: ${message}`);
|
|
342
|
+
throw new TransportError(`connection failed: ${message}`, { errorType });
|
|
236
343
|
}
|
|
237
344
|
}
|
|
238
345
|
|
|
239
|
-
async upload(
|
|
346
|
+
async upload(
|
|
347
|
+
path: string,
|
|
348
|
+
formData: FormData,
|
|
349
|
+
_onProgress?: (loaded: number, total: number) => void,
|
|
350
|
+
): Promise<TransportResponse> {
|
|
351
|
+
// no byte-level progress possible here - unlike CharnelTransport's tauri
|
|
352
|
+
// IPC path (which self-chunks and can report per-chunk progress), the
|
|
353
|
+
// midden wasm binding's `import_blob(bytes)` is a single opaque call
|
|
354
|
+
// with no progress callback exposed, and the base64 fallback also sends
|
|
355
|
+
// one JSON request.
|
|
240
356
|
const file = formData.get("file") as File | null;
|
|
241
357
|
if (!file) {
|
|
242
358
|
return {
|
|
@@ -255,9 +371,11 @@ export class WasmTransport implements Transport {
|
|
|
255
371
|
};
|
|
256
372
|
}
|
|
257
373
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
374
|
+
// music and video uploads both go through the iroh-blobs pull model if
|
|
375
|
+
// available - chunked/verified streaming, no base64/raw-bytes framing.
|
|
376
|
+
const blobPullPaths = ["/api/upload/music", "/api/upload/video"];
|
|
377
|
+
if (blobPullPaths.includes(path) && this.node.import_blob) {
|
|
378
|
+
return this.uploadViaIrohBlobs(path, file, formData);
|
|
261
379
|
}
|
|
262
380
|
|
|
263
381
|
// fallback: base64 encode and send via api_request (works for image uploads)
|
|
@@ -267,11 +385,12 @@ export class WasmTransport implements Transport {
|
|
|
267
385
|
/**
|
|
268
386
|
* upload via iroh-blobs pull model:
|
|
269
387
|
* 1. import file bytes into local iroh-blobs store (returns blake3 hash)
|
|
270
|
-
* 2. tell remote peer the hash via /api/upload/music-by-blake3
|
|
388
|
+
* 2. tell remote peer the hash via `${path}-by-blake3` (e.g. /api/upload/music-by-blake3)
|
|
271
389
|
* 3. remote peer pulls the blob from us via iroh-blobs verified streaming
|
|
272
390
|
* 4. release the TempTag so local GC can reclaim the blob
|
|
273
391
|
*/
|
|
274
392
|
private async uploadViaIrohBlobs(
|
|
393
|
+
path: string,
|
|
275
394
|
file: File,
|
|
276
395
|
formData: FormData,
|
|
277
396
|
): Promise<TransportResponse> {
|
|
@@ -297,9 +416,7 @@ export class WasmTransport implements Transport {
|
|
|
297
416
|
}
|
|
298
417
|
|
|
299
418
|
// include associate_with if present
|
|
300
|
-
const associateWithStr = formData.get("associate_with") as
|
|
301
|
-
| string
|
|
302
|
-
| null;
|
|
419
|
+
const associateWithStr = formData.get("associate_with") as string | null;
|
|
303
420
|
if (associateWithStr) {
|
|
304
421
|
try {
|
|
305
422
|
body.associate_with = JSON.parse(associateWithStr);
|
|
@@ -308,11 +425,7 @@ export class WasmTransport implements Transport {
|
|
|
308
425
|
}
|
|
309
426
|
}
|
|
310
427
|
|
|
311
|
-
const response = await this.request(
|
|
312
|
-
"POST",
|
|
313
|
-
"/api/upload/music-by-blake3",
|
|
314
|
-
JSON.stringify(body),
|
|
315
|
-
);
|
|
428
|
+
const response = await this.request("POST", `${path}-by-blake3`, JSON.stringify(body));
|
|
316
429
|
return response;
|
|
317
430
|
} finally {
|
|
318
431
|
// release TempTag so local GC can reclaim the blob
|
|
@@ -320,7 +433,11 @@ export class WasmTransport implements Transport {
|
|
|
320
433
|
}
|
|
321
434
|
} catch (error) {
|
|
322
435
|
console.error("[WasmTransport] upload failed:", error);
|
|
323
|
-
|
|
436
|
+
// preserve the original error (structure, stack, any extra
|
|
437
|
+
// properties) via `cause` rather than flattening it into a plain
|
|
438
|
+
// string - only the message/error_type get normalized here.
|
|
439
|
+
const { message, errorType } = extractErrorType(error);
|
|
440
|
+
throw new TransportError(`upload failed: ${message}`, { errorType, cause: error });
|
|
324
441
|
}
|
|
325
442
|
}
|
|
326
443
|
|
|
@@ -334,18 +451,19 @@ export class WasmTransport implements Transport {
|
|
|
334
451
|
file: File,
|
|
335
452
|
formData: FormData,
|
|
336
453
|
): Promise<TransportResponse> {
|
|
337
|
-
if (path === "/api/upload/music") {
|
|
454
|
+
if (path === "/api/upload/music" || path === "/api/upload/video") {
|
|
338
455
|
console.warn(
|
|
339
|
-
|
|
456
|
+
`[WasmTransport] falling back to base64 upload for ${path} (import_blob not available)`,
|
|
340
457
|
);
|
|
341
458
|
}
|
|
342
459
|
|
|
460
|
+
if (file.size > MAX_BASE64_UPLOAD_BYTES) {
|
|
461
|
+
throw uploadTooLargeError(file, path);
|
|
462
|
+
}
|
|
463
|
+
|
|
343
464
|
const arrayBuffer = await file.arrayBuffer();
|
|
344
465
|
const base64 = btoa(
|
|
345
|
-
new Uint8Array(arrayBuffer).reduce(
|
|
346
|
-
(data, byte) => data + String.fromCharCode(byte),
|
|
347
|
-
"",
|
|
348
|
-
),
|
|
466
|
+
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), ""),
|
|
349
467
|
);
|
|
350
468
|
|
|
351
469
|
const body: Record<string, unknown> = {
|
|
@@ -367,46 +485,61 @@ export class WasmTransport implements Transport {
|
|
|
367
485
|
return this.request("POST", path, JSON.stringify(body));
|
|
368
486
|
}
|
|
369
487
|
|
|
370
|
-
|
|
488
|
+
/** write fetched bytes into the Cache API, unless the caller opted out.
|
|
489
|
+
* centralized so every fallback-chain step in `fetchBlob` shares one
|
|
490
|
+
* definition of the cache policy. */
|
|
491
|
+
private async storeInCache(
|
|
492
|
+
cache: Cache,
|
|
493
|
+
blobId: string,
|
|
494
|
+
data: Uint8Array,
|
|
495
|
+
contentType: string,
|
|
496
|
+
opts?: BlobFetchOptions,
|
|
497
|
+
): Promise<void> {
|
|
498
|
+
if (opts?.cache === "skip") return;
|
|
499
|
+
const arrayBuffer = data.buffer.slice(
|
|
500
|
+
data.byteOffset,
|
|
501
|
+
data.byteOffset + data.byteLength,
|
|
502
|
+
) as ArrayBuffer;
|
|
503
|
+
const response = new Response(arrayBuffer, {
|
|
504
|
+
headers: { "Content-Type": contentType },
|
|
505
|
+
});
|
|
506
|
+
await cache.put(cacheKey(blobId), response);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async fetchBlob(blobId: string, blake3?: string, opts?: BlobFetchOptions): Promise<BlobData> {
|
|
371
510
|
// check Cache API first
|
|
372
511
|
const cache = await caches.open(this.cacheName);
|
|
373
512
|
const cached = await cache.match(cacheKey(blobId));
|
|
374
513
|
|
|
375
514
|
if (cached) {
|
|
376
515
|
const data = new Uint8Array(await cached.arrayBuffer());
|
|
377
|
-
const contentType =
|
|
378
|
-
cached.headers.get("Content-Type") || "application/octet-stream";
|
|
516
|
+
const contentType = cached.headers.get("Content-Type") || "application/octet-stream";
|
|
379
517
|
return { data, contentType };
|
|
380
518
|
}
|
|
381
519
|
|
|
520
|
+
// per-step failure breakdown, attached to the final thrown error so a
|
|
521
|
+
// bug report/log has the full fallback-chain picture, not just the
|
|
522
|
+
// last step's message.
|
|
523
|
+
const attempts: Array<{ step: string; reason: string }> = [];
|
|
524
|
+
|
|
382
525
|
// if blake3 is provided, try iroh-blobs verified download
|
|
383
526
|
// prefer download_verified_with_ensure (handles on-demand loading)
|
|
384
527
|
// fall back to download_verified if that's not available
|
|
385
528
|
if (blake3) {
|
|
386
|
-
const downloadFn =
|
|
387
|
-
this.node.download_verified_with_ensure ?? this.node.download_verified;
|
|
529
|
+
const downloadFn = this.node.download_verified_with_ensure ?? this.node.download_verified;
|
|
388
530
|
|
|
389
531
|
if (downloadFn) {
|
|
390
532
|
try {
|
|
391
533
|
const data = await downloadFn.call(this.node, this.peerAddr, blake3);
|
|
392
534
|
const contentType = "audio/mpeg"; // iroh-blobs doesn't track content type, assume audio
|
|
393
535
|
|
|
394
|
-
|
|
395
|
-
const arrayBuffer = data.buffer.slice(
|
|
396
|
-
data.byteOffset,
|
|
397
|
-
data.byteOffset + data.byteLength,
|
|
398
|
-
) as ArrayBuffer;
|
|
399
|
-
const response = new Response(arrayBuffer, {
|
|
400
|
-
headers: { "Content-Type": contentType },
|
|
401
|
-
});
|
|
402
|
-
await cache.put(cacheKey(blobId), response);
|
|
536
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
403
537
|
|
|
404
538
|
return { data, contentType };
|
|
405
539
|
} catch (e) {
|
|
406
540
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
);
|
|
541
|
+
attempts.push({ step: "verified_download", reason: errorMessage });
|
|
542
|
+
console.warn(`[WasmTransport] verified download failed, falling back: ${errorMessage}`);
|
|
410
543
|
// fall through to api fetch
|
|
411
544
|
}
|
|
412
545
|
}
|
|
@@ -427,50 +560,37 @@ export class WasmTransport implements Transport {
|
|
|
427
560
|
const data = base64ToBytes(parsed.data.data);
|
|
428
561
|
const contentType = parsed.data.mime || "application/octet-stream";
|
|
429
562
|
|
|
430
|
-
|
|
431
|
-
const arrayBuffer = data.buffer.slice(
|
|
432
|
-
data.byteOffset,
|
|
433
|
-
data.byteOffset + data.byteLength,
|
|
434
|
-
) as ArrayBuffer;
|
|
435
|
-
const response = new Response(arrayBuffer, {
|
|
436
|
-
headers: { "Content-Type": contentType },
|
|
437
|
-
});
|
|
438
|
-
await cache.put(cacheKey(blobId), response);
|
|
563
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
439
564
|
|
|
440
565
|
return { data, contentType };
|
|
441
566
|
}
|
|
567
|
+
attempts.push({
|
|
568
|
+
step: "api_request",
|
|
569
|
+
reason: `unexpected response shape (status ${result.status})`,
|
|
570
|
+
});
|
|
571
|
+
} else {
|
|
572
|
+
attempts.push({ step: "api_request", reason: `http ${result.status}` });
|
|
442
573
|
}
|
|
443
574
|
} catch (e) {
|
|
444
575
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
);
|
|
576
|
+
attempts.push({ step: "api_request", reason: errorMessage });
|
|
577
|
+
console.warn(`[WasmTransport] api blob data request failed, falling back: ${errorMessage}`);
|
|
448
578
|
}
|
|
449
579
|
|
|
450
580
|
// fallback: try on-demand blake3 computation + verified download via iroh-blobs
|
|
451
581
|
if (!blake3 && this.node.download_verified_by_id) {
|
|
452
582
|
try {
|
|
453
|
-
const result = await this.node.download_verified_by_id(
|
|
454
|
-
this.peerAddr,
|
|
455
|
-
blobId,
|
|
456
|
-
);
|
|
583
|
+
const result = await this.node.download_verified_by_id(this.peerAddr, blobId);
|
|
457
584
|
const data = result[0] as Uint8Array;
|
|
458
585
|
// result[1] is the computed blake3 hash
|
|
459
586
|
const contentType = "application/octet-stream";
|
|
460
587
|
|
|
461
|
-
|
|
462
|
-
const arrayBuffer = data.buffer.slice(
|
|
463
|
-
data.byteOffset,
|
|
464
|
-
data.byteOffset + data.byteLength,
|
|
465
|
-
) as ArrayBuffer;
|
|
466
|
-
const response = new Response(arrayBuffer, {
|
|
467
|
-
headers: { "Content-Type": contentType },
|
|
468
|
-
});
|
|
469
|
-
await cache.put(cacheKey(blobId), response);
|
|
588
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
470
589
|
|
|
471
590
|
return { data, contentType };
|
|
472
591
|
} catch (e) {
|
|
473
592
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
593
|
+
attempts.push({ step: "on_demand_blake3", reason: errorMessage });
|
|
474
594
|
console.warn(
|
|
475
595
|
`[WasmTransport] on-demand verified download failed, falling back: ${errorMessage}`,
|
|
476
596
|
);
|
|
@@ -485,26 +605,20 @@ export class WasmTransport implements Transport {
|
|
|
485
605
|
const data = result.data();
|
|
486
606
|
const contentType = result.content_type() ?? "application/octet-stream";
|
|
487
607
|
|
|
488
|
-
|
|
489
|
-
const arrayBuffer = data.buffer.slice(
|
|
490
|
-
data.byteOffset,
|
|
491
|
-
data.byteOffset + data.byteLength,
|
|
492
|
-
) as ArrayBuffer;
|
|
493
|
-
const response = new Response(arrayBuffer, {
|
|
494
|
-
headers: { "Content-Type": contentType },
|
|
495
|
-
});
|
|
496
|
-
await cache.put(cacheKey(blobId), response);
|
|
608
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
497
609
|
|
|
498
610
|
return { data, contentType };
|
|
499
611
|
} catch (e) {
|
|
500
612
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
613
|
+
attempts.push({ step: "legacy_fetch_blob", reason: errorMessage });
|
|
501
614
|
console.warn(`[WasmTransport] P2P fetch_blob failed: ${errorMessage}`);
|
|
502
|
-
throw new
|
|
615
|
+
throw new TransportError(`P2P fetch_blob failed: ${errorMessage}`, { attempts });
|
|
503
616
|
}
|
|
504
617
|
}
|
|
505
618
|
|
|
506
|
-
throw new
|
|
619
|
+
throw new TransportError(
|
|
507
620
|
"no download method available for this blob (no blake3 hash and no legacy fetch_blob)",
|
|
621
|
+
{ attempts },
|
|
508
622
|
);
|
|
509
623
|
}
|
|
510
624
|
|
|
@@ -529,6 +643,7 @@ export class WasmTransport implements Transport {
|
|
|
529
643
|
blake3?: string,
|
|
530
644
|
totalBytes?: number,
|
|
531
645
|
mimeType?: string,
|
|
646
|
+
opts?: BlobFetchOptions,
|
|
532
647
|
): Promise<BlobData> {
|
|
533
648
|
// check Cache API first
|
|
534
649
|
const cache = await caches.open(this.cacheName);
|
|
@@ -536,21 +651,24 @@ export class WasmTransport implements Transport {
|
|
|
536
651
|
|
|
537
652
|
if (cached) {
|
|
538
653
|
const data = new Uint8Array(await cached.arrayBuffer());
|
|
539
|
-
const contentType =
|
|
540
|
-
cached.headers.get("Content-Type") || "application/octet-stream";
|
|
654
|
+
const contentType = cached.headers.get("Content-Type") || "application/octet-stream";
|
|
541
655
|
// report 100% progress for cached blobs
|
|
542
656
|
onProgress(data.length, data.length);
|
|
543
657
|
return { data, contentType };
|
|
544
658
|
}
|
|
545
659
|
|
|
660
|
+
// per-step failure breakdown, attached to the final thrown error so a
|
|
661
|
+
// bug report/log has the full fallback-chain picture, not just the
|
|
662
|
+
// last step's message (see docs/error-handling-tasks.md track P0-E).
|
|
663
|
+
const attempts: Array<{ step: string; reason: string }> = [];
|
|
664
|
+
|
|
546
665
|
// if blake3 is provided, try iroh-blobs verified download
|
|
547
666
|
// prefer streaming path (chunk-by-chunk into a Blob) for large files —
|
|
548
667
|
// it avoids materializing the whole blob in wasm linear memory which
|
|
549
668
|
// fails around 32MB+ with "encode error".
|
|
550
669
|
if (blake3) {
|
|
551
670
|
const streamingFn =
|
|
552
|
-
this.node.download_verified_streaming_with_ensure ??
|
|
553
|
-
this.node.download_verified_streaming;
|
|
671
|
+
this.node.download_verified_streaming_with_ensure ?? this.node.download_verified_streaming;
|
|
554
672
|
|
|
555
673
|
if (streamingFn) {
|
|
556
674
|
try {
|
|
@@ -579,10 +697,7 @@ export class WasmTransport implements Transport {
|
|
|
579
697
|
// truth for the UI when totalBytes is known so we get a smooth
|
|
580
698
|
// 0..100% even before chunks start flowing in the read phase.
|
|
581
699
|
if (totalBytes && totalBytes > 0) {
|
|
582
|
-
const received = Math.min(
|
|
583
|
-
totalBytes,
|
|
584
|
-
Math.floor(fraction * totalBytes),
|
|
585
|
-
);
|
|
700
|
+
const received = Math.min(totalBytes, Math.floor(fraction * totalBytes));
|
|
586
701
|
onProgress(received, totalBytes);
|
|
587
702
|
}
|
|
588
703
|
},
|
|
@@ -600,19 +715,12 @@ export class WasmTransport implements Transport {
|
|
|
600
715
|
}
|
|
601
716
|
chunks.length = 0; // free per-chunk refs early
|
|
602
717
|
|
|
603
|
-
|
|
604
|
-
const cacheArrayBuffer = data.buffer.slice(
|
|
605
|
-
data.byteOffset,
|
|
606
|
-
data.byteOffset + data.byteLength,
|
|
607
|
-
) as ArrayBuffer;
|
|
608
|
-
const response = new Response(cacheArrayBuffer, {
|
|
609
|
-
headers: { "Content-Type": contentType },
|
|
610
|
-
});
|
|
611
|
-
await cache.put(cacheKey(blobId), response);
|
|
718
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
612
719
|
|
|
613
720
|
return { data, contentType };
|
|
614
721
|
} catch (e) {
|
|
615
722
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
723
|
+
attempts.push({ step: "streaming_verified_download", reason: errorMessage });
|
|
616
724
|
console.warn(
|
|
617
725
|
`[WasmTransport] streaming verified download failed, trying non-streaming: ${errorMessage}`,
|
|
618
726
|
);
|
|
@@ -620,8 +728,7 @@ export class WasmTransport implements Transport {
|
|
|
620
728
|
}
|
|
621
729
|
}
|
|
622
730
|
|
|
623
|
-
const downloadFn =
|
|
624
|
-
this.node.download_verified_with_ensure ?? this.node.download_verified;
|
|
731
|
+
const downloadFn = this.node.download_verified_with_ensure ?? this.node.download_verified;
|
|
625
732
|
|
|
626
733
|
if (downloadFn) {
|
|
627
734
|
try {
|
|
@@ -631,22 +738,13 @@ export class WasmTransport implements Transport {
|
|
|
631
738
|
// report 100% progress
|
|
632
739
|
onProgress(data.length, data.length);
|
|
633
740
|
|
|
634
|
-
|
|
635
|
-
const arrayBuffer = data.buffer.slice(
|
|
636
|
-
data.byteOffset,
|
|
637
|
-
data.byteOffset + data.byteLength,
|
|
638
|
-
) as ArrayBuffer;
|
|
639
|
-
const response = new Response(arrayBuffer, {
|
|
640
|
-
headers: { "Content-Type": contentType },
|
|
641
|
-
});
|
|
642
|
-
await cache.put(cacheKey(blobId), response);
|
|
741
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
643
742
|
|
|
644
743
|
return { data, contentType };
|
|
645
744
|
} catch (e) {
|
|
646
745
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
);
|
|
746
|
+
attempts.push({ step: "verified_download", reason: errorMessage });
|
|
747
|
+
console.warn(`[WasmTransport] verified download failed, falling back: ${errorMessage}`);
|
|
650
748
|
// fall through to regular fetch_blob
|
|
651
749
|
}
|
|
652
750
|
}
|
|
@@ -657,33 +755,20 @@ export class WasmTransport implements Transport {
|
|
|
657
755
|
// fallback because `/api/blobs/{id}/data` only works for DB-backed blobs
|
|
658
756
|
// and file-backed media blobs would otherwise fail first, then fall back
|
|
659
757
|
// anyway.
|
|
660
|
-
if (
|
|
661
|
-
!blake3 &&
|
|
662
|
-
this.node.download_verified_by_id &&
|
|
663
|
-
mimeType?.startsWith("audio/")
|
|
664
|
-
) {
|
|
758
|
+
if (!blake3 && this.node.download_verified_by_id && mimeType?.startsWith("audio/")) {
|
|
665
759
|
try {
|
|
666
|
-
const result = await this.node.download_verified_by_id(
|
|
667
|
-
this.peerAddr,
|
|
668
|
-
blobId,
|
|
669
|
-
);
|
|
760
|
+
const result = await this.node.download_verified_by_id(this.peerAddr, blobId);
|
|
670
761
|
const data = result[0] as Uint8Array;
|
|
671
762
|
const contentType = mimeType || "application/octet-stream";
|
|
672
763
|
|
|
673
764
|
onProgress(data.length, data.length);
|
|
674
765
|
|
|
675
|
-
|
|
676
|
-
data.byteOffset,
|
|
677
|
-
data.byteOffset + data.byteLength,
|
|
678
|
-
) as ArrayBuffer;
|
|
679
|
-
const response = new Response(arrayBuffer, {
|
|
680
|
-
headers: { "Content-Type": contentType },
|
|
681
|
-
});
|
|
682
|
-
await cache.put(cacheKey(blobId), response);
|
|
766
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
683
767
|
|
|
684
768
|
return { data, contentType };
|
|
685
769
|
} catch (e) {
|
|
686
770
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
771
|
+
attempts.push({ step: "audio_verified_by_id", reason: errorMessage });
|
|
687
772
|
console.warn(
|
|
688
773
|
`[WasmTransport] audio verified-by-id failed, falling back to api_request: ${errorMessage}`,
|
|
689
774
|
);
|
|
@@ -708,30 +793,23 @@ export class WasmTransport implements Transport {
|
|
|
708
793
|
// report 100% progress
|
|
709
794
|
onProgress(data.length, data.length);
|
|
710
795
|
|
|
711
|
-
|
|
712
|
-
const arrayBuffer = data.buffer.slice(
|
|
713
|
-
data.byteOffset,
|
|
714
|
-
data.byteOffset + data.byteLength,
|
|
715
|
-
) as ArrayBuffer;
|
|
716
|
-
const response = new Response(arrayBuffer, {
|
|
717
|
-
headers: { "Content-Type": contentType },
|
|
718
|
-
});
|
|
719
|
-
await cache.put(cacheKey(blobId), response);
|
|
796
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
720
797
|
|
|
721
798
|
return { data, contentType };
|
|
722
799
|
}
|
|
723
800
|
apiFailureReason = `success=false in api response body for blob ${blobId}`;
|
|
801
|
+
attempts.push({ step: "api_request", reason: apiFailureReason });
|
|
724
802
|
console.warn(`[WasmTransport] ${apiFailureReason}`);
|
|
725
803
|
} else {
|
|
726
804
|
apiFailureReason = `api request returned status ${result.status} for blob ${blobId}`;
|
|
805
|
+
attempts.push({ step: "api_request", reason: apiFailureReason });
|
|
727
806
|
console.warn(`[WasmTransport] ${apiFailureReason}`);
|
|
728
807
|
}
|
|
729
808
|
} catch (e) {
|
|
730
809
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
731
810
|
apiFailureReason = `api request threw: ${errorMessage}`;
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
);
|
|
811
|
+
attempts.push({ step: "api_request", reason: apiFailureReason });
|
|
812
|
+
console.warn(`[WasmTransport] api blob data request failed, falling back: ${errorMessage}`);
|
|
735
813
|
}
|
|
736
814
|
|
|
737
815
|
// fallback: try on-demand blake3 computation + verified download via iroh-blobs.
|
|
@@ -740,30 +818,19 @@ export class WasmTransport implements Transport {
|
|
|
740
818
|
// blake3 for a song.
|
|
741
819
|
if (!blake3 && this.node.download_verified_by_id) {
|
|
742
820
|
try {
|
|
743
|
-
const result = await this.node.download_verified_by_id(
|
|
744
|
-
this.peerAddr,
|
|
745
|
-
blobId,
|
|
746
|
-
);
|
|
821
|
+
const result = await this.node.download_verified_by_id(this.peerAddr, blobId);
|
|
747
822
|
const data = result[0] as Uint8Array;
|
|
748
823
|
const contentType = mimeType || "application/octet-stream";
|
|
749
824
|
|
|
750
825
|
onProgress(data.length, data.length);
|
|
751
826
|
|
|
752
|
-
|
|
753
|
-
data.byteOffset,
|
|
754
|
-
data.byteOffset + data.byteLength,
|
|
755
|
-
) as ArrayBuffer;
|
|
756
|
-
const response = new Response(arrayBuffer, {
|
|
757
|
-
headers: { "Content-Type": contentType },
|
|
758
|
-
});
|
|
759
|
-
await cache.put(cacheKey(blobId), response);
|
|
827
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
760
828
|
|
|
761
829
|
return { data, contentType };
|
|
762
830
|
} catch (e) {
|
|
763
831
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
);
|
|
832
|
+
attempts.push({ step: "on_demand_blake3", reason: errorMessage });
|
|
833
|
+
console.warn(`[WasmTransport] on-demand verified download failed: ${errorMessage}`);
|
|
767
834
|
}
|
|
768
835
|
}
|
|
769
836
|
|
|
@@ -772,11 +839,7 @@ export class WasmTransport implements Transport {
|
|
|
772
839
|
try {
|
|
773
840
|
let result: BlobResultLike;
|
|
774
841
|
if (this.node.fetch_blob_with_progress) {
|
|
775
|
-
result = await this.node.fetch_blob_with_progress(
|
|
776
|
-
this.peerAddr,
|
|
777
|
-
blobId,
|
|
778
|
-
onProgress,
|
|
779
|
-
);
|
|
842
|
+
result = await this.node.fetch_blob_with_progress(this.peerAddr, blobId, onProgress);
|
|
780
843
|
} else {
|
|
781
844
|
// fallback to non-progress fetch
|
|
782
845
|
result = await this.node.fetch_blob!(this.peerAddr, blobId);
|
|
@@ -784,33 +847,27 @@ export class WasmTransport implements Transport {
|
|
|
784
847
|
const data = result.data();
|
|
785
848
|
const contentType = result.content_type() ?? "application/octet-stream";
|
|
786
849
|
|
|
787
|
-
|
|
788
|
-
const arrayBuffer = data.buffer.slice(
|
|
789
|
-
data.byteOffset,
|
|
790
|
-
data.byteOffset + data.byteLength,
|
|
791
|
-
) as ArrayBuffer;
|
|
792
|
-
const response = new Response(arrayBuffer, {
|
|
793
|
-
headers: { "Content-Type": contentType },
|
|
794
|
-
});
|
|
795
|
-
await cache.put(cacheKey(blobId), response);
|
|
850
|
+
await this.storeInCache(cache, blobId, data, contentType, opts);
|
|
796
851
|
|
|
797
852
|
return { data, contentType };
|
|
798
853
|
} catch (e) {
|
|
799
854
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
855
|
+
attempts.push({ step: "legacy_fetch_blob", reason: errorMessage });
|
|
800
856
|
console.warn(`[WasmTransport] P2P fetch_blob failed: ${errorMessage}`);
|
|
801
|
-
throw new
|
|
857
|
+
throw new TransportError(`P2P fetch_blob failed: ${errorMessage}`, { attempts });
|
|
802
858
|
}
|
|
803
859
|
}
|
|
804
860
|
|
|
805
|
-
throw new
|
|
861
|
+
throw new TransportError(
|
|
806
862
|
`no download method available for blob ${blobId} ` +
|
|
807
863
|
`(blake3=${blake3 ? "yes" : "no"}, ` +
|
|
808
864
|
`api=${apiFailureReason ?? "not attempted"}, ` +
|
|
809
865
|
`legacy_fetch_blob=${this.node.fetch_blob ? "available" : "missing"})`,
|
|
866
|
+
{ attempts },
|
|
810
867
|
);
|
|
811
868
|
}
|
|
812
869
|
|
|
813
|
-
async getBlobUrl(blobId: string, blake3?: string): Promise<string> {
|
|
870
|
+
async getBlobUrl(blobId: string, blake3?: string, opts?: BlobFetchOptions): Promise<string> {
|
|
814
871
|
// check in-memory cache first
|
|
815
872
|
const cached = this.blobUrlCache.get(blobId);
|
|
816
873
|
if (cached) {
|
|
@@ -818,7 +875,7 @@ export class WasmTransport implements Transport {
|
|
|
818
875
|
}
|
|
819
876
|
|
|
820
877
|
// fetch and create object URL (pass blake3 for verified download)
|
|
821
|
-
const { data, contentType } = await this.fetchBlob(blobId, blake3);
|
|
878
|
+
const { data, contentType } = await this.fetchBlob(blobId, blake3, opts);
|
|
822
879
|
const arrayBuffer = data.buffer.slice(
|
|
823
880
|
data.byteOffset,
|
|
824
881
|
data.byteOffset + data.byteLength,
|
|
@@ -845,6 +902,7 @@ export class WasmTransport implements Transport {
|
|
|
845
902
|
blake3?: string,
|
|
846
903
|
totalBytes?: number,
|
|
847
904
|
mimeType?: string,
|
|
905
|
+
opts?: BlobFetchOptions,
|
|
848
906
|
): Promise<string> {
|
|
849
907
|
// check in-memory cache first
|
|
850
908
|
const cached = this.blobUrlCache.get(blobId);
|
|
@@ -861,6 +919,7 @@ export class WasmTransport implements Transport {
|
|
|
861
919
|
blake3,
|
|
862
920
|
totalBytes,
|
|
863
921
|
mimeType,
|
|
922
|
+
opts,
|
|
864
923
|
);
|
|
865
924
|
const arrayBuffer = data.buffer.slice(
|
|
866
925
|
data.byteOffset,
|
|
@@ -932,9 +991,7 @@ export class WasmTransport implements Transport {
|
|
|
932
991
|
const stream = await this.node.open_bi(this.peerAddr, EVENTS_ALPN);
|
|
933
992
|
try {
|
|
934
993
|
const id = 1;
|
|
935
|
-
await stream.write_line(
|
|
936
|
-
JSON.stringify({ type: "subscribe", id, filter: filter ?? {} }),
|
|
937
|
-
);
|
|
994
|
+
await stream.write_line(JSON.stringify({ type: "subscribe", id, filter: filter ?? {} }));
|
|
938
995
|
// first server frame is always Snapshot for this id
|
|
939
996
|
const snapLine = await stream.read_line();
|
|
940
997
|
if (snapLine === null) {
|
|
@@ -963,19 +1020,11 @@ export class WasmTransport implements Transport {
|
|
|
963
1020
|
}
|
|
964
1021
|
}
|
|
965
1022
|
|
|
966
|
-
subscribeJobEvents(
|
|
967
|
-
filter?: EventFilter,
|
|
968
|
-
signal?: AbortSignal,
|
|
969
|
-
): AsyncIterable<JobEvent> {
|
|
1023
|
+
subscribeJobEvents(filter?: EventFilter, signal?: AbortSignal): AsyncIterable<JobEvent> {
|
|
970
1024
|
if (!this.node.open_bi) {
|
|
971
1025
|
return wasmFallbackPollingIterable(this, filter, signal);
|
|
972
1026
|
}
|
|
973
|
-
return wasmSubscribeJobEventsIterable(
|
|
974
|
-
this.node,
|
|
975
|
-
this.peerAddr,
|
|
976
|
-
filter,
|
|
977
|
-
signal,
|
|
978
|
-
);
|
|
1027
|
+
return wasmSubscribeJobEventsIterable(this.node, this.peerAddr, filter, signal);
|
|
979
1028
|
}
|
|
980
1029
|
}
|
|
981
1030
|
|
|
@@ -1040,9 +1089,7 @@ async function* wasmSubscribeJobEventsIterable(
|
|
|
1040
1089
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1041
1090
|
|
|
1042
1091
|
try {
|
|
1043
|
-
await stream.write_line(
|
|
1044
|
-
JSON.stringify({ type: "subscribe", id, filter: filter ?? {} }),
|
|
1045
|
-
);
|
|
1092
|
+
await stream.write_line(JSON.stringify({ type: "subscribe", id, filter: filter ?? {} }));
|
|
1046
1093
|
|
|
1047
1094
|
while (true) {
|
|
1048
1095
|
if (signal?.aborted) return;
|
|
@@ -1061,8 +1108,7 @@ async function* wasmSubscribeJobEventsIterable(
|
|
|
1061
1108
|
continue;
|
|
1062
1109
|
}
|
|
1063
1110
|
if (frame.type === "close") {
|
|
1064
|
-
const reasonKind =
|
|
1065
|
-
(frame.reason as { kind: string }).kind ?? "internal";
|
|
1111
|
+
const reasonKind = (frame.reason as { kind: string }).kind ?? "internal";
|
|
1066
1112
|
if (reasonKind === "client_unsubscribed") return;
|
|
1067
1113
|
throw new JobEventsStreamClosed(frame.reason);
|
|
1068
1114
|
}
|