@misofm/sdk 0.3.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 (54) hide show
  1. package/README.md +31 -0
  2. package/dist/auth.d.ts +73 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +193 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/catalog.d.ts +4 -17
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/catalog.js +46 -58
  9. package/dist/catalog.js.map +1 -1
  10. package/dist/cover.d.ts +10 -0
  11. package/dist/cover.d.ts.map +1 -1
  12. package/dist/cover.js +66 -19
  13. package/dist/cover.js.map +1 -1
  14. package/dist/credits.d.ts +10 -0
  15. package/dist/credits.d.ts.map +1 -1
  16. package/dist/credits.js +149 -39
  17. package/dist/credits.js.map +1 -1
  18. package/dist/drop.d.ts +7 -0
  19. package/dist/drop.d.ts.map +1 -1
  20. package/dist/drop.js +84 -34
  21. package/dist/drop.js.map +1 -1
  22. package/dist/pressing.d.ts.map +1 -1
  23. package/dist/pressing.js +59 -16
  24. package/dist/pressing.js.map +1 -1
  25. package/dist/read/artist.d.ts +1 -6
  26. package/dist/read/artist.d.ts.map +1 -1
  27. package/dist/read/artist.js +39 -22
  28. package/dist/read/artist.js.map +1 -1
  29. package/dist/read/catalog.d.ts +24 -3
  30. package/dist/read/catalog.d.ts.map +1 -1
  31. package/dist/read/catalog.js +190 -89
  32. package/dist/read/catalog.js.map +1 -1
  33. package/dist/read/index.d.ts +5 -3
  34. package/dist/read/index.d.ts.map +1 -1
  35. package/dist/read/index.js +2 -2
  36. package/dist/read/index.js.map +1 -1
  37. package/dist/read/types.d.ts +12 -1
  38. package/dist/read/types.d.ts.map +1 -1
  39. package/dist/read/works.d.ts +3 -2
  40. package/dist/read/works.d.ts.map +1 -1
  41. package/dist/read/works.js +158 -48
  42. package/dist/read/works.js.map +1 -1
  43. package/package.json +5 -1
  44. package/src/auth.ts +275 -0
  45. package/src/catalog.ts +71 -77
  46. package/src/cover.ts +107 -26
  47. package/src/credits.ts +353 -89
  48. package/src/drop.ts +150 -40
  49. package/src/pressing.ts +146 -36
  50. package/src/read/artist.ts +73 -34
  51. package/src/read/catalog.ts +324 -89
  52. package/src/read/index.ts +23 -2
  53. package/src/read/types.ts +16 -2
  54. package/src/read/works.ts +224 -59
package/src/catalog.ts CHANGED
@@ -14,16 +14,16 @@
14
14
  import type { ClientWithCoreApi } from "@mysten/sui/client";
15
15
  import type { SuiGraphQLClient } from "@mysten/sui/graphql";
16
16
  import {
17
- getCompositionAddressByShareType,
17
+ extractTypeParams2,
18
18
  getOwnedRecordingAdminCaps,
19
- getRecordingByShareType,
20
- getRecordingShareTypes,
19
+ getRecordingsByIds,
21
20
  getReleaseById,
22
21
  type Recording,
23
22
  } from "@misonetwork/sdk";
23
+ import { getWorkAddressesByShareTypes } from "./read/works.ts";
24
24
  import {
25
- getCompositionCredits,
26
- getRecordingCredits,
25
+ getCompositionCreditsByIds,
26
+ getRecordingCreditsByIds,
27
27
  type CreditView,
28
28
  type RecordingCreditsView,
29
29
  } from "./credits.ts";
@@ -85,51 +85,60 @@ export async function getTrackCreditsByRecordingIds(
85
85
  options: GetReleaseTrackCreditsOptions,
86
86
  ): Promise<Record<string, ReleaseTrackCredits>> {
87
87
  const recordingIds = [...new Set(recordingIdsInput)];
88
+ if (recordingIds.length === 0) return {};
88
89
 
89
- const recordingReads = await Promise.all(
90
- recordingIds.map(async (recordingId) => {
91
- const [recordingCredits, [, compositionShareType]] = await Promise.all([
92
- getRecordingCredits(client, recordingId, options.recordingCreditsPackageId),
93
- getRecordingShareTypes(client, recordingId),
94
- ]);
95
- return { recordingId, recordingCredits, compositionShareType };
96
- }),
97
- );
90
+ const [recordingCreditsById, recordingObjects] = await Promise.all([
91
+ getRecordingCreditsByIds(
92
+ client,
93
+ recordingIds,
94
+ options.recordingCreditsPackageId,
95
+ ),
96
+ client.core.getObjects({ objectIds: recordingIds }),
97
+ ]);
98
+ const recordingReads = recordingObjects.objects.map((object, index) => {
99
+ const recordingId = recordingIds[index]!;
100
+ if (object instanceof Error) throw object;
101
+ const [, compositionShareType] = extractTypeParams2(object.type);
102
+ return {
103
+ recordingId,
104
+ recordingCredits: recordingCreditsById[recordingId] ?? null,
105
+ compositionShareType,
106
+ };
107
+ });
98
108
 
99
- const compositionShareTypes = [...new Set(recordingReads.map((read) => read.compositionShareType))];
100
- const compositionAddressEntries = await Promise.all(
101
- compositionShareTypes.map(async (shareType) => {
102
- const compositionId = await getCompositionAddressByShareType(
103
- graphqlClient,
104
- shareType,
105
- options.misoPackageId,
106
- );
107
- if (!compositionId) throw new Error(`Composition not found for share type: ${shareType}`);
108
- return [shareType, compositionId] as const;
109
- }),
109
+ const compositionShareTypes = [
110
+ ...new Set(recordingReads.map((read) => read.compositionShareType)),
111
+ ];
112
+ const addresses = await getWorkAddressesByShareTypes(
113
+ graphqlClient,
114
+ { compositions: compositionShareTypes, recordings: [] },
115
+ options.misoPackageId,
110
116
  );
111
- const compositionAddressByShareType = new Map(compositionAddressEntries);
112
-
113
- const compositionIds = [...new Set(compositionAddressEntries.map(([, compositionId]) => compositionId))];
114
- const compositionCreditEntries = await Promise.all(
115
- compositionIds.map(async (compositionId) => {
116
- const credits = await getCompositionCredits(
117
- client,
118
- compositionId,
119
- options.compositionCreditsPackageId,
120
- );
121
- return [compositionId, credits ?? []] as const;
122
- }),
117
+ for (const shareType of compositionShareTypes) {
118
+ if (!addresses.compositions[shareType]) {
119
+ throw new Error(`Composition not found for share type: ${shareType}`);
120
+ }
121
+ }
122
+ const compositionIds = [
123
+ ...new Set(
124
+ Object.values(addresses.compositions).filter(
125
+ (id): id is string => id !== undefined,
126
+ ),
127
+ ),
128
+ ];
129
+ const compositionCreditsById = await getCompositionCreditsByIds(
130
+ client,
131
+ compositionIds,
132
+ options.compositionCreditsPackageId,
123
133
  );
124
- const compositionCreditsById = new Map(compositionCreditEntries);
125
134
 
126
135
  return Object.fromEntries(
127
136
  recordingReads.map((read) => {
128
- const compositionId = compositionAddressByShareType.get(read.compositionShareType)!;
137
+ const compositionId = addresses.compositions[read.compositionShareType]!;
129
138
  return [
130
139
  read.recordingId,
131
140
  {
132
- compositionCredits: compositionCreditsById.get(compositionId) ?? [],
141
+ compositionCredits: compositionCreditsById[compositionId] ?? [],
133
142
  recordingCredits: read.recordingCredits ?? EMPTY_RECORDING_CREDITS,
134
143
  },
135
144
  ];
@@ -139,9 +148,8 @@ export async function getTrackCreditsByRecordingIds(
139
148
 
140
149
  export interface GetAdministeredRecordingsOptions {
141
150
  /**
142
- * Maximum lookups in flight at once. The traversal is one lookup per admin
143
- * cap, so an artist with a large catalog would otherwise open as many
144
- * concurrent requests as they have recordings. Defaults to 10.
151
+ * @deprecated Resolution is batched; this option is retained for source
152
+ * compatibility and no longer affects request concurrency.
145
153
  */
146
154
  concurrency?: number;
147
155
  }
@@ -149,20 +157,8 @@ export interface GetAdministeredRecordingsOptions {
149
157
  /**
150
158
  * Every `Recording` administered by `owner`.
151
159
  *
152
- * Two stages: list the owner's `RecordingAdminCap`s over the Core API, then
153
- * resolve each cap's share type back to its recording.
154
- *
155
- * The second stage needs GraphQL and is inherently one lookup per cap, because
156
- * `RecordingAdminCap` carries no back-pointer to its recording — unlike
157
- * `ReleaseAdminCap`, which stores `release_id` and can therefore be resolved
158
- * entirely over the Core API. The caps are derived objects (recording → cap via
159
- * `deriveObjectID`), and that derivation cannot be inverted, so the share type
160
- * is the only link back. Adding a `recording_id: ID` field to
161
- * `RecordingAdminCap` on the protocol side would make this a pure Core-API
162
- * batch read and remove both the GraphQL dependency and the fan-out.
163
- *
164
- * Until then the lookups run concurrently in bounded batches rather than
165
- * serially.
160
+ * Three bounded stages: list the owner's `RecordingAdminCap`s, resolve every
161
+ * share type in one aliased GraphQL query, then batch-fetch the recordings.
166
162
  */
167
163
  export async function getAdministeredRecordings(
168
164
  client: ClientWithCoreApi,
@@ -171,25 +167,23 @@ export async function getAdministeredRecordings(
171
167
  misoPackageId: string,
172
168
  options: GetAdministeredRecordingsOptions = {},
173
169
  ): Promise<Recording[]> {
174
- const concurrency = Math.max(1, options.concurrency ?? 10);
170
+ void options;
175
171
  const caps = await getOwnedRecordingAdminCaps(client, owner, misoPackageId);
176
-
177
- const recordings: Recording[] = [];
178
- for (let i = 0; i < caps.length; i += concurrency) {
179
- const batch = caps.slice(i, i + concurrency);
180
- const settled = await Promise.all(
181
- batch.map(async (cap) => {
182
- try {
183
- return await getRecordingByShareType(client, graphqlClient, cap.shareType, misoPackageId);
184
- } catch {
185
- // A cap whose recording cannot be resolved is skipped rather than
186
- // failing the whole catalog — matching the previous behavior, where a
187
- // missing address was silently passed over.
188
- return null;
189
- }
190
- }),
191
- );
192
- for (const rec of settled) if (rec) recordings.push(rec);
193
- }
194
- return recordings;
172
+ if (caps.length === 0) return [];
173
+ const addresses = await getWorkAddressesByShareTypes(
174
+ graphqlClient,
175
+ { compositions: [], recordings: caps.map((cap) => cap.shareType) },
176
+ misoPackageId,
177
+ );
178
+ const byId = await getRecordingsByIds(
179
+ client,
180
+ Object.values(addresses.recordings).filter(
181
+ (id): id is string => id !== undefined,
182
+ ),
183
+ );
184
+ return caps.flatMap((cap) => {
185
+ const id = addresses.recordings[cap.shareType];
186
+ const recording = id ? byId[id] : undefined;
187
+ return recording ? [recording] : [];
188
+ });
195
189
  }
package/src/cover.ts CHANGED
@@ -19,7 +19,6 @@ import { bcs } from "@mysten/sui/bcs";
19
19
  import { deriveDynamicFieldID } from "@mysten/sui/utils";
20
20
  import type { TxThunk } from "./transactions.ts";
21
21
  import { OPTION_NONE, OPTION_SOME } from "./internal.ts";
22
- import { isNotFound } from "./queries.ts";
23
22
  import * as coverArt from "./contracts/cover_art/cover_art.ts";
24
23
  import * as releaseCoverArt from "./contracts/release_cover_art/release_cover_art.ts";
25
24
 
@@ -45,15 +44,27 @@ export function setReleaseCover(p: SetReleaseCoverParams): TxThunk {
45
44
  return (tx) => {
46
45
  const walrusType = `${p.oriPackageId}::walrus_data::WalrusData`;
47
46
  const blob = (id: bigint | string) =>
48
- tx.moveCall({ target: `${p.oriPackageId}::walrus_data::new_blob`, arguments: [tx.pure.u256(id)] });
47
+ tx.moveCall({
48
+ target: `${p.oriPackageId}::walrus_data::new_blob`,
49
+ arguments: [tx.pure.u256(id)],
50
+ });
49
51
 
50
52
  const still = blob(p.stillBlobId);
51
53
  const animated =
52
54
  p.animatedBlobId == null
53
55
  ? tx.moveCall({ target: OPTION_NONE, typeArguments: [walrusType] })
54
- : tx.moveCall({ target: OPTION_SOME, typeArguments: [walrusType], arguments: [blob(p.animatedBlobId)] });
55
-
56
- const cover = tx.add(coverArt._new({ package: p.coverArtPackageId, arguments: [still, animated] }));
56
+ : tx.moveCall({
57
+ target: OPTION_SOME,
58
+ typeArguments: [walrusType],
59
+ arguments: [blob(p.animatedBlobId)],
60
+ });
61
+
62
+ const cover = tx.add(
63
+ coverArt._new({
64
+ package: p.coverArtPackageId,
65
+ arguments: [still, animated],
66
+ }),
67
+ );
57
68
  tx.add(
58
69
  releaseCoverArt.setCover({
59
70
  package: p.releaseCoverArtPackageId,
@@ -72,7 +83,13 @@ export function setReleaseCover(p: SetReleaseCoverParams): TxThunk {
72
83
  */
73
84
  export type CoverImageRef =
74
85
  | { kind: "blob"; blobId: string }
75
- | { kind: "quiltPatch"; quiltId: string; version: number; startIndex: number; endIndex: number };
86
+ | {
87
+ kind: "quiltPatch";
88
+ quiltId: string;
89
+ version: number;
90
+ startIndex: number;
91
+ endIndex: number;
92
+ };
76
93
 
77
94
  /** A release's album-level cover: a still image and an optional animation. */
78
95
  export interface ReleaseCoverView {
@@ -88,17 +105,28 @@ const CoverArtField = bcs.struct("Field", {
88
105
  name: releaseCoverArt.ExtensionKey,
89
106
  value: releaseCoverArt.ReleaseCoverArt,
90
107
  });
91
- const COVER_ART_KEY_BYTES = releaseCoverArt.ExtensionKey.serialize([false]).toBytes();
108
+ const COVER_ART_KEY_BYTES = releaseCoverArt.ExtensionKey.serialize([
109
+ false,
110
+ ]).toBytes();
92
111
 
93
112
  /** A parsed `ori::WalrusData` value (a MoveEnum: `Blob` or `QuiltPatch`). */
94
113
  type ParsedWalrusData =
95
114
  | { $kind: "Blob"; Blob: [string | number | bigint, unknown] }
96
- | { $kind: "QuiltPatch"; QuiltPatch: [string | number | bigint, number, number, number] };
115
+ | {
116
+ $kind: "QuiltPatch";
117
+ QuiltPatch: [string | number | bigint, number, number, number];
118
+ };
97
119
 
98
120
  function toCoverImageRef(wd: ParsedWalrusData): CoverImageRef {
99
121
  if (wd.$kind === "Blob") return { kind: "blob", blobId: String(wd.Blob[0]) };
100
122
  const [quiltId, version, startIndex, endIndex] = wd.QuiltPatch;
101
- return { kind: "quiltPatch", quiltId: String(quiltId), version, startIndex, endIndex };
123
+ return {
124
+ kind: "quiltPatch",
125
+ quiltId: String(quiltId),
126
+ version,
127
+ startIndex,
128
+ endIndex,
129
+ };
102
130
  }
103
131
 
104
132
  /**
@@ -112,25 +140,24 @@ export async function getReleaseCover(
112
140
  releaseId: string,
113
141
  releaseCoverArtPackageId: string,
114
142
  ): Promise<ReleaseCoverView | null> {
115
- const fieldId = deriveDynamicFieldID(
116
- releaseId,
117
- `${releaseCoverArtPackageId}::release_cover_art::ExtensionKey`,
118
- COVER_ART_KEY_BYTES,
143
+ return (
144
+ (
145
+ await getReleaseCoversByIds(
146
+ client,
147
+ [releaseId],
148
+ [releaseCoverArtPackageId],
149
+ )
150
+ )[releaseId] ?? null
119
151
  );
152
+ }
120
153
 
121
- let content: Uint8Array | null;
122
- try {
123
- const { object } = await client.core.getObject({ objectId: fieldId, include: { content: true } });
124
- content = object?.content ?? null;
125
- } catch (e) {
126
- if (isNotFound(e)) return null;
127
- throw e;
128
- }
129
- if (!content) return null;
130
-
131
- const cover = CoverArtField.parse(content).value.cover as
132
- | { still: ParsedWalrusData; animated: ParsedWalrusData | null }
133
- | null;
154
+ export function parseReleaseCoverContent(
155
+ content: Uint8Array,
156
+ ): ReleaseCoverView | null {
157
+ const cover = CoverArtField.parse(content).value.cover as {
158
+ still: ParsedWalrusData;
159
+ animated: ParsedWalrusData | null;
160
+ } | null;
134
161
  if (!cover) return null;
135
162
 
136
163
  return {
@@ -138,3 +165,57 @@ export async function getReleaseCover(
138
165
  animated: cover.animated ? toCoverImageRef(cover.animated) : null,
139
166
  };
140
167
  }
168
+
169
+ /** Deterministic dynamic-field id for one release-cover package generation. */
170
+ export function releaseCoverFieldId(
171
+ releaseId: string,
172
+ releaseCoverArtPackageId: string,
173
+ ): string {
174
+ return deriveDynamicFieldID(
175
+ releaseId,
176
+ `${releaseCoverArtPackageId}::release_cover_art::ExtensionKey`,
177
+ COVER_ART_KEY_BYTES,
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Read covers for many releases and package generations in one Core request.
183
+ *
184
+ * Package IDs are ordered newest to oldest. If both fields exist, the newest
185
+ * wins; legacy fallbacks add bytes to the same bulk request, not serial probes.
186
+ */
187
+ export async function getReleaseCoversByIds(
188
+ client: ClientWithCoreApi,
189
+ releaseIdsInput: readonly string[],
190
+ releaseCoverArtPackageIds: readonly string[],
191
+ ): Promise<Partial<Record<string, ReleaseCoverView>>> {
192
+ const releaseIds = [...new Set(releaseIdsInput)];
193
+ const targets = releaseIds.flatMap((releaseId) =>
194
+ releaseCoverArtPackageIds.map((packageId, priority) => ({
195
+ releaseId,
196
+ priority,
197
+ fieldId: releaseCoverFieldId(releaseId, packageId),
198
+ })),
199
+ );
200
+ if (targets.length === 0) return {};
201
+
202
+ const { objects } = await client.core.getObjects({
203
+ objectIds: targets.map((target) => target.fieldId),
204
+ include: { content: true },
205
+ });
206
+ const out: Partial<Record<string, ReleaseCoverView>> = {};
207
+ const priorities = new Map<string, number>();
208
+ objects.forEach((object, index) => {
209
+ const target = targets[index];
210
+ if (!target || object instanceof Error || !object.content) return;
211
+ const currentPriority = priorities.get(target.releaseId);
212
+ if (currentPriority !== undefined && currentPriority <= target.priority)
213
+ return;
214
+ const cover = parseReleaseCoverContent(object.content);
215
+ if (cover) {
216
+ out[target.releaseId] = cover;
217
+ priorities.set(target.releaseId, target.priority);
218
+ }
219
+ });
220
+ return out;
221
+ }