@misofm/sdk 0.4.0 → 0.5.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 (48) hide show
  1. package/dist/catalog.d.ts +4 -17
  2. package/dist/catalog.d.ts.map +1 -1
  3. package/dist/catalog.js +46 -58
  4. package/dist/catalog.js.map +1 -1
  5. package/dist/cover.d.ts +10 -0
  6. package/dist/cover.d.ts.map +1 -1
  7. package/dist/cover.js +66 -19
  8. package/dist/cover.js.map +1 -1
  9. package/dist/credits.d.ts +10 -0
  10. package/dist/credits.d.ts.map +1 -1
  11. package/dist/credits.js +149 -39
  12. package/dist/credits.js.map +1 -1
  13. package/dist/drop.d.ts +7 -0
  14. package/dist/drop.d.ts.map +1 -1
  15. package/dist/drop.js +84 -34
  16. package/dist/drop.js.map +1 -1
  17. package/dist/pressing.d.ts.map +1 -1
  18. package/dist/pressing.js +59 -16
  19. package/dist/pressing.js.map +1 -1
  20. package/dist/read/artist.d.ts +1 -6
  21. package/dist/read/artist.d.ts.map +1 -1
  22. package/dist/read/artist.js +39 -22
  23. package/dist/read/artist.js.map +1 -1
  24. package/dist/read/catalog.d.ts +24 -3
  25. package/dist/read/catalog.d.ts.map +1 -1
  26. package/dist/read/catalog.js +190 -89
  27. package/dist/read/catalog.js.map +1 -1
  28. package/dist/read/index.d.ts +5 -3
  29. package/dist/read/index.d.ts.map +1 -1
  30. package/dist/read/index.js +2 -2
  31. package/dist/read/index.js.map +1 -1
  32. package/dist/read/types.d.ts +12 -1
  33. package/dist/read/types.d.ts.map +1 -1
  34. package/dist/read/works.d.ts +3 -2
  35. package/dist/read/works.d.ts.map +1 -1
  36. package/dist/read/works.js +158 -48
  37. package/dist/read/works.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/catalog.ts +71 -77
  40. package/src/cover.ts +107 -26
  41. package/src/credits.ts +353 -89
  42. package/src/drop.ts +150 -40
  43. package/src/pressing.ts +146 -36
  44. package/src/read/artist.ts +73 -34
  45. package/src/read/catalog.ts +324 -89
  46. package/src/read/index.ts +23 -2
  47. package/src/read/types.ts +16 -2
  48. package/src/read/works.ts +224 -59
package/src/read/works.ts CHANGED
@@ -3,14 +3,15 @@
3
3
 
4
4
  import type { ClientWithCoreApi } from "@mysten/sui/client";
5
5
  import type { SuiGraphQLClient } from "@mysten/sui/graphql";
6
+ import { normalizeSuiAddress } from "@mysten/sui/utils";
6
7
  import {
8
+ contracts,
7
9
  extractTypeParams2,
8
10
  getCompositionsByIds,
9
- getRecordingsByIds,
10
- getReleasesByIds,
11
11
  type Composition,
12
12
  type Recording,
13
13
  type Release,
14
+ type TrackState,
14
15
  } from "@misonetwork/sdk";
15
16
 
16
17
  export interface WorkShareTypes {
@@ -23,70 +24,114 @@ export interface WorkAddressesByShareType {
23
24
  recordings: Partial<Record<string, string>>;
24
25
  }
25
26
 
26
- interface ObjectsByTypeResult {
27
- objects: {
28
- nodes: Array<{
29
- address: string;
30
- asMoveObject: { contents: { type: { repr: string } } | null } | null;
31
- }>;
32
- } | null;
27
+ interface WorkAddressConnection {
28
+ nodes: Array<{
29
+ address: string;
30
+ asMoveObject?: {
31
+ contents?: { type?: { repr?: string } | null } | null;
32
+ } | null;
33
+ }>;
34
+ pageInfo?: { hasNextPage: boolean; endCursor: string | null };
33
35
  }
34
36
 
35
- /** Resolve work share types without relying on unpublished protocol helpers. */
37
+ /** Resolve all work share types in one aliased GraphQL request. */
36
38
  export async function getWorkAddressesByShareTypes(
37
39
  client: SuiGraphQLClient,
38
40
  shareTypes: WorkShareTypes,
39
41
  misoPackageId: string,
40
42
  ): Promise<WorkAddressesByShareType> {
41
43
  const compositions = [...new Set(shareTypes.compositions)];
42
- const recordingSet = new Set(shareTypes.recordings);
44
+ const recordings = new Set(shareTypes.recordings);
43
45
  const out: WorkAddressesByShareType = { compositions: {}, recordings: {} };
46
+ if (compositions.length === 0 && recordings.size === 0) return out;
44
47
 
45
- const compositionEntries = await Promise.all(
46
- compositions.map(async (shareType) => {
47
- const type = `${misoPackageId}::composition::Composition<${shareType}>`;
48
- const result = await client.query<ObjectsByTypeResult, { type: string }>({
49
- query: `query WorkByType($type: String!) {
50
- objects(filter: { type: $type }) {
51
- nodes { address asMoveObject { contents { type { repr } } } }
52
- }
53
- }`,
54
- variables: { type },
55
- });
56
- if (result.errors?.length) throw new Error(result.errors[0]!.message);
57
- return [shareType, result.data?.objects?.nodes[0]?.address] as const;
58
- }),
59
- );
60
- for (const [shareType, address] of compositionEntries) {
61
- if (address) out.compositions[shareType] = address;
48
+ const declarations: string[] = [];
49
+ const selections: string[] = [];
50
+ const variables: Record<string, string> = {};
51
+ compositions.forEach((shareType, index) => {
52
+ const variable = `compositionType${index}`;
53
+ declarations.push(`$${variable}: String!`);
54
+ selections.push(
55
+ `composition${index}: objects(first: 1, filter: { type: $${variable} }) { nodes { address } }`,
56
+ );
57
+ variables[variable] =
58
+ `${misoPackageId}::composition::Composition<${shareType}>`;
59
+ });
60
+ if (recordings.size > 0) {
61
+ declarations.push("$recordingType: String!");
62
+ selections.push(`recordings: objects(first: 50, filter: { type: $recordingType }) {
63
+ pageInfo { hasNextPage endCursor }
64
+ nodes { address asMoveObject { contents { type { repr } } } }
65
+ }`);
66
+ variables.recordingType = `${misoPackageId}::recording::Recording`;
62
67
  }
63
68
 
64
- if (recordingSet.size > 0) {
65
- const type = `${misoPackageId}::recording::Recording`;
66
- const result = await client.query<ObjectsByTypeResult, { type: string }>({
67
- query: `query RecordingsByType($type: String!) {
68
- objects(filter: { type: $type }) {
69
- nodes { address asMoveObject { contents { type { repr } } } }
70
- }
71
- }`,
72
- variables: { type },
73
- });
74
- if (result.errors?.length) throw new Error(result.errors[0]!.message);
75
- for (const node of result.data?.objects?.nodes ?? []) {
76
- const repr = node.asMoveObject?.contents?.type.repr;
69
+ const result = await client.query<
70
+ Record<string, WorkAddressConnection | null>,
71
+ Record<string, string>
72
+ >({
73
+ query: `query WorkAddressesByShareTypes(${declarations.join(", ")}) {
74
+ ${selections.join("\n")}
75
+ }`,
76
+ variables,
77
+ });
78
+ if (result.errors?.length) {
79
+ throw new AggregateError(
80
+ result.errors.map((error) => new Error(error.message)),
81
+ "Work type discovery failed",
82
+ );
83
+ }
84
+ compositions.forEach((shareType, index) => {
85
+ const address = result.data?.[`composition${index}`]?.nodes[0]?.address;
86
+ if (address) out.compositions[shareType] = address;
87
+ });
88
+
89
+ const readRecordingPage = (
90
+ page: WorkAddressConnection | null | undefined,
91
+ ) => {
92
+ for (const node of page?.nodes ?? []) {
93
+ const repr = node.asMoveObject?.contents?.type?.repr;
77
94
  if (!repr) continue;
78
- let recordingShareType: string | undefined;
79
95
  try {
80
- [recordingShareType] = extractTypeParams2(repr);
96
+ const [shareType] = extractTypeParams2(repr);
97
+ if (recordings.has(shareType)) out.recordings[shareType] = node.address;
81
98
  } catch {
82
- recordingShareType = /<(.+)>$/.exec(repr)?.[1]?.trim();
83
- }
84
- if (recordingShareType && recordingSet.has(recordingShareType)) {
85
- out.recordings[recordingShareType] = node.address;
99
+ // Ignore objects whose deployed type does not match the Recording ABI.
86
100
  }
87
101
  }
88
- }
102
+ };
89
103
 
104
+ let page = result.data?.recordings;
105
+ readRecordingPage(page);
106
+ while (
107
+ page?.pageInfo?.hasNextPage &&
108
+ page.pageInfo.endCursor &&
109
+ Object.keys(out.recordings).length < recordings.size
110
+ ) {
111
+ const next = await client.query<
112
+ { recordings: WorkAddressConnection | null },
113
+ { recordingType: string; cursor: string }
114
+ >({
115
+ query: `query RecordingWorkAddresses($recordingType: String!, $cursor: String!) {
116
+ recordings: objects(first: 50, after: $cursor, filter: { type: $recordingType }) {
117
+ pageInfo { hasNextPage endCursor }
118
+ nodes { address asMoveObject { contents { type { repr } } } }
119
+ }
120
+ }`,
121
+ variables: {
122
+ recordingType: variables.recordingType!,
123
+ cursor: page.pageInfo.endCursor,
124
+ },
125
+ });
126
+ if (next.errors?.length) {
127
+ throw new AggregateError(
128
+ next.errors.map((error) => new Error(error.message)),
129
+ "Recording type discovery failed",
130
+ );
131
+ }
132
+ page = next.data?.recordings;
133
+ readRecordingPage(page);
134
+ }
90
135
  return out;
91
136
  }
92
137
 
@@ -102,14 +147,127 @@ export interface WorksById {
102
147
  releases: Partial<Record<string, Release>>;
103
148
  }
104
149
 
105
- /** Fetch each work kind through the authoritative protocol SDK. */
106
- export async function getWorksByIds(client: ClientWithCoreApi, ids: WorkIds): Promise<WorksById> {
107
- const [compositions, recordings, releases] = await Promise.all([
108
- getCompositionsByIds(client, [...ids.compositions]),
109
- getRecordingsByIds(client, [...ids.recordings]),
110
- getReleasesByIds(client, [...ids.releases]),
111
- ]);
112
- return { compositions, recordings, releases };
150
+ type Parsed = Record<string, any>;
151
+
152
+ function workState(value: Parsed): Composition["state"] {
153
+ return value?.$kind === "Published"
154
+ ? { type: "Published", timestampMs: Number(value.Published) }
155
+ : { type: "Initialized" };
156
+ }
157
+
158
+ function parseComposition(id: string, content: Uint8Array): Composition {
159
+ const value = contracts.composition.Composition.parse(content) as Parsed;
160
+ return {
161
+ id,
162
+ state: workState(value.state),
163
+ title: String(value.title),
164
+ royaltyRate: {
165
+ value: Number(
166
+ Array.isArray(value.royalty_rate)
167
+ ? value.royalty_rate[0]
168
+ : value.royalty_rate,
169
+ ),
170
+ },
171
+ };
172
+ }
173
+
174
+ function parseRecording(id: string, content: Uint8Array): Recording {
175
+ const value = contracts.recording.Recording.parse(content) as Parsed;
176
+ return { id, state: workState(value.state) };
177
+ }
178
+
179
+ export function parseReleaseObject(
180
+ id: string,
181
+ content: Uint8Array,
182
+ json: unknown,
183
+ ): Release {
184
+ const deployed = json as {
185
+ discs?: Parsed[];
186
+ state?: Parsed;
187
+ title?: unknown;
188
+ } | null;
189
+ if (deployed?.discs) {
190
+ const tracks = deployed.discs.flatMap((disc) => disc.tracks ?? []);
191
+ const publishedAt =
192
+ deployed.state?.["@variant"] === "Published" ? deployed.state.pos0 : null;
193
+ return {
194
+ id,
195
+ state:
196
+ publishedAt == null
197
+ ? { type: "Initialized" }
198
+ : { type: "Published", timestampMs: Number(publishedAt) },
199
+ title: String(deployed.title ?? ""),
200
+ tracks: tracks.map((track) => ({
201
+ state: (track.state?.["@variant"] ?? "Unassigned") as TrackState,
202
+ recordingId: String(track.recording_id),
203
+ splitBps: { value: Number(track.split_bps?.pos0 ?? track.split_bps) },
204
+ })),
205
+ };
206
+ }
207
+ const value = contracts.release.Release.parse(content) as Parsed;
208
+ return {
209
+ id,
210
+ state: workState(value.state),
211
+ title: String(value.title),
212
+ tracks: (value.tracks ?? []).map((track: Parsed) => ({
213
+ state: (track.state?.$kind ?? "Unassigned") as TrackState,
214
+ recordingId: String(track.recording_id),
215
+ splitBps: {
216
+ value: Number(
217
+ Array.isArray(track.split_bps) ? track.split_bps[0] : track.split_bps,
218
+ ),
219
+ },
220
+ })),
221
+ };
222
+ }
223
+
224
+ /** Fetch and parse heterogeneous works through one Core bulk request. */
225
+ export async function getWorksByIds(
226
+ client: ClientWithCoreApi,
227
+ ids: WorkIds,
228
+ ): Promise<WorksById> {
229
+ const kinds = new Map<string, keyof WorksById>();
230
+ for (const [kind, objectIds] of Object.entries(ids) as Array<
231
+ [keyof WorksById, readonly string[]]
232
+ >) {
233
+ for (const objectId of objectIds) {
234
+ const normalized = normalizeSuiAddress(objectId);
235
+ const previous = kinds.get(normalized);
236
+ if (previous && previous !== kind) {
237
+ throw new Error(
238
+ `Work ${normalized} was requested as both ${previous} and ${kind}`,
239
+ );
240
+ }
241
+ kinds.set(normalized, kind);
242
+ }
243
+ }
244
+ const out: WorksById = { compositions: {}, recordings: {}, releases: {} };
245
+ if (kinds.size === 0) return out;
246
+ const { objects } = await client.core.getObjects({
247
+ objectIds: [...kinds.keys()],
248
+ include: { content: true, json: true },
249
+ });
250
+ for (const object of objects) {
251
+ if (object instanceof Error || !object.content) continue;
252
+ const kind = kinds.get(normalizeSuiAddress(object.objectId));
253
+ if (kind === "compositions")
254
+ out.compositions[object.objectId] = parseComposition(
255
+ object.objectId,
256
+ object.content,
257
+ );
258
+ else if (kind === "recordings")
259
+ out.recordings[object.objectId] = parseRecording(
260
+ object.objectId,
261
+ object.content,
262
+ );
263
+ else if (kind === "releases")
264
+ out.releases[object.objectId] = parseReleaseObject(
265
+ object.objectId,
266
+ object.content,
267
+ object.json,
268
+ );
269
+ }
270
+ return out;
113
271
  }
114
272
 
115
273
  /**
@@ -125,7 +283,10 @@ export async function getRecordingTitles(
125
283
  const ids = [...new Set(recordingIds)];
126
284
  if (ids.length === 0) return {};
127
285
 
128
- const { objects } = await client.core.getObjects({ objectIds: ids, include: { json: true } });
286
+ const { objects } = await client.core.getObjects({
287
+ objectIds: ids,
288
+ include: { json: true },
289
+ });
129
290
  const titles: Record<string, string> = {};
130
291
  const compositionShareByRecording: Record<string, string> = {};
131
292
 
@@ -155,9 +316,13 @@ export async function getRecordingTitles(
155
316
  client,
156
317
  Object.values(addresses.compositions).filter((id): id is string => !!id),
157
318
  );
158
- for (const [recordingId, shareType] of Object.entries(compositionShareByRecording)) {
319
+ for (const [recordingId, shareType] of Object.entries(
320
+ compositionShareByRecording,
321
+ )) {
159
322
  const compositionId = addresses.compositions[shareType];
160
- const title = compositionId ? compositions[compositionId]?.title : undefined;
323
+ const title = compositionId
324
+ ? compositions[compositionId]?.title
325
+ : undefined;
161
326
  if (title) titles[recordingId] = title;
162
327
  }
163
328
  return titles;