@sentroy-co/client-sdk 2.4.5 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/http.d.ts +13 -0
  2. package/dist/http.d.ts.map +1 -1
  3. package/dist/http.js +62 -0
  4. package/dist/http.js.map +1 -1
  5. package/dist/react/MediaManager.d.ts +14 -0
  6. package/dist/react/MediaManager.d.ts.map +1 -1
  7. package/dist/react/MediaManager.js +21 -14
  8. package/dist/react/MediaManager.js.map +1 -1
  9. package/dist/react/crop/CropDialog.d.ts +15 -0
  10. package/dist/react/crop/CropDialog.d.ts.map +1 -0
  11. package/dist/react/crop/CropDialog.js +126 -0
  12. package/dist/react/crop/CropDialog.js.map +1 -0
  13. package/dist/react/crop/index.d.ts +7 -0
  14. package/dist/react/crop/index.d.ts.map +1 -0
  15. package/dist/react/crop/index.js +11 -0
  16. package/dist/react/crop/index.js.map +1 -0
  17. package/dist/react/index.d.ts +2 -0
  18. package/dist/react/index.d.ts.map +1 -1
  19. package/dist/react/index.js +5 -1
  20. package/dist/react/index.js.map +1 -1
  21. package/dist/react/lib/UploadQueuePanel.d.ts +20 -0
  22. package/dist/react/lib/UploadQueuePanel.d.ts.map +1 -0
  23. package/dist/react/lib/UploadQueuePanel.js +39 -0
  24. package/dist/react/lib/UploadQueuePanel.js.map +1 -0
  25. package/dist/react/lib/use-upload-queue.d.ts +51 -0
  26. package/dist/react/lib/use-upload-queue.d.ts.map +1 -0
  27. package/dist/react/lib/use-upload-queue.js +167 -0
  28. package/dist/react/lib/use-upload-queue.js.map +1 -0
  29. package/dist/resources/media.d.ts +8 -1
  30. package/dist/resources/media.d.ts.map +1 -1
  31. package/dist/resources/media.js +6 -2
  32. package/dist/resources/media.js.map +1 -1
  33. package/package.json +10 -1
  34. package/src/http.ts +85 -0
  35. package/src/react/MediaManager.tsx +41 -11
  36. package/src/react/crop/CropDialog.tsx +344 -0
  37. package/src/react/crop/index.ts +6 -0
  38. package/src/react/index.ts +10 -0
  39. package/src/react/lib/UploadQueuePanel.tsx +273 -0
  40. package/src/react/lib/use-upload-queue.ts +250 -0
  41. package/src/resources/media.ts +13 -4
@@ -0,0 +1,250 @@
1
+ import { useCallback, useRef, useState } from "react"
2
+ import type { Sentroy } from "../.."
3
+ import type { Media } from "../../types"
4
+
5
+ /**
6
+ * Tek dosyalık upload entry — UI tarafında queue listesi olarak render edilir.
7
+ *
8
+ * State machine:
9
+ * queued → uploading → done | error | canceled
10
+ * `error` ve `canceled` terminal; `done` terminal + `media` set olur.
11
+ */
12
+ export interface UploadEntry {
13
+ id: string
14
+ file: File
15
+ status: "queued" | "uploading" | "done" | "error" | "canceled"
16
+ loaded: number
17
+ total: number
18
+ /** Server response — done iken non-null. */
19
+ media?: Media
20
+ error?: string
21
+ /** Per-entry cancel handle (XHR abort'u tetikler). */
22
+ cancel: () => void
23
+ }
24
+
25
+ export interface UseUploadQueueOptions {
26
+ /** Aynı anda kaç dosya yüklensin. Default 3. */
27
+ concurrency?: number
28
+ /** Dosya başarıyla yüklendiğinde tetiklenir — caller refresh edebilir. */
29
+ onUploaded?: (media: Media) => void
30
+ }
31
+
32
+ export interface UseUploadQueueResult {
33
+ entries: UploadEntry[]
34
+ /** Yeni dosyaları queue'ya ekler ve worker'ı tetikler. */
35
+ enqueue: (bucketSlug: string, files: File[]) => void
36
+ /** Bir entry'i iptal eder (uploading ise XHR abort, queued ise drop). */
37
+ cancel: (id: string) => void
38
+ /** Tek bir entry'i listeden temizler (terminal state'tekiler için). */
39
+ remove: (id: string) => void
40
+ /** Done/error/canceled olanları listeden temizler. */
41
+ clearDone: () => void
42
+ /** Aktif (queued + uploading) entry sayısı. */
43
+ activeCount: number
44
+ }
45
+
46
+ /**
47
+ * Upload queue hook — concurrency-pooled, per-entry progress + cancel.
48
+ *
49
+ * Storage app'in `apps/storage/lib/upload-client.ts` + `FileUploader` queue
50
+ * davranışını SDK'ya taşır. SDK consumer'ı (MediaManager veya 3rd-party)
51
+ * `entries` array'ini render eder, `enqueue`/`cancel`/`remove` çağırır.
52
+ *
53
+ * onUploaded: caller refresh için kullanır (örn `setRefreshKey + 1`).
54
+ */
55
+ export function useUploadQueue(
56
+ client: Sentroy,
57
+ opts: UseUploadQueueOptions = {},
58
+ ): UseUploadQueueResult {
59
+ const concurrency = opts.concurrency ?? 3
60
+ const [entries, setEntries] = useState<UploadEntry[]>([])
61
+ const entriesRef = useRef<UploadEntry[]>([])
62
+ entriesRef.current = entries
63
+ const onUploadedRef = useRef(opts.onUploaded)
64
+ onUploadedRef.current = opts.onUploaded
65
+
66
+ // Worker pump — queued entry varsa ve aktif < concurrency ise başlat.
67
+ const pumpRef = useRef<() => void>(() => {})
68
+
69
+ /**
70
+ * Aktif (in-flight) upload sayısını synchronous olarak takip eden ref.
71
+ * `entries` state setEntries ile async güncellendiğinden, recursive
72
+ * `pump` çağrılarında `entries.filter(e => e.status === "uploading")`
73
+ * eski listeyi görür → aynı queued entry birden çok kez başlatılır
74
+ * (Chrome side `ERR_INSUFFICIENT_RESOURCES`). Ref ile incre/decre
75
+ * synchronous; concurrency limiti gerçekten devreye girer.
76
+ */
77
+ const inFlightRef = useRef(0)
78
+ /**
79
+ * Henüz başlatılmamış queued entry id'lerinin sıralı listesi. setEntries
80
+ * async olduğu için listeden seçim yapmak yarış koşulu üretir; ref
81
+ * üzerinden FIFO push/shift hem deterministik hem hızlı.
82
+ */
83
+ const queueRef = useRef<string[]>([])
84
+
85
+ const updateEntry = useCallback(
86
+ (id: string, patch: Partial<UploadEntry>) => {
87
+ setEntries((prev) =>
88
+ prev.map((e) => (e.id === id ? { ...e, ...patch } : e)),
89
+ )
90
+ },
91
+ [],
92
+ )
93
+
94
+ pumpRef.current = () => {
95
+ // Slot doluysa veya queue boşsa erken dön.
96
+ if (inFlightRef.current >= concurrency) return
97
+ const nextId = queueRef.current.shift()
98
+ if (!nextId) return
99
+
100
+ // Slot'u synchronously rezerve et — bir sonraki pump çağrısı bu
101
+ // entry'i tekrar shift edemez.
102
+ inFlightRef.current++
103
+
104
+ const entry = entriesRef.current.find((e) => e.id === nextId)
105
+ if (!entry) {
106
+ // Cancel öncesi remove edilmiş; slot'u iade et ve pump'a devam.
107
+ inFlightRef.current--
108
+ pumpRef.current?.()
109
+ return
110
+ }
111
+
112
+ // Mark uploading
113
+ updateEntry(entry.id, { status: "uploading" })
114
+
115
+ const bucketSlug = bucketMapRef.current[entry.id]
116
+ if (!bucketSlug) {
117
+ updateEntry(entry.id, { status: "error", error: "No bucket" })
118
+ inFlightRef.current--
119
+ pumpRef.current?.()
120
+ return
121
+ }
122
+
123
+ const controller = new AbortController()
124
+ cancelMapRef.current[entry.id] = () => controller.abort()
125
+
126
+ client.media
127
+ .upload(
128
+ bucketSlug,
129
+ { body: entry.file, filename: entry.file.name },
130
+ {
131
+ onProgress: (loaded, total) => {
132
+ updateEntry(entry.id, { loaded, total })
133
+ },
134
+ signal: controller.signal,
135
+ },
136
+ )
137
+ .then((media) => {
138
+ updateEntry(entry.id, {
139
+ status: "done",
140
+ media,
141
+ loaded: entry.file.size,
142
+ total: entry.file.size,
143
+ })
144
+ onUploadedRef.current?.(media)
145
+ })
146
+ .catch((err: unknown) => {
147
+ const aborted =
148
+ (err as { message?: string })?.message === "Upload aborted"
149
+ updateEntry(entry.id, {
150
+ status: aborted ? "canceled" : "error",
151
+ error: aborted
152
+ ? undefined
153
+ : ((err as Error)?.message ?? "Upload failed"),
154
+ })
155
+ })
156
+ .finally(() => {
157
+ delete cancelMapRef.current[entry.id]
158
+ inFlightRef.current--
159
+ // Slot açıldı, sıradakini başlat.
160
+ pumpRef.current?.()
161
+ })
162
+
163
+ // Aynı tick'te kalan slot'ları doldur — concurrency artık
164
+ // synchronously inFlightRef ile guard'lı, çift başlatma yok.
165
+ if (inFlightRef.current < concurrency) pumpRef.current?.()
166
+ }
167
+
168
+ const bucketMapRef = useRef<Record<string, string>>({})
169
+ const cancelMapRef = useRef<Record<string, () => void>>({})
170
+
171
+ const enqueue = useCallback(
172
+ (bucketSlug: string, files: File[]) => {
173
+ if (files.length === 0) return
174
+ const newEntries: UploadEntry[] = files.map((file) => {
175
+ const id = `up-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
176
+ bucketMapRef.current[id] = bucketSlug
177
+ // FIFO queue — pump bu sıradan shift eder; setEntries async
178
+ // güncellemesinden bağımsız synchronous source-of-truth.
179
+ queueRef.current.push(id)
180
+ return {
181
+ id,
182
+ file,
183
+ status: "queued",
184
+ loaded: 0,
185
+ total: file.size,
186
+ cancel: () => {
187
+ const c = cancelMapRef.current[id]
188
+ if (c) c()
189
+ else {
190
+ queueRef.current = queueRef.current.filter((qid) => qid !== id)
191
+ setEntries((prev) =>
192
+ prev.map((e) =>
193
+ e.id === id && e.status === "queued"
194
+ ? { ...e, status: "canceled" }
195
+ : e,
196
+ ),
197
+ )
198
+ }
199
+ },
200
+ }
201
+ })
202
+ setEntries((prev) => [...prev, ...newEntries])
203
+ // pump on next tick — state update'ten sonra entriesRef güncel olsun.
204
+ // pumpRef kendi içinde sequential pump zincirini sürdürür (her
205
+ // başarılı slot rezervasyonundan sonra bir dahaki pump'ı çağırır),
206
+ // dolayısıyla burada tek tetikleme yeterli.
207
+ Promise.resolve().then(() => pumpRef.current?.())
208
+ },
209
+ [],
210
+ )
211
+
212
+ const cancel = useCallback((id: string) => {
213
+ const c = cancelMapRef.current[id]
214
+ if (c) c()
215
+ else {
216
+ queueRef.current = queueRef.current.filter((qid) => qid !== id)
217
+ setEntries((prev) =>
218
+ prev.map((e) =>
219
+ e.id === id && e.status === "queued"
220
+ ? { ...e, status: "canceled" }
221
+ : e,
222
+ ),
223
+ )
224
+ }
225
+ }, [])
226
+
227
+ const remove = useCallback((id: string) => {
228
+ setEntries((prev) => prev.filter((e) => e.id !== id))
229
+ queueRef.current = queueRef.current.filter((qid) => qid !== id)
230
+ delete bucketMapRef.current[id]
231
+ delete cancelMapRef.current[id]
232
+ }, [])
233
+
234
+ const clearDone = useCallback(() => {
235
+ setEntries((prev) =>
236
+ prev.filter(
237
+ (e) =>
238
+ e.status !== "done" &&
239
+ e.status !== "error" &&
240
+ e.status !== "canceled",
241
+ ),
242
+ )
243
+ }, [])
244
+
245
+ const activeCount = entries.filter(
246
+ (e) => e.status === "queued" || e.status === "uploading",
247
+ ).length
248
+
249
+ return { entries, enqueue, cancel, remove, clearDone, activeCount }
250
+ }
@@ -52,6 +52,14 @@ export class MediaResource {
52
52
  async upload(
53
53
  bucketSlug: string,
54
54
  params: UploadMediaParams,
55
+ opts?: {
56
+ /** Bytes loaded / total — XHR `upload.onprogress` event'inden gelir.
57
+ * Verilmesse fetch tabanlı path kullanılır (progress yok). */
58
+ onProgress?: (loaded: number, total: number) => void
59
+ /** AbortController.signal — kullanıcı iptal ettiğinde XHR cancel
60
+ * edilir, promise reject. */
61
+ signal?: AbortSignal
62
+ },
55
63
  ): Promise<Media> {
56
64
  const form = new FormData()
57
65
  const filename =
@@ -67,10 +75,11 @@ export class MediaResource {
67
75
  if (params.caption) form.append("caption", params.caption)
68
76
  if (params.tags?.length) form.append("tags", params.tags.join(","))
69
77
 
70
- return this.http.postForm<Media>(
71
- `/buckets/${encodeURIComponent(bucketSlug)}/media`,
72
- form,
73
- )
78
+ const path = `/buckets/${encodeURIComponent(bucketSlug)}/media`
79
+ if (opts?.onProgress || opts?.signal) {
80
+ return this.http.postFormWithProgress<Media>(path, form, opts)
81
+ }
82
+ return this.http.postForm<Media>(path, form)
74
83
  }
75
84
 
76
85
  /**