@rocksky/sdk 0.1.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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/dist/client.d.ts +107 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/errors.d.ts +26 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/http.d.ts +28 -0
  8. package/dist/http.d.ts.map +1 -0
  9. package/dist/index.d.ts +18 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +1492 -0
  12. package/dist/namespaces/_helpers.d.ts +9 -0
  13. package/dist/namespaces/_helpers.d.ts.map +1 -0
  14. package/dist/namespaces/actor.d.ts +30 -0
  15. package/dist/namespaces/actor.d.ts.map +1 -0
  16. package/dist/namespaces/album.d.ts +18 -0
  17. package/dist/namespaces/album.d.ts.map +1 -0
  18. package/dist/namespaces/apikey.d.ts +27 -0
  19. package/dist/namespaces/apikey.d.ts.map +1 -0
  20. package/dist/namespaces/artist.d.ts +31 -0
  21. package/dist/namespaces/artist.d.ts.map +1 -0
  22. package/dist/namespaces/charts.d.ts +25 -0
  23. package/dist/namespaces/charts.d.ts.map +1 -0
  24. package/dist/namespaces/dropbox.d.ts +19 -0
  25. package/dist/namespaces/dropbox.d.ts.map +1 -0
  26. package/dist/namespaces/feed.d.ts +38 -0
  27. package/dist/namespaces/feed.d.ts.map +1 -0
  28. package/dist/namespaces/googledrive.d.ts +16 -0
  29. package/dist/namespaces/googledrive.d.ts.map +1 -0
  30. package/dist/namespaces/graph.d.ts +28 -0
  31. package/dist/namespaces/graph.d.ts.map +1 -0
  32. package/dist/namespaces/like.d.ts +14 -0
  33. package/dist/namespaces/like.d.ts.map +1 -0
  34. package/dist/namespaces/mirror.d.ts +15 -0
  35. package/dist/namespaces/mirror.d.ts.map +1 -0
  36. package/dist/namespaces/player.d.ts +41 -0
  37. package/dist/namespaces/player.d.ts.map +1 -0
  38. package/dist/namespaces/playlist.d.ts +40 -0
  39. package/dist/namespaces/playlist.d.ts.map +1 -0
  40. package/dist/namespaces/scrobble.d.ts +43 -0
  41. package/dist/namespaces/scrobble.d.ts.map +1 -0
  42. package/dist/namespaces/shout.d.ts +38 -0
  43. package/dist/namespaces/shout.d.ts.map +1 -0
  44. package/dist/namespaces/song.d.ts +48 -0
  45. package/dist/namespaces/song.d.ts.map +1 -0
  46. package/dist/namespaces/spotify.d.ts +17 -0
  47. package/dist/namespaces/spotify.d.ts.map +1 -0
  48. package/dist/namespaces/stats.d.ts +14 -0
  49. package/dist/namespaces/stats.d.ts.map +1 -0
  50. package/dist/paginate.d.ts +38 -0
  51. package/dist/paginate.d.ts.map +1 -0
  52. package/dist/pipe.d.ts +36 -0
  53. package/dist/pipe.d.ts.map +1 -0
  54. package/dist/realtime.d.ts +144 -0
  55. package/dist/realtime.d.ts.map +1 -0
  56. package/dist/types.d.ts +29 -0
  57. package/dist/types.d.ts.map +1 -0
  58. package/package.json +65 -0
  59. package/src/client.ts +235 -0
  60. package/src/errors.ts +47 -0
  61. package/src/http.ts +195 -0
  62. package/src/index.ts +85 -0
  63. package/src/namespaces/_helpers.ts +17 -0
  64. package/src/namespaces/actor.ts +89 -0
  65. package/src/namespaces/album.ts +34 -0
  66. package/src/namespaces/apikey.ts +50 -0
  67. package/src/namespaces/artist.ts +74 -0
  68. package/src/namespaces/charts.ts +47 -0
  69. package/src/namespaces/dropbox.ts +47 -0
  70. package/src/namespaces/feed.ts +106 -0
  71. package/src/namespaces/googledrive.ts +36 -0
  72. package/src/namespaces/graph.ts +63 -0
  73. package/src/namespaces/like.ts +40 -0
  74. package/src/namespaces/mirror.ts +31 -0
  75. package/src/namespaces/player.ts +127 -0
  76. package/src/namespaces/playlist.ts +85 -0
  77. package/src/namespaces/scrobble.ts +67 -0
  78. package/src/namespaces/shout.ts +90 -0
  79. package/src/namespaces/song.ts +81 -0
  80. package/src/namespaces/spotify.ts +52 -0
  81. package/src/namespaces/stats.ts +23 -0
  82. package/src/paginate.ts +90 -0
  83. package/src/pipe.ts +146 -0
  84. package/src/realtime.ts +408 -0
  85. package/src/types.ts +41 -0
@@ -0,0 +1,81 @@
1
+ import type { Call } from "./_helpers";
2
+ import type { RequestOptions } from "../types";
3
+
4
+ export type GetSongParams = {
5
+ uri?: string;
6
+ mbid?: string;
7
+ isrc?: string;
8
+ spotifyId?: string;
9
+ };
10
+
11
+ export type GetSongsParams = GetSongParams & {
12
+ limit?: number;
13
+ offset?: number;
14
+ genre?: string;
15
+ };
16
+
17
+ export type MatchSongParams = {
18
+ title: string;
19
+ artist: string;
20
+ mbId?: string;
21
+ isrc?: string;
22
+ };
23
+
24
+ export type CreateSongInput = {
25
+ title: string;
26
+ artist: string;
27
+ albumArtist: string;
28
+ album: string;
29
+ duration?: number;
30
+ mbId?: string;
31
+ isrc?: string;
32
+ albumArt?: string;
33
+ trackNumber?: number;
34
+ releaseDate?: string;
35
+ year?: number;
36
+ discNumber?: number;
37
+ lyrics?: string;
38
+ };
39
+
40
+ export class SongNamespace {
41
+ constructor(private readonly call: Call) {}
42
+
43
+ getSong<T = unknown>(params: GetSongParams, opts?: RequestOptions) {
44
+ return this.call<T>("app.rocksky.song.getSong", "GET", {
45
+ params,
46
+ ...opts,
47
+ });
48
+ }
49
+
50
+ getSongs<T = unknown>(params: GetSongsParams = {}, opts?: RequestOptions) {
51
+ return this.call<T>("app.rocksky.song.getSongs", "GET", {
52
+ params,
53
+ ...opts,
54
+ });
55
+ }
56
+
57
+ getSongRecentListeners<T = unknown>(
58
+ params: { uri: string; limit?: number; offset?: number },
59
+ opts?: RequestOptions,
60
+ ) {
61
+ return this.call<T>("app.rocksky.song.getSongRecentListeners", "GET", {
62
+ params,
63
+ ...opts,
64
+ });
65
+ }
66
+
67
+ matchSong<T = unknown>(params: MatchSongParams, opts?: RequestOptions) {
68
+ return this.call<T>("app.rocksky.song.matchSong", "GET", {
69
+ params,
70
+ ...opts,
71
+ });
72
+ }
73
+
74
+ createSong<T = unknown>(input: CreateSongInput, opts?: RequestOptions) {
75
+ return this.call<T>("app.rocksky.song.createSong", "POST", {
76
+ body: input,
77
+ requireAuth: true,
78
+ ...opts,
79
+ });
80
+ }
81
+ }
@@ -0,0 +1,52 @@
1
+ import type { Call } from "./_helpers";
2
+ import type { RequestOptions } from "../types";
3
+
4
+ export class SpotifyNamespace {
5
+ constructor(private readonly call: Call) {}
6
+
7
+ getCurrentlyPlaying<T = unknown>(
8
+ params: { actor?: string } = {},
9
+ opts?: RequestOptions,
10
+ ) {
11
+ return this.call<T>("app.rocksky.spotify.getCurrentlyPlaying", "GET", {
12
+ params,
13
+ ...opts,
14
+ });
15
+ }
16
+
17
+ play<T = unknown>(opts?: RequestOptions) {
18
+ return this.call<T>("app.rocksky.spotify.play", "POST", {
19
+ requireAuth: true,
20
+ ...opts,
21
+ });
22
+ }
23
+
24
+ pause<T = unknown>(opts?: RequestOptions) {
25
+ return this.call<T>("app.rocksky.spotify.pause", "POST", {
26
+ requireAuth: true,
27
+ ...opts,
28
+ });
29
+ }
30
+
31
+ next<T = unknown>(opts?: RequestOptions) {
32
+ return this.call<T>("app.rocksky.spotify.next", "POST", {
33
+ requireAuth: true,
34
+ ...opts,
35
+ });
36
+ }
37
+
38
+ previous<T = unknown>(opts?: RequestOptions) {
39
+ return this.call<T>("app.rocksky.spotify.previous", "POST", {
40
+ requireAuth: true,
41
+ ...opts,
42
+ });
43
+ }
44
+
45
+ seek<T = unknown>(params: { position: number }, opts?: RequestOptions) {
46
+ return this.call<T>("app.rocksky.spotify.seek", "POST", {
47
+ params,
48
+ requireAuth: true,
49
+ ...opts,
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,23 @@
1
+ import type { Call } from "./_helpers";
2
+ import type { RequestOptions } from "../types";
3
+
4
+ export class StatsNamespace {
5
+ constructor(private readonly call: Call) {}
6
+
7
+ getStats<T = unknown>(params: { did: string }, opts?: RequestOptions) {
8
+ return this.call<T>("app.rocksky.stats.getStats", "GET", {
9
+ params,
10
+ ...opts,
11
+ });
12
+ }
13
+
14
+ getWrapped<T = unknown>(
15
+ params: { did: string; year?: number },
16
+ opts?: RequestOptions,
17
+ ) {
18
+ return this.call<T>("app.rocksky.stats.getWrapped", "GET", {
19
+ params,
20
+ ...opts,
21
+ });
22
+ }
23
+ }
@@ -0,0 +1,90 @@
1
+ export type PageOpts = {
2
+ limit: number;
3
+ offset: number;
4
+ cursor?: string;
5
+ };
6
+
7
+ export type PageResult<T> = {
8
+ items: T[];
9
+ cursor?: string;
10
+ };
11
+
12
+ export type PaginateArgs<T> = {
13
+ /**
14
+ * Fetch one page. Return either:
15
+ * - an array of items (offset/limit mode — pagination stops when an empty
16
+ * page or a short page comes back), or
17
+ * - a `{ items, cursor }` object (cursor mode — pagination stops when
18
+ * `cursor` is missing/null).
19
+ */
20
+ fetch: (opts: PageOpts) => Promise<PageResult<T> | T[]>;
21
+ pageSize?: number;
22
+ maxItems?: number;
23
+ signal?: AbortSignal;
24
+ };
25
+
26
+ /**
27
+ * Page through an XRPC endpoint as an async iterable.
28
+ *
29
+ * for await (const s of paginate({
30
+ * fetch: ({ limit, offset }) =>
31
+ * client.actor.getActorScrobbles({ did, limit, offset }),
32
+ * pageSize: 50,
33
+ * })) {
34
+ * console.log(s.track.title);
35
+ * }
36
+ */
37
+ export function paginate<T>(args: PaginateArgs<T>): AsyncIterable<T> & {
38
+ /** Collect every item into an array — convenience for small result sets. */
39
+ toArray(): Promise<T[]>;
40
+ } {
41
+ const pageSize = args.pageSize ?? 50;
42
+ const maxItems = args.maxItems ?? Number.POSITIVE_INFINITY;
43
+
44
+ async function* iter(): AsyncGenerator<T> {
45
+ let offset = 0;
46
+ let cursor: string | undefined;
47
+ let yielded = 0;
48
+ while (yielded < maxItems) {
49
+ checkAbort(args.signal);
50
+ const result = await args.fetch({ limit: pageSize, offset, cursor });
51
+ checkAbort(args.signal);
52
+ const { items, nextCursor } = normalize(result);
53
+ if (items.length === 0) return;
54
+ for (const item of items) {
55
+ if (yielded >= maxItems) return;
56
+ yield item;
57
+ yielded++;
58
+ }
59
+ offset += items.length;
60
+ if (nextCursor !== undefined) {
61
+ if (nextCursor === cursor || nextCursor === null) return;
62
+ cursor = nextCursor ?? undefined;
63
+ } else if (items.length < pageSize) {
64
+ return;
65
+ }
66
+ }
67
+ }
68
+
69
+ const iterable: AsyncIterable<T> = { [Symbol.asyncIterator]: iter };
70
+ return Object.assign(iterable, {
71
+ async toArray(): Promise<T[]> {
72
+ const out: T[] = [];
73
+ for await (const item of iter()) out.push(item);
74
+ return out;
75
+ },
76
+ });
77
+ }
78
+
79
+ function normalize<T>(
80
+ result: PageResult<T> | T[],
81
+ ): { items: T[]; nextCursor?: string | null } {
82
+ if (Array.isArray(result)) return { items: result };
83
+ return { items: result.items, nextCursor: result.cursor ?? null };
84
+ }
85
+
86
+ function checkAbort(signal: AbortSignal | undefined): void {
87
+ if (signal?.aborted) {
88
+ throw signal.reason ?? new DOMException("aborted", "AbortError");
89
+ }
90
+ }
package/src/pipe.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { RockskyTimeoutError } from "./errors";
2
+
3
+ /**
4
+ * Compose async operators.
5
+ *
6
+ * const handle = await pipe(
7
+ * () => client.actor.getProfile({ did }), // thunk — withRetry can re-invoke
8
+ * withRetry(3),
9
+ * withTimeout(5_000),
10
+ * tap(p => console.log("got", p.handle)),
11
+ * map(p => p.handle.toUpperCase()),
12
+ * );
13
+ *
14
+ * `pipe` accepts either a thunk `() => Promise<T>` or a bare `Promise<T>`.
15
+ * Operators take a factory so they can re-invoke the upstream chain (for
16
+ * retry). If you pass a bare Promise, the work has already started and
17
+ * `withRetry` becomes a no-op — pass a thunk if you want true retries.
18
+ */
19
+ export type Op<A, B> = (factory: () => Promise<A>) => () => Promise<B>;
20
+
21
+ type AnyOp = Op<any, any>;
22
+ type Source<A> = Promise<A> | (() => Promise<A>);
23
+
24
+ export function pipe<A>(value: Source<A>): Promise<A>;
25
+ export function pipe<A, B>(value: Source<A>, op1: Op<A, B>): Promise<B>;
26
+ export function pipe<A, B, C>(
27
+ value: Source<A>,
28
+ op1: Op<A, B>,
29
+ op2: Op<B, C>,
30
+ ): Promise<C>;
31
+ export function pipe<A, B, C, D>(
32
+ value: Source<A>,
33
+ op1: Op<A, B>,
34
+ op2: Op<B, C>,
35
+ op3: Op<C, D>,
36
+ ): Promise<D>;
37
+ export function pipe<A, B, C, D, E>(
38
+ value: Source<A>,
39
+ op1: Op<A, B>,
40
+ op2: Op<B, C>,
41
+ op3: Op<C, D>,
42
+ op4: Op<D, E>,
43
+ ): Promise<E>;
44
+ export function pipe<A, B, C, D, E, F>(
45
+ value: Source<A>,
46
+ op1: Op<A, B>,
47
+ op2: Op<B, C>,
48
+ op3: Op<C, D>,
49
+ op4: Op<D, E>,
50
+ op5: Op<E, F>,
51
+ ): Promise<F>;
52
+ export function pipe(
53
+ value: Source<unknown>,
54
+ ...ops: AnyOp[]
55
+ ): Promise<unknown> {
56
+ let factory: () => Promise<unknown> =
57
+ typeof value === "function" ? (value as () => Promise<unknown>) : () => value;
58
+ for (const op of ops) factory = op(factory);
59
+ return factory();
60
+ }
61
+
62
+ export function map<A, B>(fn: (a: A) => B | Promise<B>): Op<A, B> {
63
+ return (factory) => async () => fn(await factory());
64
+ }
65
+
66
+ export function tap<A>(fn: (a: A) => unknown | Promise<unknown>): Op<A, A> {
67
+ return (factory) => async () => {
68
+ const a = await factory();
69
+ await fn(a);
70
+ return a;
71
+ };
72
+ }
73
+
74
+ export function withRetry<A>(
75
+ times: number,
76
+ options: {
77
+ delayMs?: number;
78
+ factor?: number;
79
+ shouldRetry?: (err: unknown, attempt: number) => boolean;
80
+ } = {},
81
+ ): Op<A, A> {
82
+ const delayMs = options.delayMs ?? 200;
83
+ const factor = options.factor ?? 2;
84
+ const shouldRetry = options.shouldRetry ?? (() => true);
85
+ return (factory) => async () => {
86
+ let lastErr: unknown;
87
+ for (let attempt = 0; attempt <= times; attempt++) {
88
+ try {
89
+ return await factory();
90
+ } catch (err) {
91
+ lastErr = err;
92
+ if (attempt === times || !shouldRetry(err, attempt)) throw err;
93
+ await sleep(delayMs * Math.pow(factor, attempt));
94
+ }
95
+ }
96
+ throw lastErr;
97
+ };
98
+ }
99
+
100
+ export function withTimeout<A>(ms: number): Op<A, A> {
101
+ return (factory) => () =>
102
+ new Promise<A>((resolve, reject) => {
103
+ const timer = setTimeout(
104
+ () => reject(new RockskyTimeoutError(ms)),
105
+ ms,
106
+ );
107
+ factory().then(
108
+ (v) => {
109
+ clearTimeout(timer);
110
+ resolve(v);
111
+ },
112
+ (err) => {
113
+ clearTimeout(timer);
114
+ reject(err);
115
+ },
116
+ );
117
+ });
118
+ }
119
+
120
+ export function withFallback<A>(fallback: A | ((err: unknown) => A | Promise<A>)): Op<A, A> {
121
+ return (factory) => async () => {
122
+ try {
123
+ return await factory();
124
+ } catch (err) {
125
+ return typeof fallback === "function"
126
+ ? await (fallback as (e: unknown) => A | Promise<A>)(err)
127
+ : fallback;
128
+ }
129
+ };
130
+ }
131
+
132
+ export function catchError<A, B>(
133
+ handler: (err: unknown) => B | Promise<B>,
134
+ ): Op<A, A | B> {
135
+ return (factory) => async () => {
136
+ try {
137
+ return await factory();
138
+ } catch (err) {
139
+ return await handler(err);
140
+ }
141
+ };
142
+ }
143
+
144
+ function sleep(ms: number): Promise<void> {
145
+ return new Promise((r) => setTimeout(r, ms));
146
+ }