@misofm/api-client 0.5.0 → 0.5.2
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.
- package/LICENSE +201 -0
- package/README.md +171 -5
- package/dist/cache.d.ts +71 -0
- package/dist/cache.js +119 -0
- package/dist/client.d.ts +150 -0
- package/dist/client.js +265 -0
- package/dist/index.d.ts +6 -0
- package/{src/index.ts → dist/index.js} +3 -9
- package/dist/schemas.d.ts +1314 -0
- package/dist/schemas.js +335 -0
- package/{src/types.ts → dist/types.d.ts} +1 -13
- package/dist/types.js +6 -0
- package/package.json +26 -12
- package/src/cache.test.ts +0 -90
- package/src/cache.ts +0 -91
- package/src/client.test.ts +0 -284
- package/src/client.ts +0 -325
- package/src/schemas.ts +0 -382
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// Copyright (c) Miso Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
//
|
|
4
|
+
// The response contract for every Miso read. This file is the SOURCE OF TRUTH:
|
|
5
|
+
//
|
|
6
|
+
// · @misofm/api-client infers its types from it
|
|
7
|
+
// · miso-read-service generates its OpenAPI document from it
|
|
8
|
+
// · a contract test in that service parses every handler's real output through
|
|
9
|
+
// it, so missing, renamed, or mistyped fields fail CI rather than reaching a
|
|
10
|
+
// client. Additive fields remain forward-compatible and are stripped until
|
|
11
|
+
// the public schema adopts them.
|
|
12
|
+
//
|
|
13
|
+
// Schemas, not hand-written interfaces, precisely so that third clause is
|
|
14
|
+
// possible. The CLI and any future client read the same OpenAPI.
|
|
15
|
+
//
|
|
16
|
+
// SCALARS: every u64/u128 is a DECIMAL STRING (`u64Schema`), never a number.
|
|
17
|
+
// Prices, supply counts, and balances routinely exceed 2^53, and JSON has no
|
|
18
|
+
// integer type that holds them. Millisecond timestamps stay numbers — they are
|
|
19
|
+
// inside Number.MAX_SAFE_INTEGER and callers want to pass them to `new Date()`.
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
/** A u64/u128 in base units, as a decimal string. See the file header. */
|
|
22
|
+
export const u64Schema = z
|
|
23
|
+
.string()
|
|
24
|
+
.regex(/^\d+$/, "expected a decimal integer string");
|
|
25
|
+
/** A 0x-prefixed 32-byte Sui object id or address, in canonical form. */
|
|
26
|
+
export const suiIdSchema = z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(/^0x[0-9a-fA-F]{1,64}$/, "expected a Sui object id");
|
|
29
|
+
/** A 32-byte Walrus blob id encoded as URL-safe, unpadded base64. */
|
|
30
|
+
export const walrusBlobIdSchema = z
|
|
31
|
+
.string()
|
|
32
|
+
.regex(/^[A-Za-z0-9_-]{43}$/, "expected a Walrus blob id");
|
|
33
|
+
// ── Shared ───────────────────────────────────────────────────────────────────
|
|
34
|
+
export const workStateSchema = z.union([
|
|
35
|
+
z.object({ type: z.literal("Initialized") }),
|
|
36
|
+
z.object({ type: z.literal("Published"), timestampMs: z.number().int() }),
|
|
37
|
+
]);
|
|
38
|
+
export const coverImageSchema = z.object({
|
|
39
|
+
kind: z.enum(["blob", "quiltPatch"]),
|
|
40
|
+
/** Aggregator URL — the only field a renderer needs. */
|
|
41
|
+
url: z.string().url(),
|
|
42
|
+
});
|
|
43
|
+
export const coverSchema = z.object({
|
|
44
|
+
still: coverImageSchema,
|
|
45
|
+
animated: coverImageSchema.nullable(),
|
|
46
|
+
});
|
|
47
|
+
export const creditSchema = z.object({
|
|
48
|
+
partyId: suiIdSchema,
|
|
49
|
+
displayName: z.string(),
|
|
50
|
+
roles: z.array(z.string()),
|
|
51
|
+
});
|
|
52
|
+
export const trackViewSchema = z.object({
|
|
53
|
+
/** Display number — "1", or "1.2" (disc.track) on a multi-disc set. */
|
|
54
|
+
no: z.string(),
|
|
55
|
+
title: z.string(),
|
|
56
|
+
recordingId: suiIdSchema,
|
|
57
|
+
/** The composition underlying this track's recording. */
|
|
58
|
+
compositionId: suiIdSchema,
|
|
59
|
+
/** This track's share of the release's revenue, in basis points. */
|
|
60
|
+
splitBps: z.number().int().min(0).max(10_000),
|
|
61
|
+
disc: z.number().int().min(1),
|
|
62
|
+
/** Streaming master attached through recording_master_reference, when present. */
|
|
63
|
+
masterBlobId: walrusBlobIdSchema.optional(),
|
|
64
|
+
});
|
|
65
|
+
/** A recording's work-role credits and recording billing positions. */
|
|
66
|
+
export const recordingCreditsSchema = z.object({
|
|
67
|
+
credits: z.array(creditSchema),
|
|
68
|
+
primaryArtistIds: z.array(suiIdSchema),
|
|
69
|
+
featuredArtistIds: z.array(suiIdSchema),
|
|
70
|
+
});
|
|
71
|
+
/** Per-track credits for a release. Composition writing and recording
|
|
72
|
+
* performance/production credits remain separate. */
|
|
73
|
+
const completeTrackCreditsSchema = z.object({
|
|
74
|
+
compositionCredits: z.array(creditSchema),
|
|
75
|
+
recordingCredits: recordingCreditsSchema,
|
|
76
|
+
});
|
|
77
|
+
/** The HTTP wire accepts the complete shape and the deployed recording-only shape. */
|
|
78
|
+
export const trackCreditsWireSchema = z.union([
|
|
79
|
+
completeTrackCreditsSchema,
|
|
80
|
+
recordingCreditsSchema,
|
|
81
|
+
]);
|
|
82
|
+
/** Clients always receive the complete shape, including during the rollout. */
|
|
83
|
+
export const trackCreditsSchema = trackCreditsWireSchema.transform((credits) => "recordingCredits" in credits
|
|
84
|
+
? credits
|
|
85
|
+
: { compositionCredits: [], recordingCredits: credits });
|
|
86
|
+
// ── Catalog ──────────────────────────────────────────────────────────────────
|
|
87
|
+
const releaseDetailBaseSchema = z.object({
|
|
88
|
+
id: suiIdSchema,
|
|
89
|
+
title: z.string(),
|
|
90
|
+
subtitle: z.string().nullable(),
|
|
91
|
+
/** Self-declared `release_kind`, or null when the extension is absent. */
|
|
92
|
+
kind: z.string().nullable(),
|
|
93
|
+
state: workStateSchema,
|
|
94
|
+
publishedAtMs: z.number().int().nullable(),
|
|
95
|
+
cover: coverSchema.nullable(),
|
|
96
|
+
credits: z.array(creditSchema),
|
|
97
|
+
/** The release's primary artists in chain order — its artist line. */
|
|
98
|
+
primaryArtists: z.array(z.string()),
|
|
99
|
+
discCount: z.number().int().min(0),
|
|
100
|
+
tracks: z.array(trackViewSchema),
|
|
101
|
+
});
|
|
102
|
+
/** Transform-free wire form used by OpenAPI generation. */
|
|
103
|
+
export const releaseDetailWireSchema = releaseDetailBaseSchema.extend({
|
|
104
|
+
trackCredits: z.record(suiIdSchema, trackCreditsWireSchema).optional(),
|
|
105
|
+
});
|
|
106
|
+
export const releaseDetailSchema = releaseDetailBaseSchema.extend({
|
|
107
|
+
/** Present only when requested via `include`. */
|
|
108
|
+
trackCredits: z.record(suiIdSchema, trackCreditsSchema).optional(),
|
|
109
|
+
});
|
|
110
|
+
export const priceSchema = z.object({
|
|
111
|
+
/** `fixed` — pay exactly this. `floor` — pay at least this. */
|
|
112
|
+
kind: z.enum(["fixed", "floor"]),
|
|
113
|
+
amount: u64Schema,
|
|
114
|
+
});
|
|
115
|
+
export const currencySchema = z.object({
|
|
116
|
+
type: z.string().nullable(),
|
|
117
|
+
symbol: z.string(),
|
|
118
|
+
decimals: z.number().int().min(0).max(18),
|
|
119
|
+
});
|
|
120
|
+
export const pressingStateSchema = z.union([
|
|
121
|
+
z.object({
|
|
122
|
+
kind: z.literal("scheduled"),
|
|
123
|
+
startTimestampMs: z.number().int(),
|
|
124
|
+
}),
|
|
125
|
+
z.object({ kind: z.literal("active") }),
|
|
126
|
+
z.object({ kind: z.literal("paused") }),
|
|
127
|
+
]);
|
|
128
|
+
export const pressingViewSchema = z.object({
|
|
129
|
+
id: suiIdSchema,
|
|
130
|
+
releaseId: suiIdSchema,
|
|
131
|
+
state: pressingStateSchema,
|
|
132
|
+
/** Records pressed so far. The permanent run is intentionally uncapped. */
|
|
133
|
+
supply: u64Schema,
|
|
134
|
+
});
|
|
135
|
+
export const listingViewSchema = z.object({
|
|
136
|
+
id: suiIdSchema,
|
|
137
|
+
pressingId: suiIdSchema,
|
|
138
|
+
releaseId: suiIdSchema,
|
|
139
|
+
price: priceSchema,
|
|
140
|
+
currency: currencySchema,
|
|
141
|
+
state: z.enum(["enabled", "disabled"]),
|
|
142
|
+
});
|
|
143
|
+
export const pressingDetailSchema = z.object({
|
|
144
|
+
pressing: pressingViewSchema,
|
|
145
|
+
release: releaseDetailSchema,
|
|
146
|
+
});
|
|
147
|
+
export const pressingDetailWireSchema = z.object({
|
|
148
|
+
pressing: pressingViewSchema,
|
|
149
|
+
release: releaseDetailWireSchema,
|
|
150
|
+
});
|
|
151
|
+
export const recordAlbumSchema = z.object({
|
|
152
|
+
recordId: suiIdSchema,
|
|
153
|
+
releaseId: suiIdSchema.nullable(),
|
|
154
|
+
/** Present only when requested via `include`. */
|
|
155
|
+
release: releaseDetailSchema.nullable().optional(),
|
|
156
|
+
});
|
|
157
|
+
export const recordAlbumWireSchema = z.object({
|
|
158
|
+
recordId: suiIdSchema,
|
|
159
|
+
releaseId: suiIdSchema.nullable(),
|
|
160
|
+
release: releaseDetailWireSchema.nullable().optional(),
|
|
161
|
+
});
|
|
162
|
+
// ── Artist ───────────────────────────────────────────────────────────────────
|
|
163
|
+
export const partyMemberSchema = z.object({
|
|
164
|
+
id: suiIdSchema,
|
|
165
|
+
name: z.string(),
|
|
166
|
+
});
|
|
167
|
+
/**
|
|
168
|
+
* Every external platform the party link extensions know how to build a URL for,
|
|
169
|
+
* spanning social, music, and professional payloads. Enumerated rather than left
|
|
170
|
+
* as `string` so a renderer's icon/label switch is exhaustive at compile time —
|
|
171
|
+
* adding a platform on-chain should break the UI that has no icon for it.
|
|
172
|
+
*/
|
|
173
|
+
export const platformKeySchema = z.enum([
|
|
174
|
+
// Social (party_social)
|
|
175
|
+
"x",
|
|
176
|
+
"instagram",
|
|
177
|
+
"threads",
|
|
178
|
+
"tiktok",
|
|
179
|
+
"youtube",
|
|
180
|
+
"discord",
|
|
181
|
+
"telegram",
|
|
182
|
+
"reddit",
|
|
183
|
+
"twitch",
|
|
184
|
+
"facebook",
|
|
185
|
+
// Music (party_music)
|
|
186
|
+
"spotify",
|
|
187
|
+
"bandcamp",
|
|
188
|
+
"soundcloud",
|
|
189
|
+
"appleMusic",
|
|
190
|
+
"deezer",
|
|
191
|
+
"tidal",
|
|
192
|
+
"amazonMusic",
|
|
193
|
+
"audiomack",
|
|
194
|
+
// Professional / industry (party_pro_link)
|
|
195
|
+
"website",
|
|
196
|
+
"bookingPage",
|
|
197
|
+
"managementPage",
|
|
198
|
+
"publisherPage",
|
|
199
|
+
"labelPage",
|
|
200
|
+
"epk",
|
|
201
|
+
"patreon",
|
|
202
|
+
"substack",
|
|
203
|
+
"kofi",
|
|
204
|
+
]);
|
|
205
|
+
export const partyLinkSchema = z.object({
|
|
206
|
+
platform: platformKeySchema,
|
|
207
|
+
/** The platform-native identifier stored on-chain (handle / id / subdomain / URL). */
|
|
208
|
+
value: z.string(),
|
|
209
|
+
/** The public profile URL, rebuilt client-side from `value`. */
|
|
210
|
+
url: z.string(),
|
|
211
|
+
});
|
|
212
|
+
export const partyCtaSchema = z.object({ label: z.string(), url: z.string() });
|
|
213
|
+
export const artistProfileSchema = z.object({
|
|
214
|
+
id: suiIdSchema,
|
|
215
|
+
kind: z.enum(["individual", "group"]),
|
|
216
|
+
name: z.string(),
|
|
217
|
+
createdAtMs: z.number().int(),
|
|
218
|
+
bioShort: z.string().nullable(),
|
|
219
|
+
bioLong: z.string().nullable(),
|
|
220
|
+
country: z.string().nullable(),
|
|
221
|
+
languages: z.array(z.string()),
|
|
222
|
+
/** Display names, already humanized from the on-chain HIP_HOP form. */
|
|
223
|
+
genres: z.array(z.string()),
|
|
224
|
+
links: z.array(partyLinkSchema),
|
|
225
|
+
ctas: z.array(partyCtaSchema),
|
|
226
|
+
members: z.array(partyMemberSchema),
|
|
227
|
+
/** Present only when requested via `include` — the owner-editor fields. */
|
|
228
|
+
roles: z.array(z.string()).optional(),
|
|
229
|
+
tags: z.array(z.string()).optional(),
|
|
230
|
+
avatarUrl: z.string().url(),
|
|
231
|
+
});
|
|
232
|
+
export const partySummarySchema = z.object({
|
|
233
|
+
id: suiIdSchema,
|
|
234
|
+
name: z.string(),
|
|
235
|
+
kind: z.enum(["individual", "group"]),
|
|
236
|
+
});
|
|
237
|
+
export const partySummariesSchema = z.array(partySummarySchema);
|
|
238
|
+
// ── Wallet-scoped ────────────────────────────────────────────────────────────
|
|
239
|
+
export const ownedRecordSchema = z.object({
|
|
240
|
+
id: suiIdSchema,
|
|
241
|
+
type: z.string(),
|
|
242
|
+
releaseId: suiIdSchema.nullable(),
|
|
243
|
+
/** This copy's number in its run. */
|
|
244
|
+
number: z.number().int().nullable(),
|
|
245
|
+
});
|
|
246
|
+
export const ownedRecordsSchema = z.array(ownedRecordSchema);
|
|
247
|
+
export const ownedPartySchema = z.object({
|
|
248
|
+
partyId: suiIdSchema,
|
|
249
|
+
capId: suiIdSchema,
|
|
250
|
+
name: z.string(),
|
|
251
|
+
kind: z.enum(["individual", "group"]),
|
|
252
|
+
});
|
|
253
|
+
export const ownedPartiesSchema = z.array(ownedPartySchema);
|
|
254
|
+
/** A group invitation awaiting action by a party the wallet administers. */
|
|
255
|
+
export const pendingMembershipSchema = z.object({
|
|
256
|
+
memberPartyId: suiIdSchema,
|
|
257
|
+
memberCapId: suiIdSchema,
|
|
258
|
+
groupId: suiIdSchema,
|
|
259
|
+
groupName: z.string(),
|
|
260
|
+
});
|
|
261
|
+
export const pendingMembershipsSchema = z.array(pendingMembershipSchema);
|
|
262
|
+
export const workKindSchema = z.enum(["composition", "recording", "release"]);
|
|
263
|
+
export const ownedWorkSchema = z.object({
|
|
264
|
+
/** The ADMIN CAP object id — the catalog's routing key. */
|
|
265
|
+
capId: suiIdSchema,
|
|
266
|
+
kind: workKindSchema,
|
|
267
|
+
workId: suiIdSchema,
|
|
268
|
+
title: z.string(),
|
|
269
|
+
state: z.string(),
|
|
270
|
+
});
|
|
271
|
+
export const ownedWorksSchema = z.array(ownedWorkSchema);
|
|
272
|
+
export const workDetailSchema = ownedWorkSchema.extend({
|
|
273
|
+
subtitle: z.string().optional(),
|
|
274
|
+
royaltyRateBps: z.number().int().min(0).max(10_000).optional(),
|
|
275
|
+
shareType: z.string().optional(),
|
|
276
|
+
discCount: z.number().int().min(0).optional(),
|
|
277
|
+
trackCount: z.number().int().min(0).optional(),
|
|
278
|
+
});
|
|
279
|
+
export const balanceSchema = z.object({
|
|
280
|
+
address: suiIdSchema,
|
|
281
|
+
coinType: z.string(),
|
|
282
|
+
/** Base units. Totals coin objects AND the address balance. */
|
|
283
|
+
balance: u64Schema,
|
|
284
|
+
decimals: z.number().int().min(0).max(18),
|
|
285
|
+
});
|
|
286
|
+
export const ownershipSchema = z.object({
|
|
287
|
+
address: suiIdSchema,
|
|
288
|
+
objectId: suiIdSchema,
|
|
289
|
+
isOwner: z.boolean(),
|
|
290
|
+
/** Party checks only — the derived PartyAdminCap id owner-gated writes need. */
|
|
291
|
+
capId: suiIdSchema.optional(),
|
|
292
|
+
});
|
|
293
|
+
// ── Receipts ─────────────────────────────────────────────────────────────────
|
|
294
|
+
export const recordSaleSchema = z.object({
|
|
295
|
+
listingId: suiIdSchema,
|
|
296
|
+
pressingId: suiIdSchema,
|
|
297
|
+
releaseId: suiIdSchema,
|
|
298
|
+
recordId: suiIdSchema,
|
|
299
|
+
number: u64Schema,
|
|
300
|
+
paid: u64Schema,
|
|
301
|
+
price: priceSchema,
|
|
302
|
+
currencyType: z.string(),
|
|
303
|
+
buyer: suiIdSchema,
|
|
304
|
+
});
|
|
305
|
+
export const trackRoyaltySchema = z.object({
|
|
306
|
+
no: z.string(),
|
|
307
|
+
title: z.string(),
|
|
308
|
+
recordingId: suiIdSchema,
|
|
309
|
+
splitBps: z.number().int().min(0).max(10_000),
|
|
310
|
+
amount: u64Schema,
|
|
311
|
+
/** Both null when the composition's royalty rate could not be resolved. */
|
|
312
|
+
composition: u64Schema.nullable(),
|
|
313
|
+
recording: u64Schema.nullable(),
|
|
314
|
+
});
|
|
315
|
+
export const purchaseReceiptSchema = z.object({
|
|
316
|
+
sale: recordSaleSchema,
|
|
317
|
+
detail: pressingDetailSchema,
|
|
318
|
+
/** The Listing price — differs from `sale.paid` on a floor-price overpay. */
|
|
319
|
+
price: u64Schema,
|
|
320
|
+
tracks: z.array(trackRoyaltySchema),
|
|
321
|
+
});
|
|
322
|
+
export const purchaseReceiptWireSchema = z.object({
|
|
323
|
+
sale: recordSaleSchema,
|
|
324
|
+
detail: pressingDetailWireSchema,
|
|
325
|
+
price: u64Schema,
|
|
326
|
+
tracks: z.array(trackRoyaltySchema),
|
|
327
|
+
});
|
|
328
|
+
// ── Errors ───────────────────────────────────────────────────────────────────
|
|
329
|
+
/** The envelope every non-2xx carries, matching miso-api's `apiError`. */
|
|
330
|
+
export const apiErrorSchema = z.object({
|
|
331
|
+
error: z.object({
|
|
332
|
+
code: z.string(),
|
|
333
|
+
message: z.string(),
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
// Copyright (c) Miso Labs, Inc.
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
//
|
|
4
|
-
// Types inferred from ./schemas.ts. Nothing is hand-written here on purpose — a
|
|
5
|
-
// type and its validator that can disagree will eventually disagree.
|
|
6
|
-
|
|
7
1
|
import type { z } from "zod";
|
|
8
|
-
import type * as s from "./schemas.
|
|
9
|
-
|
|
2
|
+
import type * as s from "./schemas.js";
|
|
10
3
|
export type WorkState = z.infer<typeof s.workStateSchema>;
|
|
11
4
|
export type CoverImage = z.infer<typeof s.coverImageSchema>;
|
|
12
5
|
export type Cover = z.infer<typeof s.coverSchema>;
|
|
13
6
|
export type Credit = z.infer<typeof s.creditSchema>;
|
|
14
7
|
export type TrackView = z.infer<typeof s.trackViewSchema>;
|
|
15
|
-
|
|
16
8
|
export type ReleaseDetail = z.infer<typeof s.releaseDetailSchema>;
|
|
17
9
|
export type TrackCredits = z.infer<typeof s.trackCreditsSchema>;
|
|
18
10
|
export type Price = z.infer<typeof s.priceSchema>;
|
|
@@ -22,14 +14,12 @@ export type PressingView = z.infer<typeof s.pressingViewSchema>;
|
|
|
22
14
|
export type ListingView = z.infer<typeof s.listingViewSchema>;
|
|
23
15
|
export type PressingDetail = z.infer<typeof s.pressingDetailSchema>;
|
|
24
16
|
export type RecordAlbum = z.infer<typeof s.recordAlbumSchema>;
|
|
25
|
-
|
|
26
17
|
export type PartyMember = z.infer<typeof s.partyMemberSchema>;
|
|
27
18
|
export type PlatformKey = z.infer<typeof s.platformKeySchema>;
|
|
28
19
|
export type PartyLink = z.infer<typeof s.partyLinkSchema>;
|
|
29
20
|
export type PartyCta = z.infer<typeof s.partyCtaSchema>;
|
|
30
21
|
export type ArtistProfile = z.infer<typeof s.artistProfileSchema>;
|
|
31
22
|
export type PartySummary = z.infer<typeof s.partySummarySchema>;
|
|
32
|
-
|
|
33
23
|
export type OwnedRecord = z.infer<typeof s.ownedRecordSchema>;
|
|
34
24
|
export type OwnedParty = z.infer<typeof s.ownedPartySchema>;
|
|
35
25
|
export type PendingMembership = z.infer<typeof s.pendingMembershipSchema>;
|
|
@@ -38,9 +28,7 @@ export type OwnedWork = z.infer<typeof s.ownedWorkSchema>;
|
|
|
38
28
|
export type WorkDetail = z.infer<typeof s.workDetailSchema>;
|
|
39
29
|
export type Balance = z.infer<typeof s.balanceSchema>;
|
|
40
30
|
export type Ownership = z.infer<typeof s.ownershipSchema>;
|
|
41
|
-
|
|
42
31
|
export type RecordSale = z.infer<typeof s.recordSaleSchema>;
|
|
43
32
|
export type TrackRoyalty = z.infer<typeof s.trackRoyaltySchema>;
|
|
44
33
|
export type PurchaseReceipt = z.infer<typeof s.purchaseReceiptSchema>;
|
|
45
|
-
|
|
46
34
|
export type ApiErrorBody = z.infer<typeof s.apiErrorSchema>;
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@misofm/api-client",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Typed client and response contract for the Miso API read layer. The one definition of what a Miso read returns.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -12,33 +12,47 @@
|
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/misofm/api/issues"
|
|
14
14
|
},
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
17
18
|
"type": "module",
|
|
18
19
|
"sideEffects": false,
|
|
19
20
|
"exports": {
|
|
20
|
-
".":
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./schemas": {
|
|
26
|
+
"types": "./dist/schemas.d.ts",
|
|
27
|
+
"import": "./dist/schemas.js"
|
|
28
|
+
},
|
|
29
|
+
"./types": {
|
|
30
|
+
"types": "./dist/types.d.ts",
|
|
31
|
+
"import": "./dist/types.js"
|
|
32
|
+
},
|
|
33
|
+
"./cache": {
|
|
34
|
+
"types": "./dist/cache.d.ts",
|
|
35
|
+
"import": "./dist/cache.js"
|
|
36
|
+
}
|
|
24
37
|
},
|
|
25
38
|
"files": [
|
|
26
|
-
"
|
|
39
|
+
"dist",
|
|
40
|
+
"LICENSE",
|
|
27
41
|
"README.md",
|
|
28
42
|
"package.json"
|
|
29
43
|
],
|
|
30
44
|
"scripts": {
|
|
45
|
+
"build": "node ./scripts/clean.mjs && tsc -p tsconfig.build.json",
|
|
46
|
+
"prepack": "npm run build",
|
|
47
|
+
"pack:check": "node ./scripts/verify-package.mjs",
|
|
31
48
|
"test": "bun test",
|
|
32
49
|
"typecheck": "tsc --noEmit"
|
|
33
50
|
},
|
|
34
51
|
"dependencies": {
|
|
35
52
|
"zod": "^4.3.6"
|
|
36
53
|
},
|
|
37
|
-
"peerDependencies": {
|
|
38
|
-
"typescript": "^5"
|
|
39
|
-
},
|
|
40
54
|
"devDependencies": {
|
|
41
|
-
"@types/bun": "
|
|
55
|
+
"@types/bun": "1.3.14",
|
|
42
56
|
"typescript": "^5"
|
|
43
57
|
},
|
|
44
58
|
"publishConfig": {
|
package/src/cache.test.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Miso Labs, Inc.
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
//
|
|
4
|
-
// The cache table is a policy document that happens to be executable, so it gets
|
|
5
|
-
// tested as one: the exact header per class, and the invariants that make the
|
|
6
|
-
// classes coherent with each other.
|
|
7
|
-
|
|
8
|
-
import { describe, expect, test } from "bun:test";
|
|
9
|
-
import { CACHE_POLICIES, cacheControl, workCacheClass, type CacheClass } from "./cache.ts";
|
|
10
|
-
|
|
11
|
-
describe("cacheControl", () => {
|
|
12
|
-
const expected: Record<CacheClass, string> = {
|
|
13
|
-
immutable: "public, max-age=31536000, s-maxage=31536000, immutable",
|
|
14
|
-
published: "public, max-age=300, s-maxage=3600, stale-while-revalidate=86400",
|
|
15
|
-
draft: "public, max-age=0, s-maxage=60, stale-while-revalidate=300",
|
|
16
|
-
artist: "public, max-age=30, s-maxage=60, stale-while-revalidate=600",
|
|
17
|
-
sale: "public, max-age=0, s-maxage=60, stale-while-revalidate=300",
|
|
18
|
-
private: "private, no-store",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
for (const [cls, header] of Object.entries(expected)) {
|
|
22
|
-
test(`${cls} → ${header}`, () => {
|
|
23
|
-
expect(cacheControl(cls as CacheClass)).toBe(header);
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
test("private never reaches a shared cache", () => {
|
|
28
|
-
const header = cacheControl("private");
|
|
29
|
-
expect(header).not.toContain("public");
|
|
30
|
-
expect(header).not.toContain("s-maxage");
|
|
31
|
-
expect(header).toContain("no-store");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("policy invariants", () => {
|
|
36
|
-
const publicClasses = (Object.keys(CACHE_POLICIES) as CacheClass[]).filter((c) => c !== "private");
|
|
37
|
-
|
|
38
|
-
test("the edge always holds a response at least as long as a browser does", () => {
|
|
39
|
-
// Otherwise the browser outlives the shared copy and the edge cache is
|
|
40
|
-
// pointless — every client revalidation would miss.
|
|
41
|
-
for (const cls of publicClasses) {
|
|
42
|
-
const p = CACHE_POLICIES[cls];
|
|
43
|
-
expect(p.sMaxAge!).toBeGreaterThanOrEqual(p.maxAge);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("staleTime mirrors the browser lifetime, so the client never refetches what it already holds fresh", () => {
|
|
48
|
-
for (const cls of publicClasses) {
|
|
49
|
-
const p = CACHE_POLICIES[cls];
|
|
50
|
-
if (Number.isFinite(p.staleTimeMs)) expect(p.staleTimeMs).toBe(p.maxAge * 1000);
|
|
51
|
-
}
|
|
52
|
-
expect(CACHE_POLICIES.immutable.staleTimeMs).toBe(Number.POSITIVE_INFINITY);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("live sale data is among the shortest public TTLs", () => {
|
|
56
|
-
// No longer strictly the shortest: a pressing is UNCAPPED, so its
|
|
57
|
-
// copies-sold counter gates nothing and a minute of staleness is cosmetic.
|
|
58
|
-
// `draft` now ties it, because an editor really must see their own change.
|
|
59
|
-
const others = publicClasses.filter((c) => c !== "sale");
|
|
60
|
-
for (const cls of others) {
|
|
61
|
-
expect(CACHE_POLICIES.sale.sMaxAge!).toBeLessThanOrEqual(CACHE_POLICIES[cls].sMaxAge!);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("a draft is cached far more briefly than a published work", () => {
|
|
66
|
-
expect(CACHE_POLICIES.draft.sMaxAge!).toBeLessThan(CACHE_POLICIES.published.sMaxAge!);
|
|
67
|
-
// An editor must see their own change land.
|
|
68
|
-
expect(CACHE_POLICIES.draft.maxAge).toBe(0);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("private carries no shared lifetime at all", () => {
|
|
72
|
-
expect(CACHE_POLICIES.private.sMaxAge).toBeNull();
|
|
73
|
-
expect(CACHE_POLICIES.private.staleWhileRevalidate).toBeNull();
|
|
74
|
-
expect(CACHE_POLICIES.private.staleTimeMs).toBe(0);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe("workCacheClass", () => {
|
|
79
|
-
test("published work caches long, draft caches short", () => {
|
|
80
|
-
expect(workCacheClass({ type: "Published" })).toBe("published");
|
|
81
|
-
expect(workCacheClass({ type: "Initialized" })).toBe("draft");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("an unknown or missing state is treated as a draft", () => {
|
|
85
|
-
// Fail toward freshness: over-caching a work that is still moving is the
|
|
86
|
-
// worse error, because the artist edits and nothing happens.
|
|
87
|
-
expect(workCacheClass(null)).toBe("draft");
|
|
88
|
-
expect(workCacheClass(undefined)).toBe("draft");
|
|
89
|
-
});
|
|
90
|
-
});
|
package/src/cache.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Miso Labs, Inc.
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
//
|
|
4
|
-
// How long each kind of read stays good. ONE table, shared by both ends:
|
|
5
|
-
//
|
|
6
|
-
// miso-read-service turns a class into `Cache-Control` and a Cache API entry
|
|
7
|
-
// this client turns the same class into a TanStack Query `staleTime`
|
|
8
|
-
//
|
|
9
|
-
// They live together because a client that refetches every 30s in front of an
|
|
10
|
-
// edge that caches for 60s is just serving itself the cached body twice. Reading
|
|
11
|
-
// the same numbers keeps the browser's idea of "fresh" and the edge's identical.
|
|
12
|
-
//
|
|
13
|
-
// The classes are chosen by WHAT MOVES THE DATA, not by route:
|
|
14
|
-
//
|
|
15
|
-
// immutable nothing can change it, ever. A record's parent release is fixed
|
|
16
|
-
// at mint; a settled transaction is settled. Cache for a year.
|
|
17
|
-
// published a published work. Its metadata only moves when an artist
|
|
18
|
-
// deliberately re-sets an extension — rare, and a minute of
|
|
19
|
-
// staleness costs nobody anything.
|
|
20
|
-
// draft the same work before publication: actively being edited in
|
|
21
|
-
// studio, so the editor must see their own change land.
|
|
22
|
-
// artist a profile page. Owner edits should surface within a minute.
|
|
23
|
-
// sale a Pressing or Listing. Their state and the uncapped run's supply
|
|
24
|
-
// can move, so clients refresh them quickly. The chain remains the
|
|
25
|
-
// purchase gate when an edge response is briefly stale.
|
|
26
|
-
// private address-scoped. Never enters a shared cache — the failure mode is
|
|
27
|
-
// serving one user's library to another.
|
|
28
|
-
//
|
|
29
|
-
// THERE IS NO INVALIDATION. These TTLs are the whole freshness story: the cache
|
|
30
|
-
// is an optimization, never a source of truth, and the chain stays
|
|
31
|
-
// authoritative. A writer who must see their OWN change immediately appends a
|
|
32
|
-
// cache-buster param, which mints one throwaway entry instead of evicting the
|
|
33
|
-
// one everybody else is reading.
|
|
34
|
-
|
|
35
|
-
export type CacheClass = "immutable" | "published" | "draft" | "artist" | "sale" | "private";
|
|
36
|
-
|
|
37
|
-
export interface CachePolicy {
|
|
38
|
-
/** Shared-cache lifetime in seconds (`s-maxage`). `null` for private. */
|
|
39
|
-
sMaxAge: number | null;
|
|
40
|
-
/**
|
|
41
|
-
* How long past `sMaxAge` the edge may serve the stale body while it
|
|
42
|
-
* revalidates behind the request. This is where the latency win lives: a
|
|
43
|
-
* visitor after expiry gets the old body instantly instead of waiting on a
|
|
44
|
-
* chain round-trip.
|
|
45
|
-
*/
|
|
46
|
-
staleWhileRevalidate: number | null;
|
|
47
|
-
/** Browser lifetime (`max-age`). Deliberately shorter than the edge's. */
|
|
48
|
-
maxAge: number;
|
|
49
|
-
/** TanStack Query `staleTime`, in ms. Mirrors `maxAge`. */
|
|
50
|
-
staleTimeMs: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export const CACHE_POLICIES: Record<CacheClass, CachePolicy> = {
|
|
54
|
-
immutable: { sMaxAge: 31_536_000, staleWhileRevalidate: null, maxAge: 31_536_000, staleTimeMs: Number.POSITIVE_INFINITY },
|
|
55
|
-
published: { sMaxAge: 3_600, staleWhileRevalidate: 86_400, maxAge: 300, staleTimeMs: 300_000 },
|
|
56
|
-
draft: { sMaxAge: 60, staleWhileRevalidate: 300, maxAge: 0, staleTimeMs: 0 },
|
|
57
|
-
artist: { sMaxAge: 60, staleWhileRevalidate: 600, maxAge: 30, staleTimeMs: 30_000 },
|
|
58
|
-
sale: { sMaxAge: 60, staleWhileRevalidate: 300, maxAge: 0, staleTimeMs: 0 },
|
|
59
|
-
private: { sMaxAge: null, staleWhileRevalidate: null, maxAge: 0, staleTimeMs: 0 },
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* The `Cache-Control` header for a class.
|
|
64
|
-
*
|
|
65
|
-
* `private, no-store` on the private class is doing real work: `private` alone
|
|
66
|
-
* would still let a browser (or a misconfigured intermediary) retain the body,
|
|
67
|
-
* and these responses describe one wallet's holdings.
|
|
68
|
-
*/
|
|
69
|
-
export function cacheControl(cls: CacheClass): string {
|
|
70
|
-
const p = CACHE_POLICIES[cls];
|
|
71
|
-
if (p.sMaxAge === null) return "private, no-store";
|
|
72
|
-
|
|
73
|
-
const parts = ["public", `max-age=${p.maxAge}`, `s-maxage=${p.sMaxAge}`];
|
|
74
|
-
if (p.staleWhileRevalidate !== null) parts.push(`stale-while-revalidate=${p.staleWhileRevalidate}`);
|
|
75
|
-
if (cls === "immutable") parts.push("immutable");
|
|
76
|
-
return parts.join(", ");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** TanStack Query options for a class — spread straight into `useQuery`. */
|
|
80
|
-
export function queryPolicy(cls: CacheClass): { staleTime: number } {
|
|
81
|
-
return { staleTime: CACHE_POLICIES[cls].staleTimeMs };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* A work's class from its own state. This is why the middleware picks TTL from
|
|
86
|
-
* the RESPONSE rather than the route: the same `/releases/:id` path is a
|
|
87
|
-
* year-stable published record for one id and a live draft for another.
|
|
88
|
-
*/
|
|
89
|
-
export function workCacheClass(state: { type: string } | null | undefined): CacheClass {
|
|
90
|
-
return state?.type === "Published" ? "published" : "draft";
|
|
91
|
-
}
|