@misofm/platform 0.25.0 → 0.26.1

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 (140) hide show
  1. package/README.md +144 -45
  2. package/dist/auth.d.ts +8 -13
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +78 -59
  5. package/dist/auth.js.map +1 -1
  6. package/dist/catalog.d.ts +5 -5
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/catalog.js +33 -21
  9. package/dist/catalog.js.map +1 -1
  10. package/dist/client.d.ts +17 -30
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +53 -75
  13. package/dist/client.js.map +1 -1
  14. package/dist/cover.d.ts +4 -3
  15. package/dist/cover.d.ts.map +1 -1
  16. package/dist/cover.js +28 -17
  17. package/dist/cover.js.map +1 -1
  18. package/dist/credits.d.ts +12 -10
  19. package/dist/credits.d.ts.map +1 -1
  20. package/dist/credits.js +60 -33
  21. package/dist/credits.js.map +1 -1
  22. package/dist/deployments.d.ts +21 -13
  23. package/dist/deployments.d.ts.map +1 -1
  24. package/dist/deployments.js +84 -37
  25. package/dist/deployments.js.map +1 -1
  26. package/dist/errors.d.ts +91 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +111 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/execute.d.ts +3 -1
  31. package/dist/execute.d.ts.map +1 -1
  32. package/dist/execute.js +9 -4
  33. package/dist/execute.js.map +1 -1
  34. package/dist/genre.d.ts +25 -41
  35. package/dist/genre.d.ts.map +1 -1
  36. package/dist/genre.js +71 -20
  37. package/dist/genre.js.map +1 -1
  38. package/dist/party/client.d.ts +16 -13
  39. package/dist/party/client.d.ts.map +1 -1
  40. package/dist/party/client.js +33 -22
  41. package/dist/party/client.js.map +1 -1
  42. package/dist/party/extensions/links.d.ts +2 -1
  43. package/dist/party/extensions/links.d.ts.map +1 -1
  44. package/dist/party/extensions/links.js +2 -1
  45. package/dist/party/extensions/links.js.map +1 -1
  46. package/dist/party/queries.d.ts +12 -11
  47. package/dist/party/queries.d.ts.map +1 -1
  48. package/dist/party/queries.js +63 -57
  49. package/dist/party/queries.js.map +1 -1
  50. package/dist/party/types.d.ts +31 -20
  51. package/dist/party/types.d.ts.map +1 -1
  52. package/dist/party/types.js +82 -1
  53. package/dist/party/types.js.map +1 -1
  54. package/dist/pressing.d.ts +55 -35
  55. package/dist/pressing.d.ts.map +1 -1
  56. package/dist/pressing.js +99 -76
  57. package/dist/pressing.js.map +1 -1
  58. package/dist/queries.d.ts +1 -1
  59. package/dist/queries.d.ts.map +1 -1
  60. package/dist/queries.js +5 -4
  61. package/dist/queries.js.map +1 -1
  62. package/dist/read/artist.d.ts +6 -4
  63. package/dist/read/artist.d.ts.map +1 -1
  64. package/dist/read/artist.js +29 -27
  65. package/dist/read/artist.js.map +1 -1
  66. package/dist/read/catalog.d.ts +20 -17
  67. package/dist/read/catalog.d.ts.map +1 -1
  68. package/dist/read/catalog.js +120 -141
  69. package/dist/read/catalog.js.map +1 -1
  70. package/dist/read/client.d.ts +10 -9
  71. package/dist/read/client.d.ts.map +1 -1
  72. package/dist/read/client.js.map +1 -1
  73. package/dist/read/genres.d.ts +5 -3
  74. package/dist/read/genres.d.ts.map +1 -1
  75. package/dist/read/genres.js +18 -15
  76. package/dist/read/genres.js.map +1 -1
  77. package/dist/read/index.d.ts.map +1 -1
  78. package/dist/read/index.js +7 -1
  79. package/dist/read/index.js.map +1 -1
  80. package/dist/read/receipts.d.ts +15 -6
  81. package/dist/read/receipts.d.ts.map +1 -1
  82. package/dist/read/receipts.js +73 -60
  83. package/dist/read/receipts.js.map +1 -1
  84. package/dist/read/wallet.d.ts +11 -9
  85. package/dist/read/wallet.d.ts.map +1 -1
  86. package/dist/read/wallet.js +117 -132
  87. package/dist/read/wallet.js.map +1 -1
  88. package/dist/read/works.d.ts +9 -16
  89. package/dist/read/works.d.ts.map +1 -1
  90. package/dist/read/works.js +48 -117
  91. package/dist/read/works.js.map +1 -1
  92. package/dist/recording-extensions.d.ts +8 -7
  93. package/dist/recording-extensions.d.ts.map +1 -1
  94. package/dist/recording-extensions.js +30 -63
  95. package/dist/recording-extensions.js.map +1 -1
  96. package/dist/release-extensions.d.ts +3 -2
  97. package/dist/release-extensions.d.ts.map +1 -1
  98. package/dist/release-extensions.js +9 -10
  99. package/dist/release-extensions.js.map +1 -1
  100. package/dist/royalty.d.ts +51 -51
  101. package/dist/royalty.d.ts.map +1 -1
  102. package/dist/royalty.js +93 -11
  103. package/dist/royalty.js.map +1 -1
  104. package/dist/share.d.ts +7 -6
  105. package/dist/share.d.ts.map +1 -1
  106. package/dist/share.js +37 -30
  107. package/dist/share.js.map +1 -1
  108. package/dist/vault.d.ts +20 -8
  109. package/dist/vault.d.ts.map +1 -1
  110. package/dist/vault.js +31 -15
  111. package/dist/vault.js.map +1 -1
  112. package/package.json +11 -4
  113. package/src/auth.ts +89 -70
  114. package/src/catalog.ts +33 -49
  115. package/src/client.ts +117 -97
  116. package/src/cover.ts +17 -26
  117. package/src/credits.ts +43 -75
  118. package/src/deployments.ts +103 -69
  119. package/src/errors.ts +156 -0
  120. package/src/execute.ts +10 -6
  121. package/src/genre.ts +38 -23
  122. package/src/party/client.ts +35 -23
  123. package/src/party/extensions/links.ts +3 -2
  124. package/src/party/queries.ts +77 -65
  125. package/src/party/types.ts +52 -48
  126. package/src/pressing.ts +158 -161
  127. package/src/queries.ts +5 -5
  128. package/src/read/artist.ts +47 -42
  129. package/src/read/catalog.ts +200 -223
  130. package/src/read/client.ts +12 -11
  131. package/src/read/genres.ts +24 -16
  132. package/src/read/index.ts +7 -1
  133. package/src/read/receipts.ts +128 -67
  134. package/src/read/wallet.ts +194 -162
  135. package/src/read/works.ts +75 -194
  136. package/src/recording-extensions.ts +38 -92
  137. package/src/release-extensions.ts +7 -12
  138. package/src/royalty.ts +117 -20
  139. package/src/share.ts +44 -29
  140. package/src/vault.ts +47 -24
@@ -10,14 +10,15 @@
10
10
  // credits → recordings); the same work happens once here, inside one datacenter,
11
11
  // and every subsequent visitor is served from cache.
12
12
 
13
+ import { Effect } from "effect";
13
14
  import {
14
15
  deriveListingId,
15
16
  getListing,
16
17
  getPressing,
17
18
  getRecord,
18
19
  getSale,
19
- type ListingView as ContractListingView,
20
- type PressingView as ContractPressingView,
20
+ type Listing as ContractListing,
21
+ type Pressing as ContractPressing,
21
22
  } from "../pressing.ts";
22
23
  import { requireRecordSalesDeployment } from "../deployments.ts";
23
24
  import {
@@ -44,9 +45,19 @@ import {
44
45
  type RecordingEngineSessionView,
45
46
  } from "../recording-extensions.ts";
46
47
  import { getTrackCreditsByRecordingIds } from "../catalog.ts";
47
- import { getReleaseById, getReleasesByIds, isNotFound } from "@misofm/musicos";
48
+ import { getReleaseById, getReleasesByIds } from "@misofm/musicos";
48
49
  import type { Release } from "@misofm/musicos";
49
- import type { MisoClient } from "./client.ts";
50
+ import {
51
+ getObjectsContent,
52
+ ObjectTypeMismatchError,
53
+ type BcsDecodeError,
54
+ type ObjectNotFoundError,
55
+ type SuiClient,
56
+ type SuiGraphQL,
57
+ type SuiRpcError,
58
+ } from "@misofm/effect";
59
+ import { ReleaseNotFoundError } from "../errors.ts";
60
+ import type { MisoConfig } from "./config.ts";
50
61
  import { getRecordingTitles, parseReleaseObject } from "./works.ts";
51
62
  import { int } from "./internal/scalars.ts";
52
63
  import {
@@ -117,7 +128,7 @@ export function currencyInfo(type: string | null): Currency {
117
128
 
118
129
  // ── Projections ──────────────────────────────────────────────────────────────
119
130
 
120
- function toPressingView(pressing: ContractPressingView): PressingView {
131
+ function toPressingView(pressing: ContractPressing): PressingView {
121
132
  return {
122
133
  id: pressing.id,
123
134
  releaseId: pressing.releaseId,
@@ -128,7 +139,7 @@ function toPressingView(pressing: ContractPressingView): PressingView {
128
139
  };
129
140
  }
130
141
 
131
- function toListingView(listing: ContractListingView): ListingView {
142
+ function toListingView(listing: ContractListing): ListingView {
132
143
  const price: Price = {
133
144
  kind: listing.pricing.kind,
134
145
  amount: listing.pricing.amount,
@@ -144,37 +155,33 @@ function toListingView(listing: ContractListingView): ListingView {
144
155
  }
145
156
 
146
157
  function toSaleView(
147
- pressing: ContractPressingView,
148
- listing: ContractListingView,
158
+ pressing: ContractPressing,
159
+ listing: ContractListing,
149
160
  ): SaleView {
150
161
  return { pressing: toPressingView(pressing), listing: toListingView(listing) };
151
162
  }
152
163
 
153
164
  /** One permanent pressing, projected to the JSON-safe read boundary. */
154
- export async function getPressingView(
155
- client: MisoClient,
165
+ export const getPressingView = Effect.fn("getPressingView")(function* (
156
166
  pressingId: string,
157
- ): Promise<PressingView | null> {
158
- const sales = requireRecordSalesDeployment(client.config.recordSales);
159
- const pressing = await getPressing(
160
- client.protocol,
161
- pressingId,
162
- sales.recordPackageId,
163
- );
167
+ config: MisoConfig,
168
+ ): Effect.fn.Return<PressingView | null, ObjectTypeMismatchError | BcsDecodeError | SuiRpcError, SuiClient> {
169
+ const sales = requireRecordSalesDeployment(config.recordSales);
170
+ const pressing = yield* getPressing(pressingId, sales.recordPackageId);
164
171
  return pressing ? toPressingView(pressing) : null;
165
- }
172
+ });
166
173
 
167
174
  /** One currency-specific listing derived from its permanent pressing. */
168
- export async function getListingView(
169
- client: MisoClient,
175
+ export const getListingView = Effect.fn("getListingView")(function* (
170
176
  pressingId: string,
171
177
  currencyType: string,
172
- ): Promise<ListingView | null> {
173
- const sales = requireRecordSalesDeployment(client.config.recordSales);
178
+ config: MisoConfig,
179
+ ): Effect.fn.Return<ListingView | null, ObjectTypeMismatchError | BcsDecodeError | SuiRpcError, SuiClient> {
180
+ const sales = requireRecordSalesDeployment(config.recordSales);
174
181
  const listingId = deriveListingId(pressingId, currencyType, sales.recordShopPackageId);
175
- const listing = await getListing(client.protocol, listingId, sales.recordShopPackageId);
182
+ const listing = yield* getListing(listingId, sales.recordShopPackageId);
176
183
  return listing ? toListingView(listing) : null;
177
- }
184
+ });
178
185
 
179
186
  function toWorkState(state: Release["state"]): WorkState {
180
187
  return state.type === "Published"
@@ -182,7 +189,7 @@ function toWorkState(state: Release["state"]): WorkState {
182
189
  : { type: "Initialized" };
183
190
  }
184
191
 
185
- function toCredits(credits: CreditView[] | null): Credit[] {
192
+ function toCredits(credits: readonly CreditView[] | null): Credit[] {
186
193
  return (credits ?? []).map((c) => ({
187
194
  partyId: c.partyId,
188
195
  displayName: c.displayName,
@@ -197,10 +204,6 @@ export function primaryArtistNames(credits: Credit[]): string[] {
197
204
  .map((c) => c.displayName);
198
205
  }
199
206
 
200
- /**
201
- * Number the protocol's flat tracklist. Display grouping such as discs belongs
202
- * to metadata extensions and can be layered onto this projection later.
203
- */
204
207
  /** Per-recording audio attachments, every id already base64url. */
205
208
  export interface TrackAudio {
206
209
  masterBlobIds?: Partial<Record<string, string>>;
@@ -208,6 +211,10 @@ export interface TrackAudio {
208
211
  engineSessions?: Partial<Record<string, TrackEngineSession>>;
209
212
  }
210
213
 
214
+ /**
215
+ * Number the protocol's flat tracklist. Display grouping such as discs belongs
216
+ * to metadata extensions and can be layered onto this projection later.
217
+ */
211
218
  export function toTracks(
212
219
  release: Release,
213
220
  titles: Record<string, string>,
@@ -252,18 +259,16 @@ function toTrackEngineSession(view: RecordingEngineSessionView): TrackEngineSess
252
259
  // ── Cover ────────────────────────────────────────────────────────────────────
253
260
 
254
261
  /** A release's cover from the configured `release_cover_art` package. */
255
- export async function readReleaseCover(
256
- client: MisoClient,
262
+ export const readReleaseCover = Effect.fn("readReleaseCover")(function* (
257
263
  releaseId: string,
258
- ): Promise<Cover | null> {
259
- const { releaseCoverArt } = client.config.protocol;
260
- const covers = await getReleaseCoversByIds(
261
- client.protocol,
262
- [releaseId],
263
- releaseCoverArt,
264
- ).catch(() => ({}) as Partial<Record<string, ReleaseCoverView>>);
265
- return toCover(client.config.walrusAggregatorUrl, covers[releaseId] ?? null);
266
- }
264
+ config: MisoConfig,
265
+ ): Effect.fn.Return<Cover | null, never, SuiClient> {
266
+ const { releaseCoverArt } = config.protocol;
267
+ const covers = yield* getReleaseCoversByIds([releaseId], releaseCoverArt).pipe(
268
+ Effect.catch(() => Effect.succeed({} as Partial<Record<string, ReleaseCoverView>>)),
269
+ );
270
+ return toCover(config.walrusAggregatorUrl, covers[releaseId] ?? null);
271
+ });
267
272
 
268
273
  export type ReleaseResourceInclude = "cover" | "credits" | "kind";
269
274
 
@@ -279,66 +284,50 @@ export interface ReleaseResources {
279
284
  * Every extension id is deterministic, so separate object calls only add
280
285
  * network round trips without discovering anything new.
281
286
  */
282
- export async function getReleaseResources(
283
- client: MisoClient,
287
+ export const getReleaseResources = Effect.fn("getReleaseResources")(function* (
284
288
  releaseId: string,
289
+ config: MisoConfig,
285
290
  include: readonly ReleaseResourceInclude[] = [],
286
- ): Promise<ReleaseResources> {
291
+ ): Effect.fn.Return<ReleaseResources, ReleaseNotFoundError | BcsDecodeError | SuiRpcError, SuiClient> {
287
292
  const wantsCover = include.includes("cover");
288
293
  const wantsCredits = include.includes("credits");
289
294
  const wantsKind = include.includes("kind");
290
- const coverFieldIds = wantsCover
291
- ? [releaseCoverFieldId(releaseId, client.config.protocol.releaseCoverArt)]
292
- : [];
293
- const creditsFieldId = wantsCredits
294
- ? releaseCreditsFieldId(releaseId, client.config.protocol.releaseCredits)
295
- : null;
296
- const kindFieldId = wantsKind
297
- ? releaseKindFieldId(releaseId, client.config.protocol.releaseKind)
298
- : null;
295
+ const coverFieldId = wantsCover ? releaseCoverFieldId(releaseId, config.protocol.releaseCoverArt) : null;
296
+ const creditsFieldId = wantsCredits ? releaseCreditsFieldId(releaseId, config.protocol.releaseCredits) : null;
297
+ const kindFieldId = wantsKind ? releaseKindFieldId(releaseId, config.protocol.releaseKind) : null;
299
298
  const objectIds = [
300
299
  releaseId,
301
- ...coverFieldIds,
300
+ ...(coverFieldId ? [coverFieldId] : []),
302
301
  ...(creditsFieldId ? [creditsFieldId] : []),
303
302
  ...(kindFieldId ? [kindFieldId] : []),
304
303
  ];
305
- const { objects } = await client.protocol.core.getObjects({
306
- objectIds,
307
- include: { content: true },
308
- });
309
- const releaseObject = objects[0];
310
- if (!releaseObject) throw new Error(`Release not found: ${releaseId}`);
311
- if (releaseObject instanceof Error) throw releaseObject;
312
- if (!releaseObject.content)
313
- throw new Error(`Release has no content: ${releaseId}`);
314
- const release = parseReleaseObject(
315
- releaseObject.objectId,
316
- releaseObject.content,
317
- );
304
+ const contentById = yield* getObjectsContent(objectIds);
305
+
306
+ const releaseFound = contentById.get(releaseId);
307
+ if (!releaseFound) return yield* new ReleaseNotFoundError({ releaseId });
308
+ const release = yield* parseReleaseObject(releaseId, releaseFound.content);
318
309
 
319
310
  let cover: Cover | null | undefined;
320
311
  if (wantsCover) {
312
+ const found = coverFieldId ? contentById.get(coverFieldId) : undefined;
321
313
  let view: ReleaseCoverView | null = null;
322
- for (let index = 0; index < coverFieldIds.length; index += 1) {
323
- const object = objects[index + 1];
324
- if (!object || object instanceof Error || !object.content) continue;
314
+ if (found) {
325
315
  try {
326
- view = parseReleaseCoverContent(object.content);
316
+ view = parseReleaseCoverContent(found.content);
327
317
  } catch {
328
318
  view = null;
329
319
  }
330
- if (view) break;
331
320
  }
332
- cover = toCover(client.config.walrusAggregatorUrl, view);
321
+ cover = toCover(config.walrusAggregatorUrl, view);
333
322
  }
334
323
 
335
324
  let credits: Credit[] | undefined;
336
325
  if (wantsCredits) {
337
- const object = objects[1 + coverFieldIds.length];
326
+ const found = creditsFieldId ? contentById.get(creditsFieldId) : undefined;
338
327
  let view: CreditView[] = [];
339
- if (object && !(object instanceof Error) && object.content) {
328
+ if (found) {
340
329
  try {
341
- view = parseReleaseCreditsContent(object.content);
330
+ view = parseReleaseCreditsContent(found.content);
342
331
  } catch {
343
332
  view = [];
344
333
  }
@@ -347,13 +336,8 @@ export async function getReleaseResources(
347
336
  }
348
337
  let kind: string | null | undefined;
349
338
  if (wantsKind) {
350
- const object = objects[
351
- 1 + coverFieldIds.length + (creditsFieldId ? 1 : 0)
352
- ];
353
- kind =
354
- object && !(object instanceof Error) && object.content
355
- ? parseReleaseKindContent(object.content)
356
- : null;
339
+ const found = kindFieldId ? contentById.get(kindFieldId) : undefined;
340
+ kind = found ? parseReleaseKindContent(found.content) : null;
357
341
  }
358
342
  return {
359
343
  release,
@@ -361,7 +345,7 @@ export async function getReleaseResources(
361
345
  ...(wantsCredits ? { credits: credits ?? [] } : {}),
362
346
  ...(wantsKind ? { kind: kind ?? null } : {}),
363
347
  };
364
- }
348
+ });
365
349
 
366
350
  // ── Release ──────────────────────────────────────────────────────────────────
367
351
 
@@ -380,42 +364,39 @@ export interface GetReleaseOptions {
380
364
  include?: readonly ReleaseInclude[];
381
365
  }
382
366
 
383
- export async function getReleaseDetail(
384
- client: MisoClient,
367
+ export const getReleaseDetail = Effect.fn("getReleaseDetail")(function* (
385
368
  releaseId: string,
369
+ config: MisoConfig,
386
370
  options: GetReleaseOptions = {},
387
- ): Promise<ReleaseDetail> {
388
- const { release, cover, credits, kind } = await getReleaseResources(
389
- client,
390
- releaseId,
391
- ["cover", "credits", "kind"],
392
- );
371
+ ): Effect.fn.Return<ReleaseDetail, ReleaseNotFoundError | BcsDecodeError | SuiRpcError, SuiClient | SuiGraphQL> {
372
+ const { release, cover, credits, kind } = yield* getReleaseResources(releaseId, config, [
373
+ "cover",
374
+ "credits",
375
+ "kind",
376
+ ]);
393
377
 
394
378
  const recordingIds = release.tracks.map((track) => track.recordingId);
395
- const { recordingStreamingTranscode, recordingEngineSession } = client.config.protocol;
396
- const [titles, masterReferences, transcodes, engineSessions, trackCredits] = await Promise.all([
397
- getRecordingTitles(
398
- client.protocol,
399
- client.graphql,
400
- recordingIds,
401
- client.config.deployment.musicos,
379
+ const { recordingStreamingTranscode, recordingEngineSession } = config.protocol;
380
+ const [titles, masterReferences, transcodes, engineSessions, trackCredits] = yield* Effect.all([
381
+ getRecordingTitles(recordingIds, config.deployment.musicos).pipe(
382
+ Effect.catch(() => Effect.succeed({} as Record<string, string>)),
383
+ ),
384
+ getRecordingMasterReferencesByIds(recordingIds, config.protocol.recordingMasterReference).pipe(
385
+ Effect.catch(() => Effect.succeed({} as Partial<Record<string, string>>)),
402
386
  ),
403
- getRecordingMasterReferencesByIds(
404
- client.protocol,
405
- recordingIds,
406
- client.config.protocol.recordingMasterReference,
407
- ).catch(() => ({})),
408
387
  recordingStreamingTranscode
409
- ? getRecordingStreamingTranscodesByIds(client.protocol, recordingIds, recordingStreamingTranscode).catch(() => ({}))
410
- : Promise.resolve({}),
388
+ ? getRecordingStreamingTranscodesByIds(recordingIds, recordingStreamingTranscode).pipe(
389
+ Effect.catch(() => Effect.succeed({} as Partial<Record<string, string>>)),
390
+ )
391
+ : Effect.succeed({} as Partial<Record<string, string>>),
411
392
  recordingEngineSession
412
- ? getRecordingEngineSessionsByIds(client.protocol, recordingIds, recordingEngineSession).catch(
413
- (): Partial<Record<string, RecordingEngineSessionView>> => ({}),
393
+ ? getRecordingEngineSessionsByIds(recordingIds, recordingEngineSession).pipe(
394
+ Effect.catch(() => Effect.succeed({} as Partial<Record<string, RecordingEngineSessionView>>)),
414
395
  )
415
- : Promise.resolve<Partial<Record<string, RecordingEngineSessionView>>>({}),
396
+ : Effect.succeed({} as Partial<Record<string, RecordingEngineSessionView>>),
416
397
  options.include?.includes("trackCredits")
417
- ? getTrackCreditsForRecordingIds(client, recordingIds)
418
- : Promise.resolve(undefined),
398
+ ? getTrackCreditsForRecordingIds(recordingIds, config)
399
+ : Effect.succeed(undefined),
419
400
  ]);
420
401
  const audio: TrackAudio = {
421
402
  masterBlobIds: b64UrlByRecording(masterReferences),
@@ -443,23 +424,18 @@ export async function getReleaseDetail(
443
424
  tracks: toTracks(release, titles, audio),
444
425
  ...(trackCredits !== undefined ? { trackCredits } : {}),
445
426
  };
446
- }
427
+ });
447
428
 
448
- async function getTrackCreditsForRecordingIds(
449
- client: MisoClient,
429
+ const getTrackCreditsForRecordingIds = Effect.fn("getTrackCreditsForRecordingIds")(function* (
450
430
  recordingIds: readonly string[],
451
- ): Promise<Record<string, TrackCredits>> {
452
- const { compositionCredits, recordingCredits } = client.config.protocol;
453
- const tracks = await getTrackCreditsByRecordingIds(
454
- client.protocol,
455
- client.graphql,
456
- recordingIds,
457
- {
458
- misoPackageId: client.config.deployment.musicos,
459
- compositionCreditsPackageId: compositionCredits,
460
- recordingCreditsPackageId: recordingCredits,
461
- },
462
- );
431
+ config: MisoConfig,
432
+ ): Effect.fn.Return<Record<string, TrackCredits>, SuiRpcError | BcsDecodeError, SuiClient | SuiGraphQL> {
433
+ const { compositionCredits, recordingCredits } = config.protocol;
434
+ const tracks = yield* getTrackCreditsByRecordingIds(recordingIds, {
435
+ misoPackageId: config.deployment.musicos,
436
+ compositionCreditsPackageId: compositionCredits,
437
+ recordingCreditsPackageId: recordingCredits,
438
+ });
463
439
  return Object.fromEntries(
464
440
  Object.entries(tracks).map(([id, track]) => [
465
441
  id,
@@ -473,75 +449,83 @@ async function getTrackCreditsForRecordingIds(
473
449
  } satisfies TrackCredits,
474
450
  ]),
475
451
  );
476
- }
452
+ });
477
453
 
478
454
  /**
479
455
  * Per-track credits for a release, keyed by recording id. A track with no credits
480
456
  * set maps to an empty entry rather than being absent, so a caller can tell
481
457
  * "nothing credited" from "no such track".
482
458
  */
483
- export async function getTrackCredits(
484
- client: MisoClient,
459
+ export const getTrackCredits = Effect.fn("getTrackCredits")(function* (
485
460
  releaseId: string,
486
- ): Promise<Record<string, TrackCredits>> {
487
- const release = await getReleaseById(client.protocol, releaseId);
461
+ config: MisoConfig,
462
+ ): Effect.fn.Return<Record<string, TrackCredits>, ObjectNotFoundError | SuiRpcError | BcsDecodeError, SuiClient | SuiGraphQL> {
463
+ const release = yield* getReleaseById(releaseId);
488
464
  const recordingIds = release.tracks.map((track) => track.recordingId);
489
- return getTrackCreditsForRecordingIds(client, recordingIds);
490
- }
465
+ return yield* getTrackCreditsForRecordingIds(recordingIds, config);
466
+ });
491
467
 
492
468
  // ── Pressing ─────────────────────────────────────────────────────────────────
493
469
 
494
470
  /** Everything a Pressing page renders. `null` when no such Pressing exists. */
495
- export async function getPressingDetail(
496
- client: MisoClient,
471
+ export const getPressingDetail = Effect.fn("getPressingDetail")(function* (
497
472
  pressingId: string,
473
+ config: MisoConfig,
498
474
  options: GetReleaseOptions = {},
499
- ): Promise<PressingDetail | null> {
500
- const pressing = await getPressingView(client, pressingId);
475
+ ): Effect.fn.Return<
476
+ PressingDetail | null,
477
+ ObjectTypeMismatchError | ReleaseNotFoundError | BcsDecodeError | SuiRpcError,
478
+ SuiClient | SuiGraphQL
479
+ > {
480
+ const pressing = yield* getPressingView(pressingId, config);
501
481
  if (!pressing) return null;
502
- const release = await getReleaseDetail(client, pressing.releaseId, options);
482
+ const release = yield* getReleaseDetail(pressing.releaseId, config, options);
503
483
  return { pressing, release };
504
- }
484
+ });
505
485
 
506
486
  /**
507
487
  * A Pressing page plus one currency-specific Listing. Unlike `getSaleDetail`,
508
488
  * this starts from a Pressing id, which is the durable route and Party-feature
509
489
  * reference exposed to users.
510
490
  */
511
- export async function getPressingSaleDetail(
512
- client: MisoClient,
491
+ export const getPressingSaleDetail = Effect.fn("getPressingSaleDetail")(function* (
513
492
  pressingId: string,
514
493
  currencyType: string,
494
+ config: MisoConfig,
515
495
  options: GetReleaseOptions = {},
516
- ): Promise<SaleDetail | null> {
517
- const pressing = await getPressingView(client, pressingId);
496
+ ): Effect.fn.Return<
497
+ SaleDetail | null,
498
+ ObjectTypeMismatchError | ReleaseNotFoundError | BcsDecodeError | SuiRpcError,
499
+ SuiClient | SuiGraphQL
500
+ > {
501
+ const pressing = yield* getPressingView(pressingId, config);
518
502
  if (!pressing) return null;
519
503
 
520
- const [listing, release] = await Promise.all([
521
- getListingView(client, pressing.id, currencyType),
522
- getReleaseDetail(client, pressing.releaseId, options),
504
+ const [listing, release] = yield* Effect.all([
505
+ getListingView(pressing.id, currencyType, config),
506
+ getReleaseDetail(pressing.releaseId, config, options),
523
507
  ]);
524
508
  if (!listing) return null;
525
509
  return { sale: { pressing, listing }, release };
526
- }
510
+ });
527
511
 
528
512
  /**
529
513
  * The confirmation preview behind "paste a pressing id to pin it". Verifies the
530
514
  * object really is a `Pressing` before spending reads on it — a release id or a
531
515
  * record id pasted by mistake must come back as `null`, not as a half-built card.
532
516
  */
533
- export async function getPressingPreview(
534
- client: MisoClient,
517
+ export const getPressingPreview = Effect.fn("getPressingPreview")(function* (
535
518
  pressingId: string,
536
- ): Promise<PressingPreview | null> {
537
- const pressing = await getPressingView(client, pressingId);
519
+ config: MisoConfig,
520
+ ): Effect.fn.Return<
521
+ PressingPreview | null,
522
+ ObjectTypeMismatchError | ReleaseNotFoundError | BcsDecodeError | SuiRpcError,
523
+ SuiClient
524
+ > {
525
+ const pressing = yield* getPressingView(pressingId, config);
538
526
  if (!pressing) return null;
539
527
 
540
- const { release, cover } = await getReleaseResources(
541
- client,
542
- pressing.releaseId,
543
- ["cover"],
544
- );
528
+ const { release, cover } = yield* getReleaseResources(pressing.releaseId, config, ["cover"]);
545
529
 
546
530
  return {
547
531
  pressingId,
@@ -553,22 +537,26 @@ export async function getPressingPreview(
553
537
  maxSupply: pressing.maxSupply,
554
538
  trackCount: release.tracks.length,
555
539
  };
556
- }
540
+ });
557
541
 
558
542
  /**
559
543
  * Everything a currency-specific buy page renders. The Listing is derived from
560
544
  * the release's Pressing and the requested currency, never found through mutable
561
545
  * lookup state.
562
546
  */
563
- export async function getSaleDetail(
564
- client: MisoClient,
547
+ export const getSaleDetail = Effect.fn("getSaleDetail")(function* (
565
548
  releaseId: string,
566
549
  edition: number,
567
550
  currencyType: string,
551
+ config: MisoConfig,
568
552
  options: GetReleaseOptions = {},
569
- ): Promise<SaleDetail | null> {
570
- const sales = requireRecordSalesDeployment(client.config.recordSales);
571
- const sale = await getSale(client.protocol, {
553
+ ): Effect.fn.Return<
554
+ SaleDetail | null,
555
+ ObjectTypeMismatchError | ReleaseNotFoundError | BcsDecodeError | SuiRpcError,
556
+ SuiClient | SuiGraphQL
557
+ > {
558
+ const sales = requireRecordSalesDeployment(config.recordSales);
559
+ const sale = yield* getSale({
572
560
  releaseId,
573
561
  edition,
574
562
  currencyType,
@@ -576,9 +564,9 @@ export async function getSaleDetail(
576
564
  recordShopPackageId: sales.recordShopPackageId,
577
565
  });
578
566
  if (!sale.pressing || !sale.listing) return null;
579
- const release = await getReleaseDetail(client, sale.pressing.releaseId, options);
567
+ const release = yield* getReleaseDetail(sale.pressing.releaseId, config, options);
580
568
  return { sale: toSaleView(sale.pressing, sale.listing), release };
581
- }
569
+ });
582
570
 
583
571
  // ── Discover ─────────────────────────────────────────────────────────────────
584
572
 
@@ -587,60 +575,55 @@ export async function getSaleDetail(
587
575
  *
588
576
  * Configured by release + edition + currency: both addresses are deterministic.
589
577
  */
590
- export async function getDiscoverShelf(
591
- client: MisoClient,
592
- ): Promise<DiscoverItem[]> {
593
- const configuredSales = [...client.config.discoverSales];
594
- const sales = requireRecordSalesDeployment(client.config.recordSales);
595
- const settled = await Promise.all(
596
- configuredSales.map(async (configured) => ({
597
- configured,
598
- result: await getSale(client.protocol, {
578
+ export const getDiscoverShelf = Effect.fn("getDiscoverShelf")(function* (
579
+ config: MisoConfig,
580
+ ): Effect.fn.Return<DiscoverItem[], ObjectTypeMismatchError | BcsDecodeError | SuiRpcError, SuiClient> {
581
+ const configuredSales = [...config.discoverSales];
582
+ const sales = requireRecordSalesDeployment(config.recordSales);
583
+ const settled = yield* Effect.forEach(
584
+ configuredSales,
585
+ (configured) =>
586
+ getSale({
599
587
  ...configured,
600
588
  recordPackageId: sales.recordPackageId,
601
589
  recordShopPackageId: sales.recordShopPackageId,
602
- }),
603
- })),
590
+ }).pipe(Effect.map((result) => ({ configured, result }))),
591
+ { concurrency: "unbounded" },
604
592
  );
605
593
  const available = settled.filter(
606
594
  (item): item is typeof item & {
607
- result: { pressing: ContractPressingView; listing: ContractListingView };
595
+ result: { pressing: ContractPressing; listing: ContractListing };
608
596
  } => item.result.pressing !== null && item.result.listing !== null,
609
597
  );
610
598
  const releaseIds = [...new Set(available.map((item) => item.result.pressing.releaseId))];
611
599
  if (releaseIds.length === 0) return [];
612
600
 
613
- const [releases, coverViews, credits] = await Promise.all([
614
- getReleasesByIds(client.protocol, releaseIds),
615
- getReleaseCoversByIds(client.protocol, releaseIds, client.config.protocol.releaseCoverArt).catch(() => ({}) as Partial<Record<string, ReleaseCoverView>>),
616
- getReleaseCreditsByIds(
617
- client.protocol,
618
- releaseIds,
619
- client.config.protocol.releaseCredits,
620
- ).catch(() => ({}) as Partial<Record<string, CreditView[]>>),
601
+ const [releases, coverViews, credits] = yield* Effect.all([
602
+ getReleasesByIds(releaseIds),
603
+ getReleaseCoversByIds(releaseIds, config.protocol.releaseCoverArt).pipe(
604
+ Effect.catch(() => Effect.succeed({} as Partial<Record<string, ReleaseCoverView>>)),
605
+ ),
606
+ getReleaseCreditsByIds(releaseIds, config.protocol.releaseCredits).pipe(
607
+ Effect.catch(() => Effect.succeed({} as Partial<Record<string, CreditView[]>>)),
608
+ ),
621
609
  ]);
622
610
 
623
611
  return available.flatMap(({ result }) => {
624
612
  const releaseId = result.pressing.releaseId;
625
613
  const release = releases[releaseId];
626
614
  if (!release) return [];
627
- const cover = toCover(
628
- client.config.walrusAggregatorUrl,
629
- coverViews[releaseId] ?? null,
630
- );
615
+ const cover = toCover(config.walrusAggregatorUrl, coverViews[releaseId] ?? null);
631
616
  return [
632
617
  {
633
618
  sale: toSaleView(result.pressing, result.listing),
634
619
  releaseId,
635
620
  title: release.title,
636
- artist: primaryArtistNames(toCredits(credits[releaseId] ?? null)).join(
637
- ", ",
638
- ),
621
+ artist: primaryArtistNames(toCredits(credits[releaseId] ?? null)).join(", "),
639
622
  coverUrl: cover?.still.url ?? null,
640
623
  },
641
624
  ];
642
625
  });
643
- }
626
+ });
644
627
 
645
628
  // ── Record → release ─────────────────────────────────────────────────────────
646
629
 
@@ -656,37 +639,31 @@ export interface GetRecordAlbumOptions {
656
639
  include?: readonly RecordAlbumInclude[];
657
640
  }
658
641
 
659
- export async function getRecordAlbum(
660
- client: MisoClient,
642
+ export const getRecordAlbum = Effect.fn("getRecordAlbum")(function* (
661
643
  recordId: string,
644
+ config: MisoConfig,
662
645
  options: GetRecordAlbumOptions = {},
663
- ): Promise<RecordAlbum | null> {
664
- try {
665
- const sales = requireRecordSalesDeployment(client.config.recordSales);
666
- const record = await getRecord(client.protocol, recordId, sales.recordPackageId);
667
- if (!record) return null;
668
- const releaseId = record.releaseId;
669
- const includeRelease =
670
- options.include?.some(
671
- (part) => part === "release" || part === "trackCredits",
672
- ) ?? false;
673
- const release =
674
- includeRelease && releaseId
675
- ? await getReleaseDetail(client, releaseId, {
676
- include: options.include?.includes("trackCredits")
677
- ? ["trackCredits"]
678
- : [],
679
- })
680
- : includeRelease
681
- ? null
682
- : undefined;
683
- return {
684
- recordId,
685
- releaseId,
686
- ...(includeRelease ? { release } : {}),
687
- };
688
- } catch (e) {
689
- if (isNotFound(e)) return null;
690
- throw e;
691
- }
692
- }
646
+ ): Effect.fn.Return<
647
+ RecordAlbum | null,
648
+ ObjectTypeMismatchError | ReleaseNotFoundError | BcsDecodeError | SuiRpcError,
649
+ SuiClient | SuiGraphQL
650
+ > {
651
+ const sales = requireRecordSalesDeployment(config.recordSales);
652
+ const record = yield* getRecord(recordId, sales.recordPackageId);
653
+ if (!record) return null;
654
+ const releaseId = record.releaseId;
655
+ const includeRelease = options.include?.some((part) => part === "release" || part === "trackCredits") ?? false;
656
+ const release =
657
+ includeRelease && releaseId
658
+ ? yield* getReleaseDetail(releaseId, config, {
659
+ include: options.include?.includes("trackCredits") ? ["trackCredits"] : [],
660
+ })
661
+ : includeRelease
662
+ ? null
663
+ : undefined;
664
+ return {
665
+ recordId,
666
+ releaseId,
667
+ ...(includeRelease ? { release } : {}),
668
+ };
669
+ });