@sschepis/magazine 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,343 @@
1
+ /**
2
+ * Paginator - Efficient pagination for large event datasets
3
+ */
4
+ export default class Paginator {
5
+ constructor(logger) {
6
+ this.logger = logger.child({ context: 'Paginator' });
7
+ this.logger.info('Paginator initialized');
8
+ }
9
+
10
+ /**
11
+ * Create a paginated result set
12
+ * @param {Array} items - Items to paginate
13
+ * @param {Object} options - Pagination options
14
+ * @param {number} options.page - Current page (1-based)
15
+ * @param {number} options.pageSize - Items per page
16
+ * @param {boolean} options.includeMeta - Include pagination metadata
17
+ * @returns {Object} Paginated result
18
+ */
19
+ paginate(items, options = {}) {
20
+ const {
21
+ page = 1,
22
+ pageSize = 100,
23
+ includeMeta = true
24
+ } = options;
25
+
26
+ // Validate inputs
27
+ if (!Array.isArray(items)) {
28
+ this.logger.warn('Invalid items array provided to paginate');
29
+ return this._createEmptyResult(includeMeta);
30
+ }
31
+
32
+ if (page < 1 || pageSize < 1) {
33
+ this.logger.warn('Invalid pagination parameters', { page, pageSize });
34
+ return this._createEmptyResult(includeMeta);
35
+ }
36
+
37
+ const totalItems = items.length;
38
+ const totalPages = Math.ceil(totalItems / pageSize);
39
+ const startIndex = (page - 1) * pageSize;
40
+ const endIndex = Math.min(startIndex + pageSize, totalItems);
41
+
42
+ // Get page items
43
+ const pageItems = items.slice(startIndex, endIndex);
44
+
45
+ this.logger.debug('Created paginated result', {
46
+ page,
47
+ pageSize,
48
+ totalItems,
49
+ totalPages,
50
+ itemsInPage: pageItems.length
51
+ });
52
+
53
+ // Build result
54
+ const result = {
55
+ data: pageItems
56
+ };
57
+
58
+ if (includeMeta) {
59
+ result.meta = {
60
+ page,
61
+ pageSize,
62
+ totalItems,
63
+ totalPages,
64
+ hasNextPage: page < totalPages,
65
+ hasPreviousPage: page > 1,
66
+ startIndex: startIndex + 1, // 1-based for display
67
+ endIndex: endIndex
68
+ };
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+ /**
75
+ * Create an async iterator for large datasets
76
+ * @param {Array|Function} itemsOrGetter - Items array or async function to get items
77
+ * @param {Object} options - Iterator options
78
+ * @param {number} options.pageSize - Items per page
79
+ * @returns {AsyncIterator} Async iterator
80
+ */
81
+ async *iterate(itemsOrGetter, options = {}) {
82
+ const { pageSize = 100 } = options;
83
+ let page = 1;
84
+ let hasMore = true;
85
+
86
+ while (hasMore) {
87
+ let items;
88
+
89
+ // Handle both array and async getter function
90
+ if (typeof itemsOrGetter === 'function') {
91
+ items = await itemsOrGetter(page, pageSize);
92
+ } else if (Array.isArray(itemsOrGetter)) {
93
+ const result = this.paginate(itemsOrGetter, { page, pageSize, includeMeta: true });
94
+ items = result.data;
95
+ hasMore = result.meta.hasNextPage;
96
+ } else {
97
+ this.logger.error('Invalid itemsOrGetter provided to iterate');
98
+ return;
99
+ }
100
+
101
+ if (!items || items.length === 0) {
102
+ hasMore = false;
103
+ break;
104
+ }
105
+
106
+ // Yield each item
107
+ for (const item of items) {
108
+ yield item;
109
+ }
110
+
111
+ // If using array, check if we have more pages
112
+ if (Array.isArray(itemsOrGetter)) {
113
+ page++;
114
+ } else {
115
+ // For async getter, assume no more items if less than pageSize returned
116
+ hasMore = items.length === pageSize;
117
+ page++;
118
+ }
119
+ }
120
+
121
+ this.logger.debug('Iterator completed', { totalPages: page - 1 });
122
+ }
123
+
124
+ /**
125
+ * Create a cursor-based pagination result
126
+ * @param {Array} items - Items to paginate
127
+ * @param {Object} options - Cursor pagination options
128
+ * @param {string} options.cursor - Current cursor position
129
+ * @param {number} options.limit - Number of items to return
130
+ * @param {string} options.cursorField - Field to use for cursor (default: 'id')
131
+ * @param {string} options.direction - Pagination direction ('forward' or 'backward')
132
+ * @returns {Object} Cursor-paginated result
133
+ */
134
+ paginateCursor(items, options = {}) {
135
+ const {
136
+ cursor = null,
137
+ limit = 100,
138
+ cursorField = 'id',
139
+ direction = 'forward'
140
+ } = options;
141
+
142
+ if (!Array.isArray(items)) {
143
+ this.logger.warn('Invalid items array provided to paginateCursor');
144
+ return this._createEmptyCursorResult();
145
+ }
146
+
147
+ let filteredItems = [...items];
148
+
149
+ // Apply cursor filter
150
+ if (cursor !== null) {
151
+ filteredItems = filteredItems.filter(item => {
152
+ const itemValue = item[cursorField];
153
+ if (direction === 'forward') {
154
+ return itemValue > cursor;
155
+ } else {
156
+ return itemValue < cursor;
157
+ }
158
+ });
159
+ }
160
+
161
+ // Sort items based on direction
162
+ filteredItems.sort((a, b) => {
163
+ const aVal = a[cursorField];
164
+ const bVal = b[cursorField];
165
+ if (direction === 'forward') {
166
+ return aVal < bVal ? -1 : 1;
167
+ } else {
168
+ return aVal > bVal ? -1 : 1;
169
+ }
170
+ });
171
+
172
+ // Get limited items
173
+ const pageItems = filteredItems.slice(0, limit);
174
+ const hasMore = filteredItems.length > limit;
175
+
176
+ // Determine cursors
177
+ const firstItem = pageItems[0];
178
+ const lastItem = pageItems[pageItems.length - 1];
179
+
180
+ const result = {
181
+ data: pageItems,
182
+ cursors: {
183
+ before: firstItem ? firstItem[cursorField] : null,
184
+ after: lastItem ? lastItem[cursorField] : null,
185
+ hasNext: hasMore,
186
+ hasPrevious: cursor !== null
187
+ }
188
+ };
189
+
190
+ this.logger.debug('Created cursor-paginated result', {
191
+ cursor,
192
+ limit,
193
+ cursorField,
194
+ direction,
195
+ itemsReturned: pageItems.length,
196
+ hasMore
197
+ });
198
+
199
+ return result;
200
+ }
201
+
202
+ /**
203
+ * Create a page info object for GraphQL-style pagination
204
+ * @param {Object} paginationMeta - Pagination metadata
205
+ * @returns {Object} Page info
206
+ */
207
+ createPageInfo(paginationMeta) {
208
+ return {
209
+ hasNextPage: paginationMeta.hasNextPage || false,
210
+ hasPreviousPage: paginationMeta.hasPreviousPage || false,
211
+ startCursor: paginationMeta.startCursor || null,
212
+ endCursor: paginationMeta.endCursor || null,
213
+ totalCount: paginationMeta.totalItems || 0
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Calculate optimal page size based on data characteristics
219
+ * @param {Object} options - Options for calculation
220
+ * @param {number} options.totalItems - Total number of items
221
+ * @param {number} options.avgItemSize - Average item size in bytes
222
+ * @param {number} options.maxMemory - Maximum memory to use (bytes)
223
+ * @param {number} options.minPageSize - Minimum page size
224
+ * @param {number} options.maxPageSize - Maximum page size
225
+ * @returns {number} Optimal page size
226
+ */
227
+ calculateOptimalPageSize(options = {}) {
228
+ const {
229
+ totalItems = 1000,
230
+ avgItemSize = 1024, // 1KB default
231
+ maxMemory = 10 * 1024 * 1024, // 10MB default
232
+ minPageSize = 10,
233
+ maxPageSize = 1000
234
+ } = options;
235
+
236
+ // Calculate based on memory constraints
237
+ let optimalSize = Math.floor(maxMemory / avgItemSize);
238
+
239
+ // Apply bounds
240
+ optimalSize = Math.max(minPageSize, Math.min(maxPageSize, optimalSize));
241
+
242
+ // Adjust for total items
243
+ if (totalItems < optimalSize) {
244
+ optimalSize = totalItems;
245
+ }
246
+
247
+ this.logger.debug('Calculated optimal page size', {
248
+ totalItems,
249
+ avgItemSize,
250
+ maxMemory,
251
+ optimalSize
252
+ });
253
+
254
+ return optimalSize;
255
+ }
256
+
257
+ /**
258
+ * Create empty result for error cases
259
+ * @private
260
+ */
261
+ _createEmptyResult(includeMeta) {
262
+ const result = { data: [] };
263
+ if (includeMeta) {
264
+ result.meta = {
265
+ page: 1,
266
+ pageSize: 0,
267
+ totalItems: 0,
268
+ totalPages: 0,
269
+ hasNextPage: false,
270
+ hasPreviousPage: false,
271
+ startIndex: 0,
272
+ endIndex: 0
273
+ };
274
+ }
275
+ return result;
276
+ }
277
+
278
+ /**
279
+ * Create empty cursor result for error cases
280
+ * @private
281
+ */
282
+ _createEmptyCursorResult() {
283
+ return {
284
+ data: [],
285
+ cursors: {
286
+ before: null,
287
+ after: null,
288
+ hasNext: false,
289
+ hasPrevious: false
290
+ }
291
+ };
292
+ }
293
+ }
294
+
295
+ /**
296
+ * PaginationHelper - Static utility methods for pagination
297
+ */
298
+ export class PaginationHelper {
299
+ /**
300
+ * Calculate pagination offset
301
+ */
302
+ static calculateOffset(page, pageSize) {
303
+ return (page - 1) * pageSize;
304
+ }
305
+
306
+ /**
307
+ * Calculate total pages
308
+ */
309
+ static calculateTotalPages(totalItems, pageSize) {
310
+ return Math.ceil(totalItems / pageSize);
311
+ }
312
+
313
+ /**
314
+ * Validate page number
315
+ */
316
+ static isValidPage(page, totalPages) {
317
+ return page >= 1 && page <= totalPages;
318
+ }
319
+
320
+ /**
321
+ * Get page numbers for pagination UI
322
+ */
323
+ static getPageNumbers(currentPage, totalPages, maxVisible = 5) {
324
+ const pages = [];
325
+ const halfVisible = Math.floor(maxVisible / 2);
326
+
327
+ let start = Math.max(1, currentPage - halfVisible);
328
+ let end = Math.min(totalPages, currentPage + halfVisible);
329
+
330
+ // Adjust if we're near the beginning or end
331
+ if (currentPage <= halfVisible) {
332
+ end = Math.min(totalPages, maxVisible);
333
+ } else if (currentPage >= totalPages - halfVisible) {
334
+ start = Math.max(1, totalPages - maxVisible + 1);
335
+ }
336
+
337
+ for (let i = start; i <= end; i++) {
338
+ pages.push(i);
339
+ }
340
+
341
+ return pages;
342
+ }
343
+ }