@jukasdrj/bookstrack-api-client 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +163 -0
- package/README.md +47 -21
- package/STREAMING_GUIDE.md +508 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/schema.d.ts +344 -1033
- package/dist/schema.d.ts.map +1 -1
- package/dist/streaming.d.ts +153 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +279 -0
- package/package.json +7 -5
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
# BooksTrack Streaming Guide: SSE vs WebSocket
|
|
2
|
+
|
|
3
|
+
**Last Updated:** December 3, 2025
|
|
4
|
+
**API Version:** V2 (Stable) + V3 (Current)
|
|
5
|
+
|
|
6
|
+
This guide explains when to use Server-Sent Events (SSE) vs WebSockets for real-time progress tracking in BooksTrack.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## TL;DR - Which Protocol Should I Use?
|
|
11
|
+
|
|
12
|
+
| **Use Case** | **Protocol** | **Endpoint** | **Why?** |
|
|
13
|
+
|--------------|--------------|--------------|----------|
|
|
14
|
+
| **CSV Import** | SSE | `GET /api/v2/imports/:id/stream` | One-way progress, browser-native reconnect |
|
|
15
|
+
| **Batch Enrichment** | WebSocket | `GET /ws/progress?jobId=xxx` | Bidirectional, cancel support, ready acks |
|
|
16
|
+
| **Bookshelf Scanning** | WebSocket | `GET /ws/progress?jobId=xxx` | Bidirectional, cancel support, real-time feedback |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Architecture Overview
|
|
21
|
+
|
|
22
|
+
BooksTrack uses a **hybrid streaming architecture**:
|
|
23
|
+
|
|
24
|
+
- **SSE** for long-running import jobs (CSV parsing, Gemini processing)
|
|
25
|
+
- **WebSocket** for interactive batch operations (enrichment, scanning)
|
|
26
|
+
|
|
27
|
+
Both protocols coexist to provide optimal UX for different workflows.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Server-Sent Events (SSE)
|
|
32
|
+
|
|
33
|
+
### When to Use
|
|
34
|
+
|
|
35
|
+
✅ **CSV Import Progress** - One-way updates from server to client
|
|
36
|
+
✅ **Long-running jobs** - Import processing can take 30+ seconds
|
|
37
|
+
✅ **Firewall-friendly environments** - SSE uses HTTP/1.1
|
|
38
|
+
✅ **Browser-native reconnection** - Automatic retry with `Last-Event-ID`
|
|
39
|
+
|
|
40
|
+
### Endpoints
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
POST /api/v2/imports → Returns { jobId, authToken, sseUrl }
|
|
44
|
+
GET /api/v2/imports/:id/stream → SSE stream
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Client Example (EventSource API)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { createBooksTrackClient } from '@bookstrack/api-client'
|
|
51
|
+
|
|
52
|
+
const client = createBooksTrackClient({ baseUrl: 'https://api.oooefam.net' })
|
|
53
|
+
|
|
54
|
+
// 1. Upload CSV file
|
|
55
|
+
const formData = new FormData()
|
|
56
|
+
formData.append('file', csvFile)
|
|
57
|
+
|
|
58
|
+
const { data } = await client.POST('/api/v2/imports', {
|
|
59
|
+
body: formData
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const { jobId, sseUrl } = data.data
|
|
63
|
+
|
|
64
|
+
// 2. Connect to SSE stream
|
|
65
|
+
const eventSource = new EventSource(sseUrl)
|
|
66
|
+
|
|
67
|
+
// Listen for progress events
|
|
68
|
+
eventSource.addEventListener('progress', (e) => {
|
|
69
|
+
const update = JSON.parse(e.data)
|
|
70
|
+
console.log(`Progress: ${update.progress * 100}%`)
|
|
71
|
+
console.log(`Status: ${update.status}`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Listen for completion
|
|
75
|
+
eventSource.addEventListener('completed', (e) => {
|
|
76
|
+
const result = JSON.parse(e.data)
|
|
77
|
+
console.log(`✅ Import complete: ${result.processedCount} books`)
|
|
78
|
+
console.log('Books:', result.books)
|
|
79
|
+
eventSource.close()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Listen for errors
|
|
83
|
+
eventSource.addEventListener('failed', (e) => {
|
|
84
|
+
const error = JSON.parse(e.data)
|
|
85
|
+
console.error('❌ Import failed:', error.error.message)
|
|
86
|
+
eventSource.close()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Handle connection errors
|
|
90
|
+
eventSource.onerror = (e) => {
|
|
91
|
+
console.error('SSE connection error:', e)
|
|
92
|
+
// EventSource will automatically reconnect with Last-Event-ID
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### SSE Event Types
|
|
97
|
+
|
|
98
|
+
| Event | Description | When Sent |
|
|
99
|
+
|-------|-------------|-----------|
|
|
100
|
+
| `progress` | Import progress update | Every book processed |
|
|
101
|
+
| `completed` | Import finished successfully | Job complete |
|
|
102
|
+
| `failed` | Import failed with error | Job failed |
|
|
103
|
+
| `timeout` | Stream timeout (5 min no updates) | Connection idle |
|
|
104
|
+
|
|
105
|
+
### SSE Payload Structure
|
|
106
|
+
|
|
107
|
+
**Progress Event:**
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"jobId": "uuid",
|
|
111
|
+
"status": "processing",
|
|
112
|
+
"progress": 0.65,
|
|
113
|
+
"processedCount": 32,
|
|
114
|
+
"totalCount": 50,
|
|
115
|
+
"startedAt": "2025-12-03T12:00:00Z"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Completed Event:**
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"jobId": "uuid",
|
|
123
|
+
"status": "completed",
|
|
124
|
+
"progress": 1.0,
|
|
125
|
+
"processedCount": 50,
|
|
126
|
+
"totalCount": 50,
|
|
127
|
+
"completedAt": "2025-12-03T12:05:30Z",
|
|
128
|
+
"books": [
|
|
129
|
+
{
|
|
130
|
+
"isbn": "9780439708180",
|
|
131
|
+
"title": "Harry Potter",
|
|
132
|
+
"authors": ["J.K. Rowling"],
|
|
133
|
+
"language": "en"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### SSE Features
|
|
140
|
+
|
|
141
|
+
- ✅ **Automatic reconnection** - Browser retries with `Last-Event-ID` header
|
|
142
|
+
- ✅ **Heartbeat** - Server sends `: heartbeat\n\n` every 30 seconds
|
|
143
|
+
- ✅ **Adaptive polling** - Backend uses 500ms → 3s backoff to reduce DO load
|
|
144
|
+
- ✅ **Books array on completion** - iOS can persist immediately without separate fetch
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## WebSocket
|
|
149
|
+
|
|
150
|
+
### When to Use
|
|
151
|
+
|
|
152
|
+
✅ **Batch Enrichment** - Bidirectional with cancel support
|
|
153
|
+
✅ **Bookshelf Scanning** - Real-time feedback during AI processing
|
|
154
|
+
✅ **Ready acknowledgments** - Client signals readiness to receive updates
|
|
155
|
+
✅ **Job cancellation** - Client can cancel mid-stream
|
|
156
|
+
|
|
157
|
+
### Endpoints
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
POST /v1/enrichment/batch → Returns { jobId, authToken, websocketUrl }
|
|
161
|
+
POST /api/batch-enrich → iOS compatibility alias
|
|
162
|
+
GET /ws/progress?jobId=xxx → WebSocket upgrade
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Client Example (WebSocket API)
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { createBooksTrackClient } from '@bookstrack/api-client'
|
|
169
|
+
|
|
170
|
+
const client = createBooksTrackClient({ baseUrl: 'https://api.oooefam.net' })
|
|
171
|
+
|
|
172
|
+
// 1. Start batch enrichment
|
|
173
|
+
const { data } = await client.POST('/v1/enrichment/batch', {
|
|
174
|
+
body: {
|
|
175
|
+
books: [
|
|
176
|
+
{ title: 'Harry Potter', author: 'J.K. Rowling' },
|
|
177
|
+
{ isbn: '9780439708180' }
|
|
178
|
+
],
|
|
179
|
+
jobId: crypto.randomUUID()
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const { jobId, authToken, websocketUrl } = data.data
|
|
184
|
+
|
|
185
|
+
// 2. Connect to WebSocket
|
|
186
|
+
const ws = new WebSocket(websocketUrl)
|
|
187
|
+
|
|
188
|
+
// Send auth token via Sec-WebSocket-Protocol header (RECOMMENDED)
|
|
189
|
+
// OR include in URL query params (deprecated, leaks in logs)
|
|
190
|
+
const wsSecure = new WebSocket(`wss://api.oooefam.net/ws/progress?jobId=${jobId}`, [
|
|
191
|
+
authToken
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
ws.onopen = () => {
|
|
195
|
+
console.log('WebSocket connected')
|
|
196
|
+
|
|
197
|
+
// Send ready acknowledgment
|
|
198
|
+
ws.send(JSON.stringify({ type: 'ready', jobId }))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ws.onmessage = (event) => {
|
|
202
|
+
const message = JSON.parse(event.data)
|
|
203
|
+
|
|
204
|
+
switch (message.type) {
|
|
205
|
+
case 'ready_ack':
|
|
206
|
+
console.log('Server acknowledged ready signal')
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
case 'progress':
|
|
210
|
+
console.log(`Progress: ${message.progress * 100}%`)
|
|
211
|
+
console.log(`Book: ${message.currentBook.title}`)
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
case 'book_enriched':
|
|
215
|
+
console.log('Book enriched:', message.book)
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
case 'job_complete':
|
|
219
|
+
console.log('✅ All books enriched')
|
|
220
|
+
ws.close()
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
case 'error':
|
|
224
|
+
console.error('❌ Error:', message.error)
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
ws.onerror = (error) => {
|
|
230
|
+
console.error('WebSocket error:', error)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
ws.onclose = (event) => {
|
|
234
|
+
if (event.wasClean) {
|
|
235
|
+
console.log('WebSocket closed cleanly')
|
|
236
|
+
} else {
|
|
237
|
+
console.error('WebSocket connection lost')
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Cancel job mid-stream
|
|
242
|
+
function cancelJob() {
|
|
243
|
+
ws.send(JSON.stringify({ type: 'cancel', jobId }))
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### WebSocket Message Types
|
|
248
|
+
|
|
249
|
+
**Client → Server:**
|
|
250
|
+
| Message Type | Description | When to Send |
|
|
251
|
+
|--------------|-------------|--------------|
|
|
252
|
+
| `ready` | Signal client is ready for updates | On connection |
|
|
253
|
+
| `cancel` | Cancel the job | User cancels |
|
|
254
|
+
|
|
255
|
+
**Server → Client:**
|
|
256
|
+
| Message Type | Description | When Sent |
|
|
257
|
+
|--------------|-------------|-----------|
|
|
258
|
+
| `ready_ack` | Acknowledge ready signal | After client sends `ready` |
|
|
259
|
+
| `progress` | Job progress update | Every book enriched |
|
|
260
|
+
| `book_enriched` | Single book enriched | Per book completion |
|
|
261
|
+
| `job_complete` | Job finished | All books processed |
|
|
262
|
+
| `error` | Error occurred | Job failed |
|
|
263
|
+
|
|
264
|
+
### WebSocket Authentication
|
|
265
|
+
|
|
266
|
+
**RECOMMENDED: Sec-WebSocket-Protocol Header**
|
|
267
|
+
```typescript
|
|
268
|
+
const ws = new WebSocket(
|
|
269
|
+
'wss://api.oooefam.net/ws/progress?jobId=xxx',
|
|
270
|
+
[authToken] // Passed as subprotocol
|
|
271
|
+
)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**DEPRECATED: URL Query Parameter** ⚠️
|
|
275
|
+
```typescript
|
|
276
|
+
// ⚠️ Leaks token in browser history, logs, and network traffic
|
|
277
|
+
const ws = new WebSocket(
|
|
278
|
+
`wss://api.oooefam.net/ws/progress?jobId=xxx&token=${authToken}`
|
|
279
|
+
)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Backward Compatible:** Backend supports both methods until March 1, 2026.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Comparison Matrix
|
|
287
|
+
|
|
288
|
+
| Feature | SSE | WebSocket |
|
|
289
|
+
|---------|-----|-----------|
|
|
290
|
+
| **Direction** | Server → Client (one-way) | Bidirectional |
|
|
291
|
+
| **Reconnection** | Browser-native with `Last-Event-ID` | Manual retry logic required |
|
|
292
|
+
| **Protocol** | HTTP/1.1 (firewall-friendly) | WebSocket (upgrade from HTTP) |
|
|
293
|
+
| **Cancel Support** | ❌ Requires separate HTTP DELETE | ✅ Send `cancel` message |
|
|
294
|
+
| **Ready Ack** | ❌ Not needed (one-way stream) | ✅ Client signals readiness |
|
|
295
|
+
| **Use Case** | CSV imports, long jobs | Batch enrichment, scanning |
|
|
296
|
+
| **Complexity** | Low (native `EventSource`) | Medium (manual reconnect) |
|
|
297
|
+
| **Browser Support** | 97%+ (all modern browsers) | 98%+ (all modern browsers) |
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Fallback: HTTP Polling
|
|
302
|
+
|
|
303
|
+
If neither SSE nor WebSocket is available (corporate firewalls, legacy browsers), use HTTP polling:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
const client = createBooksTrackClient({ baseUrl: 'https://api.oooefam.net' })
|
|
307
|
+
|
|
308
|
+
async function pollJobStatus(jobId: string) {
|
|
309
|
+
const interval = setInterval(async () => {
|
|
310
|
+
const { data } = await client.GET('/api/v2/imports/{jobId}', {
|
|
311
|
+
params: { path: { jobId } }
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const job = data.data
|
|
315
|
+
console.log(`Progress: ${job.progress * 100}%`)
|
|
316
|
+
|
|
317
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
318
|
+
clearInterval(interval)
|
|
319
|
+
console.log('Job finished:', job.status)
|
|
320
|
+
}
|
|
321
|
+
}, 2000) // Poll every 2 seconds (rate-limited to 30 req/min)
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Rate Limits:**
|
|
326
|
+
- CSV Import Status: 30 requests/minute
|
|
327
|
+
- Enrichment Status: 30 requests/minute
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Best Practices
|
|
332
|
+
|
|
333
|
+
### SSE
|
|
334
|
+
|
|
335
|
+
1. **Always handle reconnection** - `EventSource` auto-reconnects, but handle `onerror`
|
|
336
|
+
2. **Close on completion** - Call `eventSource.close()` when job completes
|
|
337
|
+
3. **Include Last-Event-ID** - Browser sends automatically for resume support
|
|
338
|
+
4. **Handle timeout events** - Stream closes after 5 min of inactivity
|
|
339
|
+
|
|
340
|
+
### WebSocket
|
|
341
|
+
|
|
342
|
+
1. **Send ready acknowledgment** - Signal readiness after connection opens
|
|
343
|
+
2. **Implement reconnection** - WebSocket doesn't auto-reconnect like SSE
|
|
344
|
+
3. **Use secure protocol** - `wss://` in production, `ws://` for local dev
|
|
345
|
+
4. **Handle cleanup** - Close connection in `useEffect` cleanup (React)
|
|
346
|
+
5. **Prefer Sec-WebSocket-Protocol** - Don't leak tokens in URL query params
|
|
347
|
+
|
|
348
|
+
### Both
|
|
349
|
+
|
|
350
|
+
1. **Show progress UI** - Update progress bar on every `progress` event
|
|
351
|
+
2. **Handle network failures** - Display "reconnecting..." message
|
|
352
|
+
3. **Timeout gracefully** - Fall back to polling if stream fails
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## React Hooks Example
|
|
357
|
+
|
|
358
|
+
### SSE Hook
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { useEffect, useState } from 'react'
|
|
362
|
+
|
|
363
|
+
export function useSSEStream(sseUrl: string | null) {
|
|
364
|
+
const [progress, setProgress] = useState(0)
|
|
365
|
+
const [status, setStatus] = useState<string>('idle')
|
|
366
|
+
const [result, setResult] = useState<any>(null)
|
|
367
|
+
const [error, setError] = useState<Error | null>(null)
|
|
368
|
+
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (!sseUrl) return
|
|
371
|
+
|
|
372
|
+
const eventSource = new EventSource(sseUrl)
|
|
373
|
+
|
|
374
|
+
eventSource.addEventListener('progress', (e) => {
|
|
375
|
+
const data = JSON.parse(e.data)
|
|
376
|
+
setProgress(data.progress)
|
|
377
|
+
setStatus(data.status)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
eventSource.addEventListener('completed', (e) => {
|
|
381
|
+
const data = JSON.parse(e.data)
|
|
382
|
+
setResult(data)
|
|
383
|
+
setStatus('completed')
|
|
384
|
+
eventSource.close()
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
eventSource.addEventListener('failed', (e) => {
|
|
388
|
+
const data = JSON.parse(e.data)
|
|
389
|
+
setError(new Error(data.error.message))
|
|
390
|
+
setStatus('failed')
|
|
391
|
+
eventSource.close()
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
return () => eventSource.close()
|
|
395
|
+
}, [sseUrl])
|
|
396
|
+
|
|
397
|
+
return { progress, status, result, error }
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### WebSocket Hook
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { useEffect, useState, useRef } from 'react'
|
|
405
|
+
|
|
406
|
+
export function useWebSocket(url: string | null, authToken: string | null) {
|
|
407
|
+
const [progress, setProgress] = useState(0)
|
|
408
|
+
const [connected, setConnected] = useState(false)
|
|
409
|
+
const [result, setResult] = useState<any>(null)
|
|
410
|
+
const wsRef = useRef<WebSocket | null>(null)
|
|
411
|
+
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
if (!url || !authToken) return
|
|
414
|
+
|
|
415
|
+
const ws = new WebSocket(url, [authToken])
|
|
416
|
+
wsRef.current = ws
|
|
417
|
+
|
|
418
|
+
ws.onopen = () => {
|
|
419
|
+
setConnected(true)
|
|
420
|
+
ws.send(JSON.stringify({ type: 'ready' }))
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
ws.onmessage = (event) => {
|
|
424
|
+
const message = JSON.parse(event.data)
|
|
425
|
+
|
|
426
|
+
if (message.type === 'progress') {
|
|
427
|
+
setProgress(message.progress)
|
|
428
|
+
} else if (message.type === 'job_complete') {
|
|
429
|
+
setResult(message)
|
|
430
|
+
ws.close()
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
ws.onclose = () => setConnected(false)
|
|
435
|
+
|
|
436
|
+
return () => ws.close()
|
|
437
|
+
}, [url, authToken])
|
|
438
|
+
|
|
439
|
+
const cancel = () => {
|
|
440
|
+
wsRef.current?.send(JSON.stringify({ type: 'cancel' }))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { progress, connected, result, cancel }
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Troubleshooting
|
|
450
|
+
|
|
451
|
+
### SSE Issues
|
|
452
|
+
|
|
453
|
+
**EventSource fails to connect:**
|
|
454
|
+
- Check CORS headers (`Access-Control-Allow-Origin`)
|
|
455
|
+
- Verify `Accept: text/event-stream` header
|
|
456
|
+
- Check firewall rules (port 443 for HTTPS)
|
|
457
|
+
|
|
458
|
+
**No events received:**
|
|
459
|
+
- Check backend heartbeat (should see `: heartbeat\n\n` every 30s)
|
|
460
|
+
- Verify job is actually processing (check polling endpoint)
|
|
461
|
+
- Check browser console for SSE errors
|
|
462
|
+
|
|
463
|
+
**Reconnection loop:**
|
|
464
|
+
- Backend may be rejecting `Last-Event-ID`
|
|
465
|
+
- Check if job already completed (SSE closes on completion)
|
|
466
|
+
|
|
467
|
+
### WebSocket Issues
|
|
468
|
+
|
|
469
|
+
**Connection refused:**
|
|
470
|
+
- Verify WebSocket URL uses `wss://` (secure) in production
|
|
471
|
+
- Check auth token is valid (generated within last 5 minutes)
|
|
472
|
+
- Ensure `jobId` exists (job was initialized)
|
|
473
|
+
|
|
474
|
+
**No messages received:**
|
|
475
|
+
- Send `ready` message after connection opens
|
|
476
|
+
- Check for `ready_ack` response
|
|
477
|
+
- Verify job is processing (not already complete)
|
|
478
|
+
|
|
479
|
+
**Disconnects immediately:**
|
|
480
|
+
- Auth token invalid or expired
|
|
481
|
+
- Job already completed/failed
|
|
482
|
+
- Backend restarted (connection lost)
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Migration Notes
|
|
487
|
+
|
|
488
|
+
**From Legacy `/search/*` API:**
|
|
489
|
+
- Old API had no streaming support
|
|
490
|
+
- Use new V2 endpoints with SSE/WebSocket
|
|
491
|
+
|
|
492
|
+
**From V1 to V2:**
|
|
493
|
+
- V1 jobs used WebSocket only
|
|
494
|
+
- V2 CSV imports use SSE (better UX)
|
|
495
|
+
- V2 batch enrichment still uses WebSocket (bidirectional needed)
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Support
|
|
500
|
+
|
|
501
|
+
- **Full API Docs:** [OpenAPI Specification](../../docs/openapi.yaml)
|
|
502
|
+
- **Issues:** [GitHub Issues](https://github.com/yourusername/bendv3/issues)
|
|
503
|
+
- **Backend Code:** [Router](../../src/router.ts), [SSE Handler](../../src/handlers/v2/sse-stream.ts)
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
**Last Updated:** December 3, 2025
|
|
508
|
+
**Maintained By:** BooksTrack Team (@jukasdrj)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { paths } from './schema';
|
|
2
2
|
export type { paths, components } from './schema';
|
|
3
|
+
export { createSSEStream, createWebSocketStream, useSSEStream_Example, useWebSocketStream_Example, type SSEProgressEvent, type WebSocketProgressMessage, type SSEStreamOptions, type WebSocketStreamOptions, } from './streaming';
|
|
3
4
|
/**
|
|
4
5
|
* Create a type-safe BooksTrack API client
|
|
5
6
|
*
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAErC,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAErC,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAGjD,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAA;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,WAAW,CAAC,EAAE,kBAAkB,CAAA;CACjC,gEAWA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,MAAM,8DAA2B,CAAA"}
|
package/dist/index.js
CHANGED