@chirpier/chirpier-js 0.1.1 → 0.1.2

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.
@@ -1,5 +1,2 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
1
  export {};
5
2
  //# sourceMappingURL=sdk.test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sdk.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
1
+ {"version":3,"file":"sdk.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sdk.test.ts"],"names":[],"mappings":""}
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * @jest-environment jsdom
4
- */
5
2
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
6
3
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
7
4
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -47,6 +44,8 @@ var constants_1 = require("../constants");
47
44
  var axios_mock_adapter_1 = __importDefault(require("axios-mock-adapter"));
48
45
  var axios_1 = __importDefault(require("axios"));
49
46
  var server_1 = require("./mocks/server");
47
+ var uuid_1 = require("@lukeed/uuid");
48
+ jest.mock("@lukeed/uuid");
50
49
  describe("Chirpier SDK", function () {
51
50
  var chirpier;
52
51
  afterEach(function () {
@@ -88,10 +87,17 @@ describe("Chirpier SDK", function () {
88
87
  mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
89
88
  expect(function () { return new index_1.Chirpier({}); }).toThrow(index_1.ChirpierError);
90
89
  });
90
+ test("should throw error if key is not a valid JWT", function () {
91
+ expect(function () { return (0, index_1.initialize)({ key: "invalid_key" }); }).toThrow(index_1.ChirpierError);
92
+ });
93
+ test("should initialize successfully with a valid JWT", function () {
94
+ var validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
95
+ expect(function () { return (0, index_1.initialize)({ key: validJWT }); }).not.toThrow();
96
+ });
91
97
  });
92
98
  describe("monitor", function () {
93
99
  test("event should be sent", function () { return __awaiter(void 0, void 0, void 0, function () {
94
- var mock, event;
100
+ var mock, validJWT, event;
95
101
  return __generator(this, function (_a) {
96
102
  switch (_a.label) {
97
103
  case 0:
@@ -100,6 +106,8 @@ describe("Chirpier SDK", function () {
100
106
  });
101
107
  mock = new axios_mock_adapter_1.default(axios_1.default);
102
108
  mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
109
+ validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
110
+ (0, index_1.initialize)({ key: validJWT });
103
111
  event = {
104
112
  group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
105
113
  stream: "test-stream",
@@ -108,7 +116,9 @@ describe("Chirpier SDK", function () {
108
116
  return [4 /*yield*/, chirpier.monitor(event)];
109
117
  case 1:
110
118
  _a.sent();
111
- // Verify that the event was sent successfully
119
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1000); })];
120
+ case 2:
121
+ _a.sent(); // Wait for flush
112
122
  expect(mock.history.post.length).toBe(1);
113
123
  expect(mock.history.post[0].url).toBe(constants_1.DEFAULT_API_ENDPOINT);
114
124
  expect(JSON.parse(mock.history.post[0].data)).toEqual([
@@ -116,7 +126,6 @@ describe("Chirpier SDK", function () {
116
126
  group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
117
127
  stream: "test-stream",
118
128
  value: 1,
119
- event_id: expect.any(String),
120
129
  },
121
130
  ]);
122
131
  // Clean up the mock
@@ -125,11 +134,8 @@ describe("Chirpier SDK", function () {
125
134
  }
126
135
  });
127
136
  }); });
128
- // Setup mock server
129
- var mock = new axios_mock_adapter_1.default(axios_1.default);
130
- mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
131
137
  test("should throw error for invalid event", function () { return __awaiter(void 0, void 0, void 0, function () {
132
- var invalidEvent;
138
+ var invalidEvent, mock;
133
139
  return __generator(this, function (_a) {
134
140
  switch (_a.label) {
135
141
  case 0:
@@ -142,7 +148,110 @@ describe("Chirpier SDK", function () {
142
148
  return [4 /*yield*/, expect(chirpier.monitor(invalidEvent)).rejects.toThrow(index_1.ChirpierError)];
143
149
  case 1:
144
150
  _a.sent();
145
- // Clean up the mock
151
+ mock = new axios_mock_adapter_1.default(axios_1.default);
152
+ mock.reset();
153
+ return [2 /*return*/];
154
+ }
155
+ });
156
+ }); });
157
+ test("should batch events and flush when batch size is reached", function () { return __awaiter(void 0, void 0, void 0, function () {
158
+ var mock, validJWT, event;
159
+ return __generator(this, function (_a) {
160
+ switch (_a.label) {
161
+ case 0:
162
+ mock = new axios_mock_adapter_1.default(axios_1.default);
163
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
164
+ validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
165
+ (0, index_1.initialize)({ key: validJWT, batchSize: 2 });
166
+ event = {
167
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
168
+ stream: "test-stream",
169
+ value: 1,
170
+ };
171
+ (0, index_1.monitor)(event);
172
+ (0, index_1.monitor)(event);
173
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 100); })];
174
+ case 1:
175
+ _a.sent(); // Wait for flush
176
+ expect(mock.history.post.length).toBe(1);
177
+ expect(JSON.parse(mock.history.post[0].data).length).toBe(2);
178
+ mock.reset();
179
+ return [2 /*return*/];
180
+ }
181
+ });
182
+ }); });
183
+ test("should flush events after interval", function () { return __awaiter(void 0, void 0, void 0, function () {
184
+ var mock, validJWT, event;
185
+ return __generator(this, function (_a) {
186
+ switch (_a.label) {
187
+ case 0:
188
+ mock = new axios_mock_adapter_1.default(axios_1.default);
189
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
190
+ validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
191
+ (0, index_1.initialize)({ key: validJWT, flushInterval: 100 });
192
+ event = {
193
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
194
+ stream: "test-stream",
195
+ value: 1,
196
+ };
197
+ (0, index_1.monitor)(event);
198
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1000); })];
199
+ case 1:
200
+ _a.sent(); // Wait for flush
201
+ expect(mock.history.post.length).toBe(1);
202
+ expect(JSON.parse(mock.history.post[0].data).length).toBe(1);
203
+ mock.reset();
204
+ return [2 /*return*/];
205
+ }
206
+ });
207
+ }); });
208
+ test("should use provided event_id if available", function () { return __awaiter(void 0, void 0, void 0, function () {
209
+ var mock, validJWT, event;
210
+ return __generator(this, function (_a) {
211
+ switch (_a.label) {
212
+ case 0:
213
+ mock = new axios_mock_adapter_1.default(axios_1.default);
214
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
215
+ validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
216
+ (0, index_1.initialize)({ key: validJWT });
217
+ event = {
218
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
219
+ stream: "test-stream",
220
+ value: 1,
221
+ event_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
222
+ };
223
+ (0, index_1.monitor)(event);
224
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1000); })];
225
+ case 1:
226
+ _a.sent(); // Wait for flush
227
+ expect(mock.history.post.length).toBe(1);
228
+ expect(JSON.parse(mock.history.post[0].data)[0].event_id).toBe("f3438ee9-b964-48aa-b938-a803df440a3c");
229
+ mock.reset();
230
+ return [2 /*return*/];
231
+ }
232
+ });
233
+ }); });
234
+ test("should generate event_id if not provided", function () { return __awaiter(void 0, void 0, void 0, function () {
235
+ var mock, validJWT, event;
236
+ return __generator(this, function (_a) {
237
+ switch (_a.label) {
238
+ case 0:
239
+ mock = new axios_mock_adapter_1.default(axios_1.default);
240
+ mock.onPost(constants_1.DEFAULT_API_ENDPOINT).reply(200, { success: true });
241
+ uuid_1.v4.mockReturnValue("generated-uuid");
242
+ validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
243
+ (0, index_1.initialize)({ key: validJWT });
244
+ event = {
245
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
246
+ stream: "test-stream",
247
+ value: 1,
248
+ };
249
+ (0, index_1.monitor)(event);
250
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1000); })];
251
+ case 1:
252
+ _a.sent(); // Wait for flush
253
+ expect(mock.history.post.length).toBe(1);
254
+ expect(JSON.parse(mock.history.post[0].data)[0].event_id).toBe("generated-uuid");
146
255
  mock.reset();
147
256
  return [2 /*return*/];
148
257
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ interface Options {
3
3
  apiEndpoint?: string;
4
4
  retries?: number;
5
5
  timeout?: number;
6
+ batchSize?: number;
7
+ flushInterval?: number;
6
8
  }
7
9
  export interface Event {
8
10
  group_id: string;
@@ -22,11 +24,15 @@ export declare class Chirpier {
22
24
  private readonly retries;
23
25
  private readonly timeout;
24
26
  private readonly axiosInstance;
27
+ private eventQueue;
28
+ private flushTimeout;
29
+ private readonly batchSize;
30
+ private readonly flushInterval;
25
31
  /**
26
32
  * Initializes a new instance of the Chirpier class.
27
33
  * @param options - Configuration options for the SDK.
28
34
  */
29
- constructor({ key, apiEndpoint, retries, timeout, }: Options);
35
+ constructor({ key, apiEndpoint, retries, timeout, batchSize, flushInterval, }: Options);
30
36
  /**
31
37
  * Validates the event structure.
32
38
  * @param event - The event to validate.
@@ -34,15 +40,14 @@ export declare class Chirpier {
34
40
  */
35
41
  private isValidEvent;
36
42
  /**
37
- * Monitors an event by sending it to the API or storing it for retry.
43
+ * Monitors an event by adding it to the queue and scheduling a flush if necessary.
38
44
  * @param event - The event to monitor.
39
45
  */
40
46
  monitor(event: Event): Promise<void>;
41
47
  /**
42
- * Sends an event to the API.
43
- * @param event - The event to send.
48
+ * Flushes the event queue by sending all events to the API.
44
49
  */
45
- private sendEvent;
50
+ private flushQueue;
46
51
  /**
47
52
  * Sends multiple events to the API in a batch.
48
53
  * @param events - The array of events to send.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAE9C;;;OAGG;gBACS,EACV,GAAG,EACH,WAAkC,EAClC,OAAyB,EACzB,OAAyB,GAC1B,EAAE,OAAO;IAuCV;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAUpB;;;OAGG;IACU,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBjD;;;OAGG;YACW,SAAS;IAIvB;;;OAGG;YACW,UAAU;CAGzB;AAwCD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkBjD;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAS1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAGD,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,UAAU,CAAe;IACjC,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC;;;OAGG;gBACS,EACV,GAAG,EACH,WAAkC,EAClC,OAAyB,EACzB,OAAyB,EACzB,SAAe,EACf,aAAmB,GACpB,EAAE,OAAO;IAyCV;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAapB;;;OAGG;IACU,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BjD;;OAEG;YACW,UAAU;IA6BxB;;;OAGG;YACW,UAAU;CAGzB;AAwCD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkBjD;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAS1C"}
package/dist/index.js CHANGED
@@ -61,6 +61,15 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
61
61
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
62
62
  }
63
63
  };
64
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
65
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
66
+ if (ar || !(i in from)) {
67
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
68
+ ar[i] = from[i];
69
+ }
70
+ }
71
+ return to.concat(ar || Array.prototype.slice.call(from));
72
+ };
64
73
  var __importDefault = (this && this.__importDefault) || function (mod) {
65
74
  return (mod && mod.__esModule) ? mod : { "default": mod };
66
75
  };
@@ -93,7 +102,9 @@ var Chirpier = /** @class */ (function () {
93
102
  * @param options - Configuration options for the SDK.
94
103
  */
95
104
  function Chirpier(_a) {
96
- var key = _a.key, _b = _a.apiEndpoint, apiEndpoint = _b === void 0 ? constants_1.DEFAULT_API_ENDPOINT : _b, _c = _a.retries, retries = _c === void 0 ? constants_1.DEFAULT_RETRIES : _c, _d = _a.timeout, timeout = _d === void 0 ? constants_1.DEFAULT_TIMEOUT : _d;
105
+ var key = _a.key, _b = _a.apiEndpoint, apiEndpoint = _b === void 0 ? constants_1.DEFAULT_API_ENDPOINT : _b, _c = _a.retries, retries = _c === void 0 ? constants_1.DEFAULT_RETRIES : _c, _d = _a.timeout, timeout = _d === void 0 ? constants_1.DEFAULT_TIMEOUT : _d, _e = _a.batchSize, batchSize = _e === void 0 ? 100 : _e, _f = _a.flushInterval, flushInterval = _f === void 0 ? 500 : _f;
106
+ this.eventQueue = [];
107
+ this.flushTimeout = null;
97
108
  if (!key || typeof key !== "string") {
98
109
  throw new ChirpierError("API key is required and must be a string");
99
110
  }
@@ -101,6 +112,8 @@ var Chirpier = /** @class */ (function () {
101
112
  this.apiEndpoint = apiEndpoint;
102
113
  this.retries = retries;
103
114
  this.timeout = timeout;
115
+ this.batchSize = batchSize;
116
+ this.flushInterval = flushInterval;
104
117
  // Create axios instance with authorization header
105
118
  this.axiosInstance = axios_1.default.create({
106
119
  headers: { Authorization: "Bearer ".concat(this.apiKey) },
@@ -114,8 +127,8 @@ var Chirpier = /** @class */ (function () {
114
127
  // Apply axios-retry to your Axios instance
115
128
  (0, axios_retry_1.default)(this.axiosInstance, {
116
129
  retries: this.retries,
117
- retryDelay: function (retryCount, error) {
118
- return Math.pow(2, retryCount) * 2000; // Exponential backoff starting at 1 second
130
+ retryDelay: function (retryCount) {
131
+ return Math.pow(2, retryCount) * 1000; // Exponential backoff starting at 1 second
119
132
  },
120
133
  retryCondition: function (error) {
121
134
  return (axios_retry_1.default.isNetworkError(error) || axios_retry_1.default.isRetryableError(error));
@@ -129,54 +142,76 @@ var Chirpier = /** @class */ (function () {
129
142
  * @returns True if valid, false otherwise.
130
143
  */
131
144
  Chirpier.prototype.isValidEvent = function (event) {
132
- return (typeof event.group_id === "string" && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(event.group_id) &&
145
+ return (typeof event.group_id === "string" &&
146
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(event.group_id) &&
133
147
  event.group_id.trim().length > 0 &&
134
148
  typeof event.stream === "string" &&
135
149
  event.stream.trim().length > 0 &&
136
150
  typeof event.value === "number");
137
151
  };
138
152
  /**
139
- * Monitors an event by sending it to the API or storing it for retry.
153
+ * Monitors an event by adding it to the queue and scheduling a flush if necessary.
140
154
  * @param event - The event to monitor.
141
155
  */
142
156
  Chirpier.prototype.monitor = function (event) {
143
157
  return __awaiter(this, void 0, void 0, function () {
144
- var eventWithID, error_1;
158
+ var eventWithID;
159
+ var _this = this;
160
+ return __generator(this, function (_a) {
161
+ if (!this.apiKey) {
162
+ throw new ChirpierError("Chirpier SDK must be initialized before calling monitor()");
163
+ }
164
+ if (!this.isValidEvent(event)) {
165
+ throw new ChirpierError("Invalid event format. Must include group_id, stream, and numeric value.");
166
+ }
167
+ eventWithID = __assign(__assign({}, event), { event_id: event.event_id || (0, uuid_1.v4)() });
168
+ this.eventQueue.push(eventWithID);
169
+ if (this.eventQueue.length >= this.batchSize) {
170
+ this.flushQueue();
171
+ }
172
+ else if (!this.flushTimeout) {
173
+ this.flushTimeout = setTimeout(function () { return _this.flushQueue(); }, this.flushInterval);
174
+ }
175
+ return [2 /*return*/];
176
+ });
177
+ });
178
+ };
179
+ /**
180
+ * Flushes the event queue by sending all events to the API.
181
+ */
182
+ Chirpier.prototype.flushQueue = function () {
183
+ return __awaiter(this, void 0, void 0, function () {
184
+ var eventsToSend, error_1;
185
+ var _this = this;
145
186
  return __generator(this, function (_a) {
146
187
  switch (_a.label) {
147
188
  case 0:
148
- if (!this.isValidEvent(event)) {
149
- throw new ChirpierError("Invalid event format. Must include group_id, stream, and numeric value.");
189
+ if (this.eventQueue.length === 0) {
190
+ return [2 /*return*/];
150
191
  }
151
- eventWithID = __assign(__assign({}, event), { event_id: event.event_id || (0, uuid_1.v4)() });
192
+ if (this.flushTimeout) {
193
+ clearTimeout(this.flushTimeout);
194
+ this.flushTimeout = null;
195
+ }
196
+ eventsToSend = __spreadArray([], this.eventQueue, true);
197
+ this.eventQueue = [];
152
198
  _a.label = 1;
153
199
  case 1:
154
200
  _a.trys.push([1, 3, , 4]);
155
- return [4 /*yield*/, this.sendEvent(eventWithID)];
201
+ return [4 /*yield*/, this.sendEvents(eventsToSend)];
156
202
  case 2:
157
203
  _a.sent();
158
- console.info("Event successfully sent:", eventWithID.event_id);
204
+ console.info("Successfully sent ".concat(eventsToSend.length, " events"));
159
205
  return [3 /*break*/, 4];
160
206
  case 3:
161
207
  error_1 = _a.sent();
162
- console.error("Failed to send event after retries:", error_1);
208
+ console.error("Failed to send events:", error_1);
163
209
  return [3 /*break*/, 4];
164
- case 4: return [2 /*return*/];
165
- }
166
- });
167
- });
168
- };
169
- /**
170
- * Sends an event to the API.
171
- * @param event - The event to send.
172
- */
173
- Chirpier.prototype.sendEvent = function (event) {
174
- return __awaiter(this, void 0, void 0, function () {
175
- return __generator(this, function (_a) {
176
- switch (_a.label) {
177
- case 0: return [4 /*yield*/, this.sendEvents([event])];
178
- case 1:
179
- _a.sent();
210
+ case 4:
211
+ // Schedule next flush if there are more events
212
+ if (this.eventQueue.length > 0) {
213
+ this.flushTimeout = setTimeout(function () { return _this.flushQueue(); }, this.flushInterval);
214
+ }
180
215
  return [2 /*return*/];
181
216
  }
182
217
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chirpier/chirpier-js",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Chirpier SDK for JavaScript",
5
5
  "keywords": [
6
6
  "chirpier",
@@ -30,7 +30,6 @@
30
30
  "@lukeed/uuid": "^2.0.1",
31
31
  "axios": "^0.24.0",
32
32
  "axios-retry": "^4.5.0",
33
- "jest-environment-jsdom": "^29.7.0",
34
33
  "js-base64": "^3.7.7",
35
34
  "ts-node": "^10.9.2",
36
35
  "tslib": "^2.3.0"
@@ -39,7 +38,7 @@
39
38
  "@jest/globals": "^29.7.0",
40
39
  "@types/jest": "^29.5.13",
41
40
  "@types/mocha": "^10.0.8",
42
- "@types/node": "^22.5.0",
41
+ "@types/node": "^22.7.5",
43
42
  "@types/uuid": "^10.0.0",
44
43
  "axios-mock-adapter": "^2.0.0",
45
44
  "jest": "^29.7.0",
@@ -1,12 +1,11 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
-
5
- import { Chirpier, ChirpierError, Event } from "../index";
1
+ import { Chirpier, ChirpierError, Event, initialize, monitor } from "../index";
6
2
  import { DEFAULT_API_ENDPOINT, DEFAULT_RETRIES } from "../constants";
7
3
  import MockAdapter from "axios-mock-adapter";
8
4
  import axios from "axios";
9
5
  import { cleanupMockServer } from "./mocks/server";
6
+ import { v4 as uuidv4 } from "@lukeed/uuid";
7
+
8
+ jest.mock("@lukeed/uuid");
10
9
 
11
10
  describe("Chirpier SDK", () => {
12
11
  let chirpier: Chirpier;
@@ -60,6 +59,15 @@ describe("Chirpier SDK", () => {
60
59
 
61
60
  expect(() => new Chirpier({} as any)).toThrow(ChirpierError);
62
61
  });
62
+
63
+ test("should throw error if key is not a valid JWT", () => {
64
+ expect(() => initialize({ key: "invalid_key" })).toThrow(ChirpierError);
65
+ });
66
+
67
+ test("should initialize successfully with a valid JWT", () => {
68
+ const validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
69
+ expect(() => initialize({ key: validJWT })).not.toThrow();
70
+ });
63
71
  });
64
72
 
65
73
  describe("monitor", () => {
@@ -72,6 +80,9 @@ describe("Chirpier SDK", () => {
72
80
  const mock = new MockAdapter(axios);
73
81
  mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
74
82
 
83
+ const validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
84
+ initialize({ key: validJWT });
85
+
75
86
  const event: Event = {
76
87
  group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
77
88
  stream: "test-stream",
@@ -80,7 +91,8 @@ describe("Chirpier SDK", () => {
80
91
 
81
92
  await chirpier.monitor(event);
82
93
 
83
- // Verify that the event was sent successfully
94
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for flush
95
+
84
96
  expect(mock.history.post.length).toBe(1);
85
97
  expect(mock.history.post[0].url).toBe(DEFAULT_API_ENDPOINT);
86
98
  expect(JSON.parse(mock.history.post[0].data)).toEqual([
@@ -88,7 +100,6 @@ describe("Chirpier SDK", () => {
88
100
  group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
89
101
  stream: "test-stream",
90
102
  value: 1,
91
- event_id: expect.any(String),
92
103
  },
93
104
  ]);
94
105
 
@@ -96,10 +107,6 @@ describe("Chirpier SDK", () => {
96
107
  mock.reset();
97
108
  });
98
109
 
99
- // Setup mock server
100
- const mock = new MockAdapter(axios);
101
- mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
102
-
103
110
  test("should throw error for invalid event", async () => {
104
111
  chirpier = new Chirpier({
105
112
  key: "api_key",
@@ -112,7 +119,104 @@ describe("Chirpier SDK", () => {
112
119
  );
113
120
 
114
121
  // Clean up the mock
122
+ const mock = new MockAdapter(axios);
123
+ mock.reset();
124
+ });
125
+
126
+ test("should batch events and flush when batch size is reached", async () => {
127
+ const mock = new MockAdapter(axios);
128
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
129
+
130
+ const validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
131
+ initialize({ key: validJWT, batchSize: 2 });
132
+
133
+ const event: Event = {
134
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
135
+ stream: "test-stream",
136
+ value: 1,
137
+ };
138
+
139
+ monitor(event);
140
+ monitor(event);
141
+
142
+ await new Promise(resolve => setTimeout(resolve, 100)); // Wait for flush
143
+
144
+ expect(mock.history.post.length).toBe(1);
145
+ expect(JSON.parse(mock.history.post[0].data).length).toBe(2);
146
+
147
+ mock.reset();
148
+ });
149
+
150
+ test("should flush events after interval", async () => {
151
+ const mock = new MockAdapter(axios);
152
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
153
+
154
+ const validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
155
+ initialize({ key: validJWT, flushInterval: 100 });
156
+
157
+ const event: Event = {
158
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
159
+ stream: "test-stream",
160
+ value: 1,
161
+ };
162
+
163
+ monitor(event);
164
+
165
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for flush
166
+
167
+ expect(mock.history.post.length).toBe(1);
168
+ expect(JSON.parse(mock.history.post[0].data).length).toBe(1);
169
+
170
+ mock.reset();
171
+ });
172
+
173
+ test("should use provided event_id if available", async () => {
174
+ const mock = new MockAdapter(axios);
175
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
176
+
177
+ const validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
178
+ initialize({ key: validJWT });
179
+
180
+ const event: Event = {
181
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
182
+ stream: "test-stream",
183
+ value: 1,
184
+ event_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
185
+ };
186
+
187
+ monitor(event);
188
+
189
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for flush
190
+
191
+ expect(mock.history.post.length).toBe(1);
192
+ expect(JSON.parse(mock.history.post[0].data)[0].event_id).toBe("f3438ee9-b964-48aa-b938-a803df440a3c");
193
+
194
+ mock.reset();
195
+ });
196
+
197
+ test("should generate event_id if not provided", async () => {
198
+ const mock = new MockAdapter(axios);
199
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
200
+
201
+ (uuidv4 as jest.Mock).mockReturnValue("generated-uuid");
202
+
203
+ const validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
204
+ initialize({ key: validJWT });
205
+
206
+ const event: Event = {
207
+ group_id: "f3438ee9-b964-48aa-b938-a803df440a3c",
208
+ stream: "test-stream",
209
+ value: 1,
210
+ };
211
+
212
+ monitor(event);
213
+
214
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for flush
215
+
216
+ expect(mock.history.post.length).toBe(1);
217
+ expect(JSON.parse(mock.history.post[0].data)[0].event_id).toBe("generated-uuid");
218
+
115
219
  mock.reset();
116
220
  });
117
221
  });
118
- });
222
+ });
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import axios, { AxiosInstance } from "axios";
3
3
  import axiosRetry from "axios-retry";
4
4
  import { v4 as uuidv4 } from "@lukeed/uuid";
5
- import { Base64 } from 'js-base64';
5
+ import { Base64 } from "js-base64";
6
6
  import {
7
7
  DEFAULT_API_ENDPOINT,
8
8
  DEFAULT_RETRIES,
@@ -15,6 +15,8 @@ interface Options {
15
15
  apiEndpoint?: string;
16
16
  retries?: number;
17
17
  timeout?: number;
18
+ batchSize?: number;
19
+ flushInterval?: number;
18
20
  }
19
21
 
20
22
  // Define the Event interface for monitoring
@@ -43,6 +45,10 @@ export class Chirpier {
43
45
  private readonly retries: number;
44
46
  private readonly timeout: number;
45
47
  private readonly axiosInstance: AxiosInstance;
48
+ private eventQueue: Event[] = [];
49
+ private flushTimeout: NodeJS.Timeout | null = null;
50
+ private readonly batchSize: number;
51
+ private readonly flushInterval: number;
46
52
 
47
53
  /**
48
54
  * Initializes a new instance of the Chirpier class.
@@ -53,6 +59,8 @@ export class Chirpier {
53
59
  apiEndpoint = DEFAULT_API_ENDPOINT,
54
60
  retries = DEFAULT_RETRIES,
55
61
  timeout = DEFAULT_TIMEOUT,
62
+ batchSize = 100,
63
+ flushInterval = 500,
56
64
  }: Options) {
57
65
  if (!key || typeof key !== "string") {
58
66
  throw new ChirpierError("API key is required and must be a string");
@@ -61,6 +69,8 @@ export class Chirpier {
61
69
  this.apiEndpoint = apiEndpoint;
62
70
  this.retries = retries;
63
71
  this.timeout = timeout;
72
+ this.batchSize = batchSize;
73
+ this.flushInterval = flushInterval;
64
74
 
65
75
  // Create axios instance with authorization header
66
76
  this.axiosInstance = axios.create({
@@ -80,8 +90,8 @@ export class Chirpier {
80
90
  // Apply axios-retry to your Axios instance
81
91
  axiosRetry(this.axiosInstance, {
82
92
  retries: this.retries,
83
- retryDelay: (retryCount, error) => {
84
- return Math.pow(2, retryCount) * 2000; // Exponential backoff starting at 1 second
93
+ retryDelay: (retryCount) => {
94
+ return Math.pow(2, retryCount) * 1000; // Exponential backoff starting at 1 second
85
95
  },
86
96
  retryCondition: (error) => {
87
97
  return (
@@ -99,7 +109,10 @@ export class Chirpier {
99
109
  */
100
110
  private isValidEvent(event: Event): boolean {
101
111
  return (
102
- typeof event.group_id === "string" && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(event.group_id) &&
112
+ typeof event.group_id === "string" &&
113
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
114
+ event.group_id
115
+ ) &&
103
116
  event.group_id.trim().length > 0 &&
104
117
  typeof event.stream === "string" &&
105
118
  event.stream.trim().length > 0 &&
@@ -108,10 +121,14 @@ export class Chirpier {
108
121
  }
109
122
 
110
123
  /**
111
- * Monitors an event by sending it to the API or storing it for retry.
124
+ * Monitors an event by adding it to the queue and scheduling a flush if necessary.
112
125
  * @param event - The event to monitor.
113
126
  */
114
127
  public async monitor(event: Event): Promise<void> {
128
+ if (!this.apiKey) {
129
+ throw new ChirpierError("Chirpier SDK must be initialized before calling monitor()");
130
+ }
131
+
115
132
  if (!this.isValidEvent(event)) {
116
133
  throw new ChirpierError(
117
134
  "Invalid event format. Must include group_id, stream, and numeric value."
@@ -121,20 +138,48 @@ export class Chirpier {
121
138
  // Ensure event_id is only set once
122
139
  const eventWithID = { ...event, event_id: event.event_id || uuidv4() };
123
140
 
124
- try {
125
- await this.sendEvent(eventWithID);
126
- console.info("Event successfully sent:", eventWithID.event_id);
127
- } catch (error) {
128
- console.error("Failed to send event after retries:", error);
141
+ this.eventQueue.push(eventWithID);
142
+
143
+ if (this.eventQueue.length >= this.batchSize) {
144
+ this.flushQueue();
145
+ } else if (!this.flushTimeout) {
146
+ this.flushTimeout = setTimeout(
147
+ () => this.flushQueue(),
148
+ this.flushInterval
149
+ );
129
150
  }
130
151
  }
131
152
 
132
153
  /**
133
- * Sends an event to the API.
134
- * @param event - The event to send.
154
+ * Flushes the event queue by sending all events to the API.
135
155
  */
136
- private async sendEvent(event: Event): Promise<void> {
137
- await this.sendEvents([event]);
156
+ private async flushQueue(): Promise<void> {
157
+ if (this.eventQueue.length === 0) {
158
+ return;
159
+ }
160
+
161
+ if (this.flushTimeout) {
162
+ clearTimeout(this.flushTimeout);
163
+ this.flushTimeout = null;
164
+ }
165
+
166
+ const eventsToSend = [...this.eventQueue];
167
+ this.eventQueue = [];
168
+
169
+ try {
170
+ await this.sendEvents(eventsToSend);
171
+ console.info(`Successfully sent ${eventsToSend.length} events`);
172
+ } catch (error) {
173
+ console.error("Failed to send events:", error);
174
+ }
175
+
176
+ // Schedule next flush if there are more events
177
+ if (this.eventQueue.length > 0) {
178
+ this.flushTimeout = setTimeout(
179
+ () => this.flushQueue(),
180
+ this.flushInterval
181
+ );
182
+ }
138
183
  }
139
184
 
140
185
  /**
package/src/storage.ts DELETED
@@ -1,10 +0,0 @@
1
- export class LocalStorageStorage {
2
- save(events: any[]): void {
3
- localStorage.setItem('chirpier_events', JSON.stringify(events));
4
- }
5
-
6
- load(): any[] {
7
- const storedEvents = localStorage.getItem('chirpier_events');
8
- return storedEvents ? JSON.parse(storedEvents) : [];
9
- }
10
- }