@openrails/contentkit-ui 0.68.0 → 0.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +55 -10
  2. package/dist/client/client.d.ts +4 -2
  3. package/dist/client/client.js +1 -0
  4. package/dist/client/client.js.map +1 -1
  5. package/dist/client/content/bans.js +7 -7
  6. package/dist/client/content/bans.js.map +1 -1
  7. package/dist/client/content/comments.js +16 -14
  8. package/dist/client/content/comments.js.map +1 -1
  9. package/dist/client/content/favorites.js +5 -5
  10. package/dist/client/content/favorites.js.map +1 -1
  11. package/dist/client/content/moderation.js +5 -5
  12. package/dist/client/content/moderation.js.map +1 -1
  13. package/dist/client/content/polls.js +34 -31
  14. package/dist/client/content/polls.js.map +1 -1
  15. package/dist/client/content/posts.js +21 -20
  16. package/dist/client/content/posts.js.map +1 -1
  17. package/dist/client/content/reactions.js +4 -4
  18. package/dist/client/content/reactions.js.map +1 -1
  19. package/dist/client/http.d.ts +6 -2
  20. package/dist/client/http.js +11 -3
  21. package/dist/client/http.js.map +1 -1
  22. package/dist/client/media/client.js +24 -24
  23. package/dist/client/media/client.js.map +1 -1
  24. package/dist/client/media/rules.d.ts +2 -0
  25. package/dist/client/media/rules.js +6 -2
  26. package/dist/client/media/rules.js.map +1 -1
  27. package/dist/client/taxonomy.js +2 -2
  28. package/dist/client/taxonomy.js.map +1 -1
  29. package/dist/components/media-folder-editor.d.ts +17 -3
  30. package/dist/components/media-folder-editor.js +146 -99
  31. package/dist/components/media-folder-editor.js.map +1 -1
  32. package/dist/components/slot-upload.js +102 -100
  33. package/dist/components/slot-upload.js.map +1 -1
  34. package/dist/react/canonical.d.ts +2 -2
  35. package/dist/react/canonical.js +2 -12
  36. package/dist/react/canonical.js.map +1 -1
  37. package/dist/react/comments.js +35 -27
  38. package/dist/react/comments.js.map +1 -1
  39. package/dist/react/context.d.ts +7 -0
  40. package/dist/react/context.js +20 -15
  41. package/dist/react/context.js.map +1 -1
  42. package/dist/react/engagement.js +2 -2
  43. package/dist/react/engagement.js.map +1 -1
  44. package/dist/react/folder.d.ts +29 -9
  45. package/dist/react/folder.js +170 -113
  46. package/dist/react/folder.js.map +1 -1
  47. package/dist/react/polls.js +3 -3
  48. package/dist/react/polls.js.map +1 -1
  49. package/dist/react/posts.js +2 -2
  50. package/dist/react/posts.js.map +1 -1
  51. package/dist/react/provider.d.ts +7 -3
  52. package/dist/react/provider.js +34 -20
  53. package/dist/react/provider.js.map +1 -1
  54. package/dist/react/public.js +11 -11
  55. package/dist/react/public.js.map +1 -1
  56. package/dist/react/read.js +93 -92
  57. package/dist/react/read.js.map +1 -1
  58. package/dist/react/resources.d.ts +6 -3
  59. package/dist/react/resources.js +40 -40
  60. package/dist/react/resources.js.map +1 -1
  61. package/dist/react/slot.d.ts +4 -2
  62. package/dist/react/slot.js +96 -92
  63. package/dist/react/slot.js.map +1 -1
  64. package/dist/react/store.d.ts +1 -1
  65. package/dist/react/store.js +21 -20
  66. package/dist/react/store.js.map +1 -1
  67. package/dist/react/upload.d.ts +3 -2
  68. package/dist/react/upload.js +20 -15
  69. package/dist/react/upload.js.map +1 -1
  70. package/dist/react/use-resource.d.ts +1 -0
  71. package/dist/react/use-resource.js +38 -30
  72. package/dist/react/use-resource.js.map +1 -1
  73. package/dist/react.js +15 -15
  74. package/package.json +7 -5
@@ -26,51 +26,52 @@ var n = class {
26
26
  });
27
27
  }
28
28
  async create(t) {
29
- let n = await e(this.http, "POST", "/posts", { body: t });
30
- return this.http.emit({
29
+ let n = this.http.captureChanges(), r = await e(this.http, "POST", "/posts", { body: t });
30
+ return n({
31
31
  type: "post.created",
32
- post: n
33
- }), n;
32
+ post: r
33
+ }), r;
34
34
  }
35
35
  async update(t, n) {
36
- let r = await e(this.http, "PATCH", "/posts/{id}", {
36
+ let r = this.http.captureChanges(), i = await e(this.http, "PATCH", "/posts/{id}", {
37
37
  params: { id: t },
38
38
  body: n
39
39
  });
40
- return this.http.emit({
40
+ return r({
41
41
  type: "post.updated",
42
- post: r
43
- }), r;
42
+ post: i
43
+ }), i;
44
44
  }
45
45
  async delete(t) {
46
- await e(this.http, "DELETE", "/posts/{id}", { params: { id: t } }), this.http.emit({
46
+ let n = this.http.captureChanges();
47
+ await e(this.http, "DELETE", "/posts/{id}", { params: { id: t } }), n({
47
48
  type: "post.deleted",
48
49
  id: t
49
50
  });
50
51
  }
51
52
  async restore(t) {
52
- let n = await e(this.http, "POST", "/posts/{id}/restore", { params: { id: t } });
53
- return this.http.emit({
53
+ let n = this.http.captureChanges(), r = await e(this.http, "POST", "/posts/{id}/restore", { params: { id: t } });
54
+ return n({
54
55
  type: "post.restored",
55
- post: n
56
- }), n;
56
+ post: r
57
+ }), r;
57
58
  }
58
59
  async react(n, r) {
59
- let i = await e(this.http, "POST", `/posts/{id}/${t(r)}`, { params: { id: n } });
60
- return this.http.emit({
60
+ let i = this.http.captureChanges(), a = await e(this.http, "POST", `/posts/{id}/${t(r)}`, { params: { id: n } });
61
+ return i({
61
62
  type: "post.updated",
62
- post: i
63
- }), i;
63
+ post: a
64
+ }), a;
64
65
  }
65
66
  async setCover(t, n) {
66
- let r = await e(this.http, "PUT", "/posts/{id}/cover", {
67
+ let r = this.http.captureChanges(), i = await e(this.http, "PUT", "/posts/{id}/cover", {
67
68
  params: { id: t },
68
69
  body: { image: n ?? "" }
69
70
  });
70
- return this.http.emit({
71
+ return r({
71
72
  type: "post.changed",
72
73
  id: t
73
- }), r.cover_url;
74
+ }), i.cover_url;
74
75
  }
75
76
  async uploadCover(e, t, n) {
76
77
  if (!t) return this.setCover(e, null);
@@ -1 +1 @@
1
- {"version":3,"file":"posts.js","names":[],"sources":["../../../src/client/content/posts.ts"],"sourcesContent":["import type { InlineImage, Post, PostCover, PostInput } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport type { MediaClient, NamedOptions } from \"../media/client.js\";\nimport { call, type PageQuery } from \"../route.js\";\nimport { reactionVerb, type Reaction, type Sort } from \"./types.js\";\n\nexport interface PostQuery extends PageQuery {\n language?: string;\n sort?: Sort;\n}\n\nexport interface PostAdminQuery extends PageQuery {\n language?: string;\n /** true: only drafts; false: only posts that are not drafts. */\n draft?: boolean;\n /** true: only deleted posts (with deleted_at); otherwise only live ones. */\n deleted?: boolean;\n /** Only posts whose title, excerpt or body contains this text, ignoring case. */\n q?: string;\n}\n\n/** Posts: the published list, staff CRUD, search and restore, reactions, the cover and body images. */\nexport class PostsClient {\n constructor(\n private readonly http: Http,\n private readonly media: MediaClient,\n ) {}\n\n /** Published posts. */\n list(q: PostQuery = {}, signal?: AbortSignal): Promise<Post[]> {\n return call(this.http, \"GET\", \"/posts\", { query: q, signal });\n }\n /** Every post for staff: drafts, scheduled, held and rejected ones included; deleted ones on their own; q searches (PostWrite). */\n adminList(q: PostAdminQuery = {}, signal?: AbortSignal): Promise<Post[]> {\n return call(this.http, \"GET\", \"/posts/admin\", { query: q, signal });\n }\n get(id: string, signal?: AbortSignal): Promise<Post> {\n return call(this.http, \"GET\", \"/posts/{id}\", { params: { id }, signal });\n }\n /** Creates a post (PostWrite); moderation \"held\" when the moderator holds it. */\n async create(input: PostInput): Promise<Post> {\n const post: Post = await call(this.http, \"POST\", \"/posts\", { body: input });\n this.http.emit({ type: \"post.created\", post });\n return post;\n }\n /** Updates the given fields (PostWrite). */\n async update(id: string, input: PostInput): Promise<Post> {\n const post: Post = await call(this.http, \"PATCH\", \"/posts/{id}\", { params: { id }, body: input });\n this.http.emit({ type: \"post.updated\", post });\n return post;\n }\n async delete(id: string): Promise<void> {\n await call(this.http, \"DELETE\", \"/posts/{id}\", { params: { id } });\n this.http.emit({ type: \"post.deleted\", id });\n }\n /** Restores a deleted post as it was (PostWrite); conflict when a live post took its slug. */\n async restore(id: string): Promise<Post> {\n const post: Post = await call(this.http, \"POST\", \"/posts/{id}/restore\", { params: { id } });\n this.http.emit({ type: \"post.restored\", post });\n return post;\n }\n /** Sets the caller's reaction to a published post; resolves with the post's new totals. */\n async react(id: string, value: Reaction): Promise<Post> {\n const post: Post = await call(this.http, \"POST\", `/posts/{id}/${reactionVerb(value)}` as const, { params: { id } });\n this.http.emit({ type: \"post.updated\", post });\n return post;\n }\n /** Sets the cover to an inline image name (\"i-{uuid}\") of the post's folder, or clears it; resolves with its URL. */\n async setCover(id: string, image: string | null): Promise<string | null> {\n const r: PostCover = await call(this.http, \"PUT\", \"/posts/{id}/cover\", { params: { id }, body: { image: image ?? \"\" } });\n this.http.emit({ type: \"post.changed\", id });\n return r.cover_url;\n }\n /** Uploads file to the post's folder and makes it the cover (null clears it); resolves with its URL. */\n async uploadCover(id: string, file: Blob | null, o?: NamedOptions): Promise<string | null> {\n if (!file) return this.setCover(id, null);\n const up = await this.media.uploadNamed(this.media.postRef(id), file, o);\n return this.setCover(id, up.name);\n }\n /** The public URL of an inline image of the post's folder, to place in its body. */\n async imageURL(id: string, image: string, signal?: AbortSignal): Promise<string> {\n const r: InlineImage = await call(this.http, \"POST\", \"/posts/{id}/images\", { params: { id }, body: { image }, signal });\n return r.url;\n }\n}\n"],"mappings":";;;AAsBA,IAAa,IAAb,MAAyB;CAEJ;CACA;CAFnB,YACE,GACA,GACA;EADiB,AADA,KAAA,OAAA,GACA,KAAA,QAAA;CAChB;CAGH,KAAK,IAAe,CAAC,GAAG,GAAuC;EAC7D,OAAO,EAAK,KAAK,MAAM,OAAO,UAAU;GAAE,OAAO;GAAG;EAAO,CAAC;CAC9D;CAEA,UAAU,IAAoB,CAAC,GAAG,GAAuC;EACvE,OAAO,EAAK,KAAK,MAAM,OAAO,gBAAgB;GAAE,OAAO;GAAG;EAAO,CAAC;CACpE;CACA,IAAI,GAAY,GAAqC;EACnD,OAAO,EAAK,KAAK,MAAM,OAAO,eAAe;GAAE,QAAQ,EAAE,MAAG;GAAG;EAAO,CAAC;CACzE;CAEA,MAAM,OAAO,GAAiC;EAC5C,IAAM,IAAa,MAAM,EAAK,KAAK,MAAM,QAAQ,UAAU,EAAE,MAAM,EAAM,CAAC;EAE1E,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAgB;EAAK,CAAC,GACtC;CACT;CAEA,MAAM,OAAO,GAAY,GAAiC;EACxD,IAAM,IAAa,MAAM,EAAK,KAAK,MAAM,SAAS,eAAe;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM;EAAM,CAAC;EAEhG,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAgB;EAAK,CAAC,GACtC;CACT;CACA,MAAM,OAAO,GAA2B;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,eAAe,EAAE,QAAQ,EAAE,MAAG,EAAE,CAAC,GACjE,KAAK,KAAK,KAAK;GAAE,MAAM;GAAgB;EAAG,CAAC;CAC7C;CAEA,MAAM,QAAQ,GAA2B;EACvC,IAAM,IAAa,MAAM,EAAK,KAAK,MAAM,QAAQ,uBAAuB,EAAE,QAAQ,EAAE,MAAG,EAAE,CAAC;EAE1F,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAiB;EAAK,CAAC,GACvC;CACT;CAEA,MAAM,MAAM,GAAY,GAAgC;EACtD,IAAM,IAAa,MAAM,EAAK,KAAK,MAAM,QAAQ,eAAe,EAAa,CAAK,KAAc,EAAE,QAAQ,EAAE,MAAG,EAAE,CAAC;EAElH,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAgB;EAAK,CAAC,GACtC;CACT;CAEA,MAAM,SAAS,GAAY,GAA8C;EACvE,IAAM,IAAe,MAAM,EAAK,KAAK,MAAM,OAAO,qBAAqB;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM,EAAE,OAAO,KAAS,GAAG;EAAE,CAAC;EAEvH,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAgB;EAAG,CAAC,GACpC,EAAE;CACX;CAEA,MAAM,YAAY,GAAY,GAAmB,GAA0C;EACzF,IAAI,CAAC,GAAM,OAAO,KAAK,SAAS,GAAI,IAAI;EACxC,IAAM,IAAK,MAAM,KAAK,MAAM,YAAY,KAAK,MAAM,QAAQ,CAAE,GAAG,GAAM,CAAC;EACvE,OAAO,KAAK,SAAS,GAAI,EAAG,IAAI;CAClC;CAEA,MAAM,SAAS,GAAY,GAAe,GAAuC;EAE/E,QAAO,MADsB,EAAK,KAAK,MAAM,QAAQ,sBAAsB;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM,EAAE,SAAM;GAAG;EAAO,CAAC,EAAA,CAC7G;CACX;AACF"}
1
+ {"version":3,"file":"posts.js","names":[],"sources":["../../../src/client/content/posts.ts"],"sourcesContent":["import type { InlineImage, Post, PostCover, PostInput } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport type { MediaClient, NamedOptions } from \"../media/client.js\";\nimport { call, type PageQuery } from \"../route.js\";\nimport { reactionVerb, type Reaction, type Sort } from \"./types.js\";\n\nexport interface PostQuery extends PageQuery {\n language?: string;\n sort?: Sort;\n}\n\nexport interface PostAdminQuery extends PageQuery {\n language?: string;\n /** true: only drafts; false: only posts that are not drafts. */\n draft?: boolean;\n /** true: only deleted posts (with deleted_at); otherwise only live ones. */\n deleted?: boolean;\n /** Only posts whose title, excerpt or body contains this text, ignoring case. */\n q?: string;\n}\n\n/** Posts: the published list, staff CRUD, search and restore, reactions, the cover and body images. */\nexport class PostsClient {\n constructor(\n private readonly http: Http,\n private readonly media: MediaClient,\n ) {}\n\n /** Published posts. */\n list(q: PostQuery = {}, signal?: AbortSignal): Promise<Post[]> {\n return call(this.http, \"GET\", \"/posts\", { query: q, signal });\n }\n /** Every post for staff: drafts, scheduled, held and rejected ones included; deleted ones on their own; q searches (PostWrite). */\n adminList(q: PostAdminQuery = {}, signal?: AbortSignal): Promise<Post[]> {\n return call(this.http, \"GET\", \"/posts/admin\", { query: q, signal });\n }\n get(id: string, signal?: AbortSignal): Promise<Post> {\n return call(this.http, \"GET\", \"/posts/{id}\", { params: { id }, signal });\n }\n /** Creates a post (PostWrite); moderation \"held\" when the moderator holds it. */\n async create(input: PostInput): Promise<Post> {\n const emit = this.http.captureChanges();\n const post: Post = await call(this.http, \"POST\", \"/posts\", { body: input });\n emit({ type: \"post.created\", post });\n return post;\n }\n /** Updates the given fields (PostWrite). */\n async update(id: string, input: PostInput): Promise<Post> {\n const emit = this.http.captureChanges();\n const post: Post = await call(this.http, \"PATCH\", \"/posts/{id}\", { params: { id }, body: input });\n emit({ type: \"post.updated\", post });\n return post;\n }\n async delete(id: string): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"DELETE\", \"/posts/{id}\", { params: { id } });\n emit({ type: \"post.deleted\", id });\n }\n /** Restores a deleted post as it was (PostWrite); conflict when a live post took its slug. */\n async restore(id: string): Promise<Post> {\n const emit = this.http.captureChanges();\n const post: Post = await call(this.http, \"POST\", \"/posts/{id}/restore\", { params: { id } });\n emit({ type: \"post.restored\", post });\n return post;\n }\n /** Sets the caller's reaction to a published post; resolves with the post's new totals. */\n async react(id: string, value: Reaction): Promise<Post> {\n const emit = this.http.captureChanges();\n const post: Post = await call(this.http, \"POST\", `/posts/{id}/${reactionVerb(value)}` as const, { params: { id } });\n emit({ type: \"post.updated\", post });\n return post;\n }\n /** Sets the cover to an inline image name (\"i-{uuid}\") of the post's folder, or clears it; resolves with its URL. */\n async setCover(id: string, image: string | null): Promise<string | null> {\n const emit = this.http.captureChanges();\n const r: PostCover = await call(this.http, \"PUT\", \"/posts/{id}/cover\", { params: { id }, body: { image: image ?? \"\" } });\n emit({ type: \"post.changed\", id });\n return r.cover_url;\n }\n /** Uploads file to the post's folder and makes it the cover (null clears it); resolves with its URL. */\n async uploadCover(id: string, file: Blob | null, o?: NamedOptions): Promise<string | null> {\n if (!file) return this.setCover(id, null);\n const up = await this.media.uploadNamed(this.media.postRef(id), file, o);\n return this.setCover(id, up.name);\n }\n /** The public URL of an inline image of the post's folder, to place in its body. */\n async imageURL(id: string, image: string, signal?: AbortSignal): Promise<string> {\n const r: InlineImage = await call(this.http, \"POST\", \"/posts/{id}/images\", { params: { id }, body: { image }, signal });\n return r.url;\n }\n}\n"],"mappings":";;;AAsBA,IAAa,IAAb,MAAyB;CAEJ;CACA;CAFnB,YACE,GACA,GACA;EADiB,AADA,KAAA,OAAA,GACA,KAAA,QAAA;CAChB;CAGH,KAAK,IAAe,CAAC,GAAG,GAAuC;EAC7D,OAAO,EAAK,KAAK,MAAM,OAAO,UAAU;GAAE,OAAO;GAAG;EAAO,CAAC;CAC9D;CAEA,UAAU,IAAoB,CAAC,GAAG,GAAuC;EACvE,OAAO,EAAK,KAAK,MAAM,OAAO,gBAAgB;GAAE,OAAO;GAAG;EAAO,CAAC;CACpE;CACA,IAAI,GAAY,GAAqC;EACnD,OAAO,EAAK,KAAK,MAAM,OAAO,eAAe;GAAE,QAAQ,EAAE,MAAG;GAAG;EAAO,CAAC;CACzE;CAEA,MAAM,OAAO,GAAiC;EAC5C,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAa,MAAM,EAAK,KAAK,MAAM,QAAQ,UAAU,EAAE,MAAM,EAAM,CAAC;EAE1E,OADA,EAAK;GAAE,MAAM;GAAgB;EAAK,CAAC,GAC5B;CACT;CAEA,MAAM,OAAO,GAAY,GAAiC;EACxD,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAa,MAAM,EAAK,KAAK,MAAM,SAAS,eAAe;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM;EAAM,CAAC;EAEhG,OADA,EAAK;GAAE,MAAM;GAAgB;EAAK,CAAC,GAC5B;CACT;CACA,MAAM,OAAO,GAA2B;EACtC,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,UAAU,eAAe,EAAE,QAAQ,EAAE,MAAG,EAAE,CAAC,GACjE,EAAK;GAAE,MAAM;GAAgB;EAAG,CAAC;CACnC;CAEA,MAAM,QAAQ,GAA2B;EACvC,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAa,MAAM,EAAK,KAAK,MAAM,QAAQ,uBAAuB,EAAE,QAAQ,EAAE,MAAG,EAAE,CAAC;EAE1F,OADA,EAAK;GAAE,MAAM;GAAiB;EAAK,CAAC,GAC7B;CACT;CAEA,MAAM,MAAM,GAAY,GAAgC;EACtD,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAa,MAAM,EAAK,KAAK,MAAM,QAAQ,eAAe,EAAa,CAAK,KAAc,EAAE,QAAQ,EAAE,MAAG,EAAE,CAAC;EAElH,OADA,EAAK;GAAE,MAAM;GAAgB;EAAK,CAAC,GAC5B;CACT;CAEA,MAAM,SAAS,GAAY,GAA8C;EACvE,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAe,MAAM,EAAK,KAAK,MAAM,OAAO,qBAAqB;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM,EAAE,OAAO,KAAS,GAAG;EAAE,CAAC;EAEvH,OADA,EAAK;GAAE,MAAM;GAAgB;EAAG,CAAC,GAC1B,EAAE;CACX;CAEA,MAAM,YAAY,GAAY,GAAmB,GAA0C;EACzF,IAAI,CAAC,GAAM,OAAO,KAAK,SAAS,GAAI,IAAI;EACxC,IAAM,IAAK,MAAM,KAAK,MAAM,YAAY,KAAK,MAAM,QAAQ,CAAE,GAAG,GAAM,CAAC;EACvE,OAAO,KAAK,SAAS,GAAI,EAAG,IAAI;CAClC;CAEA,MAAM,SAAS,GAAY,GAAe,GAAuC;EAE/E,QAAO,MADsB,EAAK,KAAK,MAAM,QAAQ,sBAAsB;GAAE,QAAQ,EAAE,MAAG;GAAG,MAAM,EAAE,SAAM;GAAG;EAAO,CAAC,EAAA,CAC7G;CACX;AACF"}
@@ -16,18 +16,18 @@ var n = class {
16
16
  });
17
17
  }
18
18
  async set(n, r) {
19
- let i = await e(this.http, "POST", `/{kind}/{id}/${t(r)}`, { params: {
19
+ let i = this.http.captureChanges(), a = await e(this.http, "POST", `/{kind}/{id}/${t(r)}`, { params: {
20
20
  kind: n.kind,
21
21
  id: n.id
22
22
  } });
23
- return this.http.emit({
23
+ return i({
24
24
  type: "reaction.changed",
25
25
  ref: {
26
26
  kind: n.kind,
27
27
  id: n.id
28
28
  },
29
- counts: i
30
- }), i;
29
+ counts: a
30
+ }), a;
31
31
  }
32
32
  };
33
33
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"reactions.js","names":[],"sources":["../../../src/client/content/reactions.ts"],"sourcesContent":["import type { ReactionCounts, RefBody } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call } from \"../route.js\";\nimport { reactionVerb, type Reaction } from \"./types.js\";\n\n/** Likes and dislikes of an item (a host kind, or \"post\"). */\nexport class ReactionsClient {\n constructor(private readonly http: Http) {}\n\n /** The item's counts and the caller's own reaction. */\n get(ref: RefBody, signal?: AbortSignal): Promise<ReactionCounts> {\n return call(this.http, \"GET\", \"/{kind}/{id}/reaction\", { params: { kind: ref.kind, id: ref.id }, signal });\n }\n /** Sets the caller's reaction (0 clears it); resolves with the new counts. */\n async set(ref: RefBody, value: Reaction): Promise<ReactionCounts> {\n const counts: ReactionCounts = await call(this.http, \"POST\", `/{kind}/{id}/${reactionVerb(value)}` as const, { params: { kind: ref.kind, id: ref.id } });\n this.http.emit({ type: \"reaction.changed\", ref: { kind: ref.kind, id: ref.id }, counts });\n return counts;\n }\n}\n"],"mappings":";;;AAMA,IAAa,IAAb,MAA6B;CACE;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,IAAI,GAAc,GAA+C;EAC/D,OAAO,EAAK,KAAK,MAAM,OAAO,yBAAyB;GAAE,QAAQ;IAAE,MAAM,EAAI;IAAM,IAAI,EAAI;GAAG;GAAG;EAAO,CAAC;CAC3G;CAEA,MAAM,IAAI,GAAc,GAA0C;EAChE,IAAM,IAAyB,MAAM,EAAK,KAAK,MAAM,QAAQ,gBAAgB,EAAa,CAAK,KAAc,EAAE,QAAQ;GAAE,MAAM,EAAI;GAAM,IAAI,EAAI;EAAG,EAAE,CAAC;EAEvJ,OADA,KAAK,KAAK,KAAK;GAAE,MAAM;GAAoB,KAAK;IAAE,MAAM,EAAI;IAAM,IAAI,EAAI;GAAG;GAAG;EAAO,CAAC,GACjF;CACT;AACF"}
1
+ {"version":3,"file":"reactions.js","names":[],"sources":["../../../src/client/content/reactions.ts"],"sourcesContent":["import type { ReactionCounts, RefBody } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call } from \"../route.js\";\nimport { reactionVerb, type Reaction } from \"./types.js\";\n\n/** Likes and dislikes of an item (a host kind, or \"post\"). */\nexport class ReactionsClient {\n constructor(private readonly http: Http) {}\n\n /** The item's counts and the caller's own reaction. */\n get(ref: RefBody, signal?: AbortSignal): Promise<ReactionCounts> {\n return call(this.http, \"GET\", \"/{kind}/{id}/reaction\", { params: { kind: ref.kind, id: ref.id }, signal });\n }\n /** Sets the caller's reaction (0 clears it); resolves with the new counts. */\n async set(ref: RefBody, value: Reaction): Promise<ReactionCounts> {\n const emit = this.http.captureChanges();\n const counts: ReactionCounts = await call(this.http, \"POST\", `/{kind}/{id}/${reactionVerb(value)}` as const, { params: { kind: ref.kind, id: ref.id } });\n emit({ type: \"reaction.changed\", ref: { kind: ref.kind, id: ref.id }, counts });\n return counts;\n }\n}\n"],"mappings":";;;AAMA,IAAa,IAAb,MAA6B;CACE;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,IAAI,GAAc,GAA+C;EAC/D,OAAO,EAAK,KAAK,MAAM,OAAO,yBAAyB;GAAE,QAAQ;IAAE,MAAM,EAAI;IAAM,IAAI,EAAI;GAAG;GAAG;EAAO,CAAC;CAC3G;CAEA,MAAM,IAAI,GAAc,GAA0C;EAChE,IAAM,IAAO,KAAK,KAAK,eAAe,GAChC,IAAyB,MAAM,EAAK,KAAK,MAAM,QAAQ,gBAAgB,EAAa,CAAK,KAAc,EAAE,QAAQ;GAAE,MAAM,EAAI;GAAM,IAAI,EAAI;EAAG,EAAE,CAAC;EAEvJ,OADA,EAAK;GAAE,MAAM;GAAoB,KAAK;IAAE,MAAM,EAAI;IAAM,IAAI,EAAI;GAAG;GAAG;EAAO,CAAC,GACvE;CACT;AACF"}
@@ -63,6 +63,7 @@ export interface RequestOptions {
63
63
  export declare class Http {
64
64
  private readonly o;
65
65
  private readonly listeners;
66
+ private readScope;
66
67
  private readonly base;
67
68
  constructor(o: HttpOptions);
68
69
  /** The module's base URL, without a trailing slash. */
@@ -73,7 +74,10 @@ export declare class Http {
73
74
  * "include", and adds the bearer only on the API's origin (never a media host).
74
75
  */
75
76
  readonly xhrSetup: (xhr: XMLHttpRequest, url: string) => Promise<void>;
76
- subscribe(listener: (change: ContentKitChange) => void): () => void;
77
- emit(change: ContentKitChange): void;
77
+ setReadScope(key: string): void;
78
+ /** Capture before awaiting a write; null tells caches to refetch after a scope change. */
79
+ captureChanges(): (change: ContentKitChange) => void;
80
+ subscribe(listener: (change: ContentKitChange, scope: string | null) => void): () => void;
81
+ private emit;
78
82
  private onApiOrigin;
79
83
  }
@@ -3,6 +3,7 @@ import { ContentKitError as e, aborted as t, readContentKitError as n } from "./
3
3
  var r = (e) => e.replace(/\/+$/, ""), i = class {
4
4
  o;
5
5
  listeners = /* @__PURE__ */ new Set();
6
+ readScope = { key: "{}" };
6
7
  base;
7
8
  constructor(e) {
8
9
  if (this.o = e, typeof e.baseUrl != "string") throw Error("contentkit: createContentKitClient needs baseUrl");
@@ -49,12 +50,19 @@ var r = (e) => e.replace(/\/+$/, ""), i = class {
49
50
  let n = await this.o.token?.();
50
51
  n && e.setRequestHeader("Authorization", `Bearer ${n}`);
51
52
  };
53
+ setReadScope(e) {
54
+ e !== this.readScope.key && (this.readScope = { key: e });
55
+ }
56
+ captureChanges() {
57
+ let e = this.readScope;
58
+ return (t) => this.emit(t, e === this.readScope ? e.key : null);
59
+ }
52
60
  subscribe(e) {
53
61
  return this.listeners.add(e), () => void this.listeners.delete(e);
54
62
  }
55
- emit(e) {
56
- for (let t of this.listeners) try {
57
- t(e);
63
+ emit(e, t) {
64
+ for (let n of this.listeners) try {
65
+ n(e, t);
58
66
  } catch (e) {
59
67
  console.error("contentkit: change listener failed", e);
60
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","names":[],"sources":["../../src/client/http.ts"],"sourcesContent":["import { ContentKitError, aborted, readContentKitError } from \"./errors.js\";\nimport type { ContentChange } from \"./content/types.js\";\nimport type { FileInfo, RefBody } from \"./generated/wire.js\";\n\n/** A module's base URL, or one per item (a host that splits uploads by kind). */\nexport type Mount = string | ((ref: RefBody) => string);\n\n/**\n * Where each module lives when the host does not serve them all under\n * baseUrl's fixed sub-paths.\n */\nexport interface ContentKitMounts {\n /** Posts, comments, reactions, favorites, polls, bans, moderation. Default baseUrl. */\n content?: string;\n /** Media reads and HLS playlists. Default `{baseUrl}/media`. */\n media?: Mount;\n /** Uploads and commits. Default `{baseUrl}/media/upload`. */\n upload?: Mount;\n /** Content-code lookup. Default `{baseUrl}/codes`. */\n codes?: string;\n /** Taxonomy. Default `{baseUrl}/taxonomy`. */\n taxonomy?: string;\n}\n\nexport type ContentKitModule = keyof ContentKitMounts;\n\n/** A mutation made through the client: the host's cache-invalidation hook gets each one. */\nexport type ContentKitChange =\n /** A commit applied; files are the item's uploads as an editor reads them, without URLs. */\n | { type: \"media.committed\"; ref: RefBody; files: FileInfo[] }\n /** An upload finished processing, as an editor read returns it. */\n | { type: \"media.processed\"; ref: RefBody; file: FileInfo }\n | ContentChange;\n\nexport type TokenSource = () => string | null | undefined | Promise<string | null | undefined>;\n\nexport interface HttpOptions {\n /** Where the host mounts ContentKit, e.g. \"/api/v1/contentkit\". */\n baseUrl: string;\n mounts?: ContentKitMounts;\n /** Transport for API calls; pass an authenticating fetch (auth-ui's authFetch). */\n fetch?: typeof fetch;\n /**\n * Bearer for API calls and for HLS playlists on the API origin (hls.js\n * loads them with XHR, not fetch). An authenticating fetch overrides it.\n */\n token?: TokenSource;\n /** Sent as Accept-Language. */\n language?: () => string | null | undefined;\n /** Extra headers per call (CSRF). */\n headers?: () => HeadersInit | Promise<HeadersInit>;\n /** Default \"same-origin\"; \"include\" also sends cookies with HLS requests. */\n credentials?: RequestCredentials;\n}\n\nexport interface RequestOptions {\n method?: string;\n body?: unknown;\n signal?: AbortSignal;\n /** Read the body as a Blob instead of JSON. */\n blob?: boolean;\n}\n\nconst trim = (u: string) => u.replace(/\\/+$/, \"\");\n\n/** The transport every module shares: mounts, auth, language, errors and change events. */\nexport class Http {\n private readonly listeners = new Set<(change: ContentKitChange) => void>();\n private readonly base: string;\n\n constructor(private readonly o: HttpOptions) {\n if (typeof o.baseUrl !== \"string\") throw new Error(\"contentkit: createContentKitClient needs baseUrl\");\n this.base = trim(o.baseUrl);\n }\n\n /** The module's base URL, without a trailing slash. */\n mount(module: ContentKitModule, ref?: RefBody): string {\n const m = this.o.mounts?.[module];\n if (typeof m === \"function\") {\n if (!ref) throw new Error(`contentkit: the ${module} mount is per item; pass a ref`);\n return trim(m(ref));\n }\n if (m !== undefined) return trim(m);\n switch (module) {\n case \"content\":\n return this.base;\n case \"media\":\n return `${this.base}/media`;\n case \"upload\":\n return `${this.base}/media/upload`;\n default:\n return `${this.base}/${module}`;\n }\n }\n\n async request<T>(url: string, { method, body, signal, blob }: RequestOptions = {}): Promise<T> {\n const f = this.o.fetch ?? fetch;\n const headers = new Headers(await this.o.headers?.());\n if (body !== undefined) headers.set(\"Content-Type\", \"application/json\");\n const token = await this.o.token?.();\n if (token && !headers.has(\"Authorization\")) headers.set(\"Authorization\", `Bearer ${token}`);\n const lang = this.o.language?.();\n if (lang) headers.set(\"Accept-Language\", lang);\n let res: Response;\n try {\n res = await f(url, {\n method: method ?? (body === undefined ? \"GET\" : \"POST\"),\n headers,\n body: body === undefined ? null : JSON.stringify(body),\n credentials: this.o.credentials ?? \"same-origin\",\n signal: signal ?? null,\n });\n } catch (err) {\n if (signal?.aborted) throw aborted(signal);\n throw new ContentKitError(\"network\", `ContentKit API ${new URL(url, \"http://x\").pathname} unreachable`, { cause: err });\n }\n if (!res.ok) throw await readContentKitError(res);\n if (blob) return (await res.blob()) as T;\n return (res.status === 204 ? undefined : await res.json()) as T;\n }\n\n /**\n * hls.js `xhrSetup`: opens the request, sends cookies when credentials is\n * \"include\", and adds the bearer only on the API's origin (never a media host).\n */\n readonly xhrSetup = async (xhr: XMLHttpRequest, url: string): Promise<void> => {\n if (!xhr.readyState) xhr.open(\"GET\", url, true);\n if (this.o.credentials === \"include\") xhr.withCredentials = true;\n if (!this.onApiOrigin(url)) return;\n const token = await this.o.token?.();\n if (token) xhr.setRequestHeader(\"Authorization\", `Bearer ${token}`);\n };\n\n subscribe(listener: (change: ContentKitChange) => void): () => void {\n this.listeners.add(listener);\n return () => void this.listeners.delete(listener);\n }\n\n emit(change: ContentKitChange): void {\n for (const l of this.listeners) {\n try {\n l(change);\n } catch (e) {\n // A host listener's fault must not fail the mutation that succeeded.\n console.error(\"contentkit: change listener failed\", e);\n }\n }\n }\n\n private onApiOrigin(url: string): boolean {\n const here = typeof location === \"undefined\" ? \"http://localhost\" : location.href;\n try {\n return new URL(url, here).origin === new URL(this.base || \"/\", here).origin;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":";;AA+DA,IAAM,KAAQ,MAAc,EAAE,QAAQ,QAAQ,EAAE,GAGnC,IAAb,MAAkB;CAIa;CAH7B,4BAA6B,IAAI,IAAwC;CACzE;CAEA,YAAY,GAAiC;EAC3C,IAD2B,KAAA,IAAA,GACvB,OAAO,EAAE,WAAY,UAAU,MAAU,MAAM,kDAAkD;EACrG,KAAK,OAAO,EAAK,EAAE,OAAO;CAC5B;CAGA,MAAM,GAA0B,GAAuB;EACrD,IAAM,IAAI,KAAK,EAAE,SAAS;EAC1B,IAAI,OAAO,KAAM,YAAY;GAC3B,IAAI,CAAC,GAAK,MAAU,MAAM,mBAAmB,EAAO,+BAA+B;GACnF,OAAO,EAAK,EAAE,CAAG,CAAC;EACpB;EACA,IAAI,MAAM,KAAA,GAAW,OAAO,EAAK,CAAC;EAClC,QAAQ,GAAR;GACE,KAAK,WACH,OAAO,KAAK;GACd,KAAK,SACH,OAAO,GAAG,KAAK,KAAK;GACtB,KAAK,UACH,OAAO,GAAG,KAAK,KAAK;GACtB,SACE,OAAO,GAAG,KAAK,KAAK,GAAG;EAC3B;CACF;CAEA,MAAM,QAAW,GAAa,EAAE,WAAQ,SAAM,WAAQ,YAAyB,CAAC,GAAe;EAC7F,IAAM,IAAI,KAAK,EAAE,SAAS,OACpB,IAAU,IAAI,QAAQ,MAAM,KAAK,EAAE,UAAU,CAAC;EACpD,AAAI,MAAS,KAAA,KAAW,EAAQ,IAAI,gBAAgB,kBAAkB;EACtE,IAAM,IAAQ,MAAM,KAAK,EAAE,QAAQ;EACnC,AAAI,KAAS,CAAC,EAAQ,IAAI,eAAe,KAAG,EAAQ,IAAI,iBAAiB,UAAU,GAAO;EAC1F,IAAM,IAAO,KAAK,EAAE,WAAW;EAC/B,AAAI,KAAM,EAAQ,IAAI,mBAAmB,CAAI;EAC7C,IAAI;EACJ,IAAI;GACF,IAAM,MAAM,EAAE,GAAK;IACjB,QAAQ,MAAW,MAAS,KAAA,IAAY,QAAQ;IAChD;IACA,MAAM,MAAS,KAAA,IAAY,OAAO,KAAK,UAAU,CAAI;IACrD,aAAa,KAAK,EAAE,eAAe;IACnC,QAAQ,KAAU;GACpB,CAAC;EACH,SAAS,GAAK;GAEZ,MADI,GAAQ,UAAe,EAAQ,CAAM,IACnC,IAAI,EAAgB,WAAW,kBAAkB,IAAI,IAAI,GAAK,UAAU,CAAC,CAAC,SAAS,eAAe,EAAE,OAAO,EAAI,CAAC;EACxH;EACA,IAAI,CAAC,EAAI,IAAI,MAAM,MAAM,EAAoB,CAAG;EAEhD,OADI,IAAc,MAAM,EAAI,KAAK,IACzB,EAAI,WAAW,MAAM,KAAA,IAAY,MAAM,EAAI,KAAK;CAC1D;CAMA,WAAoB,OAAO,GAAqB,MAA+B;EAG7E,IAFK,EAAI,cAAY,EAAI,KAAK,OAAO,GAAK,EAAI,GAC1C,KAAK,EAAE,gBAAgB,cAAW,EAAI,kBAAkB,KACxD,CAAC,KAAK,YAAY,CAAG,GAAG;EAC5B,IAAM,IAAQ,MAAM,KAAK,EAAE,QAAQ;EACnC,AAAI,KAAO,EAAI,iBAAiB,iBAAiB,UAAU,GAAO;CACpE;CAEA,UAAU,GAA0D;EAElE,OADA,KAAK,UAAU,IAAI,CAAQ,SACd,KAAK,KAAK,UAAU,OAAO,CAAQ;CAClD;CAEA,KAAK,GAAgC;EACnC,KAAK,IAAM,KAAK,KAAK,WACnB,IAAI;GACF,EAAE,CAAM;EACV,SAAS,GAAG;GAEV,QAAQ,MAAM,sCAAsC,CAAC;EACvD;CAEJ;CAEA,YAAoB,GAAsB;EACxC,IAAM,IAAO,OAAO,WAAa,MAAc,qBAAqB,SAAS;EAC7E,IAAI;GACF,OAAO,IAAI,IAAI,GAAK,CAAI,CAAC,CAAC,WAAW,IAAI,IAAI,KAAK,QAAQ,KAAK,CAAI,CAAC,CAAC;EACvE,QAAQ;GACN,OAAO;EACT;CACF;AACF"}
1
+ {"version":3,"file":"http.js","names":[],"sources":["../../src/client/http.ts"],"sourcesContent":["import { ContentKitError, aborted, readContentKitError } from \"./errors.js\";\nimport type { ContentChange } from \"./content/types.js\";\nimport type { FileInfo, RefBody } from \"./generated/wire.js\";\n\n/** A module's base URL, or one per item (a host that splits uploads by kind). */\nexport type Mount = string | ((ref: RefBody) => string);\n\n/**\n * Where each module lives when the host does not serve them all under\n * baseUrl's fixed sub-paths.\n */\nexport interface ContentKitMounts {\n /** Posts, comments, reactions, favorites, polls, bans, moderation. Default baseUrl. */\n content?: string;\n /** Media reads and HLS playlists. Default `{baseUrl}/media`. */\n media?: Mount;\n /** Uploads and commits. Default `{baseUrl}/media/upload`. */\n upload?: Mount;\n /** Content-code lookup. Default `{baseUrl}/codes`. */\n codes?: string;\n /** Taxonomy. Default `{baseUrl}/taxonomy`. */\n taxonomy?: string;\n}\n\nexport type ContentKitModule = keyof ContentKitMounts;\n\n/** A mutation made through the client: the host's cache-invalidation hook gets each one. */\nexport type ContentKitChange =\n /** A commit applied; files are the item's uploads as an editor reads them, without URLs. */\n | { type: \"media.committed\"; ref: RefBody; files: FileInfo[] }\n /** An upload finished processing, as an editor read returns it. */\n | { type: \"media.processed\"; ref: RefBody; file: FileInfo }\n | ContentChange;\n\nexport type TokenSource = () => string | null | undefined | Promise<string | null | undefined>;\n\nexport interface HttpOptions {\n /** Where the host mounts ContentKit, e.g. \"/api/v1/contentkit\". */\n baseUrl: string;\n mounts?: ContentKitMounts;\n /** Transport for API calls; pass an authenticating fetch (auth-ui's authFetch). */\n fetch?: typeof fetch;\n /**\n * Bearer for API calls and for HLS playlists on the API origin (hls.js\n * loads them with XHR, not fetch). An authenticating fetch overrides it.\n */\n token?: TokenSource;\n /** Sent as Accept-Language. */\n language?: () => string | null | undefined;\n /** Extra headers per call (CSRF). */\n headers?: () => HeadersInit | Promise<HeadersInit>;\n /** Default \"same-origin\"; \"include\" also sends cookies with HLS requests. */\n credentials?: RequestCredentials;\n}\n\nexport interface RequestOptions {\n method?: string;\n body?: unknown;\n signal?: AbortSignal;\n /** Read the body as a Blob instead of JSON. */\n blob?: boolean;\n}\n\nconst trim = (u: string) => u.replace(/\\/+$/, \"\");\n\n/** The transport every module shares: mounts, auth, language, errors and change events. */\nexport class Http {\n private readonly listeners = new Set<(change: ContentKitChange, scope: string | null) => void>();\n private readScope = { key: \"{}\" };\n private readonly base: string;\n\n constructor(private readonly o: HttpOptions) {\n if (typeof o.baseUrl !== \"string\") throw new Error(\"contentkit: createContentKitClient needs baseUrl\");\n this.base = trim(o.baseUrl);\n }\n\n /** The module's base URL, without a trailing slash. */\n mount(module: ContentKitModule, ref?: RefBody): string {\n const m = this.o.mounts?.[module];\n if (typeof m === \"function\") {\n if (!ref) throw new Error(`contentkit: the ${module} mount is per item; pass a ref`);\n return trim(m(ref));\n }\n if (m !== undefined) return trim(m);\n switch (module) {\n case \"content\":\n return this.base;\n case \"media\":\n return `${this.base}/media`;\n case \"upload\":\n return `${this.base}/media/upload`;\n default:\n return `${this.base}/${module}`;\n }\n }\n\n async request<T>(url: string, { method, body, signal, blob }: RequestOptions = {}): Promise<T> {\n const f = this.o.fetch ?? fetch;\n const headers = new Headers(await this.o.headers?.());\n if (body !== undefined) headers.set(\"Content-Type\", \"application/json\");\n const token = await this.o.token?.();\n if (token && !headers.has(\"Authorization\")) headers.set(\"Authorization\", `Bearer ${token}`);\n const lang = this.o.language?.();\n if (lang) headers.set(\"Accept-Language\", lang);\n let res: Response;\n try {\n res = await f(url, {\n method: method ?? (body === undefined ? \"GET\" : \"POST\"),\n headers,\n body: body === undefined ? null : JSON.stringify(body),\n credentials: this.o.credentials ?? \"same-origin\",\n signal: signal ?? null,\n });\n } catch (err) {\n if (signal?.aborted) throw aborted(signal);\n throw new ContentKitError(\"network\", `ContentKit API ${new URL(url, \"http://x\").pathname} unreachable`, { cause: err });\n }\n if (!res.ok) throw await readContentKitError(res);\n if (blob) return (await res.blob()) as T;\n return (res.status === 204 ? undefined : await res.json()) as T;\n }\n\n /**\n * hls.js `xhrSetup`: opens the request, sends cookies when credentials is\n * \"include\", and adds the bearer only on the API's origin (never a media host).\n */\n readonly xhrSetup = async (xhr: XMLHttpRequest, url: string): Promise<void> => {\n if (!xhr.readyState) xhr.open(\"GET\", url, true);\n if (this.o.credentials === \"include\") xhr.withCredentials = true;\n if (!this.onApiOrigin(url)) return;\n const token = await this.o.token?.();\n if (token) xhr.setRequestHeader(\"Authorization\", `Bearer ${token}`);\n };\n\n setReadScope(key: string): void {\n if (key !== this.readScope.key) this.readScope = { key };\n }\n\n /** Capture before awaiting a write; null tells caches to refetch after a scope change. */\n captureChanges(): (change: ContentKitChange) => void {\n const scope = this.readScope;\n return (change) => this.emit(change, scope === this.readScope ? scope.key : null);\n }\n\n subscribe(listener: (change: ContentKitChange, scope: string | null) => void): () => void {\n this.listeners.add(listener);\n return () => void this.listeners.delete(listener);\n }\n\n private emit(change: ContentKitChange, scope: string | null): void {\n for (const l of this.listeners) {\n try {\n l(change, scope);\n } catch (e) {\n // A host listener's fault must not fail the mutation that succeeded.\n console.error(\"contentkit: change listener failed\", e);\n }\n }\n }\n\n private onApiOrigin(url: string): boolean {\n const here = typeof location === \"undefined\" ? \"http://localhost\" : location.href;\n try {\n return new URL(url, here).origin === new URL(this.base || \"/\", here).origin;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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"}
@@ -111,71 +111,71 @@ var u = /^i-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, d =
111
111
  }));
112
112
  }
113
113
  async commit(t, n, r = {}) {
114
- let i = structuredClone({
114
+ let i = this.http.captureChanges(), a = structuredClone({
115
115
  ref: t,
116
116
  ops: n,
117
117
  operation_id: r.operationID ?? crypto.randomUUID()
118
118
  });
119
- r.onState?.(structuredClone(i));
120
- let a;
119
+ r.onState?.(structuredClone(a));
120
+ let o;
121
121
  try {
122
- a = (await this.retry(() => this.api.commit(i, r.signal), r.signal)).files;
122
+ o = (await this.retry(() => this.api.commit(a, r.signal), r.signal)).files;
123
123
  } catch (t) {
124
124
  if (!(t instanceof e) || t.code !== "not_uploaded") throw t;
125
- let n = r.sources ?? {}, o = i.ops.filter((e) => e.op === "put" && e.blob && e.blob in n), s = [...new Set(o.map((e) => e.blob))].filter((e) => !t.blobs || t.blobs.includes(e));
125
+ let n = r.sources ?? {}, i = a.ops.filter((e) => e.op === "put" && e.blob && e.blob in n), s = [...new Set(i.map((e) => e.blob))].filter((e) => !t.blobs || t.blobs.includes(e));
126
126
  if (s.length === 0) throw t;
127
127
  let c = /* @__PURE__ */ new Map();
128
128
  for (let e of s) {
129
- let t = n[e], a = t instanceof Blob ? t : t.file, s = t instanceof Blob ? void 0 : t.type, l = o.find((t) => t.blob === e);
130
- c.set(e, (await this.upload(a, {
131
- ref: i.ref,
129
+ let t = n[e], o = t instanceof Blob ? t : t.file, s = t instanceof Blob ? void 0 : t.type, l = i.find((t) => t.blob === e);
130
+ c.set(e, (await this.upload(o, {
131
+ ref: a.ref,
132
132
  path: l.path,
133
133
  type: s,
134
134
  signal: r.signal
135
135
  })).blob);
136
136
  }
137
- let l = i.ops.map((e) => e.blob && c.has(e.blob) ? {
137
+ let l = a.ops.map((e) => e.blob && c.has(e.blob) ? {
138
138
  ...e,
139
139
  blob: c.get(e.blob)
140
140
  } : e), u = {
141
- ref: i.ref,
141
+ ref: a.ref,
142
142
  ops: l,
143
143
  operation_id: crypto.randomUUID()
144
144
  };
145
- r.onState?.(structuredClone(u)), a = (await this.retry(() => this.api.commit(u, r.signal), r.signal)).files;
145
+ r.onState?.(structuredClone(u)), o = (await this.retry(() => this.api.commit(u, r.signal), r.signal)).files;
146
146
  }
147
- return this.http.emit({
147
+ return i({
148
148
  type: "media.committed",
149
149
  ref: {
150
150
  kind: t.kind,
151
151
  id: t.id
152
152
  },
153
- files: a
154
- }), a;
153
+ files: o
154
+ }), o;
155
155
  }
156
156
  async read(e, t = {}) {
157
157
  return this.api.itemURL(e), this.retry(() => this.api.read(e, t, t.signal), t.signal);
158
158
  }
159
159
  async waitFor(t, r, i = {}) {
160
- let a = Date.now() + (i.timeout ?? 12e4);
160
+ let a = this.http.captureChanges(), o = Date.now() + (i.timeout ?? 12e4);
161
161
  for (;;) {
162
- let o = await this.read(t, {
162
+ let s = await this.read(t, {
163
163
  editor: !0,
164
164
  prefix: f(r),
165
165
  signal: i.signal
166
- }), s = o.files.find((e) => e.upload && d(e.path, r));
167
- if (!s) throw new e("not_found", `no upload ${r}`, { status: 404 });
168
- if (s.failed) throw n(s.failed);
169
- if (!s.pending?.length && !s.staged && (s.size ?? 0) > 0) return this.http.emit({
166
+ }), c = s.files.find((e) => e.upload && d(e.path, r));
167
+ if (!c) throw new e("not_found", `no upload ${r}`, { status: 404 });
168
+ if (c.failed) throw n(c.failed);
169
+ if (!c.pending?.length && !c.staged && (c.size ?? 0) > 0) return a({
170
170
  type: "media.processed",
171
171
  ref: {
172
172
  kind: t.kind,
173
173
  id: t.id
174
174
  },
175
- file: s
176
- }), s;
177
- if (o.full) throw new e("too_large", `${r} waits: the item is full; remove uploads to process more`, { status: 413 });
178
- if (Date.now() >= a) throw new e("render_timeout", `${r} is still processing`);
175
+ file: c
176
+ }), c;
177
+ if (s.full) throw new e("too_large", `${r} waits: the item is full; remove uploads to process more`, { status: 413 });
178
+ if (Date.now() >= o) throw new e("render_timeout", `${r} is still processing`);
179
179
  await g(i.interval ?? 1e3, i.signal);
180
180
  }
181
181
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../../src/client/media/client.ts"],"sourcesContent":["import { ContentKitError, aborted, failureError, throwIfAborted, toContentKitError } from \"../errors.js\";\nimport type { CommitBody, Edit, FileInfo, InlineImage, Op, PresetRule, PresignReply, ReadResult, RefBody, RequestReply } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call } from \"../route.js\";\nimport { MediaApi, type ReadOptions } from \"./api.js\";\nimport { sha256Hex } from \"./hash.js\";\nimport { Pacer } from \"./pacer.js\";\nimport { defaultTransport, type Transport } from \"./transport.js\";\n\n/** Upload tuning for the media module. */\nexport interface MediaOptions {\n /** Sends presigned PUTs. Default XMLHttpRequest (upload progress), else fetch. */\n transport?: Transport;\n /** Parallel parts per multipart upload (the pacer may run fewer). Default 4. */\n concurrency?: number;\n /** Retries per part and per idempotent API call. Default 5. */\n retries?: number;\n /** Delay before retry attempt (0-based); retryAfter is the server's hint in seconds. */\n retryDelay?: (attempt: number, retryAfter?: number) => number;\n /** Seconds one part should take. Default 30. */\n targetPartSeconds?: number;\n}\n\nexport interface Progress {\n /** processing: committed, waiting for the worker (put). */\n phase: \"hashing\" | \"uploading\" | \"completing\" | \"processing\";\n loaded: number;\n total: number;\n}\n\nexport interface UploadOptions {\n ref: RefBody;\n /** The upload path: a literal (\"cover\") or under a pattern (\"originals/001.png\"); the server cleans it and adds the extension. */\n path: string;\n /** Content type; default the file's. */\n type?: string;\n /** Aborting pauses: a multipart upload stays resumable from the last state. */\n signal?: AbortSignal;\n onProgress?: (p: Progress) => void;\n /** State saved from onState, to resume a multipart upload of the same file. */\n resume?: UploadState;\n /** Resumable state after every change; null once the upload completed. Persist it to survive a reload. */\n onState?: (s: UploadState | null) => void;\n}\n\n/** An upload, ready for a put op. */\nexport interface UploadedFile {\n /** The path to commit: cleaned, with an extension, named by the server for a Named upload. */\n path: string;\n /**\n * The name to commit: the folder's identical blob (\"sha256-{hex}-{uuid}\") when\n * exists, else the staged upload (\"u-{uuid}\") the worker hashes and places\n * after the commit.\n */\n blob: string;\n type: string;\n size: number;\n /** The identical file was already in the item's folder; nothing was sent. */\n exists: boolean;\n /** The host processes files on upload: commit it unattached now (the queue does). */\n processOnUpload?: boolean;\n}\n\n/** The put op's fields besides path and blob. */\nexport interface PutOptions {\n /** Refuse to replace an existing upload. Default false. */\n createOnly?: boolean;\n edit?: Edit | null;\n meta?: Record<string, unknown>;\n index?: number;\n /** Wait until the worker processed the upload (waitFor). Default true. */\n wait?: boolean;\n /** How long to wait, ms. */\n timeout?: number;\n /** The upload, before the commit. */\n onUploaded?: (up: UploadedFile) => void;\n}\n\nexport interface WaitOptions {\n signal?: AbortSignal;\n /** Poll interval, ms. Default 1000. */\n interval?: number;\n /** ms; then render_timeout. Default 120000. */\n timeout?: number;\n}\n\nexport interface PlannedPart {\n number: number;\n offset: number;\n size: number;\n sha256?: string;\n}\n\n/** A multipart upload in flight; JSON-serializable. */\nexport interface UploadState {\n ticket: string;\n path: string;\n /** The staged upload (\"u-{uuid}\") the parts write. */\n blob: string;\n type: string;\n size: number;\n ref: RefBody;\n file: { name?: string; lastModified?: number; size: number };\n limits: { minPartSize: number; maxPartSize: number; maxParts: number };\n parts: PlannedPart[];\n processOnUpload?: boolean;\n}\n\n/** An upload to a server-named (Named) path: a post's or poll's image. */\nexport interface NamedOptions {\n /** The requested path; the server names the file. Default the file's name. */\n path?: string;\n signal?: AbortSignal;\n onProgress?: (p: Progress) => void;\n /** Wait until the worker processed it. Default true. */\n wait?: boolean;\n /** How long to wait, ms. */\n timeout?: number;\n}\n\n/** An upload to a Named path, committed. */\nexport interface NamedUpload {\n /** The server's name for it (\"i-{uuid}\"), which content routes take. */\n name: string;\n path: string;\n file: FileInfo;\n}\n\n/** The media kinds of post and poll folders (content.Media.PostKind and PollKind). */\nexport interface ContentFolders {\n /** Default \"post\". */\n post?: string;\n /** Default \"poll\". */\n poll?: string;\n}\n\nconst named = /^i-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\n/** A file to upload again when its blob is gone at commit; type defaults to the file's. */\nexport type CommitSource = Blob | { file: Blob; type?: string };\n\nexport interface CommitOptions {\n signal?: AbortSignal;\n /** UUID for this complete batch. Keep it with unchanged ops for a later retry; default a new UUID. */\n operationID?: string;\n /** Persist the active batch before sending it, including any replacement after re-upload. Resume with its ops and operation_id. */\n onState?: (batch: CommitBody) => void;\n /** Blob (\"sha256-…\") → the file it was uploaded from. */\n sources?: Record<string, CommitSource>;\n}\n\ntype Uploadable = Blob & { name?: string; lastModified?: number };\n\n/** A path names a file by its full path or, for an upload, its stem (\"cover\" for \"cover.png\"). */\nexport const samePath = (file: string, path: string) => file === path || stem(file) === path;\n\n/** The path without its extension. */\nexport const stem = (path: string) => {\n const i = path.lastIndexOf(\".\");\n return i > path.lastIndexOf(\"/\") + 1 ? path.slice(0, i) : path;\n};\n\n/** The media module: uploads, commits, reads, frames and HLS. */\nexport class MediaClient {\n readonly api: MediaApi;\n private readonly transport: Transport;\n private readonly concurrency: number;\n private readonly retries: number;\n private readonly delay: (attempt: number, retryAfter?: number) => number;\n private readonly targetSeconds: number;\n private presetList?: Promise<PresetRule[]>;\n\n private readonly folders: Required<ContentFolders>;\n\n constructor(\n private readonly http: Http,\n o: MediaOptions = {},\n folders: ContentFolders = {},\n ) {\n this.folders = { post: folders.post ?? \"post\", poll: folders.poll ?? \"poll\" };\n this.api = new MediaApi(http);\n this.transport = o.transport ?? defaultTransport;\n this.concurrency = o.concurrency ?? 4;\n this.retries = o.retries ?? 5;\n this.delay = o.retryDelay ?? backoff;\n this.targetSeconds = o.targetPartSeconds ?? 30;\n }\n\n /**\n * Uploads file to the item's folder: it hashes the whole file (off the\n * main thread) and presigns {path, type, size, sha256}, then sends nothing\n * (the folder holds the identical blob), or stages it: one checksum-bound\n * PUT up to 64 MiB, resumable multipart above. A refused presign throws\n * before any bytes move. Commit the result with a put op (or use put());\n * the worker then places a staged upload at the hash of its bytes.\n */\n async upload(file: Uploadable, o: UploadOptions): Promise<UploadedFile> {\n const type = o.type || file.type;\n if (!type) throw new ContentKitError(\"invalid_request\", \"the file has no content type\");\n throwIfAborted(o.signal);\n if (o.resume) return this.resumeMultipart(file, o.resume, o);\n const total = file.size;\n const sha256 = await sha256Hex(file, { signal: o.signal, onProgress: (loaded) => o.onProgress?.({ phase: \"hashing\", loaded, total }) });\n const presign = () => this.api.presign({ ref: o.ref, path: o.path, type, size: total, sha256 }, o.signal);\n let p = await presign();\n const out = (p: PresignReply): UploadedFile => ({ path: p.path, blob: p.blob, type, size: total, exists: !!p.exists, processOnUpload: !!p.process_on_upload });\n if (p.exists) return out(p);\n if (p.multipart) {\n const m = p.multipart;\n const state: UploadState = {\n ticket: m.ticket,\n path: p.path,\n blob: p.blob,\n type,\n size: total,\n ref: o.ref,\n file: fingerprint(file),\n limits: { minPartSize: m.min_part_size, maxPartSize: m.max_part_size, maxParts: m.max_parts },\n parts: [],\n processOnUpload: !!p.process_on_upload,\n };\n return this.multipart(file, state, new Set(), o);\n }\n await this.retry(\n async () => {\n if (p.exists) return;\n await this.transport(p.put!, file, { signal: o.signal, onProgress: (loaded) => o.onProgress?.({ phase: \"uploading\", loaded, total }) });\n },\n o.signal,\n async (err) => {\n // An expired URL: presign again (a Named upload gets a new name).\n if (err.code === \"storage\" && err.status === 403) p = await presign();\n },\n );\n return out(p);\n }\n\n /**\n * Uploads file and commits it as a put to its path (with edit, meta and\n * index), then by default waits until the worker processed it; resolves\n * with the upload as an editor reads it.\n */\n async put(file: Uploadable, o: UploadOptions & PutOptions): Promise<FileInfo> {\n const up = await this.upload(file, o);\n o.onUploaded?.(up);\n const op: Op = { op: \"put\", path: up.path, blob: up.blob };\n if (o.createOnly) op.create_id = crypto.randomUUID();\n if (o.edit) op.edit = o.edit;\n if (o.meta) op.meta = o.meta;\n if (o.index !== undefined) op.index = o.index;\n const files = await this.commit(o.ref, [op], { signal: o.signal, sources: { [up.blob]: { file, type: up.type } } });\n if (o.wait === false) return files.find((f) => f.path === up.path) ?? { path: up.path, type: up.type };\n o.onProgress?.({ phase: \"processing\", loaded: up.size, total: up.size });\n return this.waitFor(o.ref, up.path, { signal: o.signal, timeout: o.timeout });\n }\n\n /**\n * Applies ops to the item in one conditional write; returns its uploads as\n * an editor reads them. With sources (blob → the file uploaded as it), a\n * not_uploaded refusal (the blob expired or is due for cleanup) uploads\n * the affected files again and retries the commit once.\n */\n async commit(ref: RefBody, ops: Op[], o: CommitOptions = {}): Promise<FileInfo[]> {\n // Snapshot the batch once: retries must not pick up edits to the caller's\n // ops while the first request or its response is still in flight.\n const body: CommitBody = structuredClone({ ref, ops, operation_id: o.operationID ?? crypto.randomUUID() });\n o.onState?.(structuredClone(body));\n let files: FileInfo[];\n try {\n files = (await this.retry(() => this.api.commit(body, o.signal), o.signal)).files;\n } catch (e) {\n if (!(e instanceof ContentKitError) || e.code !== \"not_uploaded\") throw e;\n const sources = o.sources ?? {};\n const puts = body.ops.filter((op) => op.op === \"put\" && op.blob && op.blob in sources);\n const stale = [...new Set(puts.map((op) => op.blob!))].filter((b) => !e.blobs || e.blobs.includes(b));\n if (stale.length === 0) throw e;\n const renamed = new Map<string, string>();\n for (const blob of stale) {\n const src = sources[blob]!;\n const file = src instanceof Blob ? src : src.file;\n const type = src instanceof Blob ? undefined : src.type;\n const op = puts.find((p) => p.blob === blob)!;\n renamed.set(blob, (await this.upload(file, { ref: body.ref, path: op.path!, type, signal: o.signal })).blob);\n }\n const retried = body.ops.map((op) => (op.blob && renamed.has(op.blob) ? { ...op, blob: renamed.get(op.blob) } : op));\n // Re-uploading changes the inputs. This is a new batch, not a retry\n // under the old ID; transport retries retain its new identity.\n const replacement = { ref: body.ref, ops: retried, operation_id: crypto.randomUUID() };\n o.onState?.(structuredClone(replacement));\n files = (await this.retry(() => this.api.commit(replacement, o.signal), o.signal)).files;\n }\n this.http.emit({ type: \"media.committed\", ref: { kind: ref.kind, id: ref.id }, files });\n return files;\n }\n\n /** The read API: the item's files under prefix in manifest order, with signed URLs for what this viewer may have. */\n async read(ref: RefBody, o: ReadOptions & { signal?: AbortSignal } = {}): Promise<ReadResult> {\n this.api.itemURL(ref); // a bad ref fails at once\n return this.retry(() => this.api.read(ref, o, o.signal), o.signal);\n }\n\n /**\n * Polls an editor read until the upload at path (or with that stem) is\n * processed: placed (not staged), nothing pending and its blob present (a\n * frame grabbed).\n * Rejects with its failure, not_found once it is gone, too_large while the\n * item is full (nothing is processed until uploads are removed),\n * render_timeout after the timeout.\n */\n async waitFor(ref: RefBody, path: string, o: WaitOptions = {}): Promise<FileInfo> {\n const until = Date.now() + (o.timeout ?? 120_000);\n for (;;) {\n const r = await this.read(ref, { editor: true, prefix: stem(path), signal: o.signal });\n const f = r.files.find((x) => x.upload && samePath(x.path, path));\n if (!f) throw new ContentKitError(\"not_found\", `no upload ${path}`, { status: 404 });\n if (f.failed) throw failureError(f.failed);\n if (!f.pending?.length && !f.staged && (f.size ?? 0) > 0) {\n this.http.emit({ type: \"media.processed\", ref: { kind: ref.kind, id: ref.id }, file: f });\n return f;\n }\n if (r.full) throw new ContentKitError(\"too_large\", `${path} waits: the item is full; remove uploads to process more`, { status: 413 });\n if (Date.now() >= until) throw new ContentKitError(\"render_timeout\", `${path} is still processing`);\n await sleep(o.interval ?? 1000, o.signal);\n }\n }\n\n /**\n * The upload's editor view, for re-cropping: its URL and the upload's\n * oriented size. Waits while it renders (an editor read asks for it);\n * not_found when the path has no upload, render_timeout after the timeout\n * (default 30 s).\n */\n async editorView(ref: RefBody, path: string, o: WaitOptions = {}): Promise<{ url: string; width: number; height: number; edit?: Edit }> {\n const until = Date.now() + (o.timeout ?? 30_000);\n for (;;) {\n const r = await this.read(ref, { editor: true, prefix: stem(path), signal: o.signal });\n const f = r.files.find((x) => x.upload && samePath(x.path, path));\n if (!f) throw new ContentKitError(\"not_found\", `no upload ${path}`, { status: 404 });\n if (f.editor_url && f.w && f.h) return { url: f.editor_url, width: f.w, height: f.h, ...(f.edit ? { edit: f.edit } : {}) };\n if (f.failed) throw failureError(f.failed);\n if (Date.now() >= until) throw new ContentKitError(\"render_timeout\", `the editor view of ${path} is still rendering`);\n await sleep(o.interval ?? 1000, o.signal);\n }\n }\n\n /**\n * Every kind's public presets, fetched once per client: crop bounds,\n * widths and default images. An item's current image is in its read's\n * `public`, never built from a preset. Shared by every caller, so it takes\n * no signal; a failed fetch is retried on the next call.\n */\n presets(): Promise<PresetRule[]> {\n this.presetList ??= this.retry(() => this.api.presets()).catch((e: unknown) => {\n this.presetList = undefined;\n throw e;\n });\n return this.presetList;\n }\n\n /** A JPEG still of the video upload at path, t seconds in, w pixels wide (default the frame's). */\n getFrame(ref: RefBody, path: string, t: number, w?: number, signal?: AbortSignal): Promise<Blob> {\n return this.retry(() => this.api.frame(ref, path, t, w, signal), signal);\n }\n\n /** The folder of an HLS ladder from a read's `hls`: `{media}/{kind}/{id}/hls/{dir}`; master.m3u8 and sprite.vtt resolve under it. */\n hlsBase(ref: RefBody, dir: string): string {\n return `${this.api.itemURL(ref)}/hls/${dir}`;\n }\n\n /** hls.js `xhrSetup` for playlists on the API (the client's token and credentials); pass it to players as is. */\n get xhrSetup(): (xhr: XMLHttpRequest, url: string) => Promise<void> {\n return this.http.xhrSetup;\n }\n\n /**\n * Uploads an image to the item's server-named upload path and commits it\n * (by default waiting until it is processed); resolves with the name content\n * routes take: a poll or option image, a post cover.\n */\n async uploadNamed(ref: RefBody, file: Uploadable, o: NamedOptions = {}): Promise<NamedUpload> {\n const f = await this.put(file, { ref, path: o.path ?? (file.name || \"image\"), signal: o.signal, onProgress: o.onProgress, wait: o.wait, timeout: o.timeout });\n const name = stem(f.path.slice(f.path.lastIndexOf(\"/\") + 1));\n if (!named.test(name)) throw new ContentKitError(\"invalid_request\", `${f.path} is not a server-named upload path of ${ref.kind}`);\n return { name, path: f.path, file: f };\n }\n\n /** An image for a post's rich-text body: uploaded to the post's folder; resolves with the URL to place in the body. */\n async uploadInline(post: string, file: Uploadable, o: NamedOptions = {}): Promise<NamedUpload & { url: string }> {\n const up = await this.uploadNamed(this.postRef(post), file, o);\n const r: InlineImage = await call(this.http, \"POST\", \"/posts/{id}/images\", { params: { id: post }, body: { image: up.name }, signal: o.signal });\n return { ...up, url: r.url };\n }\n\n /** The media folder of a post. */\n postRef(id: string): RefBody {\n return { kind: this.folders.post, id };\n }\n\n /** The media folder of a poll. */\n pollRef(id: string): RefBody {\n return { kind: this.folders.poll, id };\n }\n\n /** Discards a paused multipart upload. */\n async discard(state: UploadState): Promise<void> {\n await this.api.abort(state.ref, { ticket: state.ticket });\n }\n\n private async resumeMultipart(file: Uploadable, state: UploadState, o: UploadOptions): Promise<UploadedFile> {\n const fp = fingerprint(file);\n if (fp.size !== state.size || fp.name !== state.file.name || fp.lastModified !== state.file.lastModified) {\n throw new ContentKitError(\"resume_mismatch\", \"the saved upload is for a different file\");\n }\n let listed;\n try {\n listed = await this.retry(() => this.api.listParts(state.ref, { ticket: state.ticket }, o.signal), o.signal);\n } catch (err) {\n // Gone: already completed (Complete is idempotent) or expired.\n if (err instanceof ContentKitError && err.code === \"not_found\") return this.complete(state, o);\n throw err;\n }\n const landed = new Set<number>();\n for (const l of listed.parts) {\n const plan = state.parts.find((p) => p.number === l.number);\n if (!plan || plan.size !== l.size || (l.sha256 && plan.sha256 !== l.sha256)) {\n // Parts this state does not describe: the upload cannot be finished consistently.\n await this.discard(state).catch(() => {});\n o.onState?.(null);\n return this.upload(file, { ...o, resume: undefined });\n }\n landed.add(l.number);\n }\n return this.multipart(file, structuredClone(state), landed, o);\n }\n\n private async multipart(file: Uploadable, state: UploadState, landed: Set<number>, o: UploadOptions): Promise<UploadedFile> {\n const pacer = new Pacer({ ...state.limits, maxConcurrency: this.concurrency, targetSeconds: this.targetSeconds });\n const emitState = () => o.onState?.(structuredClone(state));\n const sending = new Map<number, number>();\n let sent = state.parts.filter((p) => landed.has(p.number)).reduce((n, p) => n + p.size, 0);\n const emitProgress = () => {\n let loaded = sent;\n for (const n of sending.values()) loaded += n;\n o.onProgress?.({ phase: \"uploading\", loaded, total: state.size });\n };\n const pending = state.parts.filter((p) => !landed.has(p.number));\n let offset = state.parts.reduce((end, p) => Math.max(end, p.offset + p.size), 0);\n const next = (): PlannedPart | undefined => {\n const p = pending.shift();\n if (p || offset >= state.size) return p;\n const part: PlannedPart = {\n number: state.parts.length + 1,\n offset,\n size: Math.min(pacer.partSize(), state.size - offset),\n };\n offset += part.size;\n state.parts.push(part);\n return part;\n };\n\n const stop = new AbortController();\n const signal = o.signal ? AbortSignal.any([o.signal, stop.signal]) : stop.signal;\n // Parts hash and presign ahead of the PUT slots, so a freed slot starts\n // its next PUT at once; pacer.concurrency bounds only the PUTs.\n let putting = 0;\n const waiting: (() => void)[] = [];\n const acquire = () =>\n new Promise<void>((resolve, reject) => {\n const take = () => {\n signal.removeEventListener(\"abort\", drop);\n putting++;\n resolve();\n };\n const drop = () => {\n waiting.splice(waiting.indexOf(take) >>> 0, 1);\n reject(aborted(signal));\n };\n if (putting < pacer.concurrency) return take();\n if (signal.aborted) return reject(aborted(signal));\n signal.addEventListener(\"abort\", drop, { once: true });\n waiting.push(take);\n });\n const release = () => {\n putting--;\n while (putting < pacer.concurrency && waiting.length) waiting.shift()!();\n };\n const run = async (part: PlannedPart) => {\n const body = file.slice(part.offset, part.offset + part.size);\n await this.retry(async () => {\n if (!part.sha256) {\n part.sha256 = await sha256Hex(body, { signal });\n emitState();\n }\n const { parts } = await this.api.parts(\n state.ref,\n { ticket: state.ticket, parts: [{ number: part.number, size: part.size, sha256: part.sha256 }] },\n signal,\n );\n const req = parts[0]?.request as RequestReply;\n await acquire();\n const began = performance.now();\n const alongside = putting;\n try {\n await this.transport(req, body, {\n signal,\n onProgress: (n) => {\n sending.set(part.number, n);\n emitProgress();\n },\n });\n } finally {\n sending.delete(part.number);\n release();\n }\n pacer.record(part.size, performance.now() - began, alongside);\n sent += part.size;\n emitProgress();\n }, signal);\n };\n\n emitState();\n emitProgress();\n const inFlight = new Set<Promise<void>>();\n let failure: unknown;\n for (;;) {\n let part: PlannedPart | undefined;\n while (failure === undefined && inFlight.size < pacer.concurrency + 2 && (part = next())) {\n emitState();\n const job: Promise<void> = run(part).then(\n () => void inFlight.delete(job),\n (err) => {\n inFlight.delete(job);\n if (failure === undefined) {\n failure = err;\n stop.abort();\n }\n },\n );\n inFlight.add(job);\n }\n if (inFlight.size === 0) break;\n await Promise.race(inFlight);\n }\n if (failure !== undefined) {\n throw o.signal?.aborted ? aborted(o.signal) : failure;\n }\n return this.complete(state, o);\n }\n\n private async complete(state: UploadState, o: UploadOptions): Promise<UploadedFile> {\n o.onProgress?.({ phase: \"completing\", loaded: state.size, total: state.size });\n const c = await this.retry(() => this.api.complete(state.ref, { ticket: state.ticket }, o.signal), o.signal);\n o.onState?.(null);\n return { path: state.path, blob: c.blob, type: c.type, size: c.size, exists: false, processOnUpload: state.processOnUpload };\n }\n\n private async retry<T>(fn: () => Promise<T>, signal?: AbortSignal, before?: (err: ContentKitError) => Promise<void>): Promise<T> {\n for (let attempt = 0; ; attempt++) {\n try {\n return await fn();\n } catch (e) {\n const err = toContentKitError(e);\n if (signal?.aborted) throw aborted(signal);\n if (!err.transient || attempt >= this.retries) throw err;\n await sleep(this.delay(attempt, err.retryAfter), signal);\n await before?.(err);\n }\n }\n }\n}\n\nfunction fingerprint(f: Uploadable): UploadState[\"file\"] {\n return { name: f.name, lastModified: f.lastModified, size: f.size };\n}\n\n/** Exponential backoff from 1 s to 30 s with jitter, or the server's Retry-After. */\nexport function backoff(attempt: number, retryAfter?: number): number {\n if (retryAfter) return retryAfter * 1000;\n const base = Math.min(30_000, 1000 * 2 ** attempt);\n return base * (0.75 + Math.random() * 0.5);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) return reject(aborted(signal));\n const t = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(t);\n reject(aborted(signal));\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../../src/client/media/client.ts"],"sourcesContent":["import { ContentKitError, aborted, failureError, throwIfAborted, toContentKitError } from \"../errors.js\";\nimport type { CommitBody, Edit, FileInfo, InlineImage, Op, PresetRule, PresignReply, ReadResult, RefBody, RequestReply } from \"../generated/wire.js\";\nimport type { Http } from \"../http.js\";\nimport { call } from \"../route.js\";\nimport { MediaApi, type ReadOptions } from \"./api.js\";\nimport { sha256Hex } from \"./hash.js\";\nimport { Pacer } from \"./pacer.js\";\nimport { defaultTransport, type Transport } from \"./transport.js\";\n\n/** Upload tuning for the media module. */\nexport interface MediaOptions {\n /** Sends presigned PUTs. Default XMLHttpRequest (upload progress), else fetch. */\n transport?: Transport;\n /** Parallel parts per multipart upload (the pacer may run fewer). Default 4. */\n concurrency?: number;\n /** Retries per part and per idempotent API call. Default 5. */\n retries?: number;\n /** Delay before retry attempt (0-based); retryAfter is the server's hint in seconds. */\n retryDelay?: (attempt: number, retryAfter?: number) => number;\n /** Seconds one part should take. Default 30. */\n targetPartSeconds?: number;\n}\n\nexport interface Progress {\n /** processing: committed, waiting for the worker (put). */\n phase: \"hashing\" | \"uploading\" | \"completing\" | \"processing\";\n loaded: number;\n total: number;\n}\n\nexport interface UploadOptions {\n ref: RefBody;\n /** The upload path: a literal (\"cover\") or under a pattern (\"originals/001.png\"); the server cleans it and adds the extension. */\n path: string;\n /** Content type; default the file's. */\n type?: string;\n /** Aborting pauses: a multipart upload stays resumable from the last state. */\n signal?: AbortSignal;\n onProgress?: (p: Progress) => void;\n /** State saved from onState, to resume a multipart upload of the same file. */\n resume?: UploadState;\n /** Resumable state after every change; null once the upload completed. Persist it to survive a reload. */\n onState?: (s: UploadState | null) => void;\n}\n\n/** An upload, ready for a put op. */\nexport interface UploadedFile {\n /** The path to commit: cleaned, with an extension, named by the server for a Named upload. */\n path: string;\n /**\n * The name to commit: the folder's identical blob (\"sha256-{hex}-{uuid}\") when\n * exists, else the staged upload (\"u-{uuid}\") the worker hashes and places\n * after the commit.\n */\n blob: string;\n type: string;\n size: number;\n /** The identical file was already in the item's folder; nothing was sent. */\n exists: boolean;\n /** The host processes files on upload: commit it unattached now (the queue does). */\n processOnUpload?: boolean;\n}\n\n/** The put op's fields besides path and blob. */\nexport interface PutOptions {\n /** Refuse to replace an existing upload. Default false. */\n createOnly?: boolean;\n edit?: Edit | null;\n meta?: Record<string, unknown>;\n index?: number;\n /** Wait until the worker processed the upload (waitFor). Default true. */\n wait?: boolean;\n /** How long to wait, ms. */\n timeout?: number;\n /** The upload, before the commit. */\n onUploaded?: (up: UploadedFile) => void;\n}\n\nexport interface WaitOptions {\n signal?: AbortSignal;\n /** Poll interval, ms. Default 1000. */\n interval?: number;\n /** ms; then render_timeout. Default 120000. */\n timeout?: number;\n}\n\nexport interface PlannedPart {\n number: number;\n offset: number;\n size: number;\n sha256?: string;\n}\n\n/** A multipart upload in flight; JSON-serializable. */\nexport interface UploadState {\n ticket: string;\n path: string;\n /** The staged upload (\"u-{uuid}\") the parts write. */\n blob: string;\n type: string;\n size: number;\n ref: RefBody;\n file: { name?: string; lastModified?: number; size: number };\n limits: { minPartSize: number; maxPartSize: number; maxParts: number };\n parts: PlannedPart[];\n processOnUpload?: boolean;\n}\n\n/** An upload to a server-named (Named) path: a post's or poll's image. */\nexport interface NamedOptions {\n /** The requested path; the server names the file. Default the file's name. */\n path?: string;\n signal?: AbortSignal;\n onProgress?: (p: Progress) => void;\n /** Wait until the worker processed it. Default true. */\n wait?: boolean;\n /** How long to wait, ms. */\n timeout?: number;\n}\n\n/** An upload to a Named path, committed. */\nexport interface NamedUpload {\n /** The server's name for it (\"i-{uuid}\"), which content routes take. */\n name: string;\n path: string;\n file: FileInfo;\n}\n\n/** The media kinds of post and poll folders (content.Media.PostKind and PollKind). */\nexport interface ContentFolders {\n /** Default \"post\". */\n post?: string;\n /** Default \"poll\". */\n poll?: string;\n}\n\nconst named = /^i-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\n/** A file to upload again when its blob is gone at commit; type defaults to the file's. */\nexport type CommitSource = Blob | { file: Blob; type?: string };\n\nexport interface CommitOptions {\n signal?: AbortSignal;\n /** UUID for this complete batch. Keep it with unchanged ops for a later retry; default a new UUID. */\n operationID?: string;\n /** Persist the active batch before sending it, including any replacement after re-upload. Resume with its ops and operation_id. */\n onState?: (batch: CommitBody) => void;\n /** Blob (\"sha256-…\") → the file it was uploaded from. */\n sources?: Record<string, CommitSource>;\n}\n\ntype Uploadable = Blob & { name?: string; lastModified?: number };\n\n/** A path names a file by its full path or, for an upload, its stem (\"cover\" for \"cover.png\"). */\nexport const samePath = (file: string, path: string) => file === path || stem(file) === path;\n\n/** The path without its extension. */\nexport const stem = (path: string) => {\n const i = path.lastIndexOf(\".\");\n return i > path.lastIndexOf(\"/\") + 1 ? path.slice(0, i) : path;\n};\n\n/** The media module: uploads, commits, reads, frames and HLS. */\nexport class MediaClient {\n readonly api: MediaApi;\n private readonly transport: Transport;\n private readonly concurrency: number;\n private readonly retries: number;\n private readonly delay: (attempt: number, retryAfter?: number) => number;\n private readonly targetSeconds: number;\n private presetList?: Promise<PresetRule[]>;\n\n private readonly folders: Required<ContentFolders>;\n\n constructor(\n private readonly http: Http,\n o: MediaOptions = {},\n folders: ContentFolders = {},\n ) {\n this.folders = { post: folders.post ?? \"post\", poll: folders.poll ?? \"poll\" };\n this.api = new MediaApi(http);\n this.transport = o.transport ?? defaultTransport;\n this.concurrency = o.concurrency ?? 4;\n this.retries = o.retries ?? 5;\n this.delay = o.retryDelay ?? backoff;\n this.targetSeconds = o.targetPartSeconds ?? 30;\n }\n\n /**\n * Uploads file to the item's folder: it hashes the whole file (off the\n * main thread) and presigns {path, type, size, sha256}, then sends nothing\n * (the folder holds the identical blob), or stages it: one checksum-bound\n * PUT up to 64 MiB, resumable multipart above. A refused presign throws\n * before any bytes move. Commit the result with a put op (or use put());\n * the worker then places a staged upload at the hash of its bytes.\n */\n async upload(file: Uploadable, o: UploadOptions): Promise<UploadedFile> {\n const type = o.type || file.type;\n if (!type) throw new ContentKitError(\"invalid_request\", \"the file has no content type\");\n throwIfAborted(o.signal);\n if (o.resume) return this.resumeMultipart(file, o.resume, o);\n const total = file.size;\n const sha256 = await sha256Hex(file, { signal: o.signal, onProgress: (loaded) => o.onProgress?.({ phase: \"hashing\", loaded, total }) });\n const presign = () => this.api.presign({ ref: o.ref, path: o.path, type, size: total, sha256 }, o.signal);\n let p = await presign();\n const out = (p: PresignReply): UploadedFile => ({ path: p.path, blob: p.blob, type, size: total, exists: !!p.exists, processOnUpload: !!p.process_on_upload });\n if (p.exists) return out(p);\n if (p.multipart) {\n const m = p.multipart;\n const state: UploadState = {\n ticket: m.ticket,\n path: p.path,\n blob: p.blob,\n type,\n size: total,\n ref: o.ref,\n file: fingerprint(file),\n limits: { minPartSize: m.min_part_size, maxPartSize: m.max_part_size, maxParts: m.max_parts },\n parts: [],\n processOnUpload: !!p.process_on_upload,\n };\n return this.multipart(file, state, new Set(), o);\n }\n await this.retry(\n async () => {\n if (p.exists) return;\n await this.transport(p.put!, file, { signal: o.signal, onProgress: (loaded) => o.onProgress?.({ phase: \"uploading\", loaded, total }) });\n },\n o.signal,\n async (err) => {\n // An expired URL: presign again (a Named upload gets a new name).\n if (err.code === \"storage\" && err.status === 403) p = await presign();\n },\n );\n return out(p);\n }\n\n /**\n * Uploads file and commits it as a put to its path (with edit, meta and\n * index), then by default waits until the worker processed it; resolves\n * with the upload as an editor reads it.\n */\n async put(file: Uploadable, o: UploadOptions & PutOptions): Promise<FileInfo> {\n const up = await this.upload(file, o);\n o.onUploaded?.(up);\n const op: Op = { op: \"put\", path: up.path, blob: up.blob };\n if (o.createOnly) op.create_id = crypto.randomUUID();\n if (o.edit) op.edit = o.edit;\n if (o.meta) op.meta = o.meta;\n if (o.index !== undefined) op.index = o.index;\n const files = await this.commit(o.ref, [op], { signal: o.signal, sources: { [up.blob]: { file, type: up.type } } });\n if (o.wait === false) return files.find((f) => f.path === up.path) ?? { path: up.path, type: up.type };\n o.onProgress?.({ phase: \"processing\", loaded: up.size, total: up.size });\n return this.waitFor(o.ref, up.path, { signal: o.signal, timeout: o.timeout });\n }\n\n /**\n * Applies ops to the item in one conditional write; returns its uploads as\n * an editor reads them. With sources (blob → the file uploaded as it), a\n * not_uploaded refusal (the blob expired or is due for cleanup) uploads\n * the affected files again and retries the commit once.\n */\n async commit(ref: RefBody, ops: Op[], o: CommitOptions = {}): Promise<FileInfo[]> {\n const emit = this.http.captureChanges();\n // Snapshot the batch once: retries must not pick up edits to the caller's\n // ops while the first request or its response is still in flight.\n const body: CommitBody = structuredClone({ ref, ops, operation_id: o.operationID ?? crypto.randomUUID() });\n o.onState?.(structuredClone(body));\n let files: FileInfo[];\n try {\n files = (await this.retry(() => this.api.commit(body, o.signal), o.signal)).files;\n } catch (e) {\n if (!(e instanceof ContentKitError) || e.code !== \"not_uploaded\") throw e;\n const sources = o.sources ?? {};\n const puts = body.ops.filter((op) => op.op === \"put\" && op.blob && op.blob in sources);\n const stale = [...new Set(puts.map((op) => op.blob!))].filter((b) => !e.blobs || e.blobs.includes(b));\n if (stale.length === 0) throw e;\n const renamed = new Map<string, string>();\n for (const blob of stale) {\n const src = sources[blob]!;\n const file = src instanceof Blob ? src : src.file;\n const type = src instanceof Blob ? undefined : src.type;\n const op = puts.find((p) => p.blob === blob)!;\n renamed.set(blob, (await this.upload(file, { ref: body.ref, path: op.path!, type, signal: o.signal })).blob);\n }\n const retried = body.ops.map((op) => (op.blob && renamed.has(op.blob) ? { ...op, blob: renamed.get(op.blob) } : op));\n // Re-uploading changes the inputs. This is a new batch, not a retry\n // under the old ID; transport retries retain its new identity.\n const replacement = { ref: body.ref, ops: retried, operation_id: crypto.randomUUID() };\n o.onState?.(structuredClone(replacement));\n files = (await this.retry(() => this.api.commit(replacement, o.signal), o.signal)).files;\n }\n emit({ type: \"media.committed\", ref: { kind: ref.kind, id: ref.id }, files });\n return files;\n }\n\n /** The read API: the item's files under prefix in manifest order, with signed URLs for what this viewer may have. */\n async read(ref: RefBody, o: ReadOptions & { signal?: AbortSignal } = {}): Promise<ReadResult> {\n this.api.itemURL(ref); // a bad ref fails at once\n return this.retry(() => this.api.read(ref, o, o.signal), o.signal);\n }\n\n /**\n * Polls an editor read until the upload at path (or with that stem) is\n * processed: placed (not staged), nothing pending and its blob present (a\n * frame grabbed).\n * Rejects with its failure, not_found once it is gone, too_large while the\n * item is full (nothing is processed until uploads are removed),\n * render_timeout after the timeout.\n */\n async waitFor(ref: RefBody, path: string, o: WaitOptions = {}): Promise<FileInfo> {\n const emit = this.http.captureChanges();\n const until = Date.now() + (o.timeout ?? 120_000);\n for (;;) {\n const r = await this.read(ref, { editor: true, prefix: stem(path), signal: o.signal });\n const f = r.files.find((x) => x.upload && samePath(x.path, path));\n if (!f) throw new ContentKitError(\"not_found\", `no upload ${path}`, { status: 404 });\n if (f.failed) throw failureError(f.failed);\n if (!f.pending?.length && !f.staged && (f.size ?? 0) > 0) {\n emit({ type: \"media.processed\", ref: { kind: ref.kind, id: ref.id }, file: f });\n return f;\n }\n if (r.full) throw new ContentKitError(\"too_large\", `${path} waits: the item is full; remove uploads to process more`, { status: 413 });\n if (Date.now() >= until) throw new ContentKitError(\"render_timeout\", `${path} is still processing`);\n await sleep(o.interval ?? 1000, o.signal);\n }\n }\n\n /**\n * The upload's editor view, for re-cropping: its URL and the upload's\n * oriented size. Waits while it renders (an editor read asks for it);\n * not_found when the path has no upload, render_timeout after the timeout\n * (default 30 s).\n */\n async editorView(ref: RefBody, path: string, o: WaitOptions = {}): Promise<{ url: string; width: number; height: number; edit?: Edit }> {\n const until = Date.now() + (o.timeout ?? 30_000);\n for (;;) {\n const r = await this.read(ref, { editor: true, prefix: stem(path), signal: o.signal });\n const f = r.files.find((x) => x.upload && samePath(x.path, path));\n if (!f) throw new ContentKitError(\"not_found\", `no upload ${path}`, { status: 404 });\n if (f.editor_url && f.w && f.h) return { url: f.editor_url, width: f.w, height: f.h, ...(f.edit ? { edit: f.edit } : {}) };\n if (f.failed) throw failureError(f.failed);\n if (Date.now() >= until) throw new ContentKitError(\"render_timeout\", `the editor view of ${path} is still rendering`);\n await sleep(o.interval ?? 1000, o.signal);\n }\n }\n\n /**\n * Every kind's public presets, fetched once per client: crop bounds,\n * widths and default images. An item's current image is in its read's\n * `public`, never built from a preset. Shared by every caller, so it takes\n * no signal; a failed fetch is retried on the next call.\n */\n presets(): Promise<PresetRule[]> {\n this.presetList ??= this.retry(() => this.api.presets()).catch((e: unknown) => {\n this.presetList = undefined;\n throw e;\n });\n return this.presetList;\n }\n\n /** A JPEG still of the video upload at path, t seconds in, w pixels wide (default the frame's). */\n getFrame(ref: RefBody, path: string, t: number, w?: number, signal?: AbortSignal): Promise<Blob> {\n return this.retry(() => this.api.frame(ref, path, t, w, signal), signal);\n }\n\n /** The folder of an HLS ladder from a read's `hls`: `{media}/{kind}/{id}/hls/{dir}`; master.m3u8 and sprite.vtt resolve under it. */\n hlsBase(ref: RefBody, dir: string): string {\n return `${this.api.itemURL(ref)}/hls/${dir}`;\n }\n\n /** hls.js `xhrSetup` for playlists on the API (the client's token and credentials); pass it to players as is. */\n get xhrSetup(): (xhr: XMLHttpRequest, url: string) => Promise<void> {\n return this.http.xhrSetup;\n }\n\n /**\n * Uploads an image to the item's server-named upload path and commits it\n * (by default waiting until it is processed); resolves with the name content\n * routes take: a poll or option image, a post cover.\n */\n async uploadNamed(ref: RefBody, file: Uploadable, o: NamedOptions = {}): Promise<NamedUpload> {\n const f = await this.put(file, { ref, path: o.path ?? (file.name || \"image\"), signal: o.signal, onProgress: o.onProgress, wait: o.wait, timeout: o.timeout });\n const name = stem(f.path.slice(f.path.lastIndexOf(\"/\") + 1));\n if (!named.test(name)) throw new ContentKitError(\"invalid_request\", `${f.path} is not a server-named upload path of ${ref.kind}`);\n return { name, path: f.path, file: f };\n }\n\n /** An image for a post's rich-text body: uploaded to the post's folder; resolves with the URL to place in the body. */\n async uploadInline(post: string, file: Uploadable, o: NamedOptions = {}): Promise<NamedUpload & { url: string }> {\n const up = await this.uploadNamed(this.postRef(post), file, o);\n const r: InlineImage = await call(this.http, \"POST\", \"/posts/{id}/images\", { params: { id: post }, body: { image: up.name }, signal: o.signal });\n return { ...up, url: r.url };\n }\n\n /** The media folder of a post. */\n postRef(id: string): RefBody {\n return { kind: this.folders.post, id };\n }\n\n /** The media folder of a poll. */\n pollRef(id: string): RefBody {\n return { kind: this.folders.poll, id };\n }\n\n /** Discards a paused multipart upload. */\n async discard(state: UploadState): Promise<void> {\n await this.api.abort(state.ref, { ticket: state.ticket });\n }\n\n private async resumeMultipart(file: Uploadable, state: UploadState, o: UploadOptions): Promise<UploadedFile> {\n const fp = fingerprint(file);\n if (fp.size !== state.size || fp.name !== state.file.name || fp.lastModified !== state.file.lastModified) {\n throw new ContentKitError(\"resume_mismatch\", \"the saved upload is for a different file\");\n }\n let listed;\n try {\n listed = await this.retry(() => this.api.listParts(state.ref, { ticket: state.ticket }, o.signal), o.signal);\n } catch (err) {\n // Gone: already completed (Complete is idempotent) or expired.\n if (err instanceof ContentKitError && err.code === \"not_found\") return this.complete(state, o);\n throw err;\n }\n const landed = new Set<number>();\n for (const l of listed.parts) {\n const plan = state.parts.find((p) => p.number === l.number);\n if (!plan || plan.size !== l.size || (l.sha256 && plan.sha256 !== l.sha256)) {\n // Parts this state does not describe: the upload cannot be finished consistently.\n await this.discard(state).catch(() => {});\n o.onState?.(null);\n return this.upload(file, { ...o, resume: undefined });\n }\n landed.add(l.number);\n }\n return this.multipart(file, structuredClone(state), landed, o);\n }\n\n private async multipart(file: Uploadable, state: UploadState, landed: Set<number>, o: UploadOptions): Promise<UploadedFile> {\n const pacer = new Pacer({ ...state.limits, maxConcurrency: this.concurrency, targetSeconds: this.targetSeconds });\n const emitState = () => o.onState?.(structuredClone(state));\n const sending = new Map<number, number>();\n let sent = state.parts.filter((p) => landed.has(p.number)).reduce((n, p) => n + p.size, 0);\n const emitProgress = () => {\n let loaded = sent;\n for (const n of sending.values()) loaded += n;\n o.onProgress?.({ phase: \"uploading\", loaded, total: state.size });\n };\n const pending = state.parts.filter((p) => !landed.has(p.number));\n let offset = state.parts.reduce((end, p) => Math.max(end, p.offset + p.size), 0);\n const next = (): PlannedPart | undefined => {\n const p = pending.shift();\n if (p || offset >= state.size) return p;\n const part: PlannedPart = {\n number: state.parts.length + 1,\n offset,\n size: Math.min(pacer.partSize(), state.size - offset),\n };\n offset += part.size;\n state.parts.push(part);\n return part;\n };\n\n const stop = new AbortController();\n const signal = o.signal ? AbortSignal.any([o.signal, stop.signal]) : stop.signal;\n // Parts hash and presign ahead of the PUT slots, so a freed slot starts\n // its next PUT at once; pacer.concurrency bounds only the PUTs.\n let putting = 0;\n const waiting: (() => void)[] = [];\n const acquire = () =>\n new Promise<void>((resolve, reject) => {\n const take = () => {\n signal.removeEventListener(\"abort\", drop);\n putting++;\n resolve();\n };\n const drop = () => {\n waiting.splice(waiting.indexOf(take) >>> 0, 1);\n reject(aborted(signal));\n };\n if (putting < pacer.concurrency) return take();\n if (signal.aborted) return reject(aborted(signal));\n signal.addEventListener(\"abort\", drop, { once: true });\n waiting.push(take);\n });\n const release = () => {\n putting--;\n while (putting < pacer.concurrency && waiting.length) waiting.shift()!();\n };\n const run = async (part: PlannedPart) => {\n const body = file.slice(part.offset, part.offset + part.size);\n await this.retry(async () => {\n if (!part.sha256) {\n part.sha256 = await sha256Hex(body, { signal });\n emitState();\n }\n const { parts } = await this.api.parts(\n state.ref,\n { ticket: state.ticket, parts: [{ number: part.number, size: part.size, sha256: part.sha256 }] },\n signal,\n );\n const req = parts[0]?.request as RequestReply;\n await acquire();\n const began = performance.now();\n const alongside = putting;\n try {\n await this.transport(req, body, {\n signal,\n onProgress: (n) => {\n sending.set(part.number, n);\n emitProgress();\n },\n });\n } finally {\n sending.delete(part.number);\n release();\n }\n pacer.record(part.size, performance.now() - began, alongside);\n sent += part.size;\n emitProgress();\n }, signal);\n };\n\n emitState();\n emitProgress();\n const inFlight = new Set<Promise<void>>();\n let failure: unknown;\n for (;;) {\n let part: PlannedPart | undefined;\n while (failure === undefined && inFlight.size < pacer.concurrency + 2 && (part = next())) {\n emitState();\n const job: Promise<void> = run(part).then(\n () => void inFlight.delete(job),\n (err) => {\n inFlight.delete(job);\n if (failure === undefined) {\n failure = err;\n stop.abort();\n }\n },\n );\n inFlight.add(job);\n }\n if (inFlight.size === 0) break;\n await Promise.race(inFlight);\n }\n if (failure !== undefined) {\n throw o.signal?.aborted ? aborted(o.signal) : failure;\n }\n return this.complete(state, o);\n }\n\n private async complete(state: UploadState, o: UploadOptions): Promise<UploadedFile> {\n o.onProgress?.({ phase: \"completing\", loaded: state.size, total: state.size });\n const c = await this.retry(() => this.api.complete(state.ref, { ticket: state.ticket }, o.signal), o.signal);\n o.onState?.(null);\n return { path: state.path, blob: c.blob, type: c.type, size: c.size, exists: false, processOnUpload: state.processOnUpload };\n }\n\n private async retry<T>(fn: () => Promise<T>, signal?: AbortSignal, before?: (err: ContentKitError) => Promise<void>): Promise<T> {\n for (let attempt = 0; ; attempt++) {\n try {\n return await fn();\n } catch (e) {\n const err = toContentKitError(e);\n if (signal?.aborted) throw aborted(signal);\n if (!err.transient || attempt >= this.retries) throw err;\n await sleep(this.delay(attempt, err.retryAfter), signal);\n await before?.(err);\n }\n }\n }\n}\n\nfunction fingerprint(f: Uploadable): UploadState[\"file\"] {\n return { name: f.name, lastModified: f.lastModified, size: f.size };\n}\n\n/** Exponential backoff from 1 s to 30 s with jitter, or the server's Retry-After. */\nexport function backoff(attempt: number, retryAfter?: number): number {\n if (retryAfter) return retryAfter * 1000;\n const base = Math.min(30_000, 1000 * 2 ** attempt);\n return base * (0.75 + Math.random() * 0.5);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) return reject(aborted(signal));\n const t = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(t);\n reject(aborted(signal));\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n"],"mappings":"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"}
@@ -44,6 +44,8 @@ export declare function screenFiles(files: Iterable<File>, rules: readonly Uploa
44
44
  export declare function uniqueName(name: string, taken: Iterable<string>): string;
45
45
  /** "16:9" style label of a width/height ratio: 2.4 → "2.4:1", 0.4167 → "1:2.4". */
46
46
  export declare function ratioLabel(r: number): string;
47
+ /** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */
48
+ export declare function presetFor(presets: readonly PresetRule[], kind: string, path: string, name?: string): PresetRule | undefined;
47
49
  /**
48
50
  * The kind's default image for a preset, at every width: what the gateway
49
51
  * serves for an item without its own (a URL from the template never names a
@@ -117,7 +117,11 @@ function g(e) {
117
117
  let t = (e) => String(Math.round(e * 100) / 100);
118
118
  return e >= 1 ? `${t(e)}:1` : `1:${t(1 / e)}`;
119
119
  }
120
- function _(e, i) {
120
+ function _(e, t, n, r) {
121
+ let i = e.filter((e) => e.kind === t && l([{ path: e.from }], n));
122
+ return (r ? i.find((e) => e.name === r) : void 0) ?? i.find((e) => e.aspect) ?? i[0];
123
+ }
124
+ function v(e, i) {
121
125
  let a = r(e.aspect);
122
126
  return e.first || e.to.includes(o) ? {
123
127
  preset: e.name,
@@ -134,6 +138,6 @@ function _(e, i) {
134
138
  };
135
139
  }
136
140
  //#endregion
137
- export { p as acceptOf, _ as defaultImage, u as filesFor, s as isPattern, g as ratioLabel, c as ruleDir, l as ruleFor, m as screenFiles, h as uniqueName, a as uploadRules, f as withMediaType };
141
+ export { p as acceptOf, v as defaultImage, u as filesFor, s as isPattern, _ as presetFor, g as ratioLabel, c as ruleDir, l as ruleFor, m as screenFiles, h as uniqueName, a as uploadRules, f as withMediaType };
138
142
 
139
143
  //# sourceMappingURL=rules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","names":[],"sources":["../../../src/client/media/rules.ts"],"sourcesContent":["import { ContentKitError } from \"../errors.js\";\nimport type { ErrorDetails, PresetRule, ReadResult, UploadRule } from \"../generated/wire.js\";\nimport { fill, publicURL, type PublicPreset } from \"../public.js\";\nimport { ratio } from \"../aspect.js\";\nimport { stem } from \"./client.js\";\n\n/** An editor read's upload rules (a viewer read has none). */\nexport const uploadRules = (read: ReadResult | null | undefined): UploadRule[] => read?.uploads ?? [];\n\nconst NAME = \"{name}\";\n\n/** A pattern path (\"originals/{name}\") holds many files; a literal (\"cover\") one. */\nexport const isPattern = (path: string) => path.endsWith(NAME);\n\n/** The directory of a pattern (\"originals/\"), or the literal itself. */\nexport const ruleDir = (path: string) => (isPattern(path) ? path.slice(0, -NAME.length) : path);\n\n/** The rule an upload path (with or without its extension) falls under: a literal wins over a pattern. */\nexport function ruleFor<R extends { path: string }>(rules: readonly R[], path: string): R | undefined {\n const s = stem(path);\n return (\n rules.find((r) => !isPattern(r.path) && r.path === s) ??\n rules.find((r) => {\n if (!isPattern(r.path)) return false;\n const dir = ruleDir(r.path);\n return s.startsWith(dir) && s.length > dir.length && !s.slice(dir.length).includes(\"/\");\n })\n );\n}\n\n/** Uploads of the read under rule (attached, in manifest order). */\nexport function filesFor(read: ReadResult | null | undefined, rule: { path: string }, rules: readonly { path: string }[] = [rule]) {\n return (read?.files ?? []).filter((f) => f.upload && !f.unattached && ruleFor(rules, f.path)?.path === rule.path);\n}\n\nconst BY_EXTENSION: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n mkv: \"video/x-matroska\",\n webm: \"video/webm\",\n m4a: \"audio/mp4\",\n mp3: \"audio/mpeg\",\n vtt: \"text/vtt\",\n srt: \"application/x-subrip\",\n ass: \"text/x-ass\",\n ssa: \"text/x-ssa\",\n zip: \"application/zip\",\n};\n\n/** The file with a content type from its extension when the browser left it empty or nonstandard (.mkv, .mov, .srt). */\nexport function withMediaType(file: File, allowed?: readonly string[]): File {\n if (file.type && (!allowed || allowed.includes(file.type))) return file;\n const type = BY_EXTENSION[file.name.split(\".\").pop()?.toLowerCase() ?? \"\"];\n return type && type !== file.type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;\n}\n\n/** `accept` for a file input from the rules' types (plus the extensions browsers leave untyped). */\nexport function acceptOf(rules: readonly UploadRule[]): string | undefined {\n const types = [...new Set(rules.flatMap((r) => r.types))];\n if (!types.length) return undefined;\n const exts = Object.entries(BY_EXTENSION).filter(([, t]) => types.includes(t)).map(([e]) => `.${e}`);\n return [...types, ...exts].join(\",\");\n}\n\nexport interface Screened {\n /** Files to upload, each with its rule (typed by extension when needed). */\n accepted: { file: File; rule: UploadRule }[];\n /** Files the server would refuse, with the refusal it would answer. */\n refused: { file: File; error: ContentKitError }[];\n}\n\n/**\n * Checks files against the rules before any upload: a type some rule takes\n * (the first that does), its size, and the rule's file cap counting\n * have(rule) files already there. A rule without types, size or cap lets\n * that check through; the server decides.\n */\nexport function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have: (rule: UploadRule) => number = () => 0): Screened {\n const out: Screened = { accepted: [], refused: [] };\n const counts = new Map<UploadRule, number>();\n const allowed = [...new Set(rules.flatMap((r) => r.types))];\n for (const picked of files) {\n const file = withMediaType(picked, allowed);\n const rule = rules.find((r) => !r.types.length || r.types.includes(file.type));\n if (!rule) {\n const details: ErrorDetails = { type: file.type || undefined, allowed };\n out.refused.push({ file, error: new ContentKitError(\"type_not_allowed\", `${file.name}: type not allowed`, { status: 415, details }) });\n continue;\n }\n if (rule.max_bytes > 0 && file.size > rule.max_bytes) {\n const details: ErrorDetails = { type: file.type, size: file.size, max_bytes: rule.max_bytes };\n out.refused.push({ file, error: new ContentKitError(\"too_large\", `${file.name}: too large`, { status: 413, details }) });\n continue;\n }\n const n = counts.get(rule) ?? have(rule);\n if (rule.max && n >= rule.max) {\n const details: ErrorDetails = { max: rule.max };\n out.refused.push({ file, error: new ContentKitError(\"too_many_files\", `${file.name}: at most ${rule.max} files`, { status: 409, details }) });\n continue;\n }\n counts.set(rule, n + 1);\n out.accepted.push({ file, rule });\n }\n return out;\n}\n\n/** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */\nexport function uniqueName(name: string, taken: Iterable<string>): string {\n const stems = new Set([...taken].map(stem));\n if (!stems.has(stem(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}${ext}`;\n if (!stems.has(stem(candidate))) return candidate;\n }\n}\n\n/** \"16:9\" style label of a width/height ratio: 2.4 → \"2.4:1\", 0.4167 → \"1:2.4\". */\nexport function ratioLabel(r: number): string {\n const round = (v: number) => String(Math.round(v * 100) / 100);\n return r >= 1 ? `${round(r)}:1` : `1:${round(1 / r)}`;\n}\n\n/**\n * The kind's default image for a preset, at every width: what the gateway\n * serves for an item without its own (a URL from the template never names a\n * current file). Empty for a preview preset.\n */\nexport function defaultImage(rule: PresetRule, id: string): PublicPreset {\n const r = ratio(rule.aspect);\n if (rule.first || rule.to.includes(NAME)) return { preset: rule.name, aspect: rule.aspect, renditions: [] };\n return {\n preset: rule.name,\n aspect: rule.aspect,\n renditions: rule.widths.map((w) => ({ url: publicURL(rule.base, rule.namespace, rule.kind, id, fill(rule.to, { w })), w, ...(r ? { h: Math.round(w / r) } : {}) })),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rules.js","names":[],"sources":["../../../src/client/media/rules.ts"],"sourcesContent":["import { ContentKitError } from \"../errors.js\";\nimport type { ErrorDetails, PresetRule, ReadResult, UploadRule } from \"../generated/wire.js\";\nimport { fill, publicURL, type PublicPreset } from \"../public.js\";\nimport { ratio } from \"../aspect.js\";\nimport { stem } from \"./client.js\";\n\n/** An editor read's upload rules (a viewer read has none). */\nexport const uploadRules = (read: ReadResult | null | undefined): UploadRule[] => read?.uploads ?? [];\n\nconst NAME = \"{name}\";\n\n/** A pattern path (\"originals/{name}\") holds many files; a literal (\"cover\") one. */\nexport const isPattern = (path: string) => path.endsWith(NAME);\n\n/** The directory of a pattern (\"originals/\"), or the literal itself. */\nexport const ruleDir = (path: string) => (isPattern(path) ? path.slice(0, -NAME.length) : path);\n\n/** The rule an upload path (with or without its extension) falls under: a literal wins over a pattern. */\nexport function ruleFor<R extends { path: string }>(rules: readonly R[], path: string): R | undefined {\n const s = stem(path);\n return (\n rules.find((r) => !isPattern(r.path) && r.path === s) ??\n rules.find((r) => {\n if (!isPattern(r.path)) return false;\n const dir = ruleDir(r.path);\n return s.startsWith(dir) && s.length > dir.length && !s.slice(dir.length).includes(\"/\");\n })\n );\n}\n\n/** Uploads of the read under rule (attached, in manifest order). */\nexport function filesFor(read: ReadResult | null | undefined, rule: { path: string }, rules: readonly { path: string }[] = [rule]) {\n return (read?.files ?? []).filter((f) => f.upload && !f.unattached && ruleFor(rules, f.path)?.path === rule.path);\n}\n\nconst BY_EXTENSION: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n mkv: \"video/x-matroska\",\n webm: \"video/webm\",\n m4a: \"audio/mp4\",\n mp3: \"audio/mpeg\",\n vtt: \"text/vtt\",\n srt: \"application/x-subrip\",\n ass: \"text/x-ass\",\n ssa: \"text/x-ssa\",\n zip: \"application/zip\",\n};\n\n/** The file with a content type from its extension when the browser left it empty or nonstandard (.mkv, .mov, .srt). */\nexport function withMediaType(file: File, allowed?: readonly string[]): File {\n if (file.type && (!allowed || allowed.includes(file.type))) return file;\n const type = BY_EXTENSION[file.name.split(\".\").pop()?.toLowerCase() ?? \"\"];\n return type && type !== file.type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;\n}\n\n/** `accept` for a file input from the rules' types (plus the extensions browsers leave untyped). */\nexport function acceptOf(rules: readonly UploadRule[]): string | undefined {\n const types = [...new Set(rules.flatMap((r) => r.types))];\n if (!types.length) return undefined;\n const exts = Object.entries(BY_EXTENSION).filter(([, t]) => types.includes(t)).map(([e]) => `.${e}`);\n return [...types, ...exts].join(\",\");\n}\n\nexport interface Screened {\n /** Files to upload, each with its rule (typed by extension when needed). */\n accepted: { file: File; rule: UploadRule }[];\n /** Files the server would refuse, with the refusal it would answer. */\n refused: { file: File; error: ContentKitError }[];\n}\n\n/**\n * Checks files against the rules before any upload: a type some rule takes\n * (the first that does), its size, and the rule's file cap counting\n * have(rule) files already there. A rule without types, size or cap lets\n * that check through; the server decides.\n */\nexport function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have: (rule: UploadRule) => number = () => 0): Screened {\n const out: Screened = { accepted: [], refused: [] };\n const counts = new Map<UploadRule, number>();\n const allowed = [...new Set(rules.flatMap((r) => r.types))];\n for (const picked of files) {\n const file = withMediaType(picked, allowed);\n const rule = rules.find((r) => !r.types.length || r.types.includes(file.type));\n if (!rule) {\n const details: ErrorDetails = { type: file.type || undefined, allowed };\n out.refused.push({ file, error: new ContentKitError(\"type_not_allowed\", `${file.name}: type not allowed`, { status: 415, details }) });\n continue;\n }\n if (rule.max_bytes > 0 && file.size > rule.max_bytes) {\n const details: ErrorDetails = { type: file.type, size: file.size, max_bytes: rule.max_bytes };\n out.refused.push({ file, error: new ContentKitError(\"too_large\", `${file.name}: too large`, { status: 413, details }) });\n continue;\n }\n const n = counts.get(rule) ?? have(rule);\n if (rule.max && n >= rule.max) {\n const details: ErrorDetails = { max: rule.max };\n out.refused.push({ file, error: new ContentKitError(\"too_many_files\", `${file.name}: at most ${rule.max} files`, { status: 409, details }) });\n continue;\n }\n counts.set(rule, n + 1);\n out.accepted.push({ file, rule });\n }\n return out;\n}\n\n/** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */\nexport function uniqueName(name: string, taken: Iterable<string>): string {\n const stems = new Set([...taken].map(stem));\n if (!stems.has(stem(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}${ext}`;\n if (!stems.has(stem(candidate))) return candidate;\n }\n}\n\n/** \"16:9\" style label of a width/height ratio: 2.4 → \"2.4:1\", 0.4167 → \"1:2.4\". */\nexport function ratioLabel(r: number): string {\n const round = (v: number) => String(Math.round(v * 100) / 100);\n return r >= 1 ? `${round(r)}:1` : `1:${round(1 / r)}`;\n}\n\n/** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */\nexport function presetFor(presets: readonly PresetRule[], kind: string, path: string, name?: string): PresetRule | undefined {\n const at = presets.filter((p) => p.kind === kind && ruleFor([{ path: p.from }], path));\n return (name ? at.find((p) => p.name === name) : undefined) ?? at.find((p) => p.aspect) ?? at[0];\n}\n\n/**\n * The kind's default image for a preset, at every width: what the gateway\n * serves for an item without its own (a URL from the template never names a\n * current file). Empty for a preview preset.\n */\nexport function defaultImage(rule: PresetRule, id: string): PublicPreset {\n const r = ratio(rule.aspect);\n if (rule.first || rule.to.includes(NAME)) return { preset: rule.name, aspect: rule.aspect, renditions: [] };\n return {\n preset: rule.name,\n aspect: rule.aspect,\n renditions: rule.widths.map((w) => ({ url: publicURL(rule.base, rule.namespace, rule.kind, id, fill(rule.to, { w })), w, ...(r ? { h: Math.round(w / r) } : {}) })),\n };\n}\n"],"mappings":"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"}