@openrails/contentkit-ui 0.86.0 → 0.87.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 (127) hide show
  1. package/README.md +39 -14
  2. package/dist/client/beacons.js +5 -1
  3. package/dist/client/beacons.js.map +1 -1
  4. package/dist/client/content/types.d.ts +4 -2
  5. package/dist/client/content/types.js.map +1 -1
  6. package/dist/client/contents.d.ts +8 -6
  7. package/dist/client/contents.js +10 -7
  8. package/dist/client/contents.js.map +1 -1
  9. package/dist/client/discovery.js +3 -0
  10. package/dist/client/discovery.js.map +1 -1
  11. package/dist/client/errors.d.ts +21 -3
  12. package/dist/client/errors.js +44 -14
  13. package/dist/client/errors.js.map +1 -1
  14. package/dist/client/gallery.d.ts +3 -5
  15. package/dist/client/gallery.js +16 -20
  16. package/dist/client/gallery.js.map +1 -1
  17. package/dist/client/generated/error-codes.d.ts +1 -1
  18. package/dist/client/generated/error-codes.js +8 -0
  19. package/dist/client/generated/error-codes.js.map +1 -1
  20. package/dist/client/generated/routes.d.ts +16 -3
  21. package/dist/client/generated/routes.js +28 -3
  22. package/dist/client/generated/routes.js.map +1 -1
  23. package/dist/client/generated/wire.d.ts +43 -14
  24. package/dist/client/generated/wire.js.map +1 -1
  25. package/dist/client/http.js +27 -27
  26. package/dist/client/http.js.map +1 -1
  27. package/dist/client/index.d.ts +2 -2
  28. package/dist/client/media/client.d.ts +10 -4
  29. package/dist/client/media/client.js +33 -7
  30. package/dist/client/media/client.js.map +1 -1
  31. package/dist/client/media/rules.d.ts +1 -1
  32. package/dist/client/media/rules.js +6 -6
  33. package/dist/client/media/rules.js.map +1 -1
  34. package/dist/client/media/transport.d.ts +1 -1
  35. package/dist/client/media/transport.js +16 -16
  36. package/dist/client/media/transport.js.map +1 -1
  37. package/dist/client/media/windows.js +1 -1
  38. package/dist/client/media/windows.js.map +1 -1
  39. package/dist/client/public.d.ts +2 -2
  40. package/dist/client/public.js +2 -2
  41. package/dist/client/public.js.map +1 -1
  42. package/dist/client/versions.d.ts +6 -4
  43. package/dist/client/versions.js +24 -26
  44. package/dist/client/versions.js.map +1 -1
  45. package/dist/client.js +25 -25
  46. package/dist/components/admin/content-manager.js +233 -232
  47. package/dist/components/admin/content-manager.js.map +1 -1
  48. package/dist/components/content/card.d.ts +5 -3
  49. package/dist/components/content/card.js +131 -109
  50. package/dist/components/content/card.js.map +1 -1
  51. package/dist/components/content/download-button.d.ts +26 -0
  52. package/dist/components/content/download-button.js +51 -0
  53. package/dist/components/content/download-button.js.map +1 -0
  54. package/dist/components/content/editors.d.ts +8 -0
  55. package/dist/components/content/editors.js +299 -229
  56. package/dist/components/content/editors.js.map +1 -1
  57. package/dist/components/content/grid.d.ts +8 -1
  58. package/dist/components/content/grid.js +125 -76
  59. package/dist/components/content/grid.js.map +1 -1
  60. package/dist/components/content/index.d.ts +3 -2
  61. package/dist/components/content/listing.d.ts +3 -1
  62. package/dist/components/content/listing.js.map +1 -1
  63. package/dist/components/content/paywall.js +61 -61
  64. package/dist/components/content/paywall.js.map +1 -1
  65. package/dist/components/media-gallery.d.ts +1 -4
  66. package/dist/components/media-gallery.js +19 -34
  67. package/dist/components/media-gallery.js.map +1 -1
  68. package/dist/components/social/comments.js +1 -1
  69. package/dist/components/social/comments.js.map +1 -1
  70. package/dist/components/social/engagement.d.ts +2 -1
  71. package/dist/components/social/engagement.js +5 -3
  72. package/dist/components/social/engagement.js.map +1 -1
  73. package/dist/i18n/messages.js +49 -46
  74. package/dist/i18n/messages.js.map +1 -1
  75. package/dist/index.js +12 -11
  76. package/dist/locales/de.js +15 -3
  77. package/dist/locales/de.js.map +1 -1
  78. package/dist/locales/discovery/de.js +3 -0
  79. package/dist/locales/discovery/de.js.map +1 -1
  80. package/dist/locales/discovery/en.d.ts +3 -0
  81. package/dist/locales/discovery/en.js +3 -0
  82. package/dist/locales/discovery/en.js.map +1 -1
  83. package/dist/locales/discovery/es.js +3 -0
  84. package/dist/locales/discovery/es.js.map +1 -1
  85. package/dist/locales/discovery/ja.js +3 -0
  86. package/dist/locales/discovery/ja.js.map +1 -1
  87. package/dist/locales/discovery/ko.js +3 -0
  88. package/dist/locales/discovery/ko.js.map +1 -1
  89. package/dist/locales/discovery/zh.js +3 -0
  90. package/dist/locales/discovery/zh.js.map +1 -1
  91. package/dist/locales/en.d.ts +17 -2
  92. package/dist/locales/en.js +15 -3
  93. package/dist/locales/en.js.map +1 -1
  94. package/dist/locales/es.js +15 -3
  95. package/dist/locales/es.js.map +1 -1
  96. package/dist/locales/ja.js +15 -3
  97. package/dist/locales/ja.js.map +1 -1
  98. package/dist/locales/ko.js +15 -3
  99. package/dist/locales/ko.js.map +1 -1
  100. package/dist/locales/zh.js +15 -3
  101. package/dist/locales/zh.js.map +1 -1
  102. package/dist/react/comments.d.ts +1 -1
  103. package/dist/react/comments.js +3 -2
  104. package/dist/react/comments.js.map +1 -1
  105. package/dist/react/contents.d.ts +13 -5
  106. package/dist/react/contents.js +163 -149
  107. package/dist/react/contents.js.map +1 -1
  108. package/dist/react/context.d.ts +1 -1
  109. package/dist/react/context.js.map +1 -1
  110. package/dist/react/detail.d.ts +7 -1
  111. package/dist/react/detail.js +9 -6
  112. package/dist/react/detail.js.map +1 -1
  113. package/dist/react/discovery.d.ts +11 -0
  114. package/dist/react/discovery.js +49 -30
  115. package/dist/react/discovery.js.map +1 -1
  116. package/dist/react/engagement.d.ts +6 -2
  117. package/dist/react/engagement.js +2 -6
  118. package/dist/react/engagement.js.map +1 -1
  119. package/dist/react/index.d.ts +3 -3
  120. package/dist/react/public.js +17 -16
  121. package/dist/react/public.js.map +1 -1
  122. package/dist/react/versions.d.ts +25 -5
  123. package/dist/react/versions.js +58 -37
  124. package/dist/react/versions.js.map +1 -1
  125. package/dist/react.js +9 -9
  126. package/dist/styles.css +1 -1
  127. package/package.json +1 -1
package/README.md CHANGED
@@ -93,14 +93,16 @@ operation. This does not change the operation's result or its authentication.
93
93
  `groups.uploadCover(group, file)` its cover; `client.kinds()` the content model.
94
94
  - **Images of content and polls** go to the item's server-named upload path:
95
95
  `media.uploadNamed(ref, file)` resolves the name content routes take
96
- (`contents.setCover`); `media.uploadInline({kind, id}, file)` resolves a
97
- body image's reference (`ref`, `contentkit:i-{uuid}`, what the body stores:
96
+ (`contents.setCover(id, version, name)`: every file of a content lives in a
97
+ version); `media.uploadInline({kind, id, version}, file)` puts a body image
98
+ in the version's private files, copied into the other versions, and
99
+ resolves its reference (`ref`, `contentkit:i-{uuid}`, what the body stores:
98
100
  `imageRef(name)`) and a URL that shows it now; `polls.uploadImage` and
99
101
  `polls.uploadOptionImage` do both steps. Reads resolve references: a
100
- content's `body` shows the images' current public files and `images` maps
101
- each name to its URL. `folders` renames the poll media kind. Public image
102
- files exist only while the item is live; `useContent` shows an unpublished
103
- item's images to its editors (below).
102
+ content's `body` shows its images' signed URLs (the opened version's
103
+ private files) and `images` maps each name to its URL. `folders` renames
104
+ the poll media kind. Public image files exist only while the item is live;
105
+ `useContent` shows an unpublished item's images to its editors (below).
104
106
  - **`client.config()`** says what the content module allows: which
105
107
  interactions signed-out visitors may make (`content.Options.Anonymous`,
106
108
  none by default) and the longest comment (`CommentMaxLength`, 2,200 by
@@ -281,6 +283,20 @@ slot crops at its image's aspect, else its public preset's
281
283
  (`GET /media/presets`), so an item without an image yet still crops a 3:1
282
284
  cover at 3:1.
283
285
 
286
+ ### Card lists: decorations and locked cards
287
+
288
+ Every card list (`ContentGrid`, `ContentListing`, `ContentRail`,
289
+ `SearchResults`, `ContentHistory`) and `ContentCard` take `decorate(card)`:
290
+ the host's badges, drawn over the cover (place them yourself; clicks reach
291
+ the card, and click signals stay intact). Cards carry what badges need:
292
+ `access.level` and `access.tiers` (the effective rule, the same for every
293
+ reader), `version.has_audio`, `viewer.favorited` and `groups`.
294
+
295
+ `ContentGrid` and `ContentListing` take `locked`: `show` (default) marks the
296
+ cards the reader doesn't open, `hide` leaves them out, and `collapse` lists
297
+ the open ones and puts the rest behind a counted toggle that loads them on
298
+ opening (`useLockedCount` counts them).
299
+
284
300
  ### MediaFolderEditor
285
301
 
286
302
  ```tsx
@@ -325,8 +341,9 @@ the starting shape and edit.
325
341
  `<MediaGallery item={post} prefix="low-res/" renderLocked={unlock} />` reads
326
342
  through the client: the read (refreshed, reloaded after commits), HLS bases,
327
343
  playlist auth and the grant refresh. Passing `read`, `hlsBase`, `xhrSetup`
328
- and `refresh` yourself still works. A locked item's public teaser carries
329
- the locked count and `renderLocked`, so the unlock is on the first slide;
344
+ and `refresh` yourself still works. A locked read lists no files, so the
345
+ lock is uncounted; its public teaser carries `renderLocked`, so the unlock is
346
+ on the first slide;
330
347
  the stage is never taller than `maxHeight` (default `80svh`).
331
348
 
332
349
  ### VideoPosterPicker frames from another video
@@ -411,7 +428,10 @@ free-text answer and its groups; `results="always"` shows results before
411
428
  voting). Signed-out visitors comment under a name, react and vote where the
412
429
  server allows it (`content.Options.Anonymous`, read from the comment standing
413
430
  and `client.config()`); elsewhere they are asked to sign in (`onSignIn`, the
414
- provider's or their own) or, without one, told to.
431
+ provider's or their own) or, without one, told to. On locked content they
432
+ follow its kind's locked view (`kinds.Kind.Locked`): hidden comments show an
433
+ unlock note without a count, hidden reactions render nothing, a hidden
434
+ favorite count is left out, and cards drop hidden counts, never showing 0.
415
435
 
416
436
  Staff building blocks: `CommentBans` (paged) with `CommentBanDialog`, and
417
437
  `PollEditor` (deleting asks for a typed confirmation). The complete staff screens are the admin flows below.
@@ -569,11 +589,16 @@ const nav = useMemo(() => ({ pathname: location.pathname, search: location.searc
569
589
  ## Errors
570
590
 
571
591
  Every call rejects with `ContentKitError`: `code` (the server's, or `network`,
572
- `storage`, `aborted`, `resume_mismatch`, `decode`, `render_timeout`),
573
- `status`, `retryAfter`, `details` (image and video limits), `blobs`
574
- (`not_uploaded`), `ban` (`comment_banned`) and `action` (`rate_limited`),
575
- plus `refusal`, `isLimit`, `isCeiling`, `blocksQueue` and `transient`. Every
576
- code has a message in every bundle (`useMessages().error(e)`).
592
+ `server_unavailable`, `storage`, `aborted`, `resume_mismatch`, `decode`,
593
+ `render_timeout`), `status`, `retryAfter`, `details` (image and video limits),
594
+ `blobs` (`not_uploaded`), `ban` (`comment_banned`), `action` (`rate_limited`)
595
+ and `unknownCode` (a code this client doesn't know; the message is the
596
+ server's), plus `refusal`, `isLimit`, `isCeiling`, `blocksQueue` and
597
+ `transient`. Every code has a message in every bundle (`useMessages().error(e)`).
598
+
599
+ Failures are classified like auth-ui's and billing-ui's (`transportFailure`):
600
+ no server reached is `network`; a 502, 503, 504 or 520–524 without an API
601
+ error body, or a timeout, is `server_unavailable`.
577
602
 
578
603
  ## Migrating from the two old packages
579
604
 
@@ -62,7 +62,11 @@ var o = class {
62
62
  let a = this.session(), o = [], { discovery: s } = this.client;
63
63
  for (let e = 0; e < r.length; e += t) o.push(s.signals(r.slice(e, e + t), a, { keepalive: n }));
64
64
  for (let e = 0; e < i.length; e += t) o.push(s.exposures(i.slice(e, e + t), a, { keepalive: n }));
65
- (await Promise.allSettled(o)).some((t) => t.status === "rejected" && ["not_configured", "unavailable"].includes(e(t.reason).code)) && this.pause();
65
+ (await Promise.allSettled(o)).some((t) => t.status === "rejected" && [
66
+ "not_configured",
67
+ "unavailable",
68
+ "server_unavailable"
69
+ ].includes(e(t.reason).code)) && this.pause();
66
70
  }
67
71
  signal(e) {
68
72
  this.paused() || (e.type === "view" && (this.signals = this.signals.filter((t) => t.event_id !== e.event_id)), this.signals.push(e), this.schedule());
@@ -1 +1 @@
1
- {"version":3,"file":"beacons.js","names":[],"sources":["../../src/client/beacons.ts"],"sourcesContent":["import type { ContentKitClient } from \"./client.js\";\nimport { toContentKitError } from \"./errors.js\";\nimport type { ExposureInput, SignalInput } from \"./generated/wire.js\";\n\nexport interface BeaconOptions {\n /** A signed-out visitor's session; default one per browser tab. */\n session?: () => string | undefined;\n /** How long events gather before they are sent, ms. Default 2000. */\n delay?: number;\n /** How long beacons stay off after the site records no signals, ms, before the content model is read again. Default 60000. */\n recheck?: number;\n}\n\n/** Where a click or a view came from: the list render it was shown in. */\nexport interface Attribution {\n render_id?: string;\n surface?: string;\n /** 1-based position in that render. */\n position?: number;\n}\n\nexport interface ViewSession {\n /** Reports the view so far: pages read, seconds watched or percent scrolled; resume is where to pick up. */\n update(o: { progress?: number; duration_s?: number; resume?: string }): void;\n}\n\nconst MAX = 100;\nconst SESSION_KEY = \"contentkit.session\";\nexport const eventId = () => (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`);\n\nlet unstored: string | undefined;\n\n/** A signed-out reader's session: one per browser tab, kept across reloads. */\nexport function readerSession(): string {\n try {\n const kept = sessionStorage.getItem(SESSION_KEY);\n if (kept) return kept;\n const made = eventId();\n sessionStorage.setItem(SESSION_KEY, made);\n return made;\n } catch {\n return (unstored ??= eventId());\n }\n}\n\n/**\n * Batched, best-effort signals and exposures: gathered for `delay`, sent in\n * one request each, flushed with keepalive when the page hides. While the\n * content model says the site records no signals (or a send is refused as\n * not_configured or unavailable) events are dropped; after `recheck` the\n * next event reads the model again, so signals resume when the site's\n * signal plane is back.\n */\nexport class Beacons {\n private signals: SignalInput[] = [];\n private exposures: ExposureInput[] = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n private offUntil = 0;\n private recorded: Promise<boolean> | undefined;\n private readonly session: () => string | undefined;\n private readonly delay: number;\n private readonly recheck: number;\n\n constructor(\n private readonly client: Pick<ContentKitClient, \"discovery\" | \"kinds\">,\n o: BeaconOptions = {},\n ) {\n this.session = o.session ?? readerSession;\n this.delay = o.delay ?? 2000;\n this.recheck = o.recheck ?? 60_000;\n if (typeof addEventListener === \"function\") {\n addEventListener(\"pagehide\", () => void this.flush(true));\n addEventListener(\"visibilitychange\", () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") void this.flush(true);\n });\n }\n }\n\n /** A click on a listed card. */\n click(content: string, a: Attribution & { version_id?: string } = {}): void {\n this.signal({ content_id: content, type: \"click\", event_id: eventId(), occurred_at: new Date().toISOString(), ...a });\n }\n\n /** A consumption session of a content (or one version): each update a revision of one event. */\n view(content: string, version?: string, a: Attribution = {}): ViewSession {\n const event = eventId();\n let revision = 0;\n return {\n update: (o) => this.signal({ content_id: content, version_id: version, type: \"view\", event_id: event, revision: ++revision, occurred_at: new Date().toISOString(), ...a, ...o }),\n };\n }\n\n /** A result list served, rendered or shown. */\n exposure(e: ExposureInput): void {\n if (this.paused() || !e.shown.length) return;\n this.exposures.push(e);\n this.schedule();\n }\n\n /** Sends what has gathered now. */\n async flush(keepalive = false): Promise<void> {\n clearTimeout(this.timer);\n this.timer = undefined;\n const signals = this.signals.splice(0);\n const exposures = this.exposures.splice(0);\n if (this.paused() || (!signals.length && !exposures.length)) return;\n if (!(await this.recording())) {\n this.pause();\n return;\n }\n const session = this.session();\n const sends: Promise<void>[] = [];\n const { discovery } = this.client;\n for (let i = 0; i < signals.length; i += MAX) sends.push(discovery.signals(signals.slice(i, i + MAX), session, { keepalive }));\n for (let i = 0; i < exposures.length; i += MAX) sends.push(discovery.exposures(exposures.slice(i, i + MAX), session, { keepalive }));\n const done = await Promise.allSettled(sends);\n if (done.some((r) => r.status === \"rejected\" && [\"not_configured\", \"unavailable\"].includes(toContentKitError(r.reason).code))) this.pause();\n }\n\n private signal(s: SignalInput): void {\n if (this.paused()) return;\n // A view's later revision replaces its earlier one still waiting.\n if (s.type === \"view\") this.signals = this.signals.filter((x) => x.event_id !== s.event_id);\n this.signals.push(s);\n this.schedule();\n }\n\n /** Off until `recheck` passes; then the model is read again. */\n private paused(): boolean {\n if (this.offUntil > Date.now()) return true;\n this.offUntil = 0;\n return false;\n }\n\n private pause(): void {\n this.offUntil = Date.now() + this.recheck;\n this.recorded = undefined;\n }\n\n /** Whether the site records signals, asked at the first event and after a pause; an unreadable model sends anyway (a refusal still pauses them). */\n private recording(): Promise<boolean> {\n return (this.recorded ??= this.client.kinds().then(\n (m) => m.signals,\n () => true,\n ));\n }\n\n private schedule(): void {\n void this.recording();\n if (this.signals.length + this.exposures.length >= MAX) return void this.flush();\n this.timer ??= setTimeout(() => void this.flush(), this.delay);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"beacons.js","names":[],"sources":["../../src/client/beacons.ts"],"sourcesContent":["import type { ContentKitClient } from \"./client.js\";\nimport { toContentKitError } from \"./errors.js\";\nimport type { ExposureInput, SignalInput } from \"./generated/wire.js\";\n\nexport interface BeaconOptions {\n /** A signed-out visitor's session; default one per browser tab. */\n session?: () => string | undefined;\n /** How long events gather before they are sent, ms. Default 2000. */\n delay?: number;\n /** How long beacons stay off after the site records no signals, ms, before the content model is read again. Default 60000. */\n recheck?: number;\n}\n\n/** Where a click or a view came from: the list render it was shown in. */\nexport interface Attribution {\n render_id?: string;\n surface?: string;\n /** 1-based position in that render. */\n position?: number;\n}\n\nexport interface ViewSession {\n /** Reports the view so far: pages read, seconds watched or percent scrolled; resume is where to pick up. */\n update(o: { progress?: number; duration_s?: number; resume?: string }): void;\n}\n\nconst MAX = 100;\nconst SESSION_KEY = \"contentkit.session\";\nexport const eventId = () => (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`);\n\nlet unstored: string | undefined;\n\n/** A signed-out reader's session: one per browser tab, kept across reloads. */\nexport function readerSession(): string {\n try {\n const kept = sessionStorage.getItem(SESSION_KEY);\n if (kept) return kept;\n const made = eventId();\n sessionStorage.setItem(SESSION_KEY, made);\n return made;\n } catch {\n return (unstored ??= eventId());\n }\n}\n\n/**\n * Batched, best-effort signals and exposures: gathered for `delay`, sent in\n * one request each, flushed with keepalive when the page hides. While the\n * content model says the site records no signals (or a send is refused as\n * not_configured or unavailable) events are dropped; after `recheck` the\n * next event reads the model again, so signals resume when the site's\n * signal plane is back.\n */\nexport class Beacons {\n private signals: SignalInput[] = [];\n private exposures: ExposureInput[] = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n private offUntil = 0;\n private recorded: Promise<boolean> | undefined;\n private readonly session: () => string | undefined;\n private readonly delay: number;\n private readonly recheck: number;\n\n constructor(\n private readonly client: Pick<ContentKitClient, \"discovery\" | \"kinds\">,\n o: BeaconOptions = {},\n ) {\n this.session = o.session ?? readerSession;\n this.delay = o.delay ?? 2000;\n this.recheck = o.recheck ?? 60_000;\n if (typeof addEventListener === \"function\") {\n addEventListener(\"pagehide\", () => void this.flush(true));\n addEventListener(\"visibilitychange\", () => {\n if (typeof document !== \"undefined\" && document.visibilityState === \"hidden\") void this.flush(true);\n });\n }\n }\n\n /** A click on a listed card. */\n click(content: string, a: Attribution & { version_id?: string } = {}): void {\n this.signal({ content_id: content, type: \"click\", event_id: eventId(), occurred_at: new Date().toISOString(), ...a });\n }\n\n /** A consumption session of a content (or one version): each update a revision of one event. */\n view(content: string, version?: string, a: Attribution = {}): ViewSession {\n const event = eventId();\n let revision = 0;\n return {\n update: (o) => this.signal({ content_id: content, version_id: version, type: \"view\", event_id: event, revision: ++revision, occurred_at: new Date().toISOString(), ...a, ...o }),\n };\n }\n\n /** A result list served, rendered or shown. */\n exposure(e: ExposureInput): void {\n if (this.paused() || !e.shown.length) return;\n this.exposures.push(e);\n this.schedule();\n }\n\n /** Sends what has gathered now. */\n async flush(keepalive = false): Promise<void> {\n clearTimeout(this.timer);\n this.timer = undefined;\n const signals = this.signals.splice(0);\n const exposures = this.exposures.splice(0);\n if (this.paused() || (!signals.length && !exposures.length)) return;\n if (!(await this.recording())) {\n this.pause();\n return;\n }\n const session = this.session();\n const sends: Promise<void>[] = [];\n const { discovery } = this.client;\n for (let i = 0; i < signals.length; i += MAX) sends.push(discovery.signals(signals.slice(i, i + MAX), session, { keepalive }));\n for (let i = 0; i < exposures.length; i += MAX) sends.push(discovery.exposures(exposures.slice(i, i + MAX), session, { keepalive }));\n const done = await Promise.allSettled(sends);\n if (done.some((r) => r.status === \"rejected\" && [\"not_configured\", \"unavailable\", \"server_unavailable\"].includes(toContentKitError(r.reason).code))) this.pause();\n }\n\n private signal(s: SignalInput): void {\n if (this.paused()) return;\n // A view's later revision replaces its earlier one still waiting.\n if (s.type === \"view\") this.signals = this.signals.filter((x) => x.event_id !== s.event_id);\n this.signals.push(s);\n this.schedule();\n }\n\n /** Off until `recheck` passes; then the model is read again. */\n private paused(): boolean {\n if (this.offUntil > Date.now()) return true;\n this.offUntil = 0;\n return false;\n }\n\n private pause(): void {\n this.offUntil = Date.now() + this.recheck;\n this.recorded = undefined;\n }\n\n /** Whether the site records signals, asked at the first event and after a pause; an unreadable model sends anyway (a refusal still pauses them). */\n private recording(): Promise<boolean> {\n return (this.recorded ??= this.client.kinds().then(\n (m) => m.signals,\n () => true,\n ));\n }\n\n private schedule(): void {\n void this.recording();\n if (this.signals.length + this.exposures.length >= MAX) return void this.flush();\n this.timer ??= setTimeout(() => void this.flush(), this.delay);\n }\n}\n"],"mappings":"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"}
@@ -59,11 +59,13 @@ export type ContentChange =
59
59
  type: "reaction.changed";
60
60
  target: Target;
61
61
  counts: ReactionCounts;
62
- } | {
62
+ }
63
+ /** count is absent where the target's kind's locked view hides it. */
64
+ | {
63
65
  type: "favorite.changed";
64
66
  target: Target;
65
67
  favorited: boolean;
66
- count: number;
68
+ count?: number;
67
69
  }
68
70
  /** Created, updated, voted or answered: the poll as the server now answers it. */
69
71
  | {
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../src/client/content/types.ts"],"sourcesContent":["import type { CommentBan, CommentStanding, Comment, Content, Poll, ReactionCounts, ResolveInput } from \"../generated/wire.js\";\n\n/** What an interaction targets: a content item (optionally one of its versions) or a group. */\nexport interface Target {\n type: \"contents\" | \"groups\";\n id: string;\n /** One version of a content item. */\n version?: string;\n}\n\n/** The same target. */\nexport function sameTarget(a?: Target, b?: Target): boolean {\n return !!a && !!b && a.type === b.type && a.id === b.id && (a.version ?? \"\") === (b.version ?? \"\");\n}\n\n/** A reaction: like 1, dislike -1, none 0. */\nexport type Reaction = -1 | 0 | 1;\n\n/** The route verb that sets a like or dislike; clearing is DELETE …/reaction. */\nexport function reactionVerb(value: -1 | 1): \"like\" | \"dislike\" {\n return value === 1 ? \"like\" : \"dislike\";\n}\n\n/** Comment order: newest (the default), likes (most liked) or best (Wilson lower bound). */\nexport type Sort = \"newest\" | \"likes\" | \"best\";\n\n/** A ban route family: \"owner\" bans from the caller's own content, \"global\" from the whole site. */\nexport type BanScope = CommentStanding[\"ban_scopes\"][number];\n\n/** What the review queue holds: comments and content items. */\nexport type HeldKind = \"comment\" | \"content\";\n\n/** The moderation rules: banned words or IP bans. */\nexport type RuleSet = \"words\" | \"ips\";\n\n/** A reviewer's decision on a held item. */\nexport type Decision = Exclude<ResolveInput[\"decision\"], \"review\">;\n\n/** A content-module mutation made through the client. */\nexport type ContentChange =\n /** Created or edited: the item as the server now answers it. */\n | { type: \"content.saved\"; content: Content }\n /** A version changed: refetch the item. */\n | { type: \"content.changed\"; id: string }\n | { type: \"content.deleted\"; id: string }\n /** target is the commented item. */\n | { type: \"comment.created\"; target: Target; comment: Comment }\n | { type: \"comment.updated\"; comment: Comment }\n | { type: \"comment.deleted\"; id: string }\n | { type: \"comment.restored\"; id: string }\n | { type: \"comment.reacted\"; id: string; counts: ReactionCounts }\n | { type: \"reaction.changed\"; target: Target; counts: ReactionCounts }\n | { type: \"favorite.changed\"; target: Target; favorited: boolean; count: number }\n /** Created, updated, voted or answered: the poll as the server now answers it. */\n | { type: \"poll.created\"; poll: Poll }\n | { type: \"poll.updated\"; poll: Poll }\n /** An option or image changed: refetch the poll. */\n | { type: \"poll.changed\"; id: string }\n | { type: \"poll.deleted\"; id: string }\n | { type: \"ban.saved\"; scope: BanScope; ban: CommentBan }\n | { type: \"ban.lifted\"; scope: BanScope; user: string }\n | { type: \"moderation.resolved\"; kind: HeldKind; id: string; decision: Decision }\n /** Banned words or IP bans changed. */\n | { type: \"rules.changed\"; rules: RuleSet }\n /** A version's reader metadata changed: its reader plan reads again. */\n | { type: \"reader.changed\"; content: string; version: string }\n | { type: \"groups.changed\" }\n /** The reader's content filters changed: every list reads again. */\n | { type: \"contentFilters.changed\" }\n | { type: \"history.changed\" }\n /** A search ran or recent searches were cleared. */\n | { type: \"searches.changed\" }\n /** The reader's keys changed (a join, a purchase the host reports): reads of access re-run. */\n | { type: \"access.changed\" };\n"],"mappings":";AAWA,SAAgB,EAAW,GAAY,GAAqB;CAC1D,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,SAAS,EAAE,WAAW;AACjG;AAMA,SAAgB,EAAa,GAAmC;CAC9D,OAAO,MAAU,IAAI,SAAS;AAChC"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/client/content/types.ts"],"sourcesContent":["import type { CommentBan, CommentStanding, Comment, Content, Poll, ReactionCounts, ResolveInput } from \"../generated/wire.js\";\n\n/** What an interaction targets: a content item (optionally one of its versions) or a group. */\nexport interface Target {\n type: \"contents\" | \"groups\";\n id: string;\n /** One version of a content item. */\n version?: string;\n}\n\n/** The same target. */\nexport function sameTarget(a?: Target, b?: Target): boolean {\n return !!a && !!b && a.type === b.type && a.id === b.id && (a.version ?? \"\") === (b.version ?? \"\");\n}\n\n/** A reaction: like 1, dislike -1, none 0. */\nexport type Reaction = -1 | 0 | 1;\n\n/** The route verb that sets a like or dislike; clearing is DELETE …/reaction. */\nexport function reactionVerb(value: -1 | 1): \"like\" | \"dislike\" {\n return value === 1 ? \"like\" : \"dislike\";\n}\n\n/** Comment order: newest (the default), likes (most liked) or best (Wilson lower bound). */\nexport type Sort = \"newest\" | \"likes\" | \"best\";\n\n/** A ban route family: \"owner\" bans from the caller's own content, \"global\" from the whole site. */\nexport type BanScope = CommentStanding[\"ban_scopes\"][number];\n\n/** What the review queue holds: comments and content items. */\nexport type HeldKind = \"comment\" | \"content\";\n\n/** The moderation rules: banned words or IP bans. */\nexport type RuleSet = \"words\" | \"ips\";\n\n/** A reviewer's decision on a held item. */\nexport type Decision = Exclude<ResolveInput[\"decision\"], \"review\">;\n\n/** A content-module mutation made through the client. */\nexport type ContentChange =\n /** Created or edited: the item as the server now answers it. */\n | { type: \"content.saved\"; content: Content }\n /** A version changed: refetch the item. */\n | { type: \"content.changed\"; id: string }\n | { type: \"content.deleted\"; id: string }\n /** target is the commented item. */\n | { type: \"comment.created\"; target: Target; comment: Comment }\n | { type: \"comment.updated\"; comment: Comment }\n | { type: \"comment.deleted\"; id: string }\n | { type: \"comment.restored\"; id: string }\n | { type: \"comment.reacted\"; id: string; counts: ReactionCounts }\n | { type: \"reaction.changed\"; target: Target; counts: ReactionCounts }\n /** count is absent where the target's kind's locked view hides it. */\n | { type: \"favorite.changed\"; target: Target; favorited: boolean; count?: number }\n /** Created, updated, voted or answered: the poll as the server now answers it. */\n | { type: \"poll.created\"; poll: Poll }\n | { type: \"poll.updated\"; poll: Poll }\n /** An option or image changed: refetch the poll. */\n | { type: \"poll.changed\"; id: string }\n | { type: \"poll.deleted\"; id: string }\n | { type: \"ban.saved\"; scope: BanScope; ban: CommentBan }\n | { type: \"ban.lifted\"; scope: BanScope; user: string }\n | { type: \"moderation.resolved\"; kind: HeldKind; id: string; decision: Decision }\n /** Banned words or IP bans changed. */\n | { type: \"rules.changed\"; rules: RuleSet }\n /** A version's reader metadata changed: its reader plan reads again. */\n | { type: \"reader.changed\"; content: string; version: string }\n | { type: \"groups.changed\" }\n /** The reader's content filters changed: every list reads again. */\n | { type: \"contentFilters.changed\" }\n | { type: \"history.changed\" }\n /** A search ran or recent searches were cleared. */\n | { type: \"searches.changed\" }\n /** The reader's keys changed (a join, a purchase the host reports): reads of access re-run. */\n | { type: \"access.changed\" };\n"],"mappings":";AAWA,SAAgB,EAAW,GAAY,GAAqB;CAC1D,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,SAAS,EAAE,WAAW;AACjG;AAMA,SAAgB,EAAa,GAAmC;CAC9D,OAAO,MAAU,IAAI,SAAS;AAChC"}
@@ -9,8 +9,6 @@ export declare const IMAGE_SCHEME = "contentkit:";
9
9
  * the image's current public file (`Content.images`).
10
10
  */
11
11
  export declare const imageRef: (name: string) => string;
12
- /** A content kind's body kind: the media folder of a content item's body images ({kind}.body, id), opened with the item. */
13
- export declare const BODY_SUFFIX = ".body";
14
12
  /** Matches every image reference; the name is group 1. */
15
13
  export declare const imageRefs: () => RegExp;
16
14
  /** GET /manage/contents filters, as the route names them. */
@@ -39,10 +37,14 @@ export declare class ContentsClient {
39
37
  unpublish(id: string): Promise<Content>;
40
38
  remove(id: string): Promise<void>;
41
39
  restore(id: string): Promise<Content>;
42
- /** Sets the cover to an inline image name ("i-{uuid}") of the item's folder; null clears it. */
43
- setCover(id: string, image: string | null): Promise<Content>;
44
- /** An inline image of the item's folder: the reference to store in a body and a URL that shows it to the editor now (null while none exists). */
45
- inlineImage(id: string, image: string, signal?: AbortSignal): Promise<InlineImage>;
40
+ /** Selects a version's cover by the name ("i-{uuid}") of one of its cover uploads; null returns to its first. */
41
+ setCover(id: string, vid: string, image: string | null): Promise<Version>;
42
+ /**
43
+ * A body image uploaded to version vid (its private files), copied into
44
+ * every other version: the reference to store in the body and a URL that shows
45
+ * it to the editor now (null while none exists).
46
+ */
47
+ inlineImage(id: string, vid: string, image: string, signal?: AbortSignal): Promise<InlineImage>;
46
48
  addVersion(id: string, input: VersionInput): Promise<Version>;
47
49
  updateVersion(id: string, vid: string, input: VersionInput): Promise<Version>;
48
50
  removeVersion(id: string, vid: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import { call as e } from "./route.js";
2
2
  //#region src/client/contents.ts
3
- var t = "contentkit:", n = (e) => t + e, r = ".body", i = () => /contentkit:(i-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/g, a = class {
3
+ var t = "contentkit:", n = (e) => t + e, r = () => /contentkit:(i-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/g, i = class {
4
4
  http;
5
5
  constructor(e) {
6
6
  this.http = e;
@@ -77,14 +77,17 @@ var t = "contentkit:", n = (e) => t + e, r = ".body", i = () => /contentkit:(i-[
77
77
  restore(t) {
78
78
  return this.write(e(this.http, "POST", "/contents/{id}/restore", { params: { id: t } }));
79
79
  }
80
- setCover(e, t) {
81
- return this.update(e, { cover: t ?? "" });
80
+ setCover(e, t, n) {
81
+ return this.updateVersion(e, t, { cover: n ?? "" });
82
82
  }
83
- inlineImage(t, n, r) {
83
+ inlineImage(t, n, r, i) {
84
84
  return e(this.http, "POST", "/contents/{id}/images", {
85
85
  params: { id: t },
86
- body: { image: n },
87
- signal: r
86
+ body: {
87
+ image: r,
88
+ version: n
89
+ },
90
+ signal: i
88
91
  });
89
92
  }
90
93
  addVersion(t, n) {
@@ -139,6 +142,6 @@ var t = "contentkit:", n = (e) => t + e, r = ".body", i = () => /contentkit:(i-[
139
142
  }
140
143
  };
141
144
  //#endregion
142
- export { r as BODY_SUFFIX, a as ContentsClient, t as IMAGE_SCHEME, n as imageRef, i as imageRefs };
145
+ export { i as ContentsClient, t as IMAGE_SCHEME, n as imageRef, r as imageRefs };
143
146
 
144
147
  //# sourceMappingURL=contents.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contents.js","names":[],"sources":["../../src/client/contents.ts"],"sourcesContent":["import type {\n CatalogText,\n Content,\n ContentInput,\n ContentPatch,\n Creator,\n InlineImage,\n Segment,\n Version,\n VersionInput,\n} from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, type RouteQuery } from \"./route.js\";\n\n/** The URL scheme of an image reference in a content body. */\nexport const IMAGE_SCHEME = \"contentkit:\";\n\n/**\n * The reference a body stores for one of its images, by name\n * (\"contentkit:i-{uuid}\"), where the image's URL would go: reads resolve it to\n * the image's current public file (`Content.images`).\n */\nexport const imageRef = (name: string) => IMAGE_SCHEME + name;\n/** A content kind's body kind: the media folder of a content item's body images ({kind}.body, id), opened with the item. */\nexport const BODY_SUFFIX = \".body\";\n\n/** Matches every image reference; the name is group 1. */\nexport const imageRefs = () => /contentkit:(i-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/g;\n\n/** GET /manage/contents filters, as the route names them. */\nexport type ManageQuery = RouteQuery<\"GET\", \"/manage/contents\">;\n\n/**\n * Content items: a viewer's reads, and the writes of their creators, their\n * owner group's editors and root:content:write holders.\n */\nexport class ContentsClient {\n constructor(private readonly http: Http) {}\n\n /** The item as the caller sees it; text in language, else the site's first, else any. */\n get(id: string, language?: string, signal?: AbortSignal): Promise<Content> {\n return call(this.http, \"GET\", \"/contents/{id}\", { params: { id }, query: { language }, signal });\n }\n version(id: string, vid: string, language?: string, signal?: AbortSignal): Promise<Version> {\n return call(this.http, \"GET\", \"/contents/{id}/versions/{vid}\", { params: { id, vid }, query: { language }, signal });\n }\n /** Items the caller manages, newest first: all for root:content:write holders, else theirs or an owner group's. */\n manage(q: ManageQuery = {}, signal?: AbortSignal): Promise<Content[]> {\n return call(this.http, \"GET\", \"/manage/contents\", { query: q, signal });\n }\n /** Creates a draft. */\n create(input: ContentInput): Promise<Content> {\n return this.write(call(this.http, \"POST\", \"/contents\", { body: input }));\n }\n update(id: string, patch: ContentPatch): Promise<Content> {\n return this.write(call(this.http, \"PATCH\", \"/contents/{id}\", { params: { id }, body: patch }));\n }\n /** Sets the text in one language; moderation \"held\" when the moderator holds the edit. */\n putText(id: string, language: string, text: CatalogText): Promise<Content> {\n return this.write(call(this.http, \"PUT\", \"/contents/{id}/text/{language}\", { params: { id, language }, body: text }));\n }\n deleteText(id: string, language: string): Promise<Content> {\n return this.write(call(this.http, \"DELETE\", \"/contents/{id}/text/{language}\", { params: { id, language } }));\n }\n setCreators(id: string, creators: Creator[]): Promise<Content> {\n return this.write(call(this.http, \"PUT\", \"/contents/{id}/creators\", { params: { id }, body: { creators } }));\n }\n /** Publishes now, or schedules for at; moderation \"held\" when the moderator holds it. */\n publish(id: string, at?: string): Promise<Content> {\n return this.write(call(this.http, \"POST\", \"/contents/{id}/publish\", { params: { id }, body: { publish_at: at } }));\n }\n unpublish(id: string): Promise<Content> {\n return this.write(call(this.http, \"POST\", \"/contents/{id}/unpublish\", { params: { id } }));\n }\n async remove(id: string): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/contents/{id}\", { params: { id } });\n emit({ type: \"content.deleted\", id });\n }\n restore(id: string): Promise<Content> {\n return this.write(call(this.http, \"POST\", \"/contents/{id}/restore\", { params: { id } }));\n }\n /** Sets the cover to an inline image name (\"i-{uuid}\") of the item's folder; null clears it. */\n setCover(id: string, image: string | null): Promise<Content> {\n return this.update(id, { cover: image ?? \"\" });\n }\n /** An inline image of the item's folder: the reference to store in a body and a URL that shows it to the editor now (null while none exists). */\n inlineImage(id: string, image: string, signal?: AbortSignal): Promise<InlineImage> {\n return call(this.http, \"POST\", \"/contents/{id}/images\", { params: { id }, body: { image }, signal });\n }\n addVersion(id: string, input: VersionInput): Promise<Version> {\n return this.writeVersion(id, call(this.http, \"POST\", \"/contents/{id}/versions\", { params: { id }, body: input }));\n }\n updateVersion(id: string, vid: string, input: VersionInput): Promise<Version> {\n return this.writeVersion(id, call(this.http, \"PATCH\", \"/contents/{id}/versions/{vid}\", { params: { id, vid }, body: input }));\n }\n async removeVersion(id: string, vid: string): Promise<void> {\n await this.writeVersion(id, call(this.http, \"DELETE\", \"/contents/{id}/versions/{vid}\", { params: { id, vid } }));\n }\n /** Orders the versions: ids lists each version that is not deleted, first first. */\n orderVersions(id: string, ids: string[]): Promise<Content> {\n return this.write(call(this.http, \"PUT\", \"/contents/{id}/versions/order\", { params: { id }, body: { ids } }));\n }\n /** Replaces a version's segments (chapters). */\n setSegments(id: string, vid: string, segments: Segment[]): Promise<Version> {\n return this.writeVersion(id, call(this.http, \"PUT\", \"/contents/{id}/versions/{vid}/segments\", { params: { id, vid }, body: { segments } }));\n }\n\n private async write(p: Promise<Content>): Promise<Content> {\n const emit = this.http.captureChanges();\n const content = await p;\n emit({ type: \"content.saved\", content });\n return content;\n }\n\n private async writeVersion<T>(id: string, p: Promise<T>): Promise<T> {\n const emit = this.http.captureChanges();\n const out = await p;\n emit({ type: \"content.changed\", id });\n return out;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"contents.js","names":[],"sources":["../../src/client/contents.ts"],"sourcesContent":["import type {\n CatalogText,\n Content,\n ContentInput,\n ContentPatch,\n Creator,\n InlineImage,\n Segment,\n Version,\n VersionInput,\n} from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, type RouteQuery } from \"./route.js\";\n\n/** The URL scheme of an image reference in a content body. */\nexport const IMAGE_SCHEME = \"contentkit:\";\n\n/**\n * The reference a body stores for one of its images, by name\n * (\"contentkit:i-{uuid}\"), where the image's URL would go: reads resolve it to\n * the image's current public file (`Content.images`).\n */\nexport const imageRef = (name: string) => IMAGE_SCHEME + name;\n\n/** Matches every image reference; the name is group 1. */\nexport const imageRefs = () => /contentkit:(i-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/g;\n\n/** GET /manage/contents filters, as the route names them. */\nexport type ManageQuery = RouteQuery<\"GET\", \"/manage/contents\">;\n\n/**\n * Content items: a viewer's reads, and the writes of their creators, their\n * owner group's editors and root:content:write holders.\n */\nexport class ContentsClient {\n constructor(private readonly http: Http) {}\n\n /** The item as the caller sees it; text in language, else the site's first, else any. */\n get(id: string, language?: string, signal?: AbortSignal): Promise<Content> {\n return call(this.http, \"GET\", \"/contents/{id}\", { params: { id }, query: { language }, signal });\n }\n version(id: string, vid: string, language?: string, signal?: AbortSignal): Promise<Version> {\n return call(this.http, \"GET\", \"/contents/{id}/versions/{vid}\", { params: { id, vid }, query: { language }, signal });\n }\n /** Items the caller manages, newest first: all for root:content:write holders, else theirs or an owner group's. */\n manage(q: ManageQuery = {}, signal?: AbortSignal): Promise<Content[]> {\n return call(this.http, \"GET\", \"/manage/contents\", { query: q, signal });\n }\n /** Creates a draft. */\n create(input: ContentInput): Promise<Content> {\n return this.write(call(this.http, \"POST\", \"/contents\", { body: input }));\n }\n update(id: string, patch: ContentPatch): Promise<Content> {\n return this.write(call(this.http, \"PATCH\", \"/contents/{id}\", { params: { id }, body: patch }));\n }\n /** Sets the text in one language; moderation \"held\" when the moderator holds the edit. */\n putText(id: string, language: string, text: CatalogText): Promise<Content> {\n return this.write(call(this.http, \"PUT\", \"/contents/{id}/text/{language}\", { params: { id, language }, body: text }));\n }\n deleteText(id: string, language: string): Promise<Content> {\n return this.write(call(this.http, \"DELETE\", \"/contents/{id}/text/{language}\", { params: { id, language } }));\n }\n setCreators(id: string, creators: Creator[]): Promise<Content> {\n return this.write(call(this.http, \"PUT\", \"/contents/{id}/creators\", { params: { id }, body: { creators } }));\n }\n /** Publishes now, or schedules for at; moderation \"held\" when the moderator holds it. */\n publish(id: string, at?: string): Promise<Content> {\n return this.write(call(this.http, \"POST\", \"/contents/{id}/publish\", { params: { id }, body: { publish_at: at } }));\n }\n unpublish(id: string): Promise<Content> {\n return this.write(call(this.http, \"POST\", \"/contents/{id}/unpublish\", { params: { id } }));\n }\n async remove(id: string): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/contents/{id}\", { params: { id } });\n emit({ type: \"content.deleted\", id });\n }\n restore(id: string): Promise<Content> {\n return this.write(call(this.http, \"POST\", \"/contents/{id}/restore\", { params: { id } }));\n }\n /** Selects a version's cover by the name (\"i-{uuid}\") of one of its cover uploads; null returns to its first. */\n setCover(id: string, vid: string, image: string | null): Promise<Version> {\n return this.updateVersion(id, vid, { cover: image ?? \"\" });\n }\n /**\n * A body image uploaded to version vid (its private files), copied into\n * every other version: the reference to store in the body and a URL that shows\n * it to the editor now (null while none exists).\n */\n inlineImage(id: string, vid: string, image: string, signal?: AbortSignal): Promise<InlineImage> {\n return call(this.http, \"POST\", \"/contents/{id}/images\", { params: { id }, body: { image, version: vid }, signal });\n }\n addVersion(id: string, input: VersionInput): Promise<Version> {\n return this.writeVersion(id, call(this.http, \"POST\", \"/contents/{id}/versions\", { params: { id }, body: input }));\n }\n updateVersion(id: string, vid: string, input: VersionInput): Promise<Version> {\n return this.writeVersion(id, call(this.http, \"PATCH\", \"/contents/{id}/versions/{vid}\", { params: { id, vid }, body: input }));\n }\n async removeVersion(id: string, vid: string): Promise<void> {\n await this.writeVersion(id, call(this.http, \"DELETE\", \"/contents/{id}/versions/{vid}\", { params: { id, vid } }));\n }\n /** Orders the versions: ids lists each version that is not deleted, first first. */\n orderVersions(id: string, ids: string[]): Promise<Content> {\n return this.write(call(this.http, \"PUT\", \"/contents/{id}/versions/order\", { params: { id }, body: { ids } }));\n }\n /** Replaces a version's segments (chapters). */\n setSegments(id: string, vid: string, segments: Segment[]): Promise<Version> {\n return this.writeVersion(id, call(this.http, \"PUT\", \"/contents/{id}/versions/{vid}/segments\", { params: { id, vid }, body: { segments } }));\n }\n\n private async write(p: Promise<Content>): Promise<Content> {\n const emit = this.http.captureChanges();\n const content = await p;\n emit({ type: \"content.saved\", content });\n return content;\n }\n\n private async writeVersion<T>(id: string, p: Promise<T>): Promise<T> {\n const emit = this.http.captureChanges();\n const out = await p;\n emit({ type: \"content.changed\", id });\n return out;\n }\n}\n"],"mappings":"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"}
@@ -4,6 +4,9 @@ import { readerSession as t } from "./beacons.js";
4
4
  var n = /* @__PURE__ */ new Set([
5
5
  "groups",
6
6
  "exclude_groups",
7
+ "creators",
8
+ "access",
9
+ "tiers",
7
10
  "traits",
8
11
  "card_groups",
9
12
  "group_kinds"
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","names":[],"sources":["../../src/client/discovery.ts"],"sourcesContent":["import type {\n ContentFilters,\n ContentFiltersUpdate,\n ContentPage,\n ExposureInput,\n Exposures,\n RecentSearches,\n ReleaseDates,\n SignalInput,\n Signals,\n Typeahead,\n} from \"./generated/wire.js\";\nimport { readerSession } from \"./beacons.js\";\nimport type { Http } from \"./http.js\";\nimport { call } from \"./route.js\";\n\n/** A popularity period. */\nexport type Period = \"7d\" | \"30d\" | \"90d\" | \"365d\" | \"all\";\n/** A list's order; position orders by the first of `groups` (an ordered group). */\nexport type ListSort = \"newest\" | \"oldest\" | \"title\" | \"popular\" | `popular:${Period}` | \"top_rated\" | `top_rated:${Period}` | \"most_favorited\" | \"most_viewed\" | \"position\" | \"random\";\n/** How locked contents list: marked (lock, the default) or left out (hide). */\nexport type AccessMode = \"lock\" | \"hide\";\n\n/** Which contents a list holds. */\nexport interface ContentFilterQuery {\n kind?: string;\n /** Group ids every listed version is in. */\n groups?: readonly string[];\n /** Group ids no listed version is in. */\n exclude_groups?: readonly string[];\n /** Only contents this group owns: a channel's feed. */\n owner_group?: string;\n /** User ids: only contents one of them is credited on. */\n creators?: readonly string[];\n /** public (anyone opens it now) or the level it stays locked at. */\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n /** Only contents locked behind one of these tiers. */\n tiers?: readonly string[];\n /** Version traits every listed version has. */\n traits?: readonly string[];\n /** \"name:value\" of the kind's filterable attributes. */\n attribute?: readonly string[];\n /** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */\n released_on?: string;\n /** IANA time zone of released_on and release dates; UTC by default. */\n tz?: string;\n /** The signed-in reader's unseen, in-progress or completed contents. */\n view_status?: \"unseen\" | \"in_progress\" | \"completed\";\n}\n\n/** How a list reads to its reader. */\nexport interface ReaderQuery {\n /** Only content with a live version in this language. */\n language?: string;\n /** Titles, names and paths in this language, and each card's preferred version's; language by default. */\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** The group kinds cards name; every kind with a page by default. */\nexport interface CardQuery {\n card_groups?: readonly string[];\n}\n\nexport interface PageQuery {\n cursor?: string;\n limit?: number;\n}\n\nexport type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & { sort?: ListSort };\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n /** A signed-out reader's session, which keeps its recent searches; default readerSession(). */\n session?: string;\n };\nexport interface RecentSearchesQuery {\n /** A signed-out reader's session; default readerSession(). */\n session?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\nexport type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & { limit?: number };\nexport type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & { by?: \"month\" | \"day\" };\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\" };\nexport interface TypeaheadQuery extends ReaderQuery {\n q: string;\n /** Only contents of this kind. */\n kind?: string;\n /** The group kinds suggested; every kind with a page by default. */\n group_kinds?: readonly string[];\n /** Per section: 8 by default, at most 20. */\n limit?: number;\n}\n\n// Comma-joined list parameters; `attribute` repeats instead.\nconst LISTS = new Set([\"groups\", \"exclude_groups\", \"traits\", \"card_groups\", \"group_kinds\"]);\n\n/** A query with its list parameters comma-joined, as discovery routes read them. */\nexport function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined> {\n const out: Record<string, string | number | boolean | readonly string[] | undefined> = {};\n for (const [k, v] of Object.entries(q)) out[k] = LISTS.has(k) && Array.isArray(v) ? (v.length ? v.join(\",\") : undefined) : v;\n return out;\n}\n\n/**\n * Discovery: lists, search, typeahead, similar and recommended contents,\n * release dates, the reader's content filters and history, signals and exposures.\n */\nexport class DiscoveryClient {\n constructor(private readonly http: Http) {}\n\n /** Contents as cards, one per content item with the version that fits the reader. */\n list(q: ContentQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents\", { query: query(q) as never, signal });\n }\n /** A first page is kept among the reader's recent searches. */\n async search(q: SearchQuery, signal?: AbortSignal): Promise<ContentPage> {\n const emit = q.cursor ? null : this.http.captureChanges();\n const out: ContentPage = await call(this.http, \"GET\", \"/search\", { query: query({ session: readerSession(), ...q }) as never, signal });\n emit?.({ type: \"searches.changed\" });\n return out;\n }\n /** The reader's recent searches, newest first, since they last cleared them. */\n recentSearches(q: RecentSearchesQuery = {}, signal?: AbortSignal): Promise<RecentSearches> {\n return call(this.http, \"GET\", \"/me/searches\", { query: { session: readerSession(), ...q }, signal });\n }\n /** Clears one recent search, or all of them. */\n async clearSearches(o: { query?: string; session?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/searches\", { query: { session: readerSession(), ...o } });\n emit({ type: \"searches.changed\" });\n }\n /** Contents and groups while the reader types. */\n typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/typeahead\", { query: query(q) as never, signal });\n }\n /** Contents like one, of its kind unless q.kind names another. */\n similar(id: string, q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents/{id}/similar\", { params: { id }, query: query(q) as never, signal });\n }\n /** For the signed-in reader. */\n recommendations(q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/recommendations\", { query: query(q) as never, signal });\n }\n /** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */\n releaseDates(q: ReleaseDatesQuery = {}, signal?: AbortSignal): Promise<ReleaseDates> {\n return call(this.http, \"GET\", \"/release-dates\", { query: query(q) as never, signal });\n }\n contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters> {\n return call(this.http, \"GET\", \"/me/content-filters\", { query: { language }, signal });\n }\n /** Replaces the reader's filter settings; every read applies them. */\n async setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters> {\n const emit = this.http.captureChanges();\n const out: ContentFilters = await call(this.http, \"PUT\", \"/me/content-filters\", { query: { language }, body: update });\n emit({ type: \"contentFilters.changed\" });\n return out;\n }\n /** What the reader engaged with, most recent first. */\n history(q: HistoryQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/me/history\", { query: query(q) as never, signal });\n }\n /** Erases the reader's history: of one content item, one kind or everything. */\n async forgetHistory(o: { kind?: string; content_id?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/history\", { query: o });\n emit({ type: \"history.changed\" });\n }\n /** Views and clicks; signed out, of the session named. */\n signals(signals: SignalInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Signals = { session, signals };\n return call(this.http, \"POST\", \"/signals\", { body, keepalive: o.keepalive });\n }\n /** Result lists served, rendered or shown, for click attribution. */\n exposures(exposures: ExposureInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Exposures = { session, exposures };\n return call(this.http, \"POST\", \"/exposures\", { body, keepalive: o.keepalive });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"discovery.js","names":[],"sources":["../../src/client/discovery.ts"],"sourcesContent":["import type {\n ContentFilters,\n ContentFiltersUpdate,\n ContentPage,\n ExposureInput,\n Exposures,\n RecentSearches,\n ReleaseDates,\n SignalInput,\n Signals,\n Typeahead,\n} from \"./generated/wire.js\";\nimport { readerSession } from \"./beacons.js\";\nimport type { Http } from \"./http.js\";\nimport { call } from \"./route.js\";\n\n/** A popularity period. */\nexport type Period = \"7d\" | \"30d\" | \"90d\" | \"365d\" | \"all\";\n/** A list's order; position orders by the first of `groups` (an ordered group). */\nexport type ListSort = \"newest\" | \"oldest\" | \"title\" | \"popular\" | `popular:${Period}` | \"top_rated\" | `top_rated:${Period}` | \"most_favorited\" | \"most_viewed\" | \"position\" | \"random\";\n/** How locked contents list: marked (lock, the default) or left out (hide). */\nexport type AccessMode = \"lock\" | \"hide\";\n\n/** Which contents a list holds. */\nexport interface ContentFilterQuery {\n kind?: string;\n /** Group ids every listed version is in. */\n groups?: readonly string[];\n /** Group ids no listed version is in. */\n exclude_groups?: readonly string[];\n /** Only contents this group owns: a channel's feed. */\n owner_group?: string;\n /** User ids: only contents one of them is credited on. */\n creators?: readonly string[];\n /** public (anyone opens it now) or the level it stays locked at. */\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n /** Only contents locked behind one of these tiers. */\n tiers?: readonly string[];\n /** Version traits every listed version has. */\n traits?: readonly string[];\n /** \"name:value\" of the kind's filterable attributes. */\n attribute?: readonly string[];\n /** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */\n released_on?: string;\n /** IANA time zone of released_on and release dates; UTC by default. */\n tz?: string;\n /** The signed-in reader's unseen, in-progress or completed contents. */\n view_status?: \"unseen\" | \"in_progress\" | \"completed\";\n}\n\n/** How a list reads to its reader. */\nexport interface ReaderQuery {\n /** Only content with a live version in this language. */\n language?: string;\n /** Titles, names and paths in this language, and each card's preferred version's; language by default. */\n display_language?: string;\n mode?: AccessMode;\n}\n\n/** The group kinds cards name; every kind with a page by default. */\nexport interface CardQuery {\n card_groups?: readonly string[];\n}\n\nexport interface PageQuery {\n cursor?: string;\n limit?: number;\n}\n\nexport type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & { sort?: ListSort };\nexport type SearchQuery = ContentFilterQuery &\n ReaderQuery &\n CardQuery &\n PageQuery & {\n q: string;\n sort?: \"relevance\" | ListSort;\n personalize?: boolean;\n /** A signed-out reader's session, which keeps its recent searches; default readerSession(). */\n session?: string;\n };\nexport interface RecentSearchesQuery {\n /** A signed-out reader's session; default readerSession(). */\n session?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\nexport type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & { limit?: number };\nexport type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & { by?: \"month\" | \"day\" };\nexport type HistoryQuery = ReaderQuery & CardQuery & PageQuery & { kind?: string; status?: \"seen\" | \"in_progress\" | \"completed\" };\nexport interface TypeaheadQuery extends ReaderQuery {\n q: string;\n /** Only contents of this kind. */\n kind?: string;\n /** The group kinds suggested; every kind with a page by default. */\n group_kinds?: readonly string[];\n /** Per section: 8 by default, at most 20. */\n limit?: number;\n}\n\n// Comma-joined list parameters; `attribute` repeats instead.\nconst LISTS = new Set([\"groups\", \"exclude_groups\", \"creators\", \"access\", \"tiers\", \"traits\", \"card_groups\", \"group_kinds\"]);\n\n/** A query with its list parameters comma-joined, as discovery routes read them. */\nexport function query(q: object): Record<string, string | number | boolean | readonly string[] | undefined> {\n const out: Record<string, string | number | boolean | readonly string[] | undefined> = {};\n for (const [k, v] of Object.entries(q)) out[k] = LISTS.has(k) && Array.isArray(v) ? (v.length ? v.join(\",\") : undefined) : v;\n return out;\n}\n\n/**\n * Discovery: lists, search, typeahead, similar and recommended contents,\n * release dates, the reader's content filters and history, signals and exposures.\n */\nexport class DiscoveryClient {\n constructor(private readonly http: Http) {}\n\n /** Contents as cards, one per content item with the version that fits the reader. */\n list(q: ContentQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents\", { query: query(q) as never, signal });\n }\n /** A first page is kept among the reader's recent searches. */\n async search(q: SearchQuery, signal?: AbortSignal): Promise<ContentPage> {\n const emit = q.cursor ? null : this.http.captureChanges();\n const out: ContentPage = await call(this.http, \"GET\", \"/search\", { query: query({ session: readerSession(), ...q }) as never, signal });\n emit?.({ type: \"searches.changed\" });\n return out;\n }\n /** The reader's recent searches, newest first, since they last cleared them. */\n recentSearches(q: RecentSearchesQuery = {}, signal?: AbortSignal): Promise<RecentSearches> {\n return call(this.http, \"GET\", \"/me/searches\", { query: { session: readerSession(), ...q }, signal });\n }\n /** Clears one recent search, or all of them. */\n async clearSearches(o: { query?: string; session?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/searches\", { query: { session: readerSession(), ...o } });\n emit({ type: \"searches.changed\" });\n }\n /** Contents and groups while the reader types. */\n typeahead(q: TypeaheadQuery, signal?: AbortSignal): Promise<Typeahead> {\n return call(this.http, \"GET\", \"/typeahead\", { query: query(q) as never, signal });\n }\n /** Contents like one, of its kind unless q.kind names another. */\n similar(id: string, q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/contents/{id}/similar\", { params: { id }, query: query(q) as never, signal });\n }\n /** For the signed-in reader. */\n recommendations(q: RailQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/recommendations\", { query: query(q) as never, signal });\n }\n /** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */\n releaseDates(q: ReleaseDatesQuery = {}, signal?: AbortSignal): Promise<ReleaseDates> {\n return call(this.http, \"GET\", \"/release-dates\", { query: query(q) as never, signal });\n }\n contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters> {\n return call(this.http, \"GET\", \"/me/content-filters\", { query: { language }, signal });\n }\n /** Replaces the reader's filter settings; every read applies them. */\n async setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters> {\n const emit = this.http.captureChanges();\n const out: ContentFilters = await call(this.http, \"PUT\", \"/me/content-filters\", { query: { language }, body: update });\n emit({ type: \"contentFilters.changed\" });\n return out;\n }\n /** What the reader engaged with, most recent first. */\n history(q: HistoryQuery = {}, signal?: AbortSignal): Promise<ContentPage> {\n return call(this.http, \"GET\", \"/me/history\", { query: query(q) as never, signal });\n }\n /** Erases the reader's history: of one content item, one kind or everything. */\n async forgetHistory(o: { kind?: string; content_id?: string } = {}): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/me/history\", { query: o });\n emit({ type: \"history.changed\" });\n }\n /** Views and clicks; signed out, of the session named. */\n signals(signals: SignalInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Signals = { session, signals };\n return call(this.http, \"POST\", \"/signals\", { body, keepalive: o.keepalive });\n }\n /** Result lists served, rendered or shown, for click attribution. */\n exposures(exposures: ExposureInput[], session?: string, o: { keepalive?: boolean } = {}): Promise<void> {\n const body: Exposures = { session, exposures };\n return call(this.http, \"POST\", \"/exposures\", { body, keepalive: o.keepalive });\n }\n}\n"],"mappings":"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"}
@@ -1,12 +1,13 @@
1
1
  import { ErrorCode } from './generated/error-codes.js';
2
2
  import { BanNotice, ErrorDetails, Failure } from './generated/wire.js';
3
3
  /**
4
- * Codes raised in the browser: network (no response), storage (the bucket
4
+ * Codes raised in the browser: network (no server reached), server_unavailable
5
+ * (a gateway or timeout answered instead of ContentKit), storage (the bucket
5
6
  * refused a PUT), aborted (the caller's signal), resume_mismatch (saved state
6
7
  * is not for this file), decode (not readable as an image) and render_timeout
7
8
  * (still processing after the wait).
8
9
  */
9
- export declare const CLIENT_ERROR_CODES: readonly ["network", "storage", "aborted", "resume_mismatch", "decode", "render_timeout"];
10
+ export declare const CLIENT_ERROR_CODES: readonly ["network", "server_unavailable", "storage", "aborted", "resume_mismatch", "decode", "render_timeout"];
10
11
  export type ContentKitErrorCode = ErrorCode | (typeof CLIENT_ERROR_CODES)[number];
11
12
  /** Every code a ContentKitError can carry: the server's registry, then the client's. */
12
13
  export declare const ERROR_CODES: readonly ContentKitErrorCode[];
@@ -17,6 +18,9 @@ export interface ContentKitErrorInit extends ErrorOptions {
17
18
  blobs?: string[];
18
19
  ban?: BanNotice;
19
20
  action?: string;
21
+ reason?: string;
22
+ availableAt?: string;
23
+ unknownCode?: string;
20
24
  }
21
25
  export declare class ContentKitError extends Error {
22
26
  readonly code: ContentKitErrorCode;
@@ -28,6 +32,12 @@ export declare class ContentKitError extends Error {
28
32
  readonly blobs?: string[];
29
33
  readonly ban?: BanNotice;
30
34
  readonly action?: string;
35
+ /** A download_unavailable's code of the host's policy ("release_window"). */
36
+ readonly reason?: string;
37
+ /** A download_unavailable's opening time (RFC 3339), when a matter of time. */
38
+ readonly availableAt?: string;
39
+ /** A server code this client doesn't know (a newer server): `message` is the server's own and `code` its status's. */
40
+ readonly unknownCode?: string;
31
41
  constructor(code: ContentKitErrorCode, message: string, init?: ContentKitErrorInit);
32
42
  /**
33
43
  * The request broke a rule the message states (a 4xx or a typed refusal),
@@ -44,8 +54,16 @@ export declare class ContentKitError extends Error {
44
54
  get transient(): boolean;
45
55
  }
46
56
  export declare const isContentKitError: (e: unknown) => e is ContentKitError;
47
- /** Anything a call can throw, as a ContentKitError (network when it is not one). */
57
+ /** Anything a call can throw, as a ContentKitError: a transport failure's code, else internal_error. */
48
58
  export declare function toContentKitError(e: unknown): ContentKitError;
59
+ /** A fetch that got no response: the caller's abort, a timeout, else no server reached. */
60
+ export declare function requestFailure(err: unknown, signal: AbortSignal | undefined, target: string): ContentKitError;
61
+ /**
62
+ * The client code of a request no server answered (mirrors auth-ui's and
63
+ * billing-ui's): network when none was reached, server_unavailable when a
64
+ * gateway or timeout answered. Recognizes the sibling SDKs' codes too.
65
+ */
66
+ export declare function transportFailure(error: unknown): "network" | "server_unavailable" | undefined;
49
67
  export declare function aborted(signal?: AbortSignal): ContentKitError;
50
68
  export declare function throwIfAborted(signal?: AbortSignal): void;
51
69
  /** Reads a response that is not 2xx. */
@@ -2,6 +2,7 @@ import { CONTENTKIT_ERROR_CODES as e } from "./generated/error-codes.js";
2
2
  //#region src/client/errors.ts
3
3
  var t = [
4
4
  "network",
5
+ "server_unavailable",
5
6
  "storage",
6
7
  "aborted",
7
8
  "resume_mismatch",
@@ -16,8 +17,11 @@ var t = [
16
17
  blobs;
17
18
  ban;
18
19
  action;
20
+ reason;
21
+ availableAt;
22
+ unknownCode;
19
23
  constructor(e, t, n = {}) {
20
- super(t, n.cause === void 0 ? void 0 : { cause: n.cause }), this.code = e, this.status = n.status ?? 0, this.retryAfter = n.retryAfter, this.details = n.details, this.blobs = n.blobs, this.ban = n.ban, this.action = n.action;
24
+ super(t, n.cause === void 0 ? void 0 : { cause: n.cause }), this.code = e, this.status = n.status ?? 0, this.retryAfter = n.retryAfter, this.details = n.details, this.blobs = n.blobs, this.ban = n.ban, this.action = n.action, this.reason = n.reason, this.availableAt = n.availableAt, this.unknownCode = n.unknownCode;
21
25
  }
22
26
  get refusal() {
23
27
  return this.status >= 400 && this.status < 500 && this.code !== "storage" || this.code.startsWith("image_") || this.code === "decode";
@@ -32,19 +36,42 @@ var t = [
32
36
  return this.isLimit || this.code === "unauthorized" || this.code === "forbidden";
33
37
  }
34
38
  get transient() {
35
- return this.code === "network" || this.code === "internal_error" || this.code === "unavailable" || this.code === "storage" && (this.status >= 500 || this.status === 403 || this.status === 408 || this.status === 429);
39
+ return this.code === "network" || this.code === "server_unavailable" || this.code === "internal_error" || this.code === "unavailable" || this.code === "storage" && (this.status >= 500 || this.status === 403 || this.status === 408 || this.status === 429);
36
40
  }
37
41
  }, i = (e) => e instanceof r;
38
42
  function a(e) {
39
- return e instanceof r ? e : new r("network", e instanceof Error ? e.message : String(e), { cause: e });
43
+ return e instanceof r ? e : new r(l(e) ?? "internal_error", e instanceof Error ? e.message : String(e), { cause: e });
40
44
  }
41
- function o(e) {
45
+ function o(e, t, n) {
46
+ let i = l(e);
47
+ return i !== "server_unavailable" && t?.aborted ? u(t) : i === "server_unavailable" ? new r(i, `${n} did not respond`, { cause: e }) : new r("network", `${n} unreachable`, { cause: e });
48
+ }
49
+ var s = /* @__PURE__ */ new Set([
50
+ 502,
51
+ 503,
52
+ 504,
53
+ 520,
54
+ 521,
55
+ 522,
56
+ 523,
57
+ 524
58
+ ]), c = /fetch|network|load failed|connection/i;
59
+ function l(e) {
60
+ if (e instanceof TypeError) return globalThis.navigator?.onLine === !1 || c.test(e.message) ? "network" : void 0;
61
+ if (!e || typeof e != "object") return;
62
+ let { name: t, code: n, status: r } = e;
63
+ if (t === "TimeoutError") return "server_unavailable";
64
+ if (n === "network" || n === "network_error") return "network";
65
+ if (n === "server_unavailable") return n;
66
+ if (n === void 0 || n === "unknown_error" || n === "server_error") return typeof r == "number" && s.has(r) ? "server_unavailable" : void 0;
67
+ }
68
+ function u(e) {
42
69
  return new r("aborted", "request canceled", { cause: e?.reason });
43
70
  }
44
- function s(e) {
45
- if (e?.aborted) throw o(e);
71
+ function d(e) {
72
+ if (e?.aborted) throw u(e);
46
73
  }
47
- var c = {
74
+ var f = {
48
75
  401: "unauthorized",
49
76
  403: "forbidden",
50
77
  404: "not_found",
@@ -55,26 +82,29 @@ var c = {
55
82
  429: "rate_limited",
56
83
  501: "not_configured",
57
84
  503: "unavailable"
58
- }, l = new Set(n);
59
- async function u(e) {
85
+ }, p = new Set(n);
86
+ async function m(e) {
60
87
  let t = {};
61
88
  try {
62
89
  t = await e.json();
63
90
  } catch {}
64
91
  let n = Number(e.headers.get("Retry-After")), i = t.retry_after ?? (Number.isFinite(n) && n > 0 ? n : void 0);
65
- return new r(t.code && l.has(t.code) ? t.code : c[e.status] ?? (e.status >= 500 ? "internal_error" : "invalid_request"), t.error ?? `HTTP ${e.status}`, {
92
+ return new r(t.code && p.has(t.code) ? t.code : !t.code && s.has(e.status) ? "server_unavailable" : f[e.status] ?? (e.status >= 500 ? "internal_error" : "invalid_request"), t.error ?? `HTTP ${e.status}`, {
66
93
  status: e.status,
67
94
  retryAfter: i,
68
95
  blobs: t.blobs,
69
96
  details: t.details,
70
97
  ban: t.ban,
71
- action: t.action
98
+ action: t.action,
99
+ reason: t.reason,
100
+ availableAt: t.available_at,
101
+ unknownCode: t.code && !p.has(t.code) ? t.code : void 0
72
102
  });
73
103
  }
74
- function d(e) {
75
- return new r(e.code && l.has(e.code) ? e.code : "internal_error", e.message, { details: e.details });
104
+ function h(e) {
105
+ return new r(e.code && p.has(e.code) ? e.code : "internal_error", e.message, { details: e.details });
76
106
  }
77
107
  //#endregion
78
- export { t as CLIENT_ERROR_CODES, r as ContentKitError, n as ERROR_CODES, o as aborted, d as failureError, i as isContentKitError, u as readContentKitError, s as throwIfAborted, a as toContentKitError };
108
+ export { t as CLIENT_ERROR_CODES, r as ContentKitError, n as ERROR_CODES, u as aborted, h as failureError, i as isContentKitError, m as readContentKitError, o as requestFailure, d as throwIfAborted, a as toContentKitError, l as transportFailure };
79
109
 
80
110
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/client/errors.ts"],"sourcesContent":["import { CONTENTKIT_ERROR_CODES, type ErrorCode } from \"./generated/error-codes.js\";\nimport type { BanNotice, ErrorDetails, ErrorReply, Failure } from \"./generated/wire.js\";\n\n/**\n * Codes raised in the browser: network (no response), storage (the bucket\n * refused a PUT), aborted (the caller's signal), resume_mismatch (saved state\n * is not for this file), decode (not readable as an image) and render_timeout\n * (still processing after the wait).\n */\nexport const CLIENT_ERROR_CODES = [\"network\", \"storage\", \"aborted\", \"resume_mismatch\", \"decode\", \"render_timeout\"] as const;\n\nexport type ContentKitErrorCode = ErrorCode | (typeof CLIENT_ERROR_CODES)[number];\n\n/** Every code a ContentKitError can carry: the server's registry, then the client's. */\nexport const ERROR_CODES: readonly ContentKitErrorCode[] = [...(Object.keys(CONTENTKIT_ERROR_CODES) as ErrorCode[]), ...CLIENT_ERROR_CODES];\n\nexport interface ContentKitErrorInit extends ErrorOptions {\n status?: number;\n retryAfter?: number;\n details?: ErrorDetails;\n blobs?: string[];\n ban?: BanNotice;\n action?: string;\n}\n\nexport class ContentKitError extends Error {\n override readonly name = \"ContentKitError\";\n /** HTTP status; 0 for client-side codes. */\n readonly status: number;\n readonly retryAfter?: number;\n readonly details?: ErrorDetails;\n readonly blobs?: string[];\n readonly ban?: BanNotice;\n readonly action?: string;\n\n constructor(\n readonly code: ContentKitErrorCode,\n message: string,\n init: ContentKitErrorInit = {},\n ) {\n super(message, init.cause === undefined ? undefined : { cause: init.cause });\n this.status = init.status ?? 0;\n this.retryAfter = init.retryAfter;\n this.details = init.details;\n this.blobs = init.blobs;\n this.ban = init.ban;\n this.action = init.action;\n }\n\n /**\n * The request broke a rule the message states (a 4xx or a typed refusal),\n * as opposed to a fault on the server or the network: show it as is.\n */\n get refusal(): boolean {\n return (this.status >= 400 && this.status < 500 && this.code !== \"storage\") || this.code.startsWith(\"image_\") || this.code === \"decode\";\n }\n\n /** Rate or quota refusals: stop starting new requests until the limit frees. */\n get isLimit(): boolean {\n return this.code === \"rate_limited\" || this.code === \"quota_exceeded\";\n }\n\n /** The commit would exceed the kind's file caps (too_many_files, 409): remove files first. */\n get isCeiling(): boolean {\n return this.code === \"too_many_files\";\n }\n\n /** Refusals that apply to every upload of this caller, not just this file. */\n get blocksQueue(): boolean {\n return this.isLimit || this.code === \"unauthorized\" || this.code === \"forbidden\";\n }\n\n /** Worth retrying the same request. */\n get transient(): boolean {\n return (\n this.code === \"network\" ||\n this.code === \"internal_error\" ||\n this.code === \"unavailable\" ||\n (this.code === \"storage\" && (this.status >= 500 || this.status === 403 || this.status === 408 || this.status === 429))\n );\n }\n}\n\nexport const isContentKitError = (e: unknown): e is ContentKitError => e instanceof ContentKitError;\n\n/** Anything a call can throw, as a ContentKitError (network when it is not one). */\nexport function toContentKitError(e: unknown): ContentKitError {\n if (e instanceof ContentKitError) return e;\n return new ContentKitError(\"network\", e instanceof Error ? e.message : String(e), { cause: e });\n}\n\nexport function aborted(signal?: AbortSignal): ContentKitError {\n return new ContentKitError(\"aborted\", \"request canceled\", { cause: signal?.reason });\n}\n\nexport function throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) throw aborted(signal);\n}\n\nconst byStatus: Record<number, ContentKitErrorCode> = {\n 401: \"unauthorized\",\n 403: \"forbidden\",\n 404: \"not_found\",\n 409: \"conflict\",\n 410: \"gone\",\n 413: \"too_large\",\n 415: \"type_not_allowed\",\n 429: \"rate_limited\",\n 501: \"not_configured\",\n 503: \"unavailable\",\n};\n\nconst known = new Set<string>(ERROR_CODES);\n\n/** Reads a response that is not 2xx. */\nexport async function readContentKitError(res: Response): Promise<ContentKitError> {\n let body: Partial<ErrorReply> = {};\n try {\n body = (await res.json()) as Partial<ErrorReply>;\n } catch {\n // not JSON (a proxy error page)\n }\n const header = Number(res.headers.get(\"Retry-After\"));\n const retryAfter = body.retry_after ?? (Number.isFinite(header) && header > 0 ? header : undefined);\n const code = body.code && known.has(body.code) ? (body.code as ContentKitErrorCode) : (byStatus[res.status] ?? (res.status >= 500 ? \"internal_error\" : \"invalid_request\"));\n return new ContentKitError(code, body.error ?? `HTTP ${res.status}`, {\n status: res.status,\n retryAfter,\n blobs: body.blobs,\n details: body.details,\n ban: body.ban,\n action: body.action,\n });\n}\n\n/** An upload's recorded processing failure: its typed refusal, else a processing fault. */\nexport function failureError(f: Failure): ContentKitError {\n const code = f.code && known.has(f.code) ? (f.code as ContentKitErrorCode) : \"internal_error\";\n return new ContentKitError(code, f.message, { details: f.details });\n}\n"],"mappings":";;AASA,IAAa,IAAqB;CAAC;CAAW;CAAW;CAAW;CAAmB;CAAU;AAAgB,GAKpG,IAA8C,CAAC,GAAI,OAAO,KAAK,CAAsB,GAAmB,GAAG,CAAkB,GAW7H,IAAb,cAAqC,MAAM;CAW9B;CAVX,OAAyB;CAEzB;CACA;CACA;CACA;CACA;CACA;CAEA,YACE,GACA,GACA,IAA4B,CAAC,GAC7B;EAOA,AANA,MAAM,GAAS,EAAK,UAAU,KAAA,IAAY,KAAA,IAAY,EAAE,OAAO,EAAK,MAAM,CAAC,GAJlE,KAAA,OAAA,GAKT,KAAK,SAAS,EAAK,UAAU,GAC7B,KAAK,aAAa,EAAK,YACvB,KAAK,UAAU,EAAK,SACpB,KAAK,QAAQ,EAAK,OAClB,KAAK,MAAM,EAAK,KAChB,KAAK,SAAS,EAAK;CACrB;CAMA,IAAI,UAAmB;EACrB,OAAQ,KAAK,UAAU,OAAO,KAAK,SAAS,OAAO,KAAK,SAAS,aAAc,KAAK,KAAK,WAAW,QAAQ,KAAK,KAAK,SAAS;CACjI;CAGA,IAAI,UAAmB;EACrB,OAAO,KAAK,SAAS,kBAAkB,KAAK,SAAS;CACvD;CAGA,IAAI,YAAqB;EACvB,OAAO,KAAK,SAAS;CACvB;CAGA,IAAI,cAAuB;EACzB,OAAO,KAAK,WAAW,KAAK,SAAS,kBAAkB,KAAK,SAAS;CACvE;CAGA,IAAI,YAAqB;EACvB,OACE,KAAK,SAAS,aACd,KAAK,SAAS,oBACd,KAAK,SAAS,iBACb,KAAK,SAAS,cAAc,KAAK,UAAU,OAAO,KAAK,WAAW,OAAO,KAAK,WAAW,OAAO,KAAK,WAAW;CAErH;AACF,GAEa,KAAqB,MAAqC,aAAa;AAGpF,SAAgB,EAAkB,GAA6B;CAE7D,OADI,aAAa,IAAwB,IAClC,IAAI,EAAgB,WAAW,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC;AAChG;AAEA,SAAgB,EAAQ,GAAuC;CAC7D,OAAO,IAAI,EAAgB,WAAW,oBAAoB,EAAE,OAAO,GAAQ,OAAO,CAAC;AACrF;AAEA,SAAgB,EAAe,GAA4B;CACzD,IAAI,GAAQ,SAAS,MAAM,EAAQ,CAAM;AAC3C;AAEA,IAAM,IAAgD;CACpD,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP,GAEM,IAAQ,IAAI,IAAY,CAAW;AAGzC,eAAsB,EAAoB,GAAyC;CACjF,IAAI,IAA4B,CAAC;CACjC,IAAI;EACF,IAAQ,MAAM,EAAI,KAAK;CACzB,QAAQ,CAER;CACA,IAAM,IAAS,OAAO,EAAI,QAAQ,IAAI,aAAa,CAAC,GAC9C,IAAa,EAAK,gBAAgB,OAAO,SAAS,CAAM,KAAK,IAAS,IAAI,IAAS,KAAA;CAEzF,OAAO,IAAI,EADE,EAAK,QAAQ,EAAM,IAAI,EAAK,IAAI,IAAK,EAAK,OAAgC,EAAS,EAAI,YAAY,EAAI,UAAU,MAAM,mBAAmB,oBACtH,EAAK,SAAS,QAAQ,EAAI,UAAU;EACnE,QAAQ,EAAI;EACZ;EACA,OAAO,EAAK;EACZ,SAAS,EAAK;EACd,KAAK,EAAK;EACV,QAAQ,EAAK;CACf,CAAC;AACH;AAGA,SAAgB,EAAa,GAA6B;CAExD,OAAO,IAAI,EADE,EAAE,QAAQ,EAAM,IAAI,EAAE,IAAI,IAAK,EAAE,OAA+B,kBAC5C,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC;AACpE"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/client/errors.ts"],"sourcesContent":["import { CONTENTKIT_ERROR_CODES, type ErrorCode } from \"./generated/error-codes.js\";\nimport type { BanNotice, ErrorDetails, ErrorReply, Failure } from \"./generated/wire.js\";\n\n/**\n * Codes raised in the browser: network (no server reached), server_unavailable\n * (a gateway or timeout answered instead of ContentKit), storage (the bucket\n * refused a PUT), aborted (the caller's signal), resume_mismatch (saved state\n * is not for this file), decode (not readable as an image) and render_timeout\n * (still processing after the wait).\n */\nexport const CLIENT_ERROR_CODES = [\"network\", \"server_unavailable\", \"storage\", \"aborted\", \"resume_mismatch\", \"decode\", \"render_timeout\"] as const;\n\nexport type ContentKitErrorCode = ErrorCode | (typeof CLIENT_ERROR_CODES)[number];\n\n/** Every code a ContentKitError can carry: the server's registry, then the client's. */\nexport const ERROR_CODES: readonly ContentKitErrorCode[] = [...(Object.keys(CONTENTKIT_ERROR_CODES) as ErrorCode[]), ...CLIENT_ERROR_CODES];\n\nexport interface ContentKitErrorInit extends ErrorOptions {\n status?: number;\n retryAfter?: number;\n details?: ErrorDetails;\n blobs?: string[];\n ban?: BanNotice;\n action?: string;\n reason?: string;\n availableAt?: string;\n unknownCode?: string;\n}\n\nexport class ContentKitError extends Error {\n override readonly name = \"ContentKitError\";\n /** HTTP status; 0 for client-side codes. */\n readonly status: number;\n readonly retryAfter?: number;\n readonly details?: ErrorDetails;\n readonly blobs?: string[];\n readonly ban?: BanNotice;\n readonly action?: string;\n /** A download_unavailable's code of the host's policy (\"release_window\"). */\n readonly reason?: string;\n /** A download_unavailable's opening time (RFC 3339), when a matter of time. */\n readonly availableAt?: string;\n /** A server code this client doesn't know (a newer server): `message` is the server's own and `code` its status's. */\n readonly unknownCode?: string;\n\n constructor(\n readonly code: ContentKitErrorCode,\n message: string,\n init: ContentKitErrorInit = {},\n ) {\n super(message, init.cause === undefined ? undefined : { cause: init.cause });\n this.status = init.status ?? 0;\n this.retryAfter = init.retryAfter;\n this.details = init.details;\n this.blobs = init.blobs;\n this.ban = init.ban;\n this.action = init.action;\n this.reason = init.reason;\n this.availableAt = init.availableAt;\n this.unknownCode = init.unknownCode;\n }\n\n /**\n * The request broke a rule the message states (a 4xx or a typed refusal),\n * as opposed to a fault on the server or the network: show it as is.\n */\n get refusal(): boolean {\n return (this.status >= 400 && this.status < 500 && this.code !== \"storage\") || this.code.startsWith(\"image_\") || this.code === \"decode\";\n }\n\n /** Rate or quota refusals: stop starting new requests until the limit frees. */\n get isLimit(): boolean {\n return this.code === \"rate_limited\" || this.code === \"quota_exceeded\";\n }\n\n /** The commit would exceed the kind's file caps (too_many_files, 409): remove files first. */\n get isCeiling(): boolean {\n return this.code === \"too_many_files\";\n }\n\n /** Refusals that apply to every upload of this caller, not just this file. */\n get blocksQueue(): boolean {\n return this.isLimit || this.code === \"unauthorized\" || this.code === \"forbidden\";\n }\n\n /** Worth retrying the same request. */\n get transient(): boolean {\n return (\n this.code === \"network\" ||\n this.code === \"server_unavailable\" ||\n this.code === \"internal_error\" ||\n this.code === \"unavailable\" ||\n (this.code === \"storage\" && (this.status >= 500 || this.status === 403 || this.status === 408 || this.status === 429))\n );\n }\n}\n\nexport const isContentKitError = (e: unknown): e is ContentKitError => e instanceof ContentKitError;\n\n/** Anything a call can throw, as a ContentKitError: a transport failure's code, else internal_error. */\nexport function toContentKitError(e: unknown): ContentKitError {\n if (e instanceof ContentKitError) return e;\n return new ContentKitError(transportFailure(e) ?? \"internal_error\", e instanceof Error ? e.message : String(e), { cause: e });\n}\n\n/** A fetch that got no response: the caller's abort, a timeout, else no server reached. */\nexport function requestFailure(err: unknown, signal: AbortSignal | undefined, target: string): ContentKitError {\n const code = transportFailure(err);\n if (code !== \"server_unavailable\" && signal?.aborted) return aborted(signal);\n return code === \"server_unavailable\"\n ? new ContentKitError(code, `${target} did not respond`, { cause: err })\n : new ContentKitError(\"network\", `${target} unreachable`, { cause: err });\n}\n\n// A gateway answered for ContentKit: down, restarting or timed out (52x: Cloudflare).\nconst GATEWAY_STATUS = new Set([502, 503, 504, 520, 521, 522, 523, 524]);\n\n// fetch rejects with a TypeError when no server answers (offline, DNS,\n// connection refused, CORS); engines word it differently.\nconst FETCH_FAILURE = /fetch|network|load failed|connection/i;\n\n/**\n * The client code of a request no server answered (mirrors auth-ui's and\n * billing-ui's): network when none was reached, server_unavailable when a\n * gateway or timeout answered. Recognizes the sibling SDKs' codes too.\n */\nexport function transportFailure(error: unknown): \"network\" | \"server_unavailable\" | undefined {\n if (error instanceof TypeError) {\n const offline = globalThis.navigator?.onLine === false;\n return offline || FETCH_FAILURE.test(error.message) ? \"network\" : undefined;\n }\n if (!error || typeof error !== \"object\") return undefined;\n const { name, code, status } = error as { name?: unknown; code?: unknown; status?: unknown };\n if (name === \"TimeoutError\") return \"server_unavailable\";\n if (code === \"network\" || code === \"network_error\") return \"network\";\n if (code === \"server_unavailable\") return code;\n if (code === undefined || code === \"unknown_error\" || code === \"server_error\")\n return typeof status === \"number\" && GATEWAY_STATUS.has(status) ? \"server_unavailable\" : undefined;\n return undefined;\n}\n\nexport function aborted(signal?: AbortSignal): ContentKitError {\n return new ContentKitError(\"aborted\", \"request canceled\", { cause: signal?.reason });\n}\n\nexport function throwIfAborted(signal?: AbortSignal): void {\n if (signal?.aborted) throw aborted(signal);\n}\n\nconst byStatus: Record<number, ContentKitErrorCode> = {\n 401: \"unauthorized\",\n 403: \"forbidden\",\n 404: \"not_found\",\n 409: \"conflict\",\n 410: \"gone\",\n 413: \"too_large\",\n 415: \"type_not_allowed\",\n 429: \"rate_limited\",\n 501: \"not_configured\",\n 503: \"unavailable\",\n};\n\nconst known = new Set<string>(ERROR_CODES);\n\n/** Reads a response that is not 2xx. */\nexport async function readContentKitError(res: Response): Promise<ContentKitError> {\n let body: Partial<ErrorReply> = {};\n try {\n body = (await res.json()) as Partial<ErrorReply>;\n } catch {\n // not JSON (a proxy error page)\n }\n const header = Number(res.headers.get(\"Retry-After\"));\n const retryAfter = body.retry_after ?? (Number.isFinite(header) && header > 0 ? header : undefined);\n const code =\n body.code && known.has(body.code)\n ? (body.code as ContentKitErrorCode)\n : !body.code && GATEWAY_STATUS.has(res.status)\n ? \"server_unavailable\"\n : (byStatus[res.status] ?? (res.status >= 500 ? \"internal_error\" : \"invalid_request\"));\n return new ContentKitError(code, body.error ?? `HTTP ${res.status}`, {\n status: res.status,\n retryAfter,\n blobs: body.blobs,\n details: body.details,\n ban: body.ban,\n action: body.action,\n reason: body.reason,\n availableAt: body.available_at,\n unknownCode: body.code && !known.has(body.code) ? body.code : undefined,\n });\n}\n\n/** An upload's recorded processing failure: its typed refusal, else a processing fault. */\nexport function failureError(f: Failure): ContentKitError {\n const code = f.code && known.has(f.code) ? (f.code as ContentKitErrorCode) : \"internal_error\";\n return new ContentKitError(code, f.message, { details: f.details });\n}\n"],"mappings":"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"}