@cleverence/edge-js-sdk 1.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.
@@ -0,0 +1,649 @@
1
+ import { createContext, useState, useEffect, useMemo, useCallback, useContext } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/react/EdgeContext.tsx
5
+
6
+ // src/core/events.ts
7
+ var EventEmitter = class {
8
+ constructor() {
9
+ this.handlers = /* @__PURE__ */ new Map();
10
+ }
11
+ /**
12
+ * Subscribe to an event
13
+ */
14
+ on(event, handler) {
15
+ if (!this.handlers.has(event)) {
16
+ this.handlers.set(event, /* @__PURE__ */ new Set());
17
+ }
18
+ this.handlers.get(event).add(handler);
19
+ return this;
20
+ }
21
+ /**
22
+ * Subscribe to an event once (auto-unsubscribes after first call)
23
+ */
24
+ once(event, handler) {
25
+ const onceHandler = (data) => {
26
+ this.off(event, onceHandler);
27
+ handler(data);
28
+ };
29
+ return this.on(event, onceHandler);
30
+ }
31
+ /**
32
+ * Unsubscribe from an event
33
+ */
34
+ off(event, handler) {
35
+ const eventHandlers = this.handlers.get(event);
36
+ if (eventHandlers) {
37
+ eventHandlers.delete(handler);
38
+ }
39
+ return this;
40
+ }
41
+ /**
42
+ * Emit an event to all subscribers
43
+ */
44
+ emit(event, data) {
45
+ const eventHandlers = this.handlers.get(event);
46
+ if (eventHandlers) {
47
+ eventHandlers.forEach((handler) => {
48
+ try {
49
+ handler(data);
50
+ } catch (err) {
51
+ console.error(`Error in event handler for "${String(event)}":`, err);
52
+ }
53
+ });
54
+ }
55
+ }
56
+ /**
57
+ * Remove all listeners for an event, or all listeners if no event specified
58
+ */
59
+ removeAllListeners(event) {
60
+ if (event) {
61
+ this.handlers.delete(event);
62
+ } else {
63
+ this.handlers.clear();
64
+ }
65
+ return this;
66
+ }
67
+ /**
68
+ * Get listener count for an event
69
+ */
70
+ listenerCount(event) {
71
+ return this.handlers.get(event)?.size ?? 0;
72
+ }
73
+ };
74
+
75
+ // src/core/websocket.ts
76
+ var DEFAULT_WS_OPTIONS = {
77
+ reconnect: true,
78
+ reconnectDelay: 1e3,
79
+ maxReconnectDelay: 3e4,
80
+ pingInterval: 3e4
81
+ };
82
+ var WebSocketManager = class extends EventEmitter {
83
+ constructor(url, options = {}) {
84
+ super();
85
+ this.ws = null;
86
+ this._state = "disconnected";
87
+ this.reconnectAttempts = 0;
88
+ this.reconnectTimer = null;
89
+ this.pingTimer = null;
90
+ this.pendingRequests = /* @__PURE__ */ new Map();
91
+ this.intentionalClose = false;
92
+ this.url = url;
93
+ this.options = { ...DEFAULT_WS_OPTIONS, ...options };
94
+ }
95
+ /**
96
+ * Current connection state
97
+ */
98
+ get state() {
99
+ return this._state;
100
+ }
101
+ /**
102
+ * Whether currently connected
103
+ */
104
+ get isConnected() {
105
+ return this._state === "connected";
106
+ }
107
+ /**
108
+ * Connect to the WebSocket server
109
+ */
110
+ connect() {
111
+ return new Promise((resolve, reject) => {
112
+ if (this._state === "connected" || this._state === "connecting") {
113
+ resolve();
114
+ return;
115
+ }
116
+ this.intentionalClose = false;
117
+ this.setState("connecting");
118
+ try {
119
+ this.ws = new WebSocket(this.url);
120
+ const onOpen = () => {
121
+ this.ws?.removeEventListener("error", onError);
122
+ this.reconnectAttempts = 0;
123
+ this.setState("connected");
124
+ this.startPing();
125
+ this.emit("open", void 0);
126
+ resolve();
127
+ };
128
+ const onError = (event) => {
129
+ this.ws?.removeEventListener("open", onOpen);
130
+ const error = new Error("WebSocket connection failed");
131
+ reject(error);
132
+ };
133
+ this.ws.addEventListener("open", onOpen, { once: true });
134
+ this.ws.addEventListener("error", onError, { once: true });
135
+ this.ws.addEventListener("message", this.handleMessage.bind(this));
136
+ this.ws.addEventListener("close", this.handleClose.bind(this));
137
+ this.ws.addEventListener("error", this.handleError.bind(this));
138
+ } catch (err) {
139
+ this.setState("disconnected");
140
+ reject(err instanceof Error ? err : new Error(String(err)));
141
+ }
142
+ });
143
+ }
144
+ /**
145
+ * Disconnect from the WebSocket server
146
+ */
147
+ disconnect() {
148
+ this.intentionalClose = true;
149
+ this.cleanup();
150
+ this.setState("disconnected");
151
+ this.emit("close", void 0);
152
+ }
153
+ /**
154
+ * Send a message to the server
155
+ */
156
+ send(message) {
157
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
158
+ throw new Error("WebSocket is not connected");
159
+ }
160
+ this.ws.send(JSON.stringify(message));
161
+ }
162
+ /**
163
+ * Send a query and wait for response (request/response pattern)
164
+ */
165
+ request(query, timeoutMs = 1e4) {
166
+ return new Promise((resolve, reject) => {
167
+ const id = this.generateId();
168
+ const timeout = setTimeout(() => {
169
+ this.pendingRequests.delete(id);
170
+ reject(new Error(`Request timeout: ${query}`));
171
+ }, timeoutMs);
172
+ this.pendingRequests.set(id, {
173
+ resolve,
174
+ reject,
175
+ timeout
176
+ });
177
+ try {
178
+ this.send({ type: "query", id, query });
179
+ } catch (err) {
180
+ clearTimeout(timeout);
181
+ this.pendingRequests.delete(id);
182
+ reject(err);
183
+ }
184
+ });
185
+ }
186
+ /**
187
+ * Send a command (fire and forget, but throws if not connected)
188
+ */
189
+ command(message) {
190
+ this.send(message);
191
+ }
192
+ handleMessage(event) {
193
+ try {
194
+ const message = JSON.parse(event.data);
195
+ if (message.type === "response") {
196
+ const pending = this.pendingRequests.get(message.id);
197
+ if (pending) {
198
+ clearTimeout(pending.timeout);
199
+ this.pendingRequests.delete(message.id);
200
+ if (message.success) {
201
+ pending.resolve(message.data.result);
202
+ } else {
203
+ pending.reject(new Error(message.error));
204
+ }
205
+ return;
206
+ }
207
+ }
208
+ if (message.type === "pong") {
209
+ return;
210
+ }
211
+ this.emit("message", message);
212
+ } catch (err) {
213
+ console.error("Failed to parse WebSocket message:", err);
214
+ }
215
+ }
216
+ handleClose() {
217
+ this.cleanup();
218
+ if (this.intentionalClose) {
219
+ this.setState("disconnected");
220
+ return;
221
+ }
222
+ this.emit("close", void 0);
223
+ if (this.options.reconnect) {
224
+ this.scheduleReconnect();
225
+ } else {
226
+ this.setState("disconnected");
227
+ }
228
+ }
229
+ handleError(event) {
230
+ const error = new Error("WebSocket error");
231
+ this.emit("error", error);
232
+ }
233
+ scheduleReconnect() {
234
+ if (this.reconnectTimer) return;
235
+ this.setState("reconnecting");
236
+ const delay = Math.min(
237
+ this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts),
238
+ this.options.maxReconnectDelay
239
+ );
240
+ this.reconnectTimer = setTimeout(async () => {
241
+ this.reconnectTimer = null;
242
+ this.reconnectAttempts++;
243
+ try {
244
+ await this.connect();
245
+ } catch {
246
+ }
247
+ }, delay);
248
+ }
249
+ startPing() {
250
+ this.stopPing();
251
+ this.pingTimer = setInterval(() => {
252
+ if (this.ws?.readyState === WebSocket.OPEN) {
253
+ try {
254
+ this.send({ type: "ping" });
255
+ } catch {
256
+ }
257
+ }
258
+ }, this.options.pingInterval);
259
+ }
260
+ stopPing() {
261
+ if (this.pingTimer) {
262
+ clearInterval(this.pingTimer);
263
+ this.pingTimer = null;
264
+ }
265
+ }
266
+ cleanup() {
267
+ this.stopPing();
268
+ if (this.reconnectTimer) {
269
+ clearTimeout(this.reconnectTimer);
270
+ this.reconnectTimer = null;
271
+ }
272
+ this.pendingRequests.forEach((pending) => {
273
+ clearTimeout(pending.timeout);
274
+ pending.reject(new Error("WebSocket disconnected"));
275
+ });
276
+ this.pendingRequests.clear();
277
+ if (this.ws) {
278
+ const ws = this.ws;
279
+ this.ws = null;
280
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
281
+ ws.close();
282
+ }
283
+ }
284
+ }
285
+ setState(state) {
286
+ if (this._state !== state) {
287
+ this._state = state;
288
+ this.emit("statechange", state);
289
+ }
290
+ }
291
+ generateId() {
292
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
293
+ }
294
+ };
295
+
296
+ // src/types/messages.ts
297
+ var DEFAULT_OPTIONS = {
298
+ url: "ws://localhost:8585",
299
+ autoConnect: true,
300
+ reconnectDelay: 1e3,
301
+ maxReconnectDelay: 3e4,
302
+ pingInterval: 3e4
303
+ };
304
+
305
+ // src/core/client.ts
306
+ var CleverenceEdge = class _CleverenceEdge extends EventEmitter {
307
+ constructor(options = {}) {
308
+ super();
309
+ this._capabilities = null;
310
+ this.options = { ...DEFAULT_OPTIONS, ...options };
311
+ this.ws = new WebSocketManager(this.options.url, {
312
+ reconnect: true,
313
+ reconnectDelay: this.options.reconnectDelay,
314
+ maxReconnectDelay: this.options.maxReconnectDelay,
315
+ pingInterval: this.options.pingInterval
316
+ });
317
+ this.setupWebSocketHandlers();
318
+ if (this.options.autoConnect) {
319
+ setTimeout(() => this.connect().catch(() => {
320
+ }), 0);
321
+ }
322
+ }
323
+ /**
324
+ * Current connection state
325
+ */
326
+ get connectionState() {
327
+ return this.ws.state;
328
+ }
329
+ /**
330
+ * Whether currently connected to the Edge service
331
+ */
332
+ get isConnected() {
333
+ return this.ws.isConnected;
334
+ }
335
+ /**
336
+ * Cached device capabilities (available after connect)
337
+ */
338
+ get capabilities() {
339
+ return this._capabilities;
340
+ }
341
+ /**
342
+ * Connect to the Edge service
343
+ */
344
+ async connect() {
345
+ await this.ws.connect();
346
+ }
347
+ /**
348
+ * Disconnect from the Edge service
349
+ */
350
+ disconnect() {
351
+ this.ws.disconnect();
352
+ }
353
+ // ─────────────────────────────────────────────────────────────────────────────
354
+ // Commands
355
+ // ─────────────────────────────────────────────────────────────────────────────
356
+ /**
357
+ * Trigger a barcode scan programmatically
358
+ */
359
+ async triggerScan() {
360
+ this.ensureConnected();
361
+ this.ws.command({ type: "command", command: "trigger_scan" });
362
+ }
363
+ /**
364
+ * Set enabled barcode symbologies
365
+ */
366
+ async setSymbologies(symbologies) {
367
+ this.ensureConnected();
368
+ this.ws.command({ type: "command", command: "set_symbologies", symbologies });
369
+ }
370
+ /**
371
+ * Start RFID inventory (continuous reading)
372
+ */
373
+ async startRfidInventory(options) {
374
+ this.ensureConnected();
375
+ this.ws.command({ type: "command", command: "start_rfid_inventory", options });
376
+ }
377
+ /**
378
+ * Stop RFID inventory
379
+ */
380
+ async stopRfidInventory() {
381
+ this.ensureConnected();
382
+ this.ws.command({ type: "command", command: "stop_rfid_inventory" });
383
+ }
384
+ // ─────────────────────────────────────────────────────────────────────────────
385
+ // Queries
386
+ // ─────────────────────────────────────────────────────────────────────────────
387
+ /**
388
+ * Get current Edge service status
389
+ */
390
+ async getStatus() {
391
+ this.ensureConnected();
392
+ return this.ws.request("status");
393
+ }
394
+ /**
395
+ * Get device capabilities
396
+ */
397
+ async getCapabilities() {
398
+ this.ensureConnected();
399
+ return this.ws.request("capabilities");
400
+ }
401
+ /**
402
+ * Get current configuration
403
+ */
404
+ async getConfig() {
405
+ this.ensureConnected();
406
+ return this.ws.request("config");
407
+ }
408
+ /**
409
+ * Get RFID tags from current/last inventory
410
+ */
411
+ async getRfidTags() {
412
+ this.ensureConnected();
413
+ return this.ws.request("rfid_tags");
414
+ }
415
+ // ─────────────────────────────────────────────────────────────────────────────
416
+ // Static factory
417
+ // ─────────────────────────────────────────────────────────────────────────────
418
+ /**
419
+ * Create a new CleverenceEdge instance
420
+ */
421
+ static create(options) {
422
+ return new _CleverenceEdge(options);
423
+ }
424
+ // ─────────────────────────────────────────────────────────────────────────────
425
+ // Private methods
426
+ // ─────────────────────────────────────────────────────────────────────────────
427
+ setupWebSocketHandlers() {
428
+ this.ws.on("open", () => {
429
+ this.emit("connect", void 0);
430
+ this.getCapabilities().then((caps) => {
431
+ this._capabilities = caps;
432
+ this.emit("capabilities", caps);
433
+ }).catch(() => {
434
+ });
435
+ });
436
+ this.ws.on("close", () => {
437
+ this.emit("disconnect", void 0);
438
+ });
439
+ this.ws.on("error", (error) => {
440
+ this.emit("error", error);
441
+ });
442
+ this.ws.on("statechange", (state) => {
443
+ if (state === "reconnecting") {
444
+ this.emit("reconnecting", void 0);
445
+ }
446
+ });
447
+ this.ws.on("message", (message) => {
448
+ this.handleServerMessage(message);
449
+ });
450
+ }
451
+ handleServerMessage(message) {
452
+ switch (message.type) {
453
+ case "event":
454
+ this.handleEvent(message.event);
455
+ break;
456
+ case "capabilities":
457
+ this._capabilities = message.data;
458
+ this.emit("capabilities", message.data);
459
+ break;
460
+ case "error":
461
+ this.emit("error", new Error(message.message));
462
+ break;
463
+ }
464
+ }
465
+ handleEvent(event) {
466
+ const parsedEvent = {
467
+ ...event,
468
+ timestamp: typeof event.timestamp === "string" ? new Date(event.timestamp) : event.timestamp
469
+ };
470
+ if (event.type === "scan") {
471
+ this.emit("scan", parsedEvent);
472
+ } else if (event.type === "rfid") {
473
+ this.emit("rfid", parsedEvent);
474
+ }
475
+ }
476
+ ensureConnected() {
477
+ if (!this.isConnected) {
478
+ throw new Error("Not connected to Edge service. Call connect() first.");
479
+ }
480
+ }
481
+ };
482
+ var EdgeContext = createContext(null);
483
+ function EdgeProvider({ children, options, instance }) {
484
+ const [edge] = useState(() => instance ?? new CleverenceEdge(options));
485
+ const [connectionState, setConnectionState] = useState(edge.connectionState);
486
+ const [capabilities, setCapabilities] = useState(edge.capabilities);
487
+ const [error, setError] = useState(null);
488
+ useEffect(() => {
489
+ const handleConnect = () => {
490
+ setConnectionState("connected");
491
+ setError(null);
492
+ };
493
+ const handleDisconnect = () => {
494
+ setConnectionState("disconnected");
495
+ };
496
+ const handleReconnecting = () => {
497
+ setConnectionState("reconnecting");
498
+ };
499
+ const handleError = (err) => {
500
+ setError(err);
501
+ };
502
+ const handleCapabilities = (caps) => {
503
+ setCapabilities(caps);
504
+ };
505
+ edge.on("connect", handleConnect);
506
+ edge.on("disconnect", handleDisconnect);
507
+ edge.on("reconnecting", handleReconnecting);
508
+ edge.on("error", handleError);
509
+ edge.on("capabilities", handleCapabilities);
510
+ setConnectionState(edge.connectionState);
511
+ setCapabilities(edge.capabilities);
512
+ return () => {
513
+ edge.off("connect", handleConnect);
514
+ edge.off("disconnect", handleDisconnect);
515
+ edge.off("reconnecting", handleReconnecting);
516
+ edge.off("error", handleError);
517
+ edge.off("capabilities", handleCapabilities);
518
+ if (!instance) {
519
+ edge.disconnect();
520
+ }
521
+ };
522
+ }, [edge, instance]);
523
+ const value = useMemo(
524
+ () => ({
525
+ edge,
526
+ isConnected: connectionState === "connected",
527
+ connectionState,
528
+ capabilities,
529
+ error
530
+ }),
531
+ [edge, connectionState, capabilities, error]
532
+ );
533
+ return /* @__PURE__ */ jsx(EdgeContext.Provider, { value, children });
534
+ }
535
+ function useEdgeContext() {
536
+ const context = useContext(EdgeContext);
537
+ if (!context) {
538
+ throw new Error("useEdgeContext must be used within an EdgeProvider");
539
+ }
540
+ return context;
541
+ }
542
+
543
+ // src/react/useEdge.ts
544
+ function useEdge() {
545
+ return useEdgeContext();
546
+ }
547
+ function useBarcode(options = {}) {
548
+ const { maxHistory = 50, onScan } = options;
549
+ const { edge } = useEdgeContext();
550
+ const [lastScan, setLastScan] = useState(null);
551
+ const [scanHistory, setScanHistory] = useState([]);
552
+ useEffect(() => {
553
+ if (!edge) return;
554
+ const handleScan = (event) => {
555
+ setLastScan(event);
556
+ setScanHistory((prev) => {
557
+ const newHistory = [event, ...prev];
558
+ return newHistory.slice(0, maxHistory);
559
+ });
560
+ onScan?.(event);
561
+ };
562
+ edge.on("scan", handleScan);
563
+ return () => {
564
+ edge.off("scan", handleScan);
565
+ };
566
+ }, [edge, maxHistory, onScan]);
567
+ const clearHistory = useCallback(() => {
568
+ setLastScan(null);
569
+ setScanHistory([]);
570
+ }, []);
571
+ const triggerScan = useCallback(async () => {
572
+ if (!edge) throw new Error("Not connected");
573
+ await edge.triggerScan();
574
+ }, [edge]);
575
+ return {
576
+ lastScan,
577
+ scanHistory,
578
+ clearHistory,
579
+ triggerScan
580
+ };
581
+ }
582
+ function useRfid(options = {}) {
583
+ const { onRead } = options;
584
+ const { edge } = useEdgeContext();
585
+ const [lastRead, setLastRead] = useState(null);
586
+ const [tags, setTags] = useState(/* @__PURE__ */ new Map());
587
+ const [isInventoryActive, setIsInventoryActive] = useState(false);
588
+ useEffect(() => {
589
+ if (!edge) return;
590
+ const handleRfid = (event) => {
591
+ setLastRead(event);
592
+ setTags((prev) => {
593
+ const newTags = new Map(prev);
594
+ const existing = newTags.get(event.epc);
595
+ const now = /* @__PURE__ */ new Date();
596
+ if (existing) {
597
+ newTags.set(event.epc, {
598
+ ...existing,
599
+ rssi: event.rssi,
600
+ antenna: event.antenna,
601
+ readCount: existing.readCount + 1,
602
+ lastSeen: now
603
+ });
604
+ } else {
605
+ newTags.set(event.epc, {
606
+ epc: event.epc,
607
+ rssi: event.rssi,
608
+ antenna: event.antenna,
609
+ readCount: 1,
610
+ firstSeen: now,
611
+ lastSeen: now
612
+ });
613
+ }
614
+ return newTags;
615
+ });
616
+ onRead?.(event);
617
+ };
618
+ edge.on("rfid", handleRfid);
619
+ return () => {
620
+ edge.off("rfid", handleRfid);
621
+ };
622
+ }, [edge, onRead]);
623
+ const startInventory = useCallback(async (inventoryOptions) => {
624
+ if (!edge) throw new Error("Not connected");
625
+ await edge.startRfidInventory(inventoryOptions);
626
+ setIsInventoryActive(true);
627
+ }, [edge]);
628
+ const stopInventory = useCallback(async () => {
629
+ if (!edge) throw new Error("Not connected");
630
+ await edge.stopRfidInventory();
631
+ setIsInventoryActive(false);
632
+ }, [edge]);
633
+ const clearTags = useCallback(() => {
634
+ setLastRead(null);
635
+ setTags(/* @__PURE__ */ new Map());
636
+ }, []);
637
+ return {
638
+ lastRead,
639
+ tags,
640
+ isInventoryActive,
641
+ startInventory,
642
+ stopInventory,
643
+ clearTags
644
+ };
645
+ }
646
+
647
+ export { EdgeProvider, useBarcode, useEdge, useRfid };
648
+ //# sourceMappingURL=index.js.map
649
+ //# sourceMappingURL=index.js.map