@freqhole/api-client 0.2.15 → 0.3.2
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 +1441 -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/transport.ts
CHANGED
|
@@ -4,12 +4,7 @@
|
|
|
4
4
|
// FreqholeClient uses a transport to make requests, then handles
|
|
5
5
|
// Zod validation on top.
|
|
6
6
|
|
|
7
|
-
import type {
|
|
8
|
-
CloseReason,
|
|
9
|
-
EventFilter,
|
|
10
|
-
JobEvent,
|
|
11
|
-
JobStateSnapshot,
|
|
12
|
-
} from "./codegen/schema.js";
|
|
7
|
+
import type { CloseReason, EventFilter, JobEvent, JobStateSnapshot } from "./codegen/schema.js";
|
|
13
8
|
|
|
14
9
|
/**
|
|
15
10
|
* response from a transport request
|
|
@@ -27,6 +22,21 @@ export interface BlobData {
|
|
|
27
22
|
contentType: string;
|
|
28
23
|
}
|
|
29
24
|
|
|
25
|
+
/**
|
|
26
|
+
* cache policy for a blob fetch.
|
|
27
|
+
*
|
|
28
|
+
* transports that back their blob fetches with the Cache API write every blob
|
|
29
|
+
* they fetch by default. callers that are about to store the bytes somewhere
|
|
30
|
+
* permanent instead (the sync-to-local path writing to OPFS / the grimoire
|
|
31
|
+
* library) must pass `cache: "skip"`, or the same audio ends up stored twice.
|
|
32
|
+
*
|
|
33
|
+
* "skip" suppresses the *write* only — an existing cache entry is still read,
|
|
34
|
+
* since the bytes are already there and are blake3-verified.
|
|
35
|
+
*/
|
|
36
|
+
export interface BlobFetchOptions {
|
|
37
|
+
cache?: "write" | "skip";
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
/**
|
|
31
41
|
* transport interface - implemented by HttpTransport, AppTransport, WasmTransport
|
|
32
42
|
*/
|
|
@@ -44,25 +54,36 @@ export interface Transport {
|
|
|
44
54
|
* upload a file via FormData
|
|
45
55
|
* @param path - API path (e.g., /api/upload/music)
|
|
46
56
|
* @param formData - FormData with file and metadata
|
|
57
|
+
* @param onProgress - optional callback with (loaded, total) bytes sent so
|
|
58
|
+
* far. only HttpTransport can report real values (via XHR's upload
|
|
59
|
+
* progress event - plain fetch has no cross-browser upload progress
|
|
60
|
+
* API); P2P/tauri transports accept but ignore it since their upload
|
|
61
|
+
* path doesn't stream raw bytes over a trackable request body.
|
|
47
62
|
* @returns response with status code and body string
|
|
48
63
|
*/
|
|
49
|
-
upload(
|
|
64
|
+
upload(
|
|
65
|
+
path: string,
|
|
66
|
+
formData: FormData,
|
|
67
|
+
onProgress?: (loaded: number, total: number) => void,
|
|
68
|
+
): Promise<TransportResponse>;
|
|
50
69
|
|
|
51
70
|
/**
|
|
52
71
|
* fetch a blob by ID
|
|
53
72
|
* @param blobId - the blob ID to fetch
|
|
54
73
|
* @param blake3 - optional blake3 hash for verified streaming via iroh-blobs
|
|
74
|
+
* @param opts - optional cache policy
|
|
55
75
|
* @returns blob data with content type
|
|
56
76
|
*/
|
|
57
|
-
fetchBlob(blobId: string, blake3?: string): Promise<BlobData>;
|
|
77
|
+
fetchBlob(blobId: string, blake3?: string, opts?: BlobFetchOptions): Promise<BlobData>;
|
|
58
78
|
|
|
59
79
|
/**
|
|
60
80
|
* get a URL for a blob (for <audio>/<img> src)
|
|
61
81
|
* HTTP transport returns direct URL, P2P transports may need caching
|
|
62
82
|
* @param blobId - the blob ID to fetch
|
|
63
83
|
* @param blake3 - optional blake3 hash for verified streaming via iroh-blobs
|
|
84
|
+
* @param opts - optional cache policy
|
|
64
85
|
*/
|
|
65
|
-
getBlobUrl(blobId: string, blake3?: string): string | Promise<string>;
|
|
86
|
+
getBlobUrl(blobId: string, blake3?: string, opts?: BlobFetchOptions): string | Promise<string>;
|
|
66
87
|
|
|
67
88
|
/**
|
|
68
89
|
* get a URL for a blob with progress callback (optional).
|
|
@@ -76,6 +97,7 @@ export interface Transport {
|
|
|
76
97
|
* @param mimeType - optional content type for the assembled blob (e.g.
|
|
77
98
|
* the song's media_blob.mime). midden's streaming path doesn't surface
|
|
78
99
|
* the source mime, so callers should pass it when known.
|
|
100
|
+
* @param opts - optional cache policy
|
|
79
101
|
*/
|
|
80
102
|
getBlobUrlWithProgress?(
|
|
81
103
|
blobId: string,
|
|
@@ -83,6 +105,7 @@ export interface Transport {
|
|
|
83
105
|
blake3?: string,
|
|
84
106
|
totalBytes?: number,
|
|
85
107
|
mimeType?: string,
|
|
108
|
+
opts?: BlobFetchOptions,
|
|
86
109
|
): Promise<string>;
|
|
87
110
|
|
|
88
111
|
/**
|
|
@@ -121,10 +144,7 @@ export interface Transport {
|
|
|
121
144
|
* optional `return()` path on the iterator). transports that don't
|
|
122
145
|
* override use the default polling fallback (see `pollingJobEvents`).
|
|
123
146
|
*/
|
|
124
|
-
subscribeJobEvents?(
|
|
125
|
-
filter?: EventFilter,
|
|
126
|
-
signal?: AbortSignal,
|
|
127
|
-
): AsyncIterable<JobEvent>;
|
|
147
|
+
subscribeJobEvents?(filter?: EventFilter, signal?: AbortSignal): AsyncIterable<JobEvent>;
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
/**
|
|
@@ -172,28 +192,48 @@ export class HttpTransport implements Transport {
|
|
|
172
192
|
};
|
|
173
193
|
}
|
|
174
194
|
|
|
175
|
-
async upload(
|
|
195
|
+
async upload(
|
|
196
|
+
path: string,
|
|
197
|
+
formData: FormData,
|
|
198
|
+
onProgress?: (loaded: number, total: number) => void,
|
|
199
|
+
): Promise<TransportResponse> {
|
|
176
200
|
const url = this.baseUrl + path;
|
|
177
|
-
const headers: Record<string, string> = {};
|
|
178
201
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
// plain fetch has no cross-browser upload-progress event, so only
|
|
203
|
+
// reach for XHR when a caller actually wants progress reported.
|
|
204
|
+
if (!onProgress) {
|
|
205
|
+
const headers: Record<string, string> = {};
|
|
206
|
+
// don't set Content-Type - browser sets it with boundary for FormData
|
|
207
|
+
if (this.apiKey) {
|
|
208
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
209
|
+
}
|
|
210
|
+
const response = await fetch(url, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers,
|
|
213
|
+
body: formData,
|
|
214
|
+
credentials: this.apiKey ? "omit" : "include",
|
|
215
|
+
});
|
|
216
|
+
const responseBody = await response.text();
|
|
217
|
+
return { status: response.status, body: responseBody };
|
|
182
218
|
}
|
|
183
219
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
220
|
+
return new Promise<TransportResponse>((resolve, reject) => {
|
|
221
|
+
const xhr = new XMLHttpRequest();
|
|
222
|
+
xhr.open("POST", url);
|
|
223
|
+
if (this.apiKey) {
|
|
224
|
+
xhr.setRequestHeader("Authorization", `Bearer ${this.apiKey}`);
|
|
225
|
+
}
|
|
226
|
+
xhr.withCredentials = !this.apiKey;
|
|
227
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
228
|
+
if (e.lengthComputable) onProgress(e.loaded, e.total);
|
|
229
|
+
});
|
|
230
|
+
xhr.addEventListener("load", () => {
|
|
231
|
+
resolve({ status: xhr.status, body: xhr.responseText });
|
|
232
|
+
});
|
|
233
|
+
xhr.addEventListener("error", () => reject(new Error("upload failed: network error")));
|
|
234
|
+
xhr.addEventListener("abort", () => reject(new Error("upload failed: aborted")));
|
|
235
|
+
xhr.send(formData);
|
|
189
236
|
});
|
|
190
|
-
|
|
191
|
-
const responseBody = await response.text();
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
status: response.status,
|
|
195
|
-
body: responseBody,
|
|
196
|
-
};
|
|
197
237
|
}
|
|
198
238
|
|
|
199
239
|
async fetchBlob(blobId: string, _blake3?: string): Promise<BlobData> {
|
|
@@ -235,10 +275,7 @@ export class HttpTransport implements Transport {
|
|
|
235
275
|
return snapshotJobEventsViaRequest(this, filter);
|
|
236
276
|
}
|
|
237
277
|
|
|
238
|
-
subscribeJobEvents(
|
|
239
|
-
filter?: EventFilter,
|
|
240
|
-
signal?: AbortSignal,
|
|
241
|
-
): AsyncIterable<JobEvent> {
|
|
278
|
+
subscribeJobEvents(filter?: EventFilter, signal?: AbortSignal): AsyncIterable<JobEvent> {
|
|
242
279
|
return pollingJobEvents(this, filter, signal);
|
|
243
280
|
}
|
|
244
281
|
}
|
|
@@ -257,11 +294,7 @@ export async function snapshotJobEventsViaRequest(
|
|
|
257
294
|
filter?: EventFilter,
|
|
258
295
|
): Promise<JobStateSnapshot[]> {
|
|
259
296
|
const body = JSON.stringify(filter ?? {});
|
|
260
|
-
const resp = await transport.request(
|
|
261
|
-
"POST",
|
|
262
|
-
"/api/jobs/events/snapshot",
|
|
263
|
-
body,
|
|
264
|
-
);
|
|
297
|
+
const resp = await transport.request("POST", "/api/jobs/events/snapshot", body);
|
|
265
298
|
if (resp.status >= 400 || resp.status === 0) {
|
|
266
299
|
throw new Error(`snapshotJobEvents failed: status ${resp.status}`);
|
|
267
300
|
}
|