@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.
- package/dist/http.d.ts +13 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +62 -0
- package/dist/http.js.map +1 -1
- package/dist/react/MediaManager.d.ts +14 -0
- package/dist/react/MediaManager.d.ts.map +1 -1
- package/dist/react/MediaManager.js +21 -14
- package/dist/react/MediaManager.js.map +1 -1
- package/dist/react/crop/CropDialog.d.ts +15 -0
- package/dist/react/crop/CropDialog.d.ts.map +1 -0
- package/dist/react/crop/CropDialog.js +126 -0
- package/dist/react/crop/CropDialog.js.map +1 -0
- package/dist/react/crop/index.d.ts +7 -0
- package/dist/react/crop/index.d.ts.map +1 -0
- package/dist/react/crop/index.js +11 -0
- package/dist/react/crop/index.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/lib/UploadQueuePanel.d.ts +20 -0
- package/dist/react/lib/UploadQueuePanel.d.ts.map +1 -0
- package/dist/react/lib/UploadQueuePanel.js +39 -0
- package/dist/react/lib/UploadQueuePanel.js.map +1 -0
- package/dist/react/lib/use-upload-queue.d.ts +51 -0
- package/dist/react/lib/use-upload-queue.d.ts.map +1 -0
- package/dist/react/lib/use-upload-queue.js +167 -0
- package/dist/react/lib/use-upload-queue.js.map +1 -0
- package/dist/resources/media.d.ts +8 -1
- package/dist/resources/media.d.ts.map +1 -1
- package/dist/resources/media.js +6 -2
- package/dist/resources/media.js.map +1 -1
- package/package.json +10 -1
- package/src/http.ts +85 -0
- package/src/react/MediaManager.tsx +41 -11
- package/src/react/crop/CropDialog.tsx +344 -0
- package/src/react/crop/index.ts +6 -0
- package/src/react/index.ts +10 -0
- package/src/react/lib/UploadQueuePanel.tsx +273 -0
- package/src/react/lib/use-upload-queue.ts +250 -0
- 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
|
+
}
|
package/src/resources/media.ts
CHANGED
|
@@ -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
|
-
|
|
71
|
-
|
|
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
|
/**
|