tangled
alpha
login
or
join now
angrydutchman.peedee.es
/
plcbundle
forked from
atscan.net/plcbundle
0
fork
atom
A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
0
fork
atom
overview
issues
pulls
pipelines
update ws
tree.fail
4 months ago
e113107e
f6bc3fcd
+179
-95
1 changed file
expand all
collapse all
unified
split
cmd
plcbundle
server.go
+179
-95
cmd/plcbundle/server.go
···
119
119
}
120
120
121
121
// handleWebSocket streams all records via WebSocket starting from cursor
122
122
+
// Keeps connection alive and streams new records as they arrive
122
123
func handleWebSocket(w http.ResponseWriter, r *http.Request, mgr *bundle.Manager) {
123
124
// Parse cursor from query parameter (defaults to 0)
124
125
cursorStr := r.URL.Query().Get("cursor")
···
132
133
}
133
134
}
134
135
135
135
-
// Check if client wants to keep connection alive
136
136
-
keepAlive := r.URL.Query().Get("keepalive") == "true"
137
137
-
138
136
// Upgrade to WebSocket
139
137
conn, err := upgrader.Upgrade(w, r, nil)
140
138
if err != nil {
···
143
141
}
144
142
defer conn.Close()
145
143
146
146
-
// Set up ping/pong handlers for keepalive
144
144
+
// Set up handlers for connection management
147
145
conn.SetPongHandler(func(string) error {
148
146
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
149
147
return nil
150
148
})
151
149
152
152
-
ctx := context.Background()
153
153
-
index := mgr.GetIndex()
154
154
-
bundles := index.GetBundles()
150
150
+
// Channel to signal client disconnect
151
151
+
done := make(chan struct{})
155
152
156
156
-
if len(bundles) == 0 {
157
157
-
if !keepAlive {
158
158
-
closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "no bundles available")
159
159
-
conn.WriteMessage(websocket.CloseMessage, closeMsg)
153
153
+
// Start goroutine to detect client disconnect
154
154
+
go func() {
155
155
+
defer close(done)
156
156
+
for {
157
157
+
_, _, err := conn.ReadMessage()
158
158
+
if err != nil {
159
159
+
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
160
160
+
fmt.Fprintf(os.Stderr, "WebSocket: client closed connection\n")
161
161
+
}
162
162
+
return
163
163
+
}
160
164
}
161
161
-
return
162
162
-
}
165
165
+
}()
163
166
164
164
-
// Calculate starting bundle and position from cursor
165
165
-
startBundleIdx := cursor / bundle.BUNDLE_SIZE
166
166
-
startPosition := cursor % bundle.BUNDLE_SIZE
167
167
+
ctx := context.Background()
167
168
168
168
-
// Validate starting bundle exists
169
169
-
if startBundleIdx >= len(bundles) {
170
170
-
currentRecord := len(bundles) * bundle.BUNDLE_SIZE
171
171
-
if err := streamMempool(conn, mgr, cursor, currentRecord); err != nil {
172
172
-
return
173
173
-
}
174
174
-
if !keepAlive {
175
175
-
closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "stream complete")
176
176
-
conn.WriteMessage(websocket.CloseMessage, closeMsg)
177
177
-
}
178
178
-
return
169
169
+
// Stream all data and keep connection alive
170
170
+
if err := streamLive(ctx, conn, mgr, cursor, done); err != nil {
171
171
+
fmt.Fprintf(os.Stderr, "WebSocket stream error: %v\n", err)
179
172
}
173
173
+
}
180
174
181
181
-
currentRecord := cursor
175
175
+
// streamLive streams all historical data then continues with live updates
176
176
+
func streamLive(ctx context.Context, conn *websocket.Conn, mgr *bundle.Manager, startCursor int, done chan struct{}) error {
177
177
+
index := mgr.GetIndex()
178
178
+
currentRecord := startCursor
182
179
183
183
-
// Stream bundles starting from the calculated bundle
184
184
-
for i := startBundleIdx; i < len(bundles); i++ {
185
185
-
meta := bundles[i]
180
180
+
// Step 1: Stream all historical bundles
181
181
+
bundles := index.GetBundles()
182
182
+
if len(bundles) > 0 {
183
183
+
startBundleIdx := startCursor / bundle.BUNDLE_SIZE
184
184
+
startPosition := startCursor % bundle.BUNDLE_SIZE
186
185
187
187
-
b, err := mgr.LoadBundle(ctx, meta.BundleNumber)
188
188
-
if err != nil {
189
189
-
fmt.Fprintf(os.Stderr, "Failed to load bundle %d: %v\n", meta.BundleNumber, err)
190
190
-
continue
191
191
-
}
186
186
+
if startBundleIdx < len(bundles) {
187
187
+
for i := startBundleIdx; i < len(bundles); i++ {
188
188
+
select {
189
189
+
case <-done:
190
190
+
return nil // Client disconnected
191
191
+
default:
192
192
+
}
192
193
193
193
-
startPos := 0
194
194
-
if i == startBundleIdx {
195
195
-
startPos = startPosition
196
196
-
}
194
194
+
meta := bundles[i]
195
195
+
b, err := mgr.LoadBundle(ctx, meta.BundleNumber)
196
196
+
if err != nil {
197
197
+
fmt.Fprintf(os.Stderr, "Failed to load bundle %d: %v\n", meta.BundleNumber, err)
198
198
+
continue
199
199
+
}
197
200
198
198
-
for j := startPos; j < len(b.Operations); j++ {
199
199
-
op := b.Operations[j]
201
201
+
startPos := 0
202
202
+
if i == startBundleIdx {
203
203
+
startPos = startPosition
204
204
+
}
200
205
201
201
-
if err := sendOperation(conn, op); err != nil {
202
202
-
return
203
203
-
}
206
206
+
for j := startPos; j < len(b.Operations); j++ {
207
207
+
select {
208
208
+
case <-done:
209
209
+
return nil
210
210
+
default:
211
211
+
}
204
212
205
205
-
currentRecord++
213
213
+
if err := sendOperation(conn, b.Operations[j]); err != nil {
214
214
+
return err
215
215
+
}
216
216
+
currentRecord++
206
217
207
207
-
if currentRecord%1000 == 0 {
208
208
-
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
209
209
-
return
218
218
+
if currentRecord%1000 == 0 {
219
219
+
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
220
220
+
return err
221
221
+
}
222
222
+
}
210
223
}
211
224
}
212
225
}
213
226
}
214
227
215
215
-
// Stream mempool
216
216
-
if err := streamMempool(conn, mgr, cursor, currentRecord); err != nil {
217
217
-
return
218
218
-
}
228
228
+
// Step 2: Stream current mempool
229
229
+
lastSeenMempoolCount := 0
230
230
+
mempoolOps, err := mgr.GetMempoolOperations()
231
231
+
if err == nil {
232
232
+
bundleRecordBase := len(bundles) * bundle.BUNDLE_SIZE
219
233
220
220
-
if keepAlive {
221
221
-
// Keep connection open and wait for client to close
222
222
-
fmt.Fprintf(os.Stderr, "WebSocket: stream complete, keeping connection alive\n")
234
234
+
for i, op := range mempoolOps {
235
235
+
select {
236
236
+
case <-done:
237
237
+
return nil
238
238
+
default:
239
239
+
}
223
240
224
224
-
// Read messages from client (to detect close)
225
225
-
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
226
226
-
for {
227
227
-
_, _, err := conn.ReadMessage()
228
228
-
if err != nil {
229
229
-
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
230
230
-
fmt.Fprintf(os.Stderr, "WebSocket: client closed connection\n")
231
231
-
}
232
232
-
break
241
241
+
recordNum := bundleRecordBase + i
242
242
+
if recordNum < startCursor {
243
243
+
continue
244
244
+
}
245
245
+
246
246
+
if err := sendOperation(conn, op); err != nil {
247
247
+
return err
233
248
}
249
249
+
currentRecord++
250
250
+
lastSeenMempoolCount = i + 1
234
251
}
235
235
-
} else {
236
236
-
// Close gracefully
237
237
-
closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "stream complete")
238
238
-
conn.WriteMessage(websocket.CloseMessage, closeMsg)
239
239
-
time.Sleep(100 * time.Millisecond)
240
252
}
241
241
-
}
253
253
+
254
254
+
// Step 3: Enter live streaming loop
255
255
+
// Poll for new operations in mempool and new bundles
256
256
+
ticker := time.NewTicker(2 * time.Second)
257
257
+
defer ticker.Stop()
258
258
+
259
259
+
lastBundleCount := len(bundles)
260
260
+
261
261
+
fmt.Fprintf(os.Stderr, "WebSocket: entering live mode at cursor %d\n", currentRecord)
262
262
+
263
263
+
for {
264
264
+
select {
265
265
+
case <-done:
266
266
+
fmt.Fprintf(os.Stderr, "WebSocket: client disconnected, stopping stream\n")
267
267
+
return nil
268
268
+
269
269
+
case <-ticker.C:
270
270
+
// Refresh index to check for new bundles
271
271
+
index = mgr.GetIndex()
272
272
+
bundles = index.GetBundles()
273
273
+
274
274
+
// Check if new bundles were created
275
275
+
if len(bundles) > lastBundleCount {
276
276
+
fmt.Fprintf(os.Stderr, "WebSocket: detected %d new bundle(s)\n", len(bundles)-lastBundleCount)
277
277
+
278
278
+
// Stream new bundles
279
279
+
for i := lastBundleCount; i < len(bundles); i++ {
280
280
+
select {
281
281
+
case <-done:
282
282
+
return nil
283
283
+
default:
284
284
+
}
285
285
+
286
286
+
meta := bundles[i]
287
287
+
b, err := mgr.LoadBundle(ctx, meta.BundleNumber)
288
288
+
if err != nil {
289
289
+
fmt.Fprintf(os.Stderr, "Failed to load bundle %d: %v\n", meta.BundleNumber, err)
290
290
+
continue
291
291
+
}
292
292
+
293
293
+
for _, op := range b.Operations {
294
294
+
select {
295
295
+
case <-done:
296
296
+
return nil
297
297
+
default:
298
298
+
}
299
299
+
300
300
+
if err := sendOperation(conn, op); err != nil {
301
301
+
return err
302
302
+
}
303
303
+
currentRecord++
304
304
+
305
305
+
if currentRecord%1000 == 0 {
306
306
+
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
307
307
+
return err
308
308
+
}
309
309
+
}
310
310
+
}
311
311
+
}
242
312
243
243
-
// streamMempool streams mempool operations if cursor is in range
244
244
-
func streamMempool(conn *websocket.Conn, mgr *bundle.Manager, cursor int, currentRecord int) {
245
245
-
mempoolOps, err := mgr.GetMempoolOperations()
246
246
-
if err != nil {
247
247
-
fmt.Fprintf(os.Stderr, "Failed to get mempool operations: %v\n", err)
248
248
-
return
249
249
-
}
313
313
+
lastBundleCount = len(bundles)
314
314
+
lastSeenMempoolCount = 0 // Reset mempool count after bundle creation
315
315
+
}
250
316
251
251
-
for _, op := range mempoolOps {
252
252
-
// Skip records before cursor
253
253
-
if currentRecord < cursor {
254
254
-
currentRecord++
255
255
-
continue
256
256
-
}
317
317
+
// Check for new operations in mempool
318
318
+
mempoolOps, err := mgr.GetMempoolOperations()
319
319
+
if err != nil {
320
320
+
continue
321
321
+
}
257
322
258
258
-
// Send raw JSON
259
259
-
if err := sendOperation(conn, op); err != nil {
260
260
-
return
261
261
-
}
323
323
+
if len(mempoolOps) > lastSeenMempoolCount {
324
324
+
fmt.Fprintf(os.Stderr, "WebSocket: streaming %d new mempool operation(s)\n",
325
325
+
len(mempoolOps)-lastSeenMempoolCount)
262
326
263
263
-
currentRecord++
327
327
+
// Stream new mempool operations
328
328
+
for i := lastSeenMempoolCount; i < len(mempoolOps); i++ {
329
329
+
select {
330
330
+
case <-done:
331
331
+
return nil
332
332
+
default:
333
333
+
}
264
334
265
265
-
// Send ping periodically
266
266
-
if currentRecord%1000 == 0 {
335
335
+
if err := sendOperation(conn, mempoolOps[i]); err != nil {
336
336
+
return err
337
337
+
}
338
338
+
currentRecord++
339
339
+
}
340
340
+
341
341
+
lastSeenMempoolCount = len(mempoolOps)
342
342
+
}
343
343
+
344
344
+
// Send periodic ping to keep connection alive
267
345
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
268
268
-
return
346
346
+
return err
269
347
}
270
348
}
271
349
}
···
289
367
290
368
// Send as text message
291
369
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
292
292
-
fmt.Fprintf(os.Stderr, "WebSocket write error: %v\n", err)
370
370
+
if !websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
371
371
+
fmt.Fprintf(os.Stderr, "WebSocket write error: %v\n", err)
372
372
+
}
293
373
return err
294
374
}
295
375
···
423
503
if wsEnabled {
424
504
fmt.Fprintf(w, "\nWebSocket Endpoints\n")
425
505
fmt.Fprintf(w, "━━━━━━━━━━━━━━━━━━━\n")
426
426
-
fmt.Fprintf(w, " WS /ws?cursor=N Stream all records from cursor N\n")
427
427
-
fmt.Fprintf(w, " (cursor defaults to 0)\n")
506
506
+
fmt.Fprintf(w, " WS /ws?cursor=N Live stream all records from cursor N\n")
507
507
+
fmt.Fprintf(w, " Streams all bundles, then mempool\n")
508
508
+
fmt.Fprintf(w, " Continues streaming new operations live\n")
509
509
+
fmt.Fprintf(w, " Connection stays open until client closes\n")
510
510
+
fmt.Fprintf(w, " Cursor: global record number (0-based)\n")
511
511
+
fmt.Fprintf(w, " Example: 88410345 = bundle 8841, pos 345\n")
428
512
}
429
513
430
514
if syncMode {