@freqhole/api-client 0.3.5 → 0.3.7
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/WasmTransport.ts +65 -0
- package/src/codegen/routes.ts +1 -0
- package/src/codegen/schema.ts +23 -6
- package/src/domains/app.ts +30 -8
- package/src/transport.ts +28 -0
package/package.json
CHANGED
package/src/WasmTransport.ts
CHANGED
|
@@ -975,6 +975,71 @@ export class WasmTransport implements Transport {
|
|
|
975
975
|
return url;
|
|
976
976
|
}
|
|
977
977
|
|
|
978
|
+
/**
|
|
979
|
+
* stream a blob's bytes straight to a caller-supplied sink, without ever
|
|
980
|
+
* assembling the whole blob in one buffer here - see the `Transport`
|
|
981
|
+
* interface doc comment for why this exists (docs/blob-transfer-opfs-
|
|
982
|
+
* and-sha256-refactor-plan.md phase 2). only the verified iroh-blobs
|
|
983
|
+
* path (blake3 required) is supported - callers without a blake3 should
|
|
984
|
+
* use `getBlobUrlWithProgress`/`fetchBlob` instead, same as before.
|
|
985
|
+
*/
|
|
986
|
+
async streamBlobToSink(
|
|
987
|
+
blobId: string,
|
|
988
|
+
onChunk: (chunk: Uint8Array, offset: number) => void | Promise<void>,
|
|
989
|
+
onProgress: (received: number, total: number) => void,
|
|
990
|
+
blake3?: string,
|
|
991
|
+
totalBytes?: number,
|
|
992
|
+
mimeType?: string,
|
|
993
|
+
): Promise<{ contentType: string; totalBytes: number }> {
|
|
994
|
+
if (!blake3) {
|
|
995
|
+
throw new TransportError(
|
|
996
|
+
`streamBlobToSink requires a blake3 hash for verified streaming (blob ${blobId})`,
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const streamingFn =
|
|
1000
|
+
this.node.download_verified_streaming_with_ensure ?? this.node.download_verified_streaming;
|
|
1001
|
+
if (!streamingFn) {
|
|
1002
|
+
throw new TransportError(
|
|
1003
|
+
`streamBlobToSink: node has no streaming download method (blob ${blobId})`,
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
let totalReceived = 0;
|
|
1008
|
+
// chunk delivery is fire-and-forget from the wasm side (see
|
|
1009
|
+
// download_verified_streaming_with_ensure's own doc comment) - queue
|
|
1010
|
+
// each onChunk call after the previous one finishes so a slow sink
|
|
1011
|
+
// (e.g. an OPFS write) applies real backpressure instead of racing
|
|
1012
|
+
// ahead of disk writes.
|
|
1013
|
+
let writeChain: Promise<void> = Promise.resolve();
|
|
1014
|
+
|
|
1015
|
+
await streamingFn.call(
|
|
1016
|
+
this.node,
|
|
1017
|
+
this.peerAddr,
|
|
1018
|
+
blake3,
|
|
1019
|
+
totalBytes ?? 0,
|
|
1020
|
+
(chunk: Uint8Array, offset: number) => {
|
|
1021
|
+
// chunk is a wasm-owned Uint8Array view - copy to detach before
|
|
1022
|
+
// it's handed off across the write chain / a later tick.
|
|
1023
|
+
const owned = new Uint8Array(chunk.length);
|
|
1024
|
+
owned.set(chunk);
|
|
1025
|
+
totalReceived += owned.length;
|
|
1026
|
+
writeChain = writeChain.then(() => onChunk(owned, offset));
|
|
1027
|
+
if (!totalBytes || totalBytes <= 0) {
|
|
1028
|
+
onProgress(totalReceived, totalReceived);
|
|
1029
|
+
}
|
|
1030
|
+
},
|
|
1031
|
+
(fraction: number) => {
|
|
1032
|
+
if (totalBytes && totalBytes > 0) {
|
|
1033
|
+
const received = Math.min(totalBytes, Math.floor(fraction * totalBytes));
|
|
1034
|
+
onProgress(received, totalBytes);
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
);
|
|
1038
|
+
await writeChain;
|
|
1039
|
+
|
|
1040
|
+
return { contentType: mimeType ?? "application/octet-stream", totalBytes: totalReceived };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
978
1043
|
/**
|
|
979
1044
|
* revoke a blob URL to free memory
|
|
980
1045
|
*/
|
package/src/codegen/routes.ts
CHANGED
|
@@ -40,6 +40,7 @@ export const routes = {
|
|
|
40
40
|
radio_public_blob_thumbnail: { method: 'GET', path: '/api/radio/stations/{station_id}/blobs/{blob_id}/thumb/{size}', req: null, resp: null, auth: { type: 'public' } as const },
|
|
41
41
|
radio_public_timeline: { method: 'GET', path: '/api/radio/stations/{station_id}/timeline', req: null, resp: s.PublicTimelineManifestSchema, auth: { type: 'public' } as const },
|
|
42
42
|
radio_stations: { method: 'GET', path: '/api/radio/stations', req: null, resp: s.RadioStationsResponseSchema, auth: { type: 'public' } as const },
|
|
43
|
+
radio_stations_full: { method: 'GET', path: '/api/radio/stations/full', req: null, resp: s.RadioStationsResponseSchema, auth: { type: 'authenticated' } as const },
|
|
43
44
|
server_info: { method: 'GET', path: '/api/hello', req: null, resp: s.ServerInfoResponseSchema, auth: { type: 'public' } as const },
|
|
44
45
|
},
|
|
45
46
|
auth: {
|
package/src/codegen/schema.ts
CHANGED
|
@@ -1508,7 +1508,8 @@ export const CreateStationRequestSchema = z.object({
|
|
|
1508
1508
|
encode_args: z.string().nullish(),
|
|
1509
1509
|
codec: z.string().nullish(),
|
|
1510
1510
|
play_mode: z.string().nullish(),
|
|
1511
|
-
timeline_only_mode: z.boolean().nullish()
|
|
1511
|
+
timeline_only_mode: z.boolean().nullish(),
|
|
1512
|
+
content_mode: z.string().nullish()
|
|
1512
1513
|
});
|
|
1513
1514
|
export type CreateStationRequest = z.infer<typeof CreateStationRequestSchema>;
|
|
1514
1515
|
|
|
@@ -5190,6 +5191,7 @@ export const PublicAssetRefSchema = z.object({
|
|
|
5190
5191
|
export type PublicAssetRef = z.infer<typeof PublicAssetRefSchema>;
|
|
5191
5192
|
|
|
5192
5193
|
export const PublicNowPlayingSchema = z.object({
|
|
5194
|
+
kind: z.string(),
|
|
5193
5195
|
song_id: z.string(),
|
|
5194
5196
|
title: z.string(),
|
|
5195
5197
|
artist: z.string().nullish(),
|
|
@@ -5210,7 +5212,9 @@ export const PublicStationSchema = z.object({
|
|
|
5210
5212
|
listener_count: z.number(),
|
|
5211
5213
|
is_default: z.boolean(),
|
|
5212
5214
|
is_public: z.boolean(),
|
|
5215
|
+
is_running: z.boolean(),
|
|
5213
5216
|
now_playing: z.object({
|
|
5217
|
+
kind: z.string(),
|
|
5214
5218
|
song_id: z.string(),
|
|
5215
5219
|
title: z.string(),
|
|
5216
5220
|
artist: z.string().nullish(),
|
|
@@ -5391,7 +5395,8 @@ export type QueryVideosRequest = z.infer<typeof QueryVideosRequestSchema>;
|
|
|
5391
5395
|
export const RadioBumperSchema = z.object({
|
|
5392
5396
|
id: z.string(),
|
|
5393
5397
|
station_id: z.string(),
|
|
5394
|
-
song_id: z.string(),
|
|
5398
|
+
song_id: z.string().nullish(),
|
|
5399
|
+
video_id: z.string().nullish(),
|
|
5395
5400
|
label: z.string(),
|
|
5396
5401
|
weight: z.number(),
|
|
5397
5402
|
created_at: z.number()
|
|
@@ -5400,7 +5405,8 @@ export type RadioBumper = z.infer<typeof RadioBumperSchema>;
|
|
|
5400
5405
|
|
|
5401
5406
|
export const RadioBumpersAddRequestSchema = z.object({
|
|
5402
5407
|
station_id: z.string(),
|
|
5403
|
-
song_id: z.string(),
|
|
5408
|
+
song_id: z.string().nullish(),
|
|
5409
|
+
video_id: z.string().nullish(),
|
|
5404
5410
|
label: z.string(),
|
|
5405
5411
|
weight: z.number().nullish()
|
|
5406
5412
|
});
|
|
@@ -5424,8 +5430,12 @@ export type RadioBumpersSetFrequencyRequest = z.infer<typeof RadioBumpersSetFreq
|
|
|
5424
5430
|
|
|
5425
5431
|
export const RadioConfigPayloadSchema = z.object({
|
|
5426
5432
|
enabled: z.boolean(),
|
|
5427
|
-
encode_args: z.string(),
|
|
5428
|
-
|
|
5433
|
+
encode_args: z.string().nullish(),
|
|
5434
|
+
video_encode_args: z.string().nullish(),
|
|
5435
|
+
video_codec: z.string().nullish(),
|
|
5436
|
+
ffmpeg_available: z.boolean(),
|
|
5437
|
+
max_concurrent_audio_streams: z.number(),
|
|
5438
|
+
max_concurrent_video_streams: z.number()
|
|
5429
5439
|
});
|
|
5430
5440
|
export type RadioConfigPayload = z.infer<typeof RadioConfigPayloadSchema>;
|
|
5431
5441
|
|
|
@@ -5451,7 +5461,9 @@ export const RadioInfoResponseSchema = z.object({
|
|
|
5451
5461
|
listener_count: z.number(),
|
|
5452
5462
|
is_default: z.boolean(),
|
|
5453
5463
|
is_public: z.boolean(),
|
|
5464
|
+
is_running: z.boolean(),
|
|
5454
5465
|
now_playing: z.object({
|
|
5466
|
+
kind: z.string(),
|
|
5455
5467
|
song_id: z.string(),
|
|
5456
5468
|
title: z.string(),
|
|
5457
5469
|
artist: z.string().nullish(),
|
|
@@ -5493,6 +5505,8 @@ export const RadioStationSchema = z.object({
|
|
|
5493
5505
|
codec: z.string(),
|
|
5494
5506
|
play_mode: z.string(),
|
|
5495
5507
|
timeline_only_mode: z.number(),
|
|
5508
|
+
content_mode: z.string(),
|
|
5509
|
+
bumper_frequency_seconds: z.number().nullish(),
|
|
5496
5510
|
created_at: z.number(),
|
|
5497
5511
|
updated_at: z.number()
|
|
5498
5512
|
});
|
|
@@ -5530,7 +5544,9 @@ export const RadioStationsResponseSchema = z.object({
|
|
|
5530
5544
|
listener_count: z.number(),
|
|
5531
5545
|
is_default: z.boolean(),
|
|
5532
5546
|
is_public: z.boolean(),
|
|
5547
|
+
is_running: z.boolean(),
|
|
5533
5548
|
now_playing: z.object({
|
|
5549
|
+
kind: z.string(),
|
|
5534
5550
|
song_id: z.string(),
|
|
5535
5551
|
title: z.string(),
|
|
5536
5552
|
artist: z.string().nullish(),
|
|
@@ -7631,7 +7647,8 @@ export const UpdateStationRequestSchema = z.object({
|
|
|
7631
7647
|
encode_args: z.string().nullish(),
|
|
7632
7648
|
codec: z.string().nullish(),
|
|
7633
7649
|
play_mode: z.string().nullish(),
|
|
7634
|
-
timeline_only_mode: z.boolean().nullish()
|
|
7650
|
+
timeline_only_mode: z.boolean().nullish(),
|
|
7651
|
+
content_mode: z.string().nullish()
|
|
7635
7652
|
});
|
|
7636
7653
|
export type UpdateStationRequest = z.infer<typeof UpdateStationRequestSchema>;
|
|
7637
7654
|
|
package/src/domains/app.ts
CHANGED
|
@@ -7,7 +7,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
7
7
|
return {
|
|
8
8
|
healthCheck: () => {
|
|
9
9
|
return call(
|
|
10
|
-
"app",
|
|
10
|
+
"app",
|
|
11
|
+
"health_check",
|
|
11
12
|
routes.app.health_check.resp,
|
|
12
13
|
routes.app.health_check.req,
|
|
13
14
|
routes.app.health_check.method,
|
|
@@ -17,7 +18,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
17
18
|
|
|
18
19
|
serverInfo: () => {
|
|
19
20
|
return call(
|
|
20
|
-
"app",
|
|
21
|
+
"app",
|
|
22
|
+
"server_info",
|
|
21
23
|
routes.app.server_info.resp,
|
|
22
24
|
routes.app.server_info.req,
|
|
23
25
|
routes.app.server_info.method,
|
|
@@ -27,7 +29,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
27
29
|
|
|
28
30
|
radioInfo: () => {
|
|
29
31
|
return call(
|
|
30
|
-
"app",
|
|
32
|
+
"app",
|
|
33
|
+
"radio_info",
|
|
31
34
|
routes.app.radio_info.resp,
|
|
32
35
|
routes.app.radio_info.req,
|
|
33
36
|
routes.app.radio_info.method,
|
|
@@ -37,7 +40,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
37
40
|
|
|
38
41
|
radioStations: () => {
|
|
39
42
|
return call(
|
|
40
|
-
"app",
|
|
43
|
+
"app",
|
|
44
|
+
"radio_stations",
|
|
41
45
|
routes.app.radio_stations.resp,
|
|
42
46
|
routes.app.radio_stations.req,
|
|
43
47
|
routes.app.radio_stations.method,
|
|
@@ -45,10 +49,25 @@ export function createAppMethods(call: CallFn) {
|
|
|
45
49
|
);
|
|
46
50
|
},
|
|
47
51
|
|
|
52
|
+
// authenticated counterpart of radioStations() - includes non-public
|
|
53
|
+
// stations too, for a caller who actually resolved (real session or
|
|
54
|
+
// known iroh peer). see offal::music::radio's doc comment.
|
|
55
|
+
radioStationsFull: () => {
|
|
56
|
+
return call(
|
|
57
|
+
"app",
|
|
58
|
+
"radio_stations_full",
|
|
59
|
+
routes.app.radio_stations_full.resp,
|
|
60
|
+
routes.app.radio_stations_full.req,
|
|
61
|
+
routes.app.radio_stations_full.method,
|
|
62
|
+
routes.app.radio_stations_full.path,
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
|
|
48
66
|
// get blob metadata for a station blob
|
|
49
67
|
radioPublicBlob: (params: { station_id: string; blob_id: string }) => {
|
|
50
68
|
return call(
|
|
51
|
-
"app",
|
|
69
|
+
"app",
|
|
70
|
+
"radio_public_blob",
|
|
52
71
|
routes.app.radio_public_blob.resp,
|
|
53
72
|
routes.app.radio_public_blob.req,
|
|
54
73
|
routes.app.radio_public_blob.method,
|
|
@@ -60,7 +79,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
60
79
|
// stream the binary data for a station blob
|
|
61
80
|
radioPublicBlobData: (params: { station_id: string; blob_id: string }) => {
|
|
62
81
|
return call(
|
|
63
|
-
"app",
|
|
82
|
+
"app",
|
|
83
|
+
"radio_public_blob_data",
|
|
64
84
|
routes.app.radio_public_blob_data.resp,
|
|
65
85
|
routes.app.radio_public_blob_data.req,
|
|
66
86
|
routes.app.radio_public_blob_data.method,
|
|
@@ -72,7 +92,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
72
92
|
// get a thumbnail for a station blob at a given size
|
|
73
93
|
radioPublicBlobThumbnail: (params: { station_id: string; blob_id: string; size: string }) => {
|
|
74
94
|
return call(
|
|
75
|
-
"app",
|
|
95
|
+
"app",
|
|
96
|
+
"radio_public_blob_thumbnail",
|
|
76
97
|
routes.app.radio_public_blob_thumbnail.resp,
|
|
77
98
|
routes.app.radio_public_blob_thumbnail.req,
|
|
78
99
|
routes.app.radio_public_blob_thumbnail.method,
|
|
@@ -84,7 +105,8 @@ export function createAppMethods(call: CallFn) {
|
|
|
84
105
|
// get the public timeline manifest for a station
|
|
85
106
|
radioPublicTimeline: (params: { station_id: string }) => {
|
|
86
107
|
return call(
|
|
87
|
-
"app",
|
|
108
|
+
"app",
|
|
109
|
+
"radio_public_timeline",
|
|
88
110
|
routes.app.radio_public_timeline.resp,
|
|
89
111
|
routes.app.radio_public_timeline.req,
|
|
90
112
|
routes.app.radio_public_timeline.method,
|
package/src/transport.ts
CHANGED
|
@@ -137,6 +137,34 @@ export interface Transport {
|
|
|
137
137
|
opts?: BlobFetchOptions,
|
|
138
138
|
): Promise<string>;
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* stream a blob's bytes to a caller-supplied sink, chunk by chunk,
|
|
142
|
+
* instead of assembling the whole blob in memory first (optional).
|
|
143
|
+
* only implemented by WasmTransport - HTTP callers should stream
|
|
144
|
+
* directly from `fetch()`'s own `response.body` reader instead (see
|
|
145
|
+
* `streamVideoToOPFSWithResume`), and CharnelLocalTransport's sync-to-
|
|
146
|
+
* local path never goes through JS at all (grimoire's own disk-backed
|
|
147
|
+
* pull). intended for a caller writing straight to OPFS (or anywhere
|
|
148
|
+
* else) without ever holding the full blob in one buffer - see
|
|
149
|
+
* docs/blob-transfer-opfs-and-sha256-refactor-plan.md phase 2.
|
|
150
|
+
* @param blobId - the blob ID to fetch
|
|
151
|
+
* @param onChunk - called with each chunk as it arrives, in order
|
|
152
|
+
* @param onProgress - callback with (received, total) bytes
|
|
153
|
+
* @param blake3 - optional blake3 hash for verified streaming via iroh-blobs
|
|
154
|
+
* @param totalBytes - optional known total size in bytes
|
|
155
|
+
* @param mimeType - optional content type (midden's streaming path doesn't
|
|
156
|
+
* surface the source mime)
|
|
157
|
+
* @returns the content type and total bytes actually streamed
|
|
158
|
+
*/
|
|
159
|
+
streamBlobToSink?(
|
|
160
|
+
blobId: string,
|
|
161
|
+
onChunk: (chunk: Uint8Array, offset: number) => void | Promise<void>,
|
|
162
|
+
onProgress: (received: number, total: number) => void,
|
|
163
|
+
blake3?: string,
|
|
164
|
+
totalBytes?: number,
|
|
165
|
+
mimeType?: string,
|
|
166
|
+
): Promise<{ contentType: string; totalBytes: number }>;
|
|
167
|
+
|
|
140
168
|
/**
|
|
141
169
|
* fetch server image (public, no auth required)
|
|
142
170
|
* used during "add remote" flow before user is authenticated
|