@js-nerds/batch-collector 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 js-nerds
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @js-nerds/batch-collector
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@js-nerds/batch-collector)](https://www.npmjs.com/package/@js-nerds/batch-collector)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@js-nerds/batch-collector)](https://www.npmjs.com/package/@js-nerds/batch-collector)
5
+ [![tests](https://github.com/js-nerds/batch-collector/actions/workflows/build-test.yml/badge.svg?branch=main&label=tests)](https://github.com/js-nerds/batch-collector/actions/workflows/build-test.yml)
6
+ [![coverage](https://codecov.io/gh/js-nerds/batch-collector/branch/main/graph/badge.svg)](https://codecov.io/gh/js-nerds/batch-collector)
7
+
8
+ Lightweight TypeScript batch collector with delayed flush, optional timer reset, and local/session storage persistence.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @js-nerds/batch-collector
14
+ ```
15
+
16
+ ```bash
17
+ pnpm add @js-nerds/batch-collector
18
+ ```
19
+
20
+ ```bash
21
+ yarn add @js-nerds/batch-collector
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { BATCH_FLUSH_EVENT, BatchCollector } from "@js-nerds/batch-collector";
28
+
29
+ const collector = new BatchCollector<{ action: string; id: string }>({
30
+ delayMs: 5000,
31
+ resetTimerOnPush: true,
32
+ storageType: "localStorage",
33
+ storageKey: "my-app-log-batch"
34
+ });
35
+
36
+ collector.subscribe(BATCH_FLUSH_EVENT, (items) => {
37
+ console.log("Flush:", items);
38
+ });
39
+
40
+ collector.push({ action: "button_click", id: "submit-btn" });
41
+ collector.push({ action: "menu_open", id: "profile" });
42
+ ```
43
+
44
+ ## API
45
+
46
+ ### `new BatchCollector(config)`
47
+
48
+ Creates a collector instance.
49
+
50
+ **Config**
51
+ - `delayMs: number` — delay before flush.
52
+ - `resetTimerOnPush?: boolean` — reset timer on every push (default `true`).
53
+ - `storageType?: "memory" | "localStorage" | "sessionStorage"` — where to keep pending batch (default `"memory"`).
54
+ - `storageKey?: string` — key for local/session storage (default `"batch-collector-pending"`).
55
+ - `autoClear?: boolean` — clear after timer flush (default `true`).
56
+
57
+ ### `subscribe(event, callback)`
58
+
59
+ Subscribes to collector events.
60
+
61
+ **Params**
62
+ - `event: string` — use `BATCH_FLUSH_EVENT` (`"flush"`).
63
+ - `callback: (items: T[]) => void`
64
+
65
+ **Returns**
66
+ - `() => void` — unsubscribe function.
67
+
68
+ ### `push(item)`
69
+
70
+ Adds item to batch and schedules flush.
71
+
72
+ ### `items()`
73
+
74
+ Returns a shallow copy of current batch.
75
+
76
+ ### `clear()`
77
+
78
+ Clears timer, in-memory batch, and persisted batch.
79
+
80
+ ## License
81
+
82
+ MIT
83
+
84
+ ## Changelog
85
+
86
+ See `CHANGELOG.md`.
package/dist/index.cjs ADDED
@@ -0,0 +1,226 @@
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
+ BATCH_FLUSH_EVENT: () => BATCH_FLUSH_EVENT,
24
+ BatchCollector: () => BatchCollector
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var BATCH_FLUSH_EVENT = "flush";
28
+ var DEFAULT_STORAGE_KEY = "batch-collector-pending";
29
+ var BatchCollector = class {
30
+ /**
31
+ * Creates a batch collector instance.
32
+ *
33
+ * @param {BatchCollectorConfig} config - Configuration object with delay, optional reset behaviour, and optional storageType.
34
+ */
35
+ constructor(config) {
36
+ this.buffer = [];
37
+ this.timerId = null;
38
+ this.listeners = /* @__PURE__ */ new Map();
39
+ var _a, _b, _c, _d;
40
+ this.delayMs = config.delayMs;
41
+ this.resetTimerOnPush = (_a = config.resetTimerOnPush) != null ? _a : true;
42
+ this.storageType = (_b = config.storageType) != null ? _b : "memory";
43
+ this.storageKey = (_c = config.storageKey) != null ? _c : DEFAULT_STORAGE_KEY;
44
+ this.autoClear = (_d = config.autoClear) != null ? _d : true;
45
+ if (this.storageType !== "memory") {
46
+ const pending = this.readPersisted();
47
+ if (pending.length !== 0) {
48
+ this.writePersisted([]);
49
+ this.emit(BATCH_FLUSH_EVENT, pending);
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Subscribe to an event. When the batch is flushed, the "flush" event is emitted with the batched items.
55
+ *
56
+ * @param {string} event - Event name; use BATCH_FLUSH_EVENT ("flush") for flush events.
57
+ * @param {FlushListener<T>} callback - Function called with the batched items when the event is emitted.
58
+ * @returns {() => void} Unsubscribe function; call to remove the listener.
59
+ */
60
+ subscribe(event, callback) {
61
+ if (!this.listeners.has(event)) {
62
+ this.listeners.set(event, /* @__PURE__ */ new Set());
63
+ }
64
+ this.listeners.get(event).add(callback);
65
+ return () => {
66
+ var _a;
67
+ return (_a = this.listeners.get(event)) == null ? void 0 : _a.delete(callback);
68
+ };
69
+ }
70
+ /**
71
+ * Add an item to the batch and (re)start the flush timer. When storageType is not memory, also persists the buffer.
72
+ *
73
+ * @param {T} item - Item to add to the current batch.
74
+ * @returns {void}
75
+ */
76
+ push(item) {
77
+ this.buffer.push(item);
78
+ if (this.storageType !== "memory") {
79
+ this.writePersisted(this.buffer);
80
+ }
81
+ this.scheduleFlush();
82
+ }
83
+ /**
84
+ * Returns a copy of the current buffer. Does not emit, clear, or change state.
85
+ *
86
+ * @returns {T[]} Copy of all items in the batch.
87
+ */
88
+ items() {
89
+ return [...this.buffer];
90
+ }
91
+ /**
92
+ * Clears the pending timer, the buffer, and persisted storage. Does not emit.
93
+ *
94
+ * @returns {boolean} True.
95
+ */
96
+ clear() {
97
+ this.clearTimer();
98
+ this.buffer = [];
99
+ if (this.storageType !== "memory") {
100
+ this.writePersisted([]);
101
+ }
102
+ return true;
103
+ }
104
+ /**
105
+ * Returns the Storage instance for the configured storage type, or null if unavailable (e.g. SSR).
106
+ *
107
+ * @returns {Storage | null} window.localStorage, window.sessionStorage, or null.
108
+ */
109
+ getStorage() {
110
+ if (typeof window === "undefined") {
111
+ return null;
112
+ }
113
+ if (this.storageType === "localStorage") {
114
+ return window.localStorage;
115
+ }
116
+ if (this.storageType === "sessionStorage") {
117
+ return window.sessionStorage;
118
+ }
119
+ return null;
120
+ }
121
+ /**
122
+ * Reads the persisted buffer from storage.
123
+ *
124
+ * @returns {T[]} Parsed buffer from storage, or empty array if unavailable or parse error.
125
+ */
126
+ readPersisted() {
127
+ const storage = this.getStorage();
128
+ if (!storage) {
129
+ return [];
130
+ }
131
+ try {
132
+ const raw = storage.getItem(this.storageKey);
133
+ return raw ? JSON.parse(raw) : [];
134
+ } catch {
135
+ return [];
136
+ }
137
+ }
138
+ /**
139
+ * Writes the buffer to storage, or removes the key when the buffer is empty.
140
+ *
141
+ * @param {T[]} items - Current buffer to persist.
142
+ * @returns {boolean} True if write succeeded, false otherwise.
143
+ */
144
+ writePersisted(items) {
145
+ const storage = this.getStorage();
146
+ if (!storage) {
147
+ return false;
148
+ }
149
+ try {
150
+ if (items.length === 0) {
151
+ storage.removeItem(this.storageKey);
152
+ } else {
153
+ storage.setItem(this.storageKey, JSON.stringify(items));
154
+ }
155
+ return true;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+ /**
161
+ * Emits the current batch to subscribers and optionally clears buffer and storage. Used internally by the timer.
162
+ * When autoClear is false, the timer calls flush(false) so only manual clear() clears.
163
+ *
164
+ * @param {boolean} clear - If true (default), clear buffer and storage after emit. If false, only emit.
165
+ * @returns {boolean} True if the batch was emitted, false if buffer was empty.
166
+ */
167
+ flush(clear = true) {
168
+ this.clearTimer();
169
+ if (this.buffer.length === 0) {
170
+ return false;
171
+ }
172
+ const items = [...this.buffer];
173
+ if (clear) {
174
+ this.buffer = [];
175
+ if (this.storageType !== "memory") {
176
+ this.writePersisted([]);
177
+ }
178
+ }
179
+ this.emit(BATCH_FLUSH_EVENT, items);
180
+ return true;
181
+ }
182
+ /**
183
+ * Emits an event with the given payload to all subscribers of that event.
184
+ *
185
+ * @param {string} event - Event name to emit.
186
+ * @param {T[]} payload - Data to pass to each listener.
187
+ * @returns {void}
188
+ */
189
+ emit(event, payload) {
190
+ var _a;
191
+ (_a = this.listeners.get(event)) == null ? void 0 : _a.forEach((cb) => cb(payload));
192
+ }
193
+ /**
194
+ * Schedules a flush after the configured delay. If resetTimerOnPush is true, clears existing timer first.
195
+ *
196
+ * @returns {boolean} True if timer was scheduled, false if already scheduled (resetTimerOnPush false).
197
+ */
198
+ scheduleFlush() {
199
+ if (this.resetTimerOnPush) {
200
+ this.clearTimer();
201
+ } else if (this.timerId !== null) {
202
+ return false;
203
+ }
204
+ this.timerId = setTimeout(() => this.flush(this.autoClear), this.delayMs);
205
+ return true;
206
+ }
207
+ /**
208
+ * Clears the pending flush timer if one is set. Does nothing if no timer is set.
209
+ *
210
+ * @returns {boolean} True if a timer was cleared, false if there was no timer.
211
+ */
212
+ clearTimer() {
213
+ if (this.timerId !== null) {
214
+ clearTimeout(this.timerId);
215
+ this.timerId = null;
216
+ return true;
217
+ }
218
+ return false;
219
+ }
220
+ };
221
+ // Annotate the CommonJS export names for ESM import in node:
222
+ 0 && (module.exports = {
223
+ BATCH_FLUSH_EVENT,
224
+ BatchCollector
225
+ });
226
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Batches items and emits a \"flush\" event after a delay (e.g. after last push).\n * Event-based: create instance, subscribe via subscribe(BATCH_FLUSH_EVENT, callback), call push() from UI.\n * Optional persistence: storageType can be memory, localStorage, or sessionStorage so the batch survives refresh/close.\n */\n\nexport type BatchCollectorStorageType = \"memory\" | \"localStorage\" | \"sessionStorage\";\n\nexport type BatchCollectorConfig = {\n /** Delay in milliseconds before flushing the batch (e.g. 5000 = 5 seconds). */\n delayMs: number;\n /** If true, timer resets on each push (flush N ms after last push). If false, flush exactly N ms after first push. Default true. */\n resetTimerOnPush?: boolean;\n /** Where to keep the buffer: \"memory\" (default), \"localStorage\", or \"sessionStorage\". When not memory, buffer is persisted and re-flushed on next load if any. */\n storageType?: BatchCollectorStorageType;\n /** Storage key when storageType is localStorage or sessionStorage. Should be unique per collector instance. Default \"batch-collector-pending\". */\n storageKey?: string;\n /** If true (default), buffer and storage are cleared when the timer fires. If false, only on manual clear() call. */\n autoClear?: boolean;\n};\n\n/** Event name emitted when the batch is flushed. */\nexport const BATCH_FLUSH_EVENT = \"flush\";\n\ntype FlushListener<T> = (items: T[]) => void;\n\nconst DEFAULT_STORAGE_KEY = \"batch-collector-pending\";\n\n/**\n * Collects items in a buffer and emits BATCH_FLUSH_EVENT with the batched array after a delay (e.g. after the last push).\n * Use for batching UI events (clicks, logs) before sending to server or analytics. Item type is generic (objects, numbers, strings, or any T).\n *\n * Parameters (config):\n * - delayMs (required) — delay in ms before emitting the batch (e.g. 5000 = 5 seconds).\n * - resetTimerOnPush (optional) — if true (default), timer resets on each push; if false, emit exactly delayMs after first push.\n * - storageType (optional) — \"memory\" (default), \"localStorage\", or \"sessionStorage\". When not memory, batch is persisted and re-emitted on next load.\n * - storageKey (optional) — storage key when using localStorage/sessionStorage. Unique per instance. Default \"batch-collector-pending\".\n * - autoClear (optional) — if true (default), buffer and storage are cleared when the timer fires; if false, only on manual clear().\n *\n * Public API:\n * - subscribe(event, callback) — subscribe to flush events; callback receives the batch. Returns unsubscribe function.\n * - push(item) — add an item and (re)start the timer.\n * - items() — returns a copy of the current batch (optional; does not emit or clear).\n * - clear() — clears buffer and storage (optional; e.g. call from the subscriber after a successful send).\n *\n * @example\n * const collector = new BatchCollector<{ action: string; id: string }>({\n * delayMs: 5000,\n * storageType: \"localStorage\",\n * storageKey: \"my-app-log-batch\"\n * });\n * collector.subscribe(BATCH_FLUSH_EVENT, (items) => sendToBackend(items));\n * collector.push({ action: \"button_click\", id: \"submit-btn\" });\n *\n * Optional:\n * collector.items() // copy of current batch\n * collector.clear() // clear buffer and storage (e.g. after successful send in subscriber)\n */\nexport class BatchCollector<T = unknown> {\n private buffer: T[] = [];\n private timerId: ReturnType<typeof setTimeout> | null = null;\n\n private readonly delayMs: number;\n private readonly resetTimerOnPush: boolean;\n private readonly storageType: BatchCollectorStorageType;\n private readonly storageKey: string;\n private readonly autoClear: boolean;\n private readonly listeners = new Map<string, Set<FlushListener<T>>>();\n\n /**\n * Creates a batch collector instance.\n *\n * @param {BatchCollectorConfig} config - Configuration object with delay, optional reset behaviour, and optional storageType.\n */\n constructor(config: BatchCollectorConfig) {\n this.delayMs = config.delayMs;\n this.resetTimerOnPush = config.resetTimerOnPush ?? true;\n this.storageType = config.storageType ?? \"memory\";\n this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;\n this.autoClear = config.autoClear ?? true;\n\n if (this.storageType !== \"memory\") {\n const pending = this.readPersisted();\n\n if (pending.length !== 0) {\n this.writePersisted([]);\n this.emit(BATCH_FLUSH_EVENT, pending);\n }\n }\n }\n\n /**\n * Subscribe to an event. When the batch is flushed, the \"flush\" event is emitted with the batched items.\n *\n * @param {string} event - Event name; use BATCH_FLUSH_EVENT (\"flush\") for flush events.\n * @param {FlushListener<T>} callback - Function called with the batched items when the event is emitted.\n * @returns {() => void} Unsubscribe function; call to remove the listener.\n */\n public subscribe(event: string, callback: FlushListener<T>): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n\n this.listeners.get(event)!.add(callback);\n\n return () => this.listeners.get(event)?.delete(callback);\n }\n\n /**\n * Add an item to the batch and (re)start the flush timer. When storageType is not memory, also persists the buffer.\n *\n * @param {T} item - Item to add to the current batch.\n * @returns {void}\n */\n public push(item: T): void {\n this.buffer.push(item);\n\n if (this.storageType !== \"memory\") {\n this.writePersisted(this.buffer);\n }\n\n this.scheduleFlush();\n }\n\n /**\n * Returns a copy of the current buffer. Does not emit, clear, or change state.\n *\n * @returns {T[]} Copy of all items in the batch.\n */\n public items(): T[] {\n return [...this.buffer];\n }\n\n /**\n * Clears the pending timer, the buffer, and persisted storage. Does not emit.\n *\n * @returns {boolean} True.\n */\n public clear(): boolean {\n this.clearTimer();\n this.buffer = [];\n\n if (this.storageType !== \"memory\") {\n this.writePersisted([]);\n }\n\n return true;\n }\n\n /**\n * Returns the Storage instance for the configured storage type, or null if unavailable (e.g. SSR).\n *\n * @returns {Storage | null} window.localStorage, window.sessionStorage, or null.\n */\n private getStorage(): Storage | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n if (this.storageType === \"localStorage\") {\n return window.localStorage;\n }\n\n if (this.storageType === \"sessionStorage\") {\n return window.sessionStorage;\n }\n\n return null;\n }\n\n /**\n * Reads the persisted buffer from storage.\n *\n * @returns {T[]} Parsed buffer from storage, or empty array if unavailable or parse error.\n */\n private readPersisted(): T[] {\n const storage = this.getStorage();\n\n if (!storage) {\n return [];\n }\n\n try {\n const raw = storage.getItem(this.storageKey);\n\n return raw ? (JSON.parse(raw) as T[]) : [];\n } catch {\n return [];\n }\n }\n\n /**\n * Writes the buffer to storage, or removes the key when the buffer is empty.\n *\n * @param {T[]} items - Current buffer to persist.\n * @returns {boolean} True if write succeeded, false otherwise.\n */\n private writePersisted(items: T[]): boolean {\n const storage = this.getStorage();\n\n if (!storage) {\n return false;\n }\n\n try {\n if (items.length === 0) {\n storage.removeItem(this.storageKey);\n } else {\n storage.setItem(this.storageKey, JSON.stringify(items));\n }\n\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Emits the current batch to subscribers and optionally clears buffer and storage. Used internally by the timer.\n * When autoClear is false, the timer calls flush(false) so only manual clear() clears.\n *\n * @param {boolean} clear - If true (default), clear buffer and storage after emit. If false, only emit.\n * @returns {boolean} True if the batch was emitted, false if buffer was empty.\n */\n private flush(clear: boolean = true): boolean {\n this.clearTimer();\n\n if (this.buffer.length === 0) {\n return false;\n }\n\n const items = [...this.buffer];\n\n if (clear) {\n this.buffer = [];\n\n if (this.storageType !== \"memory\") {\n this.writePersisted([]);\n }\n }\n\n this.emit(BATCH_FLUSH_EVENT, items);\n\n return true;\n }\n\n /**\n * Emits an event with the given payload to all subscribers of that event.\n *\n * @param {string} event - Event name to emit.\n * @param {T[]} payload - Data to pass to each listener.\n * @returns {void}\n */\n private emit(event: string, payload: T[]): void {\n this.listeners.get(event)?.forEach((cb) => cb(payload));\n }\n\n /**\n * Schedules a flush after the configured delay. If resetTimerOnPush is true, clears existing timer first.\n *\n * @returns {boolean} True if timer was scheduled, false if already scheduled (resetTimerOnPush false).\n */\n private scheduleFlush(): boolean {\n if (this.resetTimerOnPush) {\n this.clearTimer();\n } else if (this.timerId !== null) {\n return false; // already scheduled from first push\n }\n\n this.timerId = setTimeout(() => this.flush(this.autoClear), this.delayMs);\n\n return true;\n }\n\n /**\n * Clears the pending flush timer if one is set. Does nothing if no timer is set.\n *\n * @returns {boolean} True if a timer was cleared, false if there was no timer.\n */\n private clearTimer(): boolean {\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n\n this.timerId = null;\n\n return true;\n }\n\n return false;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Batches items and emits a "flush" event after a delay (e.g. after last push).
3
+ * Event-based: create instance, subscribe via subscribe(BATCH_FLUSH_EVENT, callback), call push() from UI.
4
+ * Optional persistence: storageType can be memory, localStorage, or sessionStorage so the batch survives refresh/close.
5
+ */
6
+ type BatchCollectorStorageType = "memory" | "localStorage" | "sessionStorage";
7
+ type BatchCollectorConfig = {
8
+ /** Delay in milliseconds before flushing the batch (e.g. 5000 = 5 seconds). */
9
+ delayMs: number;
10
+ /** If true, timer resets on each push (flush N ms after last push). If false, flush exactly N ms after first push. Default true. */
11
+ resetTimerOnPush?: boolean;
12
+ /** Where to keep the buffer: "memory" (default), "localStorage", or "sessionStorage". When not memory, buffer is persisted and re-flushed on next load if any. */
13
+ storageType?: BatchCollectorStorageType;
14
+ /** Storage key when storageType is localStorage or sessionStorage. Should be unique per collector instance. Default "batch-collector-pending". */
15
+ storageKey?: string;
16
+ /** If true (default), buffer and storage are cleared when the timer fires. If false, only on manual clear() call. */
17
+ autoClear?: boolean;
18
+ };
19
+ /** Event name emitted when the batch is flushed. */
20
+ declare const BATCH_FLUSH_EVENT = "flush";
21
+ type FlushListener<T> = (items: T[]) => void;
22
+ /**
23
+ * Collects items in a buffer and emits BATCH_FLUSH_EVENT with the batched array after a delay (e.g. after the last push).
24
+ * Use for batching UI events (clicks, logs) before sending to server or analytics. Item type is generic (objects, numbers, strings, or any T).
25
+ *
26
+ * Parameters (config):
27
+ * - delayMs (required) — delay in ms before emitting the batch (e.g. 5000 = 5 seconds).
28
+ * - resetTimerOnPush (optional) — if true (default), timer resets on each push; if false, emit exactly delayMs after first push.
29
+ * - storageType (optional) — "memory" (default), "localStorage", or "sessionStorage". When not memory, batch is persisted and re-emitted on next load.
30
+ * - storageKey (optional) — storage key when using localStorage/sessionStorage. Unique per instance. Default "batch-collector-pending".
31
+ * - autoClear (optional) — if true (default), buffer and storage are cleared when the timer fires; if false, only on manual clear().
32
+ *
33
+ * Public API:
34
+ * - subscribe(event, callback) — subscribe to flush events; callback receives the batch. Returns unsubscribe function.
35
+ * - push(item) — add an item and (re)start the timer.
36
+ * - items() — returns a copy of the current batch (optional; does not emit or clear).
37
+ * - clear() — clears buffer and storage (optional; e.g. call from the subscriber after a successful send).
38
+ *
39
+ * @example
40
+ * const collector = new BatchCollector<{ action: string; id: string }>({
41
+ * delayMs: 5000,
42
+ * storageType: "localStorage",
43
+ * storageKey: "my-app-log-batch"
44
+ * });
45
+ * collector.subscribe(BATCH_FLUSH_EVENT, (items) => sendToBackend(items));
46
+ * collector.push({ action: "button_click", id: "submit-btn" });
47
+ *
48
+ * Optional:
49
+ * collector.items() // copy of current batch
50
+ * collector.clear() // clear buffer and storage (e.g. after successful send in subscriber)
51
+ */
52
+ declare class BatchCollector<T = unknown> {
53
+ private buffer;
54
+ private timerId;
55
+ private readonly delayMs;
56
+ private readonly resetTimerOnPush;
57
+ private readonly storageType;
58
+ private readonly storageKey;
59
+ private readonly autoClear;
60
+ private readonly listeners;
61
+ /**
62
+ * Creates a batch collector instance.
63
+ *
64
+ * @param {BatchCollectorConfig} config - Configuration object with delay, optional reset behaviour, and optional storageType.
65
+ */
66
+ constructor(config: BatchCollectorConfig);
67
+ /**
68
+ * Subscribe to an event. When the batch is flushed, the "flush" event is emitted with the batched items.
69
+ *
70
+ * @param {string} event - Event name; use BATCH_FLUSH_EVENT ("flush") for flush events.
71
+ * @param {FlushListener<T>} callback - Function called with the batched items when the event is emitted.
72
+ * @returns {() => void} Unsubscribe function; call to remove the listener.
73
+ */
74
+ subscribe(event: string, callback: FlushListener<T>): () => void;
75
+ /**
76
+ * Add an item to the batch and (re)start the flush timer. When storageType is not memory, also persists the buffer.
77
+ *
78
+ * @param {T} item - Item to add to the current batch.
79
+ * @returns {void}
80
+ */
81
+ push(item: T): void;
82
+ /**
83
+ * Returns a copy of the current buffer. Does not emit, clear, or change state.
84
+ *
85
+ * @returns {T[]} Copy of all items in the batch.
86
+ */
87
+ items(): T[];
88
+ /**
89
+ * Clears the pending timer, the buffer, and persisted storage. Does not emit.
90
+ *
91
+ * @returns {boolean} True.
92
+ */
93
+ clear(): boolean;
94
+ /**
95
+ * Returns the Storage instance for the configured storage type, or null if unavailable (e.g. SSR).
96
+ *
97
+ * @returns {Storage | null} window.localStorage, window.sessionStorage, or null.
98
+ */
99
+ private getStorage;
100
+ /**
101
+ * Reads the persisted buffer from storage.
102
+ *
103
+ * @returns {T[]} Parsed buffer from storage, or empty array if unavailable or parse error.
104
+ */
105
+ private readPersisted;
106
+ /**
107
+ * Writes the buffer to storage, or removes the key when the buffer is empty.
108
+ *
109
+ * @param {T[]} items - Current buffer to persist.
110
+ * @returns {boolean} True if write succeeded, false otherwise.
111
+ */
112
+ private writePersisted;
113
+ /**
114
+ * Emits the current batch to subscribers and optionally clears buffer and storage. Used internally by the timer.
115
+ * When autoClear is false, the timer calls flush(false) so only manual clear() clears.
116
+ *
117
+ * @param {boolean} clear - If true (default), clear buffer and storage after emit. If false, only emit.
118
+ * @returns {boolean} True if the batch was emitted, false if buffer was empty.
119
+ */
120
+ private flush;
121
+ /**
122
+ * Emits an event with the given payload to all subscribers of that event.
123
+ *
124
+ * @param {string} event - Event name to emit.
125
+ * @param {T[]} payload - Data to pass to each listener.
126
+ * @returns {void}
127
+ */
128
+ private emit;
129
+ /**
130
+ * Schedules a flush after the configured delay. If resetTimerOnPush is true, clears existing timer first.
131
+ *
132
+ * @returns {boolean} True if timer was scheduled, false if already scheduled (resetTimerOnPush false).
133
+ */
134
+ private scheduleFlush;
135
+ /**
136
+ * Clears the pending flush timer if one is set. Does nothing if no timer is set.
137
+ *
138
+ * @returns {boolean} True if a timer was cleared, false if there was no timer.
139
+ */
140
+ private clearTimer;
141
+ }
142
+
143
+ export { BATCH_FLUSH_EVENT, BatchCollector, type BatchCollectorConfig, type BatchCollectorStorageType };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Batches items and emits a "flush" event after a delay (e.g. after last push).
3
+ * Event-based: create instance, subscribe via subscribe(BATCH_FLUSH_EVENT, callback), call push() from UI.
4
+ * Optional persistence: storageType can be memory, localStorage, or sessionStorage so the batch survives refresh/close.
5
+ */
6
+ type BatchCollectorStorageType = "memory" | "localStorage" | "sessionStorage";
7
+ type BatchCollectorConfig = {
8
+ /** Delay in milliseconds before flushing the batch (e.g. 5000 = 5 seconds). */
9
+ delayMs: number;
10
+ /** If true, timer resets on each push (flush N ms after last push). If false, flush exactly N ms after first push. Default true. */
11
+ resetTimerOnPush?: boolean;
12
+ /** Where to keep the buffer: "memory" (default), "localStorage", or "sessionStorage". When not memory, buffer is persisted and re-flushed on next load if any. */
13
+ storageType?: BatchCollectorStorageType;
14
+ /** Storage key when storageType is localStorage or sessionStorage. Should be unique per collector instance. Default "batch-collector-pending". */
15
+ storageKey?: string;
16
+ /** If true (default), buffer and storage are cleared when the timer fires. If false, only on manual clear() call. */
17
+ autoClear?: boolean;
18
+ };
19
+ /** Event name emitted when the batch is flushed. */
20
+ declare const BATCH_FLUSH_EVENT = "flush";
21
+ type FlushListener<T> = (items: T[]) => void;
22
+ /**
23
+ * Collects items in a buffer and emits BATCH_FLUSH_EVENT with the batched array after a delay (e.g. after the last push).
24
+ * Use for batching UI events (clicks, logs) before sending to server or analytics. Item type is generic (objects, numbers, strings, or any T).
25
+ *
26
+ * Parameters (config):
27
+ * - delayMs (required) — delay in ms before emitting the batch (e.g. 5000 = 5 seconds).
28
+ * - resetTimerOnPush (optional) — if true (default), timer resets on each push; if false, emit exactly delayMs after first push.
29
+ * - storageType (optional) — "memory" (default), "localStorage", or "sessionStorage". When not memory, batch is persisted and re-emitted on next load.
30
+ * - storageKey (optional) — storage key when using localStorage/sessionStorage. Unique per instance. Default "batch-collector-pending".
31
+ * - autoClear (optional) — if true (default), buffer and storage are cleared when the timer fires; if false, only on manual clear().
32
+ *
33
+ * Public API:
34
+ * - subscribe(event, callback) — subscribe to flush events; callback receives the batch. Returns unsubscribe function.
35
+ * - push(item) — add an item and (re)start the timer.
36
+ * - items() — returns a copy of the current batch (optional; does not emit or clear).
37
+ * - clear() — clears buffer and storage (optional; e.g. call from the subscriber after a successful send).
38
+ *
39
+ * @example
40
+ * const collector = new BatchCollector<{ action: string; id: string }>({
41
+ * delayMs: 5000,
42
+ * storageType: "localStorage",
43
+ * storageKey: "my-app-log-batch"
44
+ * });
45
+ * collector.subscribe(BATCH_FLUSH_EVENT, (items) => sendToBackend(items));
46
+ * collector.push({ action: "button_click", id: "submit-btn" });
47
+ *
48
+ * Optional:
49
+ * collector.items() // copy of current batch
50
+ * collector.clear() // clear buffer and storage (e.g. after successful send in subscriber)
51
+ */
52
+ declare class BatchCollector<T = unknown> {
53
+ private buffer;
54
+ private timerId;
55
+ private readonly delayMs;
56
+ private readonly resetTimerOnPush;
57
+ private readonly storageType;
58
+ private readonly storageKey;
59
+ private readonly autoClear;
60
+ private readonly listeners;
61
+ /**
62
+ * Creates a batch collector instance.
63
+ *
64
+ * @param {BatchCollectorConfig} config - Configuration object with delay, optional reset behaviour, and optional storageType.
65
+ */
66
+ constructor(config: BatchCollectorConfig);
67
+ /**
68
+ * Subscribe to an event. When the batch is flushed, the "flush" event is emitted with the batched items.
69
+ *
70
+ * @param {string} event - Event name; use BATCH_FLUSH_EVENT ("flush") for flush events.
71
+ * @param {FlushListener<T>} callback - Function called with the batched items when the event is emitted.
72
+ * @returns {() => void} Unsubscribe function; call to remove the listener.
73
+ */
74
+ subscribe(event: string, callback: FlushListener<T>): () => void;
75
+ /**
76
+ * Add an item to the batch and (re)start the flush timer. When storageType is not memory, also persists the buffer.
77
+ *
78
+ * @param {T} item - Item to add to the current batch.
79
+ * @returns {void}
80
+ */
81
+ push(item: T): void;
82
+ /**
83
+ * Returns a copy of the current buffer. Does not emit, clear, or change state.
84
+ *
85
+ * @returns {T[]} Copy of all items in the batch.
86
+ */
87
+ items(): T[];
88
+ /**
89
+ * Clears the pending timer, the buffer, and persisted storage. Does not emit.
90
+ *
91
+ * @returns {boolean} True.
92
+ */
93
+ clear(): boolean;
94
+ /**
95
+ * Returns the Storage instance for the configured storage type, or null if unavailable (e.g. SSR).
96
+ *
97
+ * @returns {Storage | null} window.localStorage, window.sessionStorage, or null.
98
+ */
99
+ private getStorage;
100
+ /**
101
+ * Reads the persisted buffer from storage.
102
+ *
103
+ * @returns {T[]} Parsed buffer from storage, or empty array if unavailable or parse error.
104
+ */
105
+ private readPersisted;
106
+ /**
107
+ * Writes the buffer to storage, or removes the key when the buffer is empty.
108
+ *
109
+ * @param {T[]} items - Current buffer to persist.
110
+ * @returns {boolean} True if write succeeded, false otherwise.
111
+ */
112
+ private writePersisted;
113
+ /**
114
+ * Emits the current batch to subscribers and optionally clears buffer and storage. Used internally by the timer.
115
+ * When autoClear is false, the timer calls flush(false) so only manual clear() clears.
116
+ *
117
+ * @param {boolean} clear - If true (default), clear buffer and storage after emit. If false, only emit.
118
+ * @returns {boolean} True if the batch was emitted, false if buffer was empty.
119
+ */
120
+ private flush;
121
+ /**
122
+ * Emits an event with the given payload to all subscribers of that event.
123
+ *
124
+ * @param {string} event - Event name to emit.
125
+ * @param {T[]} payload - Data to pass to each listener.
126
+ * @returns {void}
127
+ */
128
+ private emit;
129
+ /**
130
+ * Schedules a flush after the configured delay. If resetTimerOnPush is true, clears existing timer first.
131
+ *
132
+ * @returns {boolean} True if timer was scheduled, false if already scheduled (resetTimerOnPush false).
133
+ */
134
+ private scheduleFlush;
135
+ /**
136
+ * Clears the pending flush timer if one is set. Does nothing if no timer is set.
137
+ *
138
+ * @returns {boolean} True if a timer was cleared, false if there was no timer.
139
+ */
140
+ private clearTimer;
141
+ }
142
+
143
+ export { BATCH_FLUSH_EVENT, BatchCollector, type BatchCollectorConfig, type BatchCollectorStorageType };
package/dist/index.js ADDED
@@ -0,0 +1,200 @@
1
+ // src/index.ts
2
+ var BATCH_FLUSH_EVENT = "flush";
3
+ var DEFAULT_STORAGE_KEY = "batch-collector-pending";
4
+ var BatchCollector = class {
5
+ /**
6
+ * Creates a batch collector instance.
7
+ *
8
+ * @param {BatchCollectorConfig} config - Configuration object with delay, optional reset behaviour, and optional storageType.
9
+ */
10
+ constructor(config) {
11
+ this.buffer = [];
12
+ this.timerId = null;
13
+ this.listeners = /* @__PURE__ */ new Map();
14
+ var _a, _b, _c, _d;
15
+ this.delayMs = config.delayMs;
16
+ this.resetTimerOnPush = (_a = config.resetTimerOnPush) != null ? _a : true;
17
+ this.storageType = (_b = config.storageType) != null ? _b : "memory";
18
+ this.storageKey = (_c = config.storageKey) != null ? _c : DEFAULT_STORAGE_KEY;
19
+ this.autoClear = (_d = config.autoClear) != null ? _d : true;
20
+ if (this.storageType !== "memory") {
21
+ const pending = this.readPersisted();
22
+ if (pending.length !== 0) {
23
+ this.writePersisted([]);
24
+ this.emit(BATCH_FLUSH_EVENT, pending);
25
+ }
26
+ }
27
+ }
28
+ /**
29
+ * Subscribe to an event. When the batch is flushed, the "flush" event is emitted with the batched items.
30
+ *
31
+ * @param {string} event - Event name; use BATCH_FLUSH_EVENT ("flush") for flush events.
32
+ * @param {FlushListener<T>} callback - Function called with the batched items when the event is emitted.
33
+ * @returns {() => void} Unsubscribe function; call to remove the listener.
34
+ */
35
+ subscribe(event, callback) {
36
+ if (!this.listeners.has(event)) {
37
+ this.listeners.set(event, /* @__PURE__ */ new Set());
38
+ }
39
+ this.listeners.get(event).add(callback);
40
+ return () => {
41
+ var _a;
42
+ return (_a = this.listeners.get(event)) == null ? void 0 : _a.delete(callback);
43
+ };
44
+ }
45
+ /**
46
+ * Add an item to the batch and (re)start the flush timer. When storageType is not memory, also persists the buffer.
47
+ *
48
+ * @param {T} item - Item to add to the current batch.
49
+ * @returns {void}
50
+ */
51
+ push(item) {
52
+ this.buffer.push(item);
53
+ if (this.storageType !== "memory") {
54
+ this.writePersisted(this.buffer);
55
+ }
56
+ this.scheduleFlush();
57
+ }
58
+ /**
59
+ * Returns a copy of the current buffer. Does not emit, clear, or change state.
60
+ *
61
+ * @returns {T[]} Copy of all items in the batch.
62
+ */
63
+ items() {
64
+ return [...this.buffer];
65
+ }
66
+ /**
67
+ * Clears the pending timer, the buffer, and persisted storage. Does not emit.
68
+ *
69
+ * @returns {boolean} True.
70
+ */
71
+ clear() {
72
+ this.clearTimer();
73
+ this.buffer = [];
74
+ if (this.storageType !== "memory") {
75
+ this.writePersisted([]);
76
+ }
77
+ return true;
78
+ }
79
+ /**
80
+ * Returns the Storage instance for the configured storage type, or null if unavailable (e.g. SSR).
81
+ *
82
+ * @returns {Storage | null} window.localStorage, window.sessionStorage, or null.
83
+ */
84
+ getStorage() {
85
+ if (typeof window === "undefined") {
86
+ return null;
87
+ }
88
+ if (this.storageType === "localStorage") {
89
+ return window.localStorage;
90
+ }
91
+ if (this.storageType === "sessionStorage") {
92
+ return window.sessionStorage;
93
+ }
94
+ return null;
95
+ }
96
+ /**
97
+ * Reads the persisted buffer from storage.
98
+ *
99
+ * @returns {T[]} Parsed buffer from storage, or empty array if unavailable or parse error.
100
+ */
101
+ readPersisted() {
102
+ const storage = this.getStorage();
103
+ if (!storage) {
104
+ return [];
105
+ }
106
+ try {
107
+ const raw = storage.getItem(this.storageKey);
108
+ return raw ? JSON.parse(raw) : [];
109
+ } catch {
110
+ return [];
111
+ }
112
+ }
113
+ /**
114
+ * Writes the buffer to storage, or removes the key when the buffer is empty.
115
+ *
116
+ * @param {T[]} items - Current buffer to persist.
117
+ * @returns {boolean} True if write succeeded, false otherwise.
118
+ */
119
+ writePersisted(items) {
120
+ const storage = this.getStorage();
121
+ if (!storage) {
122
+ return false;
123
+ }
124
+ try {
125
+ if (items.length === 0) {
126
+ storage.removeItem(this.storageKey);
127
+ } else {
128
+ storage.setItem(this.storageKey, JSON.stringify(items));
129
+ }
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+ /**
136
+ * Emits the current batch to subscribers and optionally clears buffer and storage. Used internally by the timer.
137
+ * When autoClear is false, the timer calls flush(false) so only manual clear() clears.
138
+ *
139
+ * @param {boolean} clear - If true (default), clear buffer and storage after emit. If false, only emit.
140
+ * @returns {boolean} True if the batch was emitted, false if buffer was empty.
141
+ */
142
+ flush(clear = true) {
143
+ this.clearTimer();
144
+ if (this.buffer.length === 0) {
145
+ return false;
146
+ }
147
+ const items = [...this.buffer];
148
+ if (clear) {
149
+ this.buffer = [];
150
+ if (this.storageType !== "memory") {
151
+ this.writePersisted([]);
152
+ }
153
+ }
154
+ this.emit(BATCH_FLUSH_EVENT, items);
155
+ return true;
156
+ }
157
+ /**
158
+ * Emits an event with the given payload to all subscribers of that event.
159
+ *
160
+ * @param {string} event - Event name to emit.
161
+ * @param {T[]} payload - Data to pass to each listener.
162
+ * @returns {void}
163
+ */
164
+ emit(event, payload) {
165
+ var _a;
166
+ (_a = this.listeners.get(event)) == null ? void 0 : _a.forEach((cb) => cb(payload));
167
+ }
168
+ /**
169
+ * Schedules a flush after the configured delay. If resetTimerOnPush is true, clears existing timer first.
170
+ *
171
+ * @returns {boolean} True if timer was scheduled, false if already scheduled (resetTimerOnPush false).
172
+ */
173
+ scheduleFlush() {
174
+ if (this.resetTimerOnPush) {
175
+ this.clearTimer();
176
+ } else if (this.timerId !== null) {
177
+ return false;
178
+ }
179
+ this.timerId = setTimeout(() => this.flush(this.autoClear), this.delayMs);
180
+ return true;
181
+ }
182
+ /**
183
+ * Clears the pending flush timer if one is set. Does nothing if no timer is set.
184
+ *
185
+ * @returns {boolean} True if a timer was cleared, false if there was no timer.
186
+ */
187
+ clearTimer() {
188
+ if (this.timerId !== null) {
189
+ clearTimeout(this.timerId);
190
+ this.timerId = null;
191
+ return true;
192
+ }
193
+ return false;
194
+ }
195
+ };
196
+ export {
197
+ BATCH_FLUSH_EVENT,
198
+ BatchCollector
199
+ };
200
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Batches items and emits a \"flush\" event after a delay (e.g. after last push).\n * Event-based: create instance, subscribe via subscribe(BATCH_FLUSH_EVENT, callback), call push() from UI.\n * Optional persistence: storageType can be memory, localStorage, or sessionStorage so the batch survives refresh/close.\n */\n\nexport type BatchCollectorStorageType = \"memory\" | \"localStorage\" | \"sessionStorage\";\n\nexport type BatchCollectorConfig = {\n /** Delay in milliseconds before flushing the batch (e.g. 5000 = 5 seconds). */\n delayMs: number;\n /** If true, timer resets on each push (flush N ms after last push). If false, flush exactly N ms after first push. Default true. */\n resetTimerOnPush?: boolean;\n /** Where to keep the buffer: \"memory\" (default), \"localStorage\", or \"sessionStorage\". When not memory, buffer is persisted and re-flushed on next load if any. */\n storageType?: BatchCollectorStorageType;\n /** Storage key when storageType is localStorage or sessionStorage. Should be unique per collector instance. Default \"batch-collector-pending\". */\n storageKey?: string;\n /** If true (default), buffer and storage are cleared when the timer fires. If false, only on manual clear() call. */\n autoClear?: boolean;\n};\n\n/** Event name emitted when the batch is flushed. */\nexport const BATCH_FLUSH_EVENT = \"flush\";\n\ntype FlushListener<T> = (items: T[]) => void;\n\nconst DEFAULT_STORAGE_KEY = \"batch-collector-pending\";\n\n/**\n * Collects items in a buffer and emits BATCH_FLUSH_EVENT with the batched array after a delay (e.g. after the last push).\n * Use for batching UI events (clicks, logs) before sending to server or analytics. Item type is generic (objects, numbers, strings, or any T).\n *\n * Parameters (config):\n * - delayMs (required) — delay in ms before emitting the batch (e.g. 5000 = 5 seconds).\n * - resetTimerOnPush (optional) — if true (default), timer resets on each push; if false, emit exactly delayMs after first push.\n * - storageType (optional) — \"memory\" (default), \"localStorage\", or \"sessionStorage\". When not memory, batch is persisted and re-emitted on next load.\n * - storageKey (optional) — storage key when using localStorage/sessionStorage. Unique per instance. Default \"batch-collector-pending\".\n * - autoClear (optional) — if true (default), buffer and storage are cleared when the timer fires; if false, only on manual clear().\n *\n * Public API:\n * - subscribe(event, callback) — subscribe to flush events; callback receives the batch. Returns unsubscribe function.\n * - push(item) — add an item and (re)start the timer.\n * - items() — returns a copy of the current batch (optional; does not emit or clear).\n * - clear() — clears buffer and storage (optional; e.g. call from the subscriber after a successful send).\n *\n * @example\n * const collector = new BatchCollector<{ action: string; id: string }>({\n * delayMs: 5000,\n * storageType: \"localStorage\",\n * storageKey: \"my-app-log-batch\"\n * });\n * collector.subscribe(BATCH_FLUSH_EVENT, (items) => sendToBackend(items));\n * collector.push({ action: \"button_click\", id: \"submit-btn\" });\n *\n * Optional:\n * collector.items() // copy of current batch\n * collector.clear() // clear buffer and storage (e.g. after successful send in subscriber)\n */\nexport class BatchCollector<T = unknown> {\n private buffer: T[] = [];\n private timerId: ReturnType<typeof setTimeout> | null = null;\n\n private readonly delayMs: number;\n private readonly resetTimerOnPush: boolean;\n private readonly storageType: BatchCollectorStorageType;\n private readonly storageKey: string;\n private readonly autoClear: boolean;\n private readonly listeners = new Map<string, Set<FlushListener<T>>>();\n\n /**\n * Creates a batch collector instance.\n *\n * @param {BatchCollectorConfig} config - Configuration object with delay, optional reset behaviour, and optional storageType.\n */\n constructor(config: BatchCollectorConfig) {\n this.delayMs = config.delayMs;\n this.resetTimerOnPush = config.resetTimerOnPush ?? true;\n this.storageType = config.storageType ?? \"memory\";\n this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY;\n this.autoClear = config.autoClear ?? true;\n\n if (this.storageType !== \"memory\") {\n const pending = this.readPersisted();\n\n if (pending.length !== 0) {\n this.writePersisted([]);\n this.emit(BATCH_FLUSH_EVENT, pending);\n }\n }\n }\n\n /**\n * Subscribe to an event. When the batch is flushed, the \"flush\" event is emitted with the batched items.\n *\n * @param {string} event - Event name; use BATCH_FLUSH_EVENT (\"flush\") for flush events.\n * @param {FlushListener<T>} callback - Function called with the batched items when the event is emitted.\n * @returns {() => void} Unsubscribe function; call to remove the listener.\n */\n public subscribe(event: string, callback: FlushListener<T>): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n\n this.listeners.get(event)!.add(callback);\n\n return () => this.listeners.get(event)?.delete(callback);\n }\n\n /**\n * Add an item to the batch and (re)start the flush timer. When storageType is not memory, also persists the buffer.\n *\n * @param {T} item - Item to add to the current batch.\n * @returns {void}\n */\n public push(item: T): void {\n this.buffer.push(item);\n\n if (this.storageType !== \"memory\") {\n this.writePersisted(this.buffer);\n }\n\n this.scheduleFlush();\n }\n\n /**\n * Returns a copy of the current buffer. Does not emit, clear, or change state.\n *\n * @returns {T[]} Copy of all items in the batch.\n */\n public items(): T[] {\n return [...this.buffer];\n }\n\n /**\n * Clears the pending timer, the buffer, and persisted storage. Does not emit.\n *\n * @returns {boolean} True.\n */\n public clear(): boolean {\n this.clearTimer();\n this.buffer = [];\n\n if (this.storageType !== \"memory\") {\n this.writePersisted([]);\n }\n\n return true;\n }\n\n /**\n * Returns the Storage instance for the configured storage type, or null if unavailable (e.g. SSR).\n *\n * @returns {Storage | null} window.localStorage, window.sessionStorage, or null.\n */\n private getStorage(): Storage | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n if (this.storageType === \"localStorage\") {\n return window.localStorage;\n }\n\n if (this.storageType === \"sessionStorage\") {\n return window.sessionStorage;\n }\n\n return null;\n }\n\n /**\n * Reads the persisted buffer from storage.\n *\n * @returns {T[]} Parsed buffer from storage, or empty array if unavailable or parse error.\n */\n private readPersisted(): T[] {\n const storage = this.getStorage();\n\n if (!storage) {\n return [];\n }\n\n try {\n const raw = storage.getItem(this.storageKey);\n\n return raw ? (JSON.parse(raw) as T[]) : [];\n } catch {\n return [];\n }\n }\n\n /**\n * Writes the buffer to storage, or removes the key when the buffer is empty.\n *\n * @param {T[]} items - Current buffer to persist.\n * @returns {boolean} True if write succeeded, false otherwise.\n */\n private writePersisted(items: T[]): boolean {\n const storage = this.getStorage();\n\n if (!storage) {\n return false;\n }\n\n try {\n if (items.length === 0) {\n storage.removeItem(this.storageKey);\n } else {\n storage.setItem(this.storageKey, JSON.stringify(items));\n }\n\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Emits the current batch to subscribers and optionally clears buffer and storage. Used internally by the timer.\n * When autoClear is false, the timer calls flush(false) so only manual clear() clears.\n *\n * @param {boolean} clear - If true (default), clear buffer and storage after emit. If false, only emit.\n * @returns {boolean} True if the batch was emitted, false if buffer was empty.\n */\n private flush(clear: boolean = true): boolean {\n this.clearTimer();\n\n if (this.buffer.length === 0) {\n return false;\n }\n\n const items = [...this.buffer];\n\n if (clear) {\n this.buffer = [];\n\n if (this.storageType !== \"memory\") {\n this.writePersisted([]);\n }\n }\n\n this.emit(BATCH_FLUSH_EVENT, items);\n\n return true;\n }\n\n /**\n * Emits an event with the given payload to all subscribers of that event.\n *\n * @param {string} event - Event name to emit.\n * @param {T[]} payload - Data to pass to each listener.\n * @returns {void}\n */\n private emit(event: string, payload: T[]): void {\n this.listeners.get(event)?.forEach((cb) => cb(payload));\n }\n\n /**\n * Schedules a flush after the configured delay. If resetTimerOnPush is true, clears existing timer first.\n *\n * @returns {boolean} True if timer was scheduled, false if already scheduled (resetTimerOnPush false).\n */\n private scheduleFlush(): boolean {\n if (this.resetTimerOnPush) {\n this.clearTimer();\n } else if (this.timerId !== null) {\n return false; // already scheduled from first push\n }\n\n this.timerId = setTimeout(() => this.flush(this.autoClear), this.delayMs);\n\n return true;\n }\n\n /**\n * Clears the pending flush timer if one is set. Does nothing if no timer is set.\n *\n * @returns {boolean} True if a timer was cleared, false if there was no timer.\n */\n private clearTimer(): boolean {\n if (this.timerId !== null) {\n clearTimeout(this.timerId);\n\n this.timerId = null;\n\n return true;\n }\n\n return false;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@js-nerds/batch-collector",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight TypeScript batch collector with delayed flush and optional persistence.",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "engines": {
10
+ "node": ">=16.0.0"
11
+ },
12
+ "keywords": [
13
+ "batch",
14
+ "collector",
15
+ "queue",
16
+ "typescript",
17
+ "buffer"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/js-nerds/batch-collector.git"
22
+ },
23
+ "homepage": "https://github.com/js-nerds/batch-collector#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/js-nerds/batch-collector/issues"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js",
36
+ "require": "./dist/index.cjs"
37
+ }
38
+ },
39
+ "files": [
40
+ "dist",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "scripts": {
45
+ "build": "tsup src/index.ts --dts --format cjs,esm --sourcemap --clean",
46
+ "lint": "tsc --noEmit",
47
+ "test": "vitest run",
48
+ "test:ui": "vitest --ui",
49
+ "test:coverage": "vitest run --coverage",
50
+ "prepublishOnly": "pnpm build"
51
+ },
52
+ "packageManager": "pnpm@9.0.0",
53
+ "devDependencies": {
54
+ "@vitest/coverage-v8": "^3.2.4",
55
+ "@vitest/ui": "^3.2.4",
56
+ "jsdom": "^25.0.1",
57
+ "tsup": "^8.5.0",
58
+ "typescript": "^5.7.3",
59
+ "vitest": "^3.2.4"
60
+ }
61
+ }