@logg/signals 0.1.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/dist/index.js ADDED
@@ -0,0 +1,514 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AsyncStorageAdapter: () => AsyncStorageAdapter,
24
+ EventQueue: () => EventQueue,
25
+ LocalStorageAdapter: () => LocalStorageAdapter,
26
+ MemoryStorageAdapter: () => MemoryStorageAdapter,
27
+ Signals: () => Signals,
28
+ getDefaultStorageAdapter: () => getDefaultStorageAdapter
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/utils/helpers.ts
33
+ function uuid() {
34
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
35
+ return crypto.randomUUID();
36
+ }
37
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
38
+ const r = Math.random() * 16 | 0;
39
+ const v = c === "x" ? r : r & 3 | 8;
40
+ return v.toString(16);
41
+ });
42
+ }
43
+ function getTimestamp() {
44
+ return (/* @__PURE__ */ new Date()).toISOString();
45
+ }
46
+ function sleep(ms) {
47
+ return new Promise((resolve) => setTimeout(resolve, ms));
48
+ }
49
+ async function retry(fn, options) {
50
+ const { maxRetries, initialDelay, onRetry } = options;
51
+ let lastError;
52
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
53
+ try {
54
+ return await fn();
55
+ } catch (error) {
56
+ lastError = error instanceof Error ? error : new Error(String(error));
57
+ if (attempt < maxRetries) {
58
+ const delay = initialDelay * Math.pow(2, attempt);
59
+ onRetry?.(attempt + 1, lastError);
60
+ await sleep(delay);
61
+ }
62
+ }
63
+ }
64
+ throw lastError;
65
+ }
66
+
67
+ // src/EventQueue.ts
68
+ var STORAGE_KEY = "session_signals_queue";
69
+ var EventQueue = class {
70
+ constructor(storage, sessionId) {
71
+ this.queue = [];
72
+ this.storage = storage;
73
+ this.sessionId = sessionId;
74
+ }
75
+ /**
76
+ * Initialize queue by loading persisted events
77
+ */
78
+ async init() {
79
+ try {
80
+ const stored = await this.storage.getItem(STORAGE_KEY);
81
+ if (stored) {
82
+ const parsed = JSON.parse(stored);
83
+ this.queue = Array.isArray(parsed) ? parsed : [];
84
+ }
85
+ } catch (error) {
86
+ console.warn("Failed to load persisted events:", error);
87
+ this.queue = [];
88
+ }
89
+ }
90
+ /**
91
+ * Add event to queue
92
+ */
93
+ async add(eventData) {
94
+ const event = {
95
+ event_id: uuid(),
96
+ timestamp: getTimestamp(),
97
+ session_id: this.sessionId,
98
+ ...eventData
99
+ };
100
+ this.queue.push({
101
+ event,
102
+ timestamp: Date.now()
103
+ });
104
+ await this.persist();
105
+ return event;
106
+ }
107
+ /**
108
+ * Get all events in queue
109
+ */
110
+ getAll() {
111
+ return this.queue.map((item) => item.event);
112
+ }
113
+ /**
114
+ * Get events up to a certain size
115
+ */
116
+ getBatch(size) {
117
+ return this.queue.slice(0, size).map((item) => item.event);
118
+ }
119
+ /**
120
+ * Remove events from queue
121
+ */
122
+ async remove(count) {
123
+ this.queue.splice(0, count);
124
+ await this.persist();
125
+ }
126
+ /**
127
+ * Clear all events from queue
128
+ */
129
+ async clear() {
130
+ this.queue = [];
131
+ await this.persist();
132
+ }
133
+ /**
134
+ * Get queue size
135
+ */
136
+ size() {
137
+ return this.queue.length;
138
+ }
139
+ /**
140
+ * Check if queue is empty
141
+ */
142
+ isEmpty() {
143
+ return this.queue.length === 0;
144
+ }
145
+ /**
146
+ * Get oldest event timestamp
147
+ */
148
+ getOldestTimestamp() {
149
+ if (this.queue.length === 0) return null;
150
+ return this.queue[0]?.timestamp ?? null;
151
+ }
152
+ /**
153
+ * Persist queue to storage
154
+ */
155
+ async persist() {
156
+ try {
157
+ await this.storage.setItem(STORAGE_KEY, JSON.stringify(this.queue));
158
+ } catch (error) {
159
+ console.warn("Failed to persist events:", error);
160
+ }
161
+ }
162
+ };
163
+
164
+ // src/storage/LocalStorageAdapter.ts
165
+ var LocalStorageAdapter = class {
166
+ async getItem(key) {
167
+ try {
168
+ return localStorage.getItem(key);
169
+ } catch (error) {
170
+ console.warn("LocalStorage not available:", error);
171
+ return null;
172
+ }
173
+ }
174
+ async setItem(key, value) {
175
+ try {
176
+ localStorage.setItem(key, value);
177
+ } catch (error) {
178
+ console.warn("LocalStorage setItem failed:", error);
179
+ }
180
+ }
181
+ async removeItem(key) {
182
+ try {
183
+ localStorage.removeItem(key);
184
+ } catch (error) {
185
+ console.warn("LocalStorage removeItem failed:", error);
186
+ }
187
+ }
188
+ /**
189
+ * Check if localStorage is available
190
+ */
191
+ static isAvailable() {
192
+ try {
193
+ const test = "__localStorage_test__";
194
+ localStorage.setItem(test, test);
195
+ localStorage.removeItem(test);
196
+ return true;
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+ };
202
+
203
+ // src/storage/AsyncStorageAdapter.ts
204
+ var AsyncStorageAdapter = class {
205
+ constructor() {
206
+ try {
207
+ this.asyncStorage = require("@react-native-async-storage/async-storage").default;
208
+ } catch (error) {
209
+ throw new Error(
210
+ "AsyncStorage not found. Please install @react-native-async-storage/async-storage"
211
+ );
212
+ }
213
+ }
214
+ async getItem(key) {
215
+ try {
216
+ return await this.asyncStorage.getItem(key);
217
+ } catch (error) {
218
+ console.warn("AsyncStorage getItem failed:", error);
219
+ return null;
220
+ }
221
+ }
222
+ async setItem(key, value) {
223
+ try {
224
+ await this.asyncStorage.setItem(key, value);
225
+ } catch (error) {
226
+ console.warn("AsyncStorage setItem failed:", error);
227
+ }
228
+ }
229
+ async removeItem(key) {
230
+ try {
231
+ await this.asyncStorage.removeItem(key);
232
+ } catch (error) {
233
+ console.warn("AsyncStorage removeItem failed:", error);
234
+ }
235
+ }
236
+ /**
237
+ * Check if AsyncStorage is available
238
+ */
239
+ static isAvailable() {
240
+ try {
241
+ require("@react-native-async-storage/async-storage");
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+ };
248
+
249
+ // src/storage/MemoryStorageAdapter.ts
250
+ var MemoryStorageAdapter = class {
251
+ constructor() {
252
+ this.storage = /* @__PURE__ */ new Map();
253
+ }
254
+ async getItem(key) {
255
+ return this.storage.get(key) ?? null;
256
+ }
257
+ async setItem(key, value) {
258
+ this.storage.set(key, value);
259
+ }
260
+ async removeItem(key) {
261
+ this.storage.delete(key);
262
+ }
263
+ };
264
+
265
+ // src/storage/index.ts
266
+ function getDefaultStorageAdapter() {
267
+ if (LocalStorageAdapter.isAvailable()) {
268
+ return new LocalStorageAdapter();
269
+ }
270
+ if (AsyncStorageAdapter.isAvailable()) {
271
+ return new AsyncStorageAdapter();
272
+ }
273
+ return new MemoryStorageAdapter();
274
+ }
275
+
276
+ // src/utils/metadata.ts
277
+ function detectClientType() {
278
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
279
+ return "react-native";
280
+ }
281
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
282
+ return "web";
283
+ }
284
+ return "node";
285
+ }
286
+ function getUserAgent() {
287
+ if (typeof navigator !== "undefined") {
288
+ return navigator.userAgent;
289
+ }
290
+ return void 0;
291
+ }
292
+ function getScreenDimensions() {
293
+ if (typeof window !== "undefined" && window.screen) {
294
+ return {
295
+ width: window.screen.width,
296
+ height: window.screen.height
297
+ };
298
+ }
299
+ try {
300
+ const { Dimensions } = require("react-native");
301
+ const { width, height } = Dimensions.get("window");
302
+ return { width, height };
303
+ } catch {
304
+ return void 0;
305
+ }
306
+ }
307
+ function getLocale() {
308
+ if (typeof navigator !== "undefined") {
309
+ return navigator.language;
310
+ }
311
+ return void 0;
312
+ }
313
+ function getTimezone() {
314
+ try {
315
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
316
+ } catch {
317
+ return void 0;
318
+ }
319
+ }
320
+ function collectMetadata(version) {
321
+ const clientType = detectClientType();
322
+ const metadata = {
323
+ type: clientType,
324
+ version
325
+ };
326
+ if (clientType === "web" || clientType === "react-native") {
327
+ metadata.user_agent = getUserAgent();
328
+ metadata.screen = getScreenDimensions();
329
+ metadata.locale = getLocale();
330
+ metadata.timezone = getTimezone();
331
+ }
332
+ return metadata;
333
+ }
334
+
335
+ // src/Signals.ts
336
+ var VERSION = "0.1.0";
337
+ var DEFAULT_BATCH_SIZE = 10;
338
+ var DEFAULT_BATCH_INTERVAL = 5e3;
339
+ var DEFAULT_MAX_RETRIES = 3;
340
+ var DEFAULT_RETRY_DELAY = 1e3;
341
+ var Signals = class {
342
+ constructor(config) {
343
+ this.flushTimer = null;
344
+ this.isDestroyed = false;
345
+ this.isFlushing = false;
346
+ if (!config.apiKey) {
347
+ throw new Error("Signals: apiKey is required");
348
+ }
349
+ if (!config.endpoint) {
350
+ throw new Error("Signals: endpoint is required");
351
+ }
352
+ this.config = {
353
+ apiKey: config.apiKey,
354
+ endpoint: config.endpoint,
355
+ batchSize: config.batchSize ?? DEFAULT_BATCH_SIZE,
356
+ batchInterval: config.batchInterval ?? DEFAULT_BATCH_INTERVAL,
357
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
358
+ retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY,
359
+ debug: config.debug ?? false,
360
+ storage: config.storage ?? getDefaultStorageAdapter(),
361
+ sessionId: config.sessionId ?? uuid()
362
+ };
363
+ this.queue = new EventQueue(this.config.storage, this.config.sessionId);
364
+ this.init();
365
+ }
366
+ /**
367
+ * Initialize client
368
+ */
369
+ async init() {
370
+ await this.queue.init();
371
+ this.log("Signals initialized", {
372
+ sessionId: this.config.sessionId,
373
+ queueSize: this.queue.size()
374
+ });
375
+ this.startFlushTimer();
376
+ if (!this.queue.isEmpty()) {
377
+ await this.flush();
378
+ }
379
+ }
380
+ /**
381
+ * Track an event
382
+ */
383
+ async event(eventData) {
384
+ if (this.isDestroyed) {
385
+ throw new Error("Signals instance has been destroyed");
386
+ }
387
+ const event = await this.queue.add(eventData);
388
+ this.log("Event tracked", event);
389
+ if (this.queue.size() >= this.config.batchSize) {
390
+ await this.flush();
391
+ }
392
+ }
393
+ /**
394
+ * Manually flush events
395
+ */
396
+ async flush() {
397
+ if (this.isDestroyed || this.queue.isEmpty() || this.isFlushing) {
398
+ return;
399
+ }
400
+ this.isFlushing = true;
401
+ const events = this.queue.getBatch(this.config.batchSize);
402
+ if (events.length === 0) {
403
+ this.isFlushing = false;
404
+ return;
405
+ }
406
+ this.log(`Flushing ${events.length} events`);
407
+ const batch = {
408
+ api_key: this.config.apiKey,
409
+ batch_id: uuid(),
410
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
411
+ metadata: collectMetadata(VERSION),
412
+ events
413
+ };
414
+ try {
415
+ await this.sendBatch(batch);
416
+ await this.queue.remove(events.length);
417
+ this.log("Batch sent successfully");
418
+ } catch (error) {
419
+ this.log("Failed to send batch", error);
420
+ } finally {
421
+ this.isFlushing = false;
422
+ }
423
+ }
424
+ /**
425
+ * Send batch to backend with retry logic
426
+ */
427
+ async sendBatch(batch) {
428
+ await retry(
429
+ async () => {
430
+ const response = await fetch(this.config.endpoint, {
431
+ method: "POST",
432
+ headers: {
433
+ "Content-Type": "application/json",
434
+ "Authorization": `Bearer ${this.config.apiKey}`
435
+ },
436
+ body: JSON.stringify(batch)
437
+ });
438
+ if (!response.ok) {
439
+ throw new Error(
440
+ `HTTP ${response.status}: ${response.statusText}`
441
+ );
442
+ }
443
+ },
444
+ {
445
+ maxRetries: this.config.maxRetries,
446
+ initialDelay: this.config.retryDelay,
447
+ onRetry: (attempt, error) => {
448
+ this.log(`Retry attempt ${attempt}`, error);
449
+ }
450
+ }
451
+ );
452
+ }
453
+ /**
454
+ * Start periodic flush timer
455
+ */
456
+ startFlushTimer() {
457
+ this.stopFlushTimer();
458
+ this.flushTimer = setInterval(() => {
459
+ if (!this.queue.isEmpty()) {
460
+ this.flush().catch((error) => {
461
+ this.log("Auto-flush failed", error);
462
+ });
463
+ }
464
+ }, this.config.batchInterval);
465
+ }
466
+ /**
467
+ * Stop flush timer
468
+ */
469
+ stopFlushTimer() {
470
+ if (this.flushTimer) {
471
+ clearInterval(this.flushTimer);
472
+ this.flushTimer = null;
473
+ }
474
+ }
475
+ /**
476
+ * Get current session ID
477
+ */
478
+ getSessionId() {
479
+ return this.config.sessionId;
480
+ }
481
+ /**
482
+ * Get queue size
483
+ */
484
+ getQueueSize() {
485
+ return this.queue.size();
486
+ }
487
+ /**
488
+ * Destroy client and cleanup
489
+ */
490
+ async destroy() {
491
+ if (this.isDestroyed) return;
492
+ this.log("Destroying Signals instance");
493
+ this.isDestroyed = true;
494
+ this.stopFlushTimer();
495
+ await this.flush();
496
+ }
497
+ /**
498
+ * Debug logging
499
+ */
500
+ log(message, data) {
501
+ if (this.config.debug) {
502
+ console.log(`[LoggClient] ${message}`, data ?? "");
503
+ }
504
+ }
505
+ };
506
+ // Annotate the CommonJS export names for ESM import in node:
507
+ 0 && (module.exports = {
508
+ AsyncStorageAdapter,
509
+ EventQueue,
510
+ LocalStorageAdapter,
511
+ MemoryStorageAdapter,
512
+ Signals,
513
+ getDefaultStorageAdapter
514
+ });