@rocksky/sdk 0.3.0 → 0.4.0

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.
Files changed (51) hide show
  1. package/README.md +44 -254
  2. package/dist/agent.d.ts +77 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/client.d.ts +26 -113
  5. package/dist/client.d.ts.map +1 -1
  6. package/dist/dedup.d.ts +38 -0
  7. package/dist/dedup.d.ts.map +1 -0
  8. package/dist/errors.d.ts +3 -23
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/generated/types.d.ts +106 -8
  11. package/dist/generated/types.d.ts.map +1 -1
  12. package/dist/hash.d.ts +7 -0
  13. package/dist/hash.d.ts.map +1 -0
  14. package/dist/index.d.ts +16 -17
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6648 -1288
  17. package/dist/jetstream.d.ts +17 -0
  18. package/dist/jetstream.d.ts.map +1 -0
  19. package/package.json +15 -11
  20. package/src/agent.ts +232 -0
  21. package/src/client.ts +82 -226
  22. package/src/dedup.ts +207 -0
  23. package/src/errors.ts +6 -43
  24. package/src/generated/types.ts +115 -8
  25. package/src/hash.ts +22 -0
  26. package/src/index.ts +16 -86
  27. package/src/jetstream.ts +122 -0
  28. package/src/http.ts +0 -195
  29. package/src/namespaces/_helpers.ts +0 -27
  30. package/src/namespaces/actor.ts +0 -93
  31. package/src/namespaces/album.ts +0 -34
  32. package/src/namespaces/apikey.ts +0 -50
  33. package/src/namespaces/artist.ts +0 -68
  34. package/src/namespaces/charts.ts +0 -38
  35. package/src/namespaces/dropbox.ts +0 -53
  36. package/src/namespaces/feed.ts +0 -112
  37. package/src/namespaces/googledrive.ts +0 -41
  38. package/src/namespaces/graph.ts +0 -62
  39. package/src/namespaces/like.ts +0 -46
  40. package/src/namespaces/mirror.ts +0 -27
  41. package/src/namespaces/player.ts +0 -125
  42. package/src/namespaces/playlist.ts +0 -95
  43. package/src/namespaces/scrobble.ts +0 -41
  44. package/src/namespaces/shout.ts +0 -99
  45. package/src/namespaces/song.ts +0 -60
  46. package/src/namespaces/spotify.ts +0 -56
  47. package/src/namespaces/stats.ts +0 -27
  48. package/src/paginate.ts +0 -90
  49. package/src/pipe.ts +0 -146
  50. package/src/realtime.ts +0 -408
  51. package/src/types.ts +0 -41
@@ -0,0 +1,17 @@
1
+ import type { RockskyIndex } from "./dedup.js";
2
+ /** The four public Bluesky Jetstream servers. */
3
+ export declare const DEFAULT_JETSTREAM_SERVERS: string[];
4
+ export interface JetstreamOptions {
5
+ /** Servers to connect to at once (defaults to {@link DEFAULT_JETSTREAM_SERVERS}). */
6
+ servers?: string[];
7
+ /** Cancels the hydration and closes all connections when aborted. */
8
+ signal?: AbortSignal;
9
+ }
10
+ /**
11
+ * Hydrate `idx` from the Bluesky Jetstream firehose for `did`, connecting to
12
+ * every server at once, filtered to app.rocksky.* + this DID. A shared watermark
13
+ * de-duplicates the overlap between servers and is the reconnect cursor. Resolves
14
+ * when opts.signal aborts; each source reconnects with backoff.
15
+ */
16
+ export declare function runJetstream(idx: RockskyIndex, did: string, opts?: JetstreamOptions): Promise<void>;
17
+ //# sourceMappingURL=jetstream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jetstream.d.ts","sourceRoot":"","sources":["../src/jetstream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,UAKrC,CAAC;AAIF,MAAM,WAAW,gBAAgB;IAC/B,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,qEAAqE;IACrE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmBD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7G"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rocksky/sdk",
3
- "version": "0.3.0",
4
- "description": "TypeScript SDK for the Rocksky XRPC API",
3
+ "version": "0.4.0",
4
+ "description": "TypeScript SDK for Rocksky built on atcute: AppView reads, AT Protocol PDS writes (scrobble, like, follow, shout), a local dedup index, and Jetstream real-time sync.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -31,16 +31,9 @@
31
31
  "scripts": {
32
32
  "build": "bun build ./src/index.ts --outdir ./dist --target node --format esm --external none && bun run build:types",
33
33
  "build:types": "tsc -p tsconfig.build.json",
34
- "test": "bun test",
35
- "test:watch": "bun test --watch",
36
34
  "typecheck": "tsc --noEmit",
37
- "example:quickstart": "bun run examples/quickstart.ts",
38
- "example:builder": "bun run examples/builder.ts",
39
- "example:pipe": "bun run examples/pipe.ts",
40
- "example:scrobble": "bun run examples/scrobble.ts",
41
- "example:pagination": "bun run examples/pagination.ts",
42
- "example:paginate": "bun run examples/paginate.ts",
43
- "example:realtime": "bun run examples/realtime.ts"
35
+ "test": "bun test",
36
+ "example": "bun run examples/native.ts"
44
37
  },
45
38
  "keywords": [
46
39
  "rocksky",
@@ -59,7 +52,18 @@
59
52
  "api"
60
53
  ],
61
54
  "devDependencies": {
55
+ "@atcute/atproto": "^4.0.3",
62
56
  "@types/bun": "latest",
63
57
  "typescript": "^5.6.0"
58
+ },
59
+ "dependencies": {
60
+ "@atcute/car": "^6.0.2",
61
+ "@atcute/cbor": "^2.3.6",
62
+ "@atcute/cid": "^2.4.2",
63
+ "@atcute/client": "^5.1.1",
64
+ "@atcute/identity": "^2.0.2",
65
+ "@atcute/identity-resolver": "^2.0.1",
66
+ "@atcute/password-session": "^1.0.2",
67
+ "classic-level": "^3.0.0"
64
68
  }
65
69
  }
package/src/agent.ts ADDED
@@ -0,0 +1,232 @@
1
+ import { Client } from "@atcute/client";
2
+ import {
3
+ CompositeDidDocumentResolver,
4
+ CompositeHandleResolver,
5
+ DohJsonHandleResolver,
6
+ LocalActorResolver,
7
+ PlcDidDocumentResolver,
8
+ WebDidDocumentResolver,
9
+ WellKnownHandleResolver,
10
+ } from "@atcute/identity-resolver";
11
+ import { PasswordSession } from "@atcute/password-session";
12
+
13
+ import { RockskyError } from "./errors.js";
14
+ import type {
15
+ AlbumRecord,
16
+ ArtistRecord,
17
+ ActorTrackView,
18
+ ScrobbleRecord,
19
+ SongRecord,
20
+ } from "./generated/types.js";
21
+ import type { IndexStats, RockskyIndex } from "./dedup.js";
22
+ import { runJetstream, type JetstreamOptions } from "./jetstream.js";
23
+
24
+ // Collection NSIDs written by the agent.
25
+ const C_SCROBBLE = "app.rocksky.scrobble";
26
+ const C_SONG = "app.rocksky.song";
27
+ const C_ALBUM = "app.rocksky.album";
28
+ const C_ARTIST = "app.rocksky.artist";
29
+ const C_LIKE = "app.rocksky.like";
30
+ const C_FOLLOW = "app.rocksky.graph.follow";
31
+ const C_SHOUT = "app.rocksky.shout";
32
+ const C_STATUS = "app.rocksky.actor.status";
33
+
34
+ function actorResolver(): LocalActorResolver {
35
+ return new LocalActorResolver({
36
+ handleResolver: new CompositeHandleResolver({
37
+ methods: {
38
+ dns: new DohJsonHandleResolver({ dohUrl: "https://mozilla.cloudflare-dns.com/dns-query" }),
39
+ http: new WellKnownHandleResolver(),
40
+ },
41
+ }),
42
+ didDocumentResolver: new CompositeDidDocumentResolver({
43
+ methods: { plc: new PlcDidDocumentResolver(), web: new WebDidDocumentResolver() },
44
+ }),
45
+ });
46
+ }
47
+
48
+ function nowISO(): string {
49
+ return new Date().toISOString();
50
+ }
51
+
52
+ // Write inputs: `createdAt` is optional (the SDK defaults it to now).
53
+ /** Input for {@link Agent.scrobble} (createdAt defaults to now). */
54
+ export type ScrobbleInput = Omit<ScrobbleRecord, "createdAt"> & { createdAt?: string };
55
+ /** Input for {@link Agent.createSong}. */
56
+ export type SongInput = Omit<SongRecord, "createdAt"> & { createdAt?: string };
57
+ /** Input for {@link Agent.createAlbum} (`artist` is the album artist). */
58
+ export type AlbumInput = Omit<AlbumRecord, "createdAt"> & { createdAt?: string };
59
+ /** Input for {@link Agent.createArtist}. */
60
+ export type ArtistInput = Omit<ArtistRecord, "createdAt"> & { createdAt?: string };
61
+
62
+ /**
63
+ * Authenticated Rocksky client: logs in with an app password and writes
64
+ * app.rocksky.* records to the user's PDS (via atcute). Attach a
65
+ * {@link RockskyIndex} with {@link Agent.useIndex} for duplicate prevention.
66
+ */
67
+ export class Agent {
68
+ private idx?: RockskyIndex;
69
+
70
+ private constructor(
71
+ private rpc: Client,
72
+ readonly did: string,
73
+ readonly session: PasswordSession,
74
+ private pds: string,
75
+ ) {}
76
+
77
+ /**
78
+ * Resolve the account's PDS, authenticate with an app password, and return an
79
+ * Agent. `identifier` is a handle or DID.
80
+ */
81
+ static async login(identifier: string, password: string): Promise<Agent> {
82
+ const actor = await actorResolver().resolve(identifier as never);
83
+ const session = await PasswordSession.login({ service: actor.pds, identifier, password });
84
+ return new Agent(new Client({ handler: session }), session.did, session, actor.pds);
85
+ }
86
+
87
+ /** Attach a local dedup index — write verbs then skip records that already exist. */
88
+ useIndex(idx: RockskyIndex): void {
89
+ this.idx = idx;
90
+ }
91
+
92
+ /**
93
+ * Download the caller's full repository and (re)build the dedup index. Requires
94
+ * an attached index ({@link Agent.useIndex}). Full backfill; keep it current
95
+ * with {@link Agent.hydrateFromJetstream}.
96
+ */
97
+ async syncRepo(): Promise<IndexStats> {
98
+ if (!this.idx) throw new Error("no dedup index attached (call useIndex)");
99
+ const url = `${this.pds}/xrpc/com.atproto.sync.getRepo?did=${encodeURIComponent(this.did)}`;
100
+ const res = await fetch(url);
101
+ if (!res.ok) throw new Error(`getRepo: ${res.status}`);
102
+ const car = new Uint8Array(await res.arrayBuffer());
103
+ return this.idx.indexCar(this.did, car);
104
+ }
105
+
106
+ /**
107
+ * Keep the dedup index live from the Bluesky Jetstream firehose (all four
108
+ * servers at once, filtered to this DID + app.rocksky.*). Resolves when the
109
+ * options' AbortSignal fires. Requires an attached index.
110
+ */
111
+ hydrateFromJetstream(opts: JetstreamOptions = {}): Promise<void> {
112
+ if (!this.idx) throw new Error("no dedup index attached (call useIndex)");
113
+ return runJetstream(this.idx, this.did, opts);
114
+ }
115
+
116
+ private async create(collection: string, record: Record<string, unknown>): Promise<string> {
117
+ const res = await this.rpc.post("com.atproto.repo.createRecord" as never, {
118
+ input: { repo: this.did, collection, record: { ...record, $type: collection } },
119
+ } as never);
120
+ if (!res.ok) throw new RockskyError(res.data);
121
+ return (res.data as { uri: string }).uri;
122
+ }
123
+
124
+ private async putRecord(collection: string, rkey: string, record: Record<string, unknown>): Promise<string> {
125
+ const res = await this.rpc.post("com.atproto.repo.putRecord" as never, {
126
+ input: { repo: this.did, collection, rkey, record: { ...record, $type: collection } },
127
+ } as never);
128
+ if (!res.ok) throw new RockskyError(res.data);
129
+ return (res.data as { uri: string }).uri;
130
+ }
131
+
132
+ /** Delete a record by collection + rkey. */
133
+ async delete(collection: string, rkey: string): Promise<void> {
134
+ const res = await this.rpc.post("com.atproto.repo.deleteRecord" as never, {
135
+ input: { repo: this.did, collection, rkey },
136
+ } as never);
137
+ if (!res.ok) throw new RockskyError(res.data);
138
+ }
139
+
140
+ /** Scrobble a play (app.rocksky.scrobble). createdAt defaults to now. */
141
+ async scrobble(rec: ScrobbleInput): Promise<string> {
142
+ const record = { ...rec, createdAt: rec.createdAt || nowISO() };
143
+ if (this.idx) {
144
+ const secs = Math.floor(Date.parse(record.createdAt) / 1000);
145
+ const existing = await this.idx.scrobbleUri(this.did, record.title!, record.artist!, record.album!, secs);
146
+ if (existing) return existing;
147
+ }
148
+ const uri = await this.create(C_SCROBBLE, record as Record<string, unknown>);
149
+ if (this.idx) {
150
+ const secs = Math.floor(Date.parse(record.createdAt) / 1000);
151
+ await this.idx.recordScrobble(this.did, record.title!, record.artist!, record.album!, secs, uri);
152
+ }
153
+ return uri;
154
+ }
155
+
156
+ /** Create a canonical track record (app.rocksky.song). */
157
+ async createSong(rec: SongInput): Promise<string> {
158
+ const record = { ...rec, createdAt: rec.createdAt || nowISO() };
159
+ if (this.idx) {
160
+ const existing = await this.idx.songUri(this.did, record.title!, record.artist!, record.album!);
161
+ if (existing) return existing;
162
+ }
163
+ const uri = await this.create(C_SONG, record as Record<string, unknown>);
164
+ if (this.idx) await this.idx.recordSong(this.did, record.title!, record.artist!, record.album!, uri);
165
+ return uri;
166
+ }
167
+
168
+ /** Create an album record (app.rocksky.album). `artist` is the album artist. */
169
+ async createAlbum(rec: AlbumInput): Promise<string> {
170
+ const record = { ...rec, createdAt: rec.createdAt || nowISO() };
171
+ if (this.idx) {
172
+ const existing = await this.idx.albumUri(this.did, record.title!, record.artist!);
173
+ if (existing) return existing;
174
+ }
175
+ const uri = await this.create(C_ALBUM, record as Record<string, unknown>);
176
+ if (this.idx) await this.idx.recordAlbum(this.did, record.title!, record.artist!, uri);
177
+ return uri;
178
+ }
179
+
180
+ /** Create an artist record (app.rocksky.artist). */
181
+ async createArtist(rec: ArtistInput): Promise<string> {
182
+ const record = { ...rec, createdAt: rec.createdAt || nowISO() };
183
+ if (this.idx) {
184
+ const existing = await this.idx.artistUri(this.did, record.name!);
185
+ if (existing) return existing;
186
+ }
187
+ const uri = await this.create(C_ARTIST, record as Record<string, unknown>);
188
+ if (this.idx) await this.idx.recordArtist(this.did, record.name!, uri);
189
+ return uri;
190
+ }
191
+
192
+ /** Like a record by strong reference (uri + cid). Returns the like URI. */
193
+ like(uri: string, cid: string): Promise<string> {
194
+ return this.create(C_LIKE, { subject: { uri, cid }, createdAt: nowISO() });
195
+ }
196
+
197
+ /** Follow an account by DID. Returns the follow URI. */
198
+ follow(did: string): Promise<string> {
199
+ return this.create(C_FOLLOW, { subject: did, createdAt: nowISO() });
200
+ }
201
+
202
+ /** Post a shout on a subject. Returns the shout URI. */
203
+ shout(subjectUri: string, subjectCid: string, message: string): Promise<string> {
204
+ return this.create(C_SHOUT, { subject: { uri: subjectUri, cid: subjectCid }, message, createdAt: nowISO() });
205
+ }
206
+
207
+ /** Reply to a shout, with a parent strong-ref. */
208
+ replyShout(
209
+ subjectUri: string,
210
+ subjectCid: string,
211
+ parentUri: string,
212
+ parentCid: string,
213
+ message: string,
214
+ ): Promise<string> {
215
+ return this.create(C_SHOUT, {
216
+ subject: { uri: subjectUri, cid: subjectCid },
217
+ parent: { uri: parentUri, cid: parentCid },
218
+ message,
219
+ createdAt: nowISO(),
220
+ });
221
+ }
222
+
223
+ /** Upsert the actor's now-playing status singleton (rkey "self"). */
224
+ setNowPlaying(track: ActorTrackView): Promise<string> {
225
+ return this.putRecord(C_STATUS, "self", { track, startedAt: nowISO() });
226
+ }
227
+
228
+ /** Delete the actor's now-playing status singleton. */
229
+ clearNowPlaying(): Promise<void> {
230
+ return this.delete(C_STATUS, "self");
231
+ }
232
+ }
package/src/client.ts CHANGED
@@ -1,252 +1,108 @@
1
- import { buildConfig, type HttpClientConfig, xrpcCall } from "./http.js";
2
- import { makeCall } from "./namespaces/_helpers.js";
3
- import {
4
- type PaginateArgs,
5
- paginate as paginateFn,
6
- } from "./paginate.js";
7
- import {
8
- RealtimeClient,
9
- type RealtimeOptions,
10
- createRealtimeClient,
11
- } from "./realtime.js";
12
- import { ActorNamespace } from "./namespaces/actor.js";
13
- import { AlbumNamespace } from "./namespaces/album.js";
14
- import { ApikeyNamespace } from "./namespaces/apikey.js";
15
- import { ArtistNamespace } from "./namespaces/artist.js";
16
- import { ChartsNamespace } from "./namespaces/charts.js";
17
- import { DropboxNamespace } from "./namespaces/dropbox.js";
18
- import { FeedNamespace } from "./namespaces/feed.js";
19
- import { GoogleDriveNamespace } from "./namespaces/googledrive.js";
20
- import { GraphNamespace } from "./namespaces/graph.js";
21
- import { LikeNamespace } from "./namespaces/like.js";
22
- import { MirrorNamespace } from "./namespaces/mirror.js";
23
- import { PlayerNamespace } from "./namespaces/player.js";
24
- import { PlaylistNamespace } from "./namespaces/playlist.js";
25
- import { ScrobbleNamespace } from "./namespaces/scrobble.js";
26
- import { ShoutNamespace } from "./namespaces/shout.js";
27
- import { SongNamespace } from "./namespaces/song.js";
28
- import { SpotifyNamespace } from "./namespaces/spotify.js";
29
- import { StatsNamespace } from "./namespaces/stats.js";
30
- import type { Endpoints } from "./generated/types.js";
31
- import type {
32
- AuthProvider,
33
- ClientOptions,
34
- FetchLike,
35
- RequestOptions,
36
- } from "./types.js";
37
-
38
- type XrpcOpts = {
39
- params?: Record<string, unknown>;
40
- body?: unknown;
41
- requireAuth?: boolean;
42
- } & RequestOptions;
1
+ import { Client, simpleFetchHandler } from "@atcute/client";
43
2
 
3
+ import { RockskyError } from "./errors.js";
4
+ import type {
5
+ ActorProfileViewDetailed,
6
+ AlbumViewBasic,
7
+ ArtistViewBasic,
8
+ FeedSearchResultsView,
9
+ GetActorAlbumsOutput,
10
+ GetActorArtistsOutput,
11
+ GetActorScrobblesOutput,
12
+ GetActorSongsOutput,
13
+ GetTopArtistsOutput,
14
+ GetTopTracksOutput,
15
+ ScrobbleViewBasic,
16
+ SongViewBasic,
17
+ StatsGlobalStatsView,
18
+ } from "./generated/types.js";
19
+
20
+ /** The default public Rocksky AppView base URL. */
21
+ export const DEFAULT_APPVIEW = "https://api.rocksky.app";
22
+
23
+ /** Unauthenticated read client over the public Rocksky AppView XRPC. */
44
24
  export class RockskyClient {
45
- readonly config: HttpClientConfig;
46
-
47
- readonly actor: ActorNamespace;
48
- readonly album: AlbumNamespace;
49
- readonly apikey: ApikeyNamespace;
50
- readonly artist: ArtistNamespace;
51
- readonly charts: ChartsNamespace;
52
- readonly dropbox: DropboxNamespace;
53
- readonly feed: FeedNamespace;
54
- readonly googledrive: GoogleDriveNamespace;
55
- readonly graph: GraphNamespace;
56
- readonly like: LikeNamespace;
57
- readonly mirror: MirrorNamespace;
58
- readonly player: PlayerNamespace;
59
- readonly playlist: PlaylistNamespace;
60
- readonly scrobble: ScrobbleNamespace;
61
- readonly shout: ShoutNamespace;
62
- readonly song: SongNamespace;
63
- readonly spotify: SpotifyNamespace;
64
- readonly stats: StatsNamespace;
25
+ private rpc: Client;
65
26
 
66
- constructor(options: ClientOptions = {}) {
67
- this.config = buildConfig(options);
68
- const call = makeCall(this.config);
69
-
70
- this.actor = new ActorNamespace(call);
71
- this.album = new AlbumNamespace(call);
72
- this.apikey = new ApikeyNamespace(call);
73
- this.artist = new ArtistNamespace(call);
74
- this.charts = new ChartsNamespace(call);
75
- this.dropbox = new DropboxNamespace(call);
76
- this.feed = new FeedNamespace(call);
77
- this.googledrive = new GoogleDriveNamespace(call);
78
- this.graph = new GraphNamespace(call);
79
- this.like = new LikeNamespace(call);
80
- this.mirror = new MirrorNamespace(call);
81
- this.player = new PlayerNamespace(call);
82
- this.playlist = new PlaylistNamespace(call);
83
- this.scrobble = new ScrobbleNamespace(call);
84
- this.shout = new ShoutNamespace(call);
85
- this.song = new SongNamespace(call);
86
- this.spotify = new SpotifyNamespace(call);
87
- this.stats = new StatsNamespace(call);
27
+ /** Build a read client against an AppView base URL (defaults to {@link DEFAULT_APPVIEW}). */
28
+ constructor(appview: string = DEFAULT_APPVIEW) {
29
+ this.rpc = new Client({ handler: simpleFetchHandler({ service: appview }) });
88
30
  }
89
31
 
90
- /** Build a one-off authenticated copy without mutating this client. */
91
- withAuth(auth: AuthProvider): RockskyClient {
92
- return new RockskyClient({
93
- ...this.optionsSnapshot(),
94
- auth,
95
- });
32
+ private async query<T>(nsid: string, params: Record<string, unknown>): Promise<T> {
33
+ const clean: Record<string, unknown> = {};
34
+ for (const [k, v] of Object.entries(params)) {
35
+ if (v !== undefined && v !== "") clean[k] = v;
36
+ }
37
+ const res = await this.rpc.get(nsid as never, { params: clean } as never);
38
+ if (!res.ok) throw new RockskyError(res.data);
39
+ return res.data as T;
96
40
  }
97
41
 
98
- /** Build a copy with an overridden base URL. */
99
- withBaseUrl(baseUrl: string): RockskyClient {
100
- return new RockskyClient({
101
- ...this.optionsSnapshot(),
102
- baseUrl,
103
- });
42
+ /** An actor's detailed profile. `actor` is a handle or DID. */
43
+ profile(actor: string): Promise<ActorProfileViewDetailed> {
44
+ return this.query("app.rocksky.actor.getProfile", { did: actor });
104
45
  }
105
46
 
106
- /**
107
- * Open a realtime WebSocket connection to /ws.
108
- *
109
- * const rt = client.realtime({ token, clientName: "my-app" });
110
- * rt.on("message", m => console.log(m));
111
- * await rt.connect();
112
- *
113
- * Defaults `baseUrl` to this client's base URL. Override anything by
114
- * passing the option, or use `RealtimeClient.builder()` for full control.
115
- */
116
- realtime(
117
- options: Omit<RealtimeOptions, "baseUrl"> &
118
- Partial<Pick<RealtimeOptions, "baseUrl">>,
119
- ): RealtimeClient {
120
- return createRealtimeClient({
121
- baseUrl: this.config.baseUrl,
122
- ...options,
47
+ /** An actor's scrobbles, newest first. */
48
+ async scrobbles(actor: string, limit = 50, offset = 0): Promise<ScrobbleViewBasic[]> {
49
+ const out = await this.query<GetActorScrobblesOutput>("app.rocksky.actor.getActorScrobbles", {
50
+ did: actor,
51
+ limit,
52
+ offset,
123
53
  });
54
+ return out.scrobbles ?? [];
124
55
  }
125
56
 
126
- /**
127
- * Page through any limit/offset or cursor-based endpoint as an async iterable.
128
- *
129
- * for await (const s of client.paginate({
130
- * fetch: ({ limit, offset }) =>
131
- * client.actor.getActorScrobbles({ did, limit, offset }),
132
- * pageSize: 50,
133
- * })) { ... }
134
- */
135
- paginate<T>(args: PaginateArgs<T>) {
136
- return paginateFn(args);
137
- }
138
-
139
- /**
140
- * Direct escape hatch — call any XRPC endpoint by NSID.
141
- *
142
- * Known NSIDs (string literals) are typed via the generated `Endpoints`
143
- * map; arbitrary strings fall back to `unknown` (override with `<T>`).
144
- */
145
- xrpc<K extends keyof Endpoints>(
146
- nsid: K,
147
- method?: "GET" | "POST",
148
- opts?: XrpcOpts,
149
- ): Promise<Endpoints[K]>;
150
- xrpc<T = unknown>(
151
- nsid: string,
152
- method?: "GET" | "POST",
153
- opts?: XrpcOpts,
154
- ): Promise<T>;
155
- xrpc(
156
- nsid: string,
157
- method: "GET" | "POST" = "GET",
158
- opts: XrpcOpts = {},
159
- ): Promise<unknown> {
160
- return xrpcCall(this.config, nsid, method, opts);
161
- }
162
-
163
- static builder(): RockskyClientBuilder {
164
- return new RockskyClientBuilder();
165
- }
166
-
167
- private optionsSnapshot(): ClientOptions {
168
- return {
169
- baseUrl: this.config.baseUrl,
170
- auth: this.config.auth,
171
- fetch: this.config.fetch,
172
- headers: this.config.headers,
173
- timeoutMs: this.config.timeoutMs,
174
- retries: this.config.retries,
175
- retryDelayMs: this.config.retryDelayMs,
176
- };
177
- }
178
- }
179
-
180
- /**
181
- * Fluent builder.
182
- *
183
- * const client = RockskyClient.builder()
184
- * .baseUrl("https://api.rocksky.app")
185
- * .auth(() => loadToken())
186
- * .timeout(10_000)
187
- * .retries(3)
188
- * .userAgent("my-app/1.0")
189
- * .build();
190
- */
191
- export class RockskyClientBuilder {
192
- private readonly opts: ClientOptions = {};
193
-
194
- baseUrl(url: string): this {
195
- this.opts.baseUrl = url;
196
- return this;
197
- }
198
-
199
- auth(auth: AuthProvider): this {
200
- this.opts.auth = auth;
201
- return this;
202
- }
203
-
204
- bearer(token: string): this {
205
- this.opts.auth = token;
206
- return this;
207
- }
208
-
209
- fetch(impl: FetchLike): this {
210
- this.opts.fetch = impl;
211
- return this;
212
- }
213
-
214
- header(key: string, value: string): this {
215
- this.opts.headers = { ...this.opts.headers, [key]: value };
216
- return this;
57
+ /** An actor's most-played songs. */
58
+ async songs(actor: string, limit = 50, offset = 0): Promise<SongViewBasic[]> {
59
+ const out = await this.query<GetActorSongsOutput>("app.rocksky.actor.getActorSongs", {
60
+ did: actor,
61
+ limit,
62
+ offset,
63
+ });
64
+ return out.songs ?? [];
217
65
  }
218
66
 
219
- headers(headers: Record<string, string>): this {
220
- this.opts.headers = { ...this.opts.headers, ...headers };
221
- return this;
67
+ /** An actor's most-played albums. */
68
+ async albums(actor: string, limit = 50, offset = 0): Promise<AlbumViewBasic[]> {
69
+ const out = await this.query<GetActorAlbumsOutput>("app.rocksky.actor.getActorAlbums", {
70
+ did: actor,
71
+ limit,
72
+ offset,
73
+ });
74
+ return out.albums ?? [];
222
75
  }
223
76
 
224
- userAgent(ua: string): this {
225
- this.opts.userAgent = ua;
226
- return this;
77
+ /** An actor's most-played artists. */
78
+ async artists(actor: string, limit = 50, offset = 0): Promise<ArtistViewBasic[]> {
79
+ const out = await this.query<GetActorArtistsOutput>("app.rocksky.actor.getActorArtists", {
80
+ did: actor,
81
+ limit,
82
+ offset,
83
+ });
84
+ return out.artists ?? [];
227
85
  }
228
86
 
229
- timeout(ms: number): this {
230
- this.opts.timeoutMs = ms;
231
- return this;
87
+ /** The platform-wide top tracks chart. */
88
+ async topTracks(limit = 50, offset = 0): Promise<SongViewBasic[]> {
89
+ const out = await this.query<GetTopTracksOutput>("app.rocksky.charts.getTopTracks", { limit, offset });
90
+ return out.tracks ?? [];
232
91
  }
233
92
 
234
- retries(n: number): this {
235
- this.opts.retries = n;
236
- return this;
93
+ /** The platform-wide top artists chart. */
94
+ async topArtists(limit = 50, offset = 0): Promise<ArtistViewBasic[]> {
95
+ const out = await this.query<GetTopArtistsOutput>("app.rocksky.charts.getTopArtists", { limit, offset });
96
+ return out.artists ?? [];
237
97
  }
238
98
 
239
- retryDelay(ms: number): this {
240
- this.opts.retryDelayMs = ms;
241
- return this;
99
+ /** Full-text search across songs, albums, artists, playlists, actors. */
100
+ search(query: string): Promise<FeedSearchResultsView> {
101
+ return this.query("app.rocksky.feed.search", { query });
242
102
  }
243
103
 
244
- build(): RockskyClient {
245
- return new RockskyClient(this.opts);
104
+ /** Platform-wide totals. */
105
+ globalStats(): Promise<StatsGlobalStatsView> {
106
+ return this.query("app.rocksky.stats.getGlobalStats", {});
246
107
  }
247
108
  }
248
-
249
- /** Convenience factory — equivalent to `new RockskyClient(options)`. */
250
- export function createClient(options: ClientOptions = {}): RockskyClient {
251
- return new RockskyClient(options);
252
- }