@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.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/magazine.cjs.js +5 -0
- package/dist/magazine.cjs.js.map +1 -0
- package/dist/magazine.es.js +4149 -0
- package/dist/magazine.es.js.map +1 -0
- package/dist/magazine.umd.js +5 -0
- package/dist/magazine.umd.js.map +1 -0
- package/package.json +74 -0
- package/src/components/ConfigManager.js +202 -0
- package/src/components/ConfigSchema.js +506 -0
- package/src/components/DataCompressor.js +378 -0
- package/src/components/DataTransformer.js +1172 -0
- package/src/components/DebugUtils.js +657 -0
- package/src/components/EventFilter.js +474 -0
- package/src/components/EventMetadata.js +273 -0
- package/src/components/EventStore.js +443 -0
- package/src/components/EventSyncer.js +353 -0
- package/src/components/Logger.js +299 -0
- package/src/components/NetworkManager.js +461 -0
- package/src/components/Paginator.js +343 -0
- package/src/index.js +571 -0
|
@@ -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
|
+
}
|