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