@drakkar.software/sunglasses-storage-http 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.
@@ -0,0 +1,50 @@
1
+ import { IAnalyticsAdapter, HttpAdapterConfig, SunglassesEvent } from '@drakkar.software/sunglasses-core';
2
+
3
+ /**
4
+ * IAnalyticsAdapter that POSTs batches of events to an HTTP endpoint.
5
+ *
6
+ * Payload shape (POST body):
7
+ * ```json
8
+ * { "batch": SunglassesEvent[], "sentAt": "<ISO-8601>" }
9
+ * ```
10
+ *
11
+ * Retry policy:
12
+ * - 2xx: success
13
+ * - 4xx (except 429): non-retriable — batch discarded with warning
14
+ * - 5xx, 429, network errors, timeout: retry with exponential backoff
15
+ *
16
+ * Note: batching (how many events per call) is controlled by SunglassesCore
17
+ * via `maxBatchSize`. This adapter simply delivers whatever batch it receives.
18
+ */
19
+ declare class HttpStorageAdapter implements IAnalyticsAdapter {
20
+ private readonly config;
21
+ constructor(config: HttpAdapterConfig);
22
+ send(batch: ReadonlyArray<SunglassesEvent>): Promise<void>;
23
+ reset(): Promise<void>;
24
+ shutdown(): Promise<void>;
25
+ private postBatch;
26
+ private scheduleRetry;
27
+ }
28
+
29
+ interface RetryTask {
30
+ attempt: number;
31
+ execute: () => Promise<void>;
32
+ onExhausted: () => void;
33
+ }
34
+ interface RetryConfig {
35
+ maxRetries: number;
36
+ baseDelayMs: number;
37
+ maxDelayMs: number;
38
+ }
39
+ /**
40
+ * Schedules a task with exponential backoff + jitter.
41
+ *
42
+ * Retry delay: min(baseDelay * 2^attempt + jitter, maxDelay)
43
+ * Jitter: random value in [0, baseDelay)
44
+ *
45
+ * Non-retriable errors (HTTP 4xx except 429) must be handled by the caller
46
+ * by not enqueuing the task at all.
47
+ */
48
+ declare function scheduleRetry(task: RetryTask, config: RetryConfig): void;
49
+
50
+ export { HttpStorageAdapter, type RetryConfig, type RetryTask, scheduleRetry };
@@ -0,0 +1,50 @@
1
+ import { IAnalyticsAdapter, HttpAdapterConfig, SunglassesEvent } from '@drakkar.software/sunglasses-core';
2
+
3
+ /**
4
+ * IAnalyticsAdapter that POSTs batches of events to an HTTP endpoint.
5
+ *
6
+ * Payload shape (POST body):
7
+ * ```json
8
+ * { "batch": SunglassesEvent[], "sentAt": "<ISO-8601>" }
9
+ * ```
10
+ *
11
+ * Retry policy:
12
+ * - 2xx: success
13
+ * - 4xx (except 429): non-retriable — batch discarded with warning
14
+ * - 5xx, 429, network errors, timeout: retry with exponential backoff
15
+ *
16
+ * Note: batching (how many events per call) is controlled by SunglassesCore
17
+ * via `maxBatchSize`. This adapter simply delivers whatever batch it receives.
18
+ */
19
+ declare class HttpStorageAdapter implements IAnalyticsAdapter {
20
+ private readonly config;
21
+ constructor(config: HttpAdapterConfig);
22
+ send(batch: ReadonlyArray<SunglassesEvent>): Promise<void>;
23
+ reset(): Promise<void>;
24
+ shutdown(): Promise<void>;
25
+ private postBatch;
26
+ private scheduleRetry;
27
+ }
28
+
29
+ interface RetryTask {
30
+ attempt: number;
31
+ execute: () => Promise<void>;
32
+ onExhausted: () => void;
33
+ }
34
+ interface RetryConfig {
35
+ maxRetries: number;
36
+ baseDelayMs: number;
37
+ maxDelayMs: number;
38
+ }
39
+ /**
40
+ * Schedules a task with exponential backoff + jitter.
41
+ *
42
+ * Retry delay: min(baseDelay * 2^attempt + jitter, maxDelay)
43
+ * Jitter: random value in [0, baseDelay)
44
+ *
45
+ * Non-retriable errors (HTTP 4xx except 429) must be handled by the caller
46
+ * by not enqueuing the task at all.
47
+ */
48
+ declare function scheduleRetry(task: RetryTask, config: RetryConfig): void;
49
+
50
+ export { HttpStorageAdapter, type RetryConfig, type RetryTask, scheduleRetry };
package/dist/index.js ADDED
@@ -0,0 +1,128 @@
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
+ HttpStorageAdapter: () => HttpStorageAdapter,
24
+ scheduleRetry: () => scheduleRetry
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/RetryQueue.ts
29
+ function scheduleRetry(task, config) {
30
+ if (task.attempt >= config.maxRetries) {
31
+ task.onExhausted();
32
+ return;
33
+ }
34
+ const delay = computeDelay(task.attempt, config);
35
+ setTimeout(() => {
36
+ task.execute().catch(() => {
37
+ scheduleRetry(
38
+ { ...task, attempt: task.attempt + 1 },
39
+ config
40
+ );
41
+ });
42
+ }, delay);
43
+ }
44
+ function computeDelay(attempt, config) {
45
+ const safeAttempt = Math.min(attempt, 30);
46
+ const exponential = config.baseDelayMs * Math.pow(2, safeAttempt);
47
+ const jitter = Math.random() * config.baseDelayMs;
48
+ return Math.min(exponential + jitter, config.maxDelayMs);
49
+ }
50
+
51
+ // src/HttpStorageAdapter.ts
52
+ var DEFAULT_MAX_RETRIES = 3;
53
+ var DEFAULT_RETRY_BASE_DELAY_MS = 1e3;
54
+ var DEFAULT_RETRY_MAX_DELAY_MS = 3e4;
55
+ var DEFAULT_TIMEOUT_MS = 1e4;
56
+ var HttpStorageAdapter = class {
57
+ constructor(config) {
58
+ this.config = {
59
+ endpoint: config.endpoint,
60
+ headers: config.headers ?? {},
61
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
62
+ retryBaseDelayMs: config.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS,
63
+ retryMaxDelayMs: config.retryMaxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS,
64
+ timeout: config.timeout ?? DEFAULT_TIMEOUT_MS
65
+ };
66
+ }
67
+ async send(batch) {
68
+ if (batch.length === 0) return;
69
+ await this.postBatch(batch, 0);
70
+ }
71
+ async reset() {
72
+ }
73
+ async shutdown() {
74
+ }
75
+ async postBatch(batch, attempt) {
76
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
77
+ const timeoutId = controller ? setTimeout(() => controller.abort(), this.config.timeout) : null;
78
+ try {
79
+ const response = await fetch(this.config.endpoint, {
80
+ method: "POST",
81
+ headers: {
82
+ "Content-Type": "application/json",
83
+ ...this.config.headers
84
+ },
85
+ body: JSON.stringify({
86
+ batch,
87
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
88
+ }),
89
+ signal: controller?.signal
90
+ });
91
+ if (timeoutId !== null) clearTimeout(timeoutId);
92
+ if (response.ok) return;
93
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
94
+ console.warn(
95
+ `[SunGlasses] HttpStorageAdapter: non-retriable error ${response.status} \u2014 batch discarded`
96
+ );
97
+ return;
98
+ }
99
+ this.scheduleRetry(batch, attempt);
100
+ } catch {
101
+ if (timeoutId !== null) clearTimeout(timeoutId);
102
+ this.scheduleRetry(batch, attempt);
103
+ }
104
+ }
105
+ scheduleRetry(batch, attempt) {
106
+ scheduleRetry(
107
+ {
108
+ attempt,
109
+ execute: () => this.postBatch(batch, attempt + 1),
110
+ onExhausted: () => {
111
+ console.warn(
112
+ `[SunGlasses] HttpStorageAdapter: max retries (${this.config.maxRetries}) exceeded \u2014 batch of ${batch.length} events discarded`
113
+ );
114
+ }
115
+ },
116
+ {
117
+ maxRetries: this.config.maxRetries,
118
+ baseDelayMs: this.config.retryBaseDelayMs,
119
+ maxDelayMs: this.config.retryMaxDelayMs
120
+ }
121
+ );
122
+ }
123
+ };
124
+ // Annotate the CommonJS export names for ESM import in node:
125
+ 0 && (module.exports = {
126
+ HttpStorageAdapter,
127
+ scheduleRetry
128
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,100 @@
1
+ // src/RetryQueue.ts
2
+ function scheduleRetry(task, config) {
3
+ if (task.attempt >= config.maxRetries) {
4
+ task.onExhausted();
5
+ return;
6
+ }
7
+ const delay = computeDelay(task.attempt, config);
8
+ setTimeout(() => {
9
+ task.execute().catch(() => {
10
+ scheduleRetry(
11
+ { ...task, attempt: task.attempt + 1 },
12
+ config
13
+ );
14
+ });
15
+ }, delay);
16
+ }
17
+ function computeDelay(attempt, config) {
18
+ const safeAttempt = Math.min(attempt, 30);
19
+ const exponential = config.baseDelayMs * Math.pow(2, safeAttempt);
20
+ const jitter = Math.random() * config.baseDelayMs;
21
+ return Math.min(exponential + jitter, config.maxDelayMs);
22
+ }
23
+
24
+ // src/HttpStorageAdapter.ts
25
+ var DEFAULT_MAX_RETRIES = 3;
26
+ var DEFAULT_RETRY_BASE_DELAY_MS = 1e3;
27
+ var DEFAULT_RETRY_MAX_DELAY_MS = 3e4;
28
+ var DEFAULT_TIMEOUT_MS = 1e4;
29
+ var HttpStorageAdapter = class {
30
+ constructor(config) {
31
+ this.config = {
32
+ endpoint: config.endpoint,
33
+ headers: config.headers ?? {},
34
+ maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
35
+ retryBaseDelayMs: config.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS,
36
+ retryMaxDelayMs: config.retryMaxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS,
37
+ timeout: config.timeout ?? DEFAULT_TIMEOUT_MS
38
+ };
39
+ }
40
+ async send(batch) {
41
+ if (batch.length === 0) return;
42
+ await this.postBatch(batch, 0);
43
+ }
44
+ async reset() {
45
+ }
46
+ async shutdown() {
47
+ }
48
+ async postBatch(batch, attempt) {
49
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
50
+ const timeoutId = controller ? setTimeout(() => controller.abort(), this.config.timeout) : null;
51
+ try {
52
+ const response = await fetch(this.config.endpoint, {
53
+ method: "POST",
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ ...this.config.headers
57
+ },
58
+ body: JSON.stringify({
59
+ batch,
60
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
61
+ }),
62
+ signal: controller?.signal
63
+ });
64
+ if (timeoutId !== null) clearTimeout(timeoutId);
65
+ if (response.ok) return;
66
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
67
+ console.warn(
68
+ `[SunGlasses] HttpStorageAdapter: non-retriable error ${response.status} \u2014 batch discarded`
69
+ );
70
+ return;
71
+ }
72
+ this.scheduleRetry(batch, attempt);
73
+ } catch {
74
+ if (timeoutId !== null) clearTimeout(timeoutId);
75
+ this.scheduleRetry(batch, attempt);
76
+ }
77
+ }
78
+ scheduleRetry(batch, attempt) {
79
+ scheduleRetry(
80
+ {
81
+ attempt,
82
+ execute: () => this.postBatch(batch, attempt + 1),
83
+ onExhausted: () => {
84
+ console.warn(
85
+ `[SunGlasses] HttpStorageAdapter: max retries (${this.config.maxRetries}) exceeded \u2014 batch of ${batch.length} events discarded`
86
+ );
87
+ }
88
+ },
89
+ {
90
+ maxRetries: this.config.maxRetries,
91
+ baseDelayMs: this.config.retryBaseDelayMs,
92
+ maxDelayMs: this.config.retryMaxDelayMs
93
+ }
94
+ );
95
+ }
96
+ };
97
+ export {
98
+ HttpStorageAdapter,
99
+ scheduleRetry
100
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@drakkar.software/sunglasses-storage-http",
3
+ "version": "0.1.0",
4
+ "description": "Batched HTTP push analytics adapter for SunGlasses",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@drakkar.software/sunglasses-core": "0.2.0"
20
+ },
21
+ "devDependencies": {
22
+ "tsup": "^8.3.5",
23
+ "typescript": "^5.7.2",
24
+ "vitest": "^2.1.8",
25
+ "@drakkar.software/sunglasses-tsconfig": "0.1.0"
26
+ },
27
+ "scripts": {
28
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
29
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "eslint src/",
32
+ "test": "vitest run",
33
+ "clean": "rm -rf dist .tsbuildinfo"
34
+ }
35
+ }