@valliappan-periannan/pagination-sorter 0.2.1

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/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # @valliappan-periannan/pagination-sorter
2
+
3
+ [![CI](https://github.com/Valliappan21/pagination-sorter/actions/workflows/ci.yml/badge.svg)](https://github.com/Valliappan21/pagination-sorter/actions/workflows/ci.yml)
4
+
5
+ Small JavaScript utility for deriving pagination metadata from an existing list,
6
+ a new list, and a page size.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @valliappan-periannan/pagination-sorter
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```js
17
+ const { sortPagination } = require('@valliappan-periannan/pagination-sorter');
18
+
19
+ const pagination = sortPagination({
20
+ existingList: [{ id: 1 }, { id: 2 }, { id: 3 }],
21
+ newList: [{ id: 4 }, { id: 5 }],
22
+ numberOfItems: 2,
23
+ filterField: 'id'
24
+ });
25
+
26
+ console.log(pagination);
27
+ ```
28
+
29
+ ## Try Now
30
+
31
+ Run the local example:
32
+
33
+ ```bash
34
+ npm run try-now
35
+ ```
36
+
37
+ The example uses these defined lists from `examples/try-now.js`:
38
+
39
+ ```js
40
+ const data = {
41
+ existingList: [
42
+ { id: 1, name: 'Alpha' },
43
+ { id: 2, name: 'Beta' },
44
+ { id: 3, name: 'Gamma' },
45
+ { id: 4, name: 'Delta' }
46
+ ],
47
+ newList: [
48
+ { id: 4, name: 'Delta duplicate' },
49
+ { id: 5, name: 'Epsilon' },
50
+ { id: 6, name: 'Zeta' },
51
+ { id: 7, name: 'Eta' }
52
+ ],
53
+ numberOfItems: 3,
54
+ filterField: 'id'
55
+ };
56
+ ```
57
+
58
+ Edit the lists, `numberOfItems`, or `filterField` in `examples/try-now.js`, then rerun
59
+ `npm run try-now` to see the updated pagination result.
60
+
61
+ ## Benchmark
62
+
63
+ Run the 10 lakh records benchmark:
64
+
65
+ ```bash
66
+ npm run benchmark
67
+ ```
68
+
69
+ The benchmark generates 1,000,000 records, validates the pagination result, and
70
+ prints elapsed time and memory usage. It reports timing only, so performance
71
+ differences between machines do not fail the run.
72
+
73
+ Generate a markdown benchmark report:
74
+
75
+ ```bash
76
+ npm run benchmark:report
77
+ ```
78
+
79
+ ## Latest Benchmark Report
80
+
81
+ The GitHub Actions workflow generates a benchmark report for 10 lakh records on
82
+ every push and pull request.
83
+
84
+ View the latest report from the CI workflow runs:
85
+
86
+ https://github.com/Valliappan21/pagination-sorter/actions/workflows/ci.yml
87
+
88
+ Each run includes the benchmark in the job summary and uploads
89
+ `benchmark-report.md` as the `benchmark-report` artifact.
90
+
91
+ ## Release
92
+
93
+ Publishing to npm runs through GitHub Actions when a GitHub Release is
94
+ published. The repository must have an `NPM_TOKEN` secret with permission to
95
+ publish `@valliappan-periannan/pagination-sorter`.
96
+
97
+ The publish workflow validates tests, generates the benchmark report, verifies
98
+ package contents with `npm pack --dry-run`, and publishes with
99
+ `npm publish --access public`.
100
+
101
+ ## API
102
+
103
+ ### `sortPagination(data)`
104
+
105
+ Accepts one object:
106
+
107
+ ```js
108
+ {
109
+ existingList: [],
110
+ newList: [],
111
+ numberOfItems: 10,
112
+ filterField: 'id'
113
+ }
114
+ ```
115
+
116
+ Returns:
117
+
118
+ ```js
119
+ {
120
+ combinedList,
121
+ currentPageItems,
122
+ totalItems,
123
+ pageSize,
124
+ totalPages,
125
+ currentPage,
126
+ nextPage,
127
+ previousPage,
128
+ hasNextPage,
129
+ hasPreviousPage
130
+ }
131
+ ```
132
+
133
+ `numberOfItems` is treated as the page size. `filterField` is mandatory and
134
+ must be the name of a field whose value is a string or number on every list
135
+ item. `combinedList` keeps the first item for each field value, checking
136
+ `existingList` first and then adding only unique items from `newList`.
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const { performance } = require('node:perf_hooks');
7
+ const packageJson = require('../package.json');
8
+ const { sortPagination } = require('../index');
9
+
10
+ const TOTAL_RECORDS = 1000000;
11
+ const EXISTING_RECORDS = 750000;
12
+ const NEW_RECORDS = TOTAL_RECORDS - EXISTING_RECORDS;
13
+ const PAGE_SIZE = 1000;
14
+ const FILTER_FIELD = 'id';
15
+
16
+ function createRecords(length, offset = 0) {
17
+ return Array.from({ length }, (_, index) => ({ id: offset + index + 1 }));
18
+ }
19
+
20
+ function formatBytes(bytes) {
21
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
22
+ }
23
+
24
+ const existingList = createRecords(EXISTING_RECORDS);
25
+ const newList = createRecords(NEW_RECORDS, EXISTING_RECORDS);
26
+
27
+ const start = performance.now();
28
+ const result = sortPagination({
29
+ existingList,
30
+ newList,
31
+ numberOfItems: PAGE_SIZE,
32
+ filterField: FILTER_FIELD
33
+ });
34
+ const duration = performance.now() - start;
35
+
36
+ assert.equal(result.totalItems, TOTAL_RECORDS);
37
+ assert.equal(result.totalPages, 1000);
38
+ assert.equal(result.currentPage, 751);
39
+ assert.equal(result.previousPage, 750);
40
+ assert.equal(result.nextPage, 752);
41
+ assert.equal(result.hasPreviousPage, true);
42
+ assert.equal(result.hasNextPage, true);
43
+ assert.equal(result.currentPageItems[0].id, 750001);
44
+ assert.equal(result.currentPageItems[result.currentPageItems.length - 1].id, 751000);
45
+
46
+ const memory = process.memoryUsage();
47
+ const report = `# Benchmark Report
48
+
49
+ ## ${packageJson.name}
50
+
51
+ | Metric | Value |
52
+ | --- | --- |
53
+ | Benchmark | 10 lakh records |
54
+ | Node.js | ${process.version} |
55
+ | Platform | ${os.platform()} ${os.release()} |
56
+ | Total records | ${TOTAL_RECORDS.toLocaleString('en-US')} |
57
+ | Existing records | ${EXISTING_RECORDS.toLocaleString('en-US')} |
58
+ | New records | ${NEW_RECORDS.toLocaleString('en-US')} |
59
+ | Page size | ${PAGE_SIZE.toLocaleString('en-US')} |
60
+ | Filter field | ${FILTER_FIELD} |
61
+ | Total pages | ${result.totalPages.toLocaleString('en-US')} |
62
+ | Current page | ${result.currentPage.toLocaleString('en-US')} |
63
+ | Elapsed time | ${duration.toFixed(3)} ms |
64
+ | Heap used | ${formatBytes(memory.heapUsed)} |
65
+ | RSS | ${formatBytes(memory.rss)} |
66
+ | Correctness | Passed |
67
+ `;
68
+
69
+ fs.writeFileSync('benchmark-report.md', report);
70
+ console.log(report);
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const { performance } = require('node:perf_hooks');
5
+ const { sortPagination } = require('../index');
6
+
7
+ const TOTAL_RECORDS = 1000000;
8
+ const EXISTING_RECORDS = 750000;
9
+ const NEW_RECORDS = TOTAL_RECORDS - EXISTING_RECORDS;
10
+ const PAGE_SIZE = 1000;
11
+
12
+ function createRecords(length, offset = 0) {
13
+ return Array.from({ length }, (_, index) => ({ id: offset + index + 1 }));
14
+ }
15
+
16
+ function formatBytes(bytes) {
17
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
18
+ }
19
+
20
+ const existingList = createRecords(EXISTING_RECORDS);
21
+ const newList = createRecords(NEW_RECORDS, EXISTING_RECORDS);
22
+
23
+ const start = performance.now();
24
+ const result = sortPagination({
25
+ existingList,
26
+ newList,
27
+ numberOfItems: PAGE_SIZE,
28
+ filterField: 'id'
29
+ });
30
+ const duration = performance.now() - start;
31
+
32
+ assert.equal(result.totalItems, TOTAL_RECORDS);
33
+ assert.equal(result.totalPages, 1000);
34
+ assert.equal(result.currentPage, 751);
35
+ assert.equal(result.previousPage, 750);
36
+ assert.equal(result.nextPage, 752);
37
+ assert.equal(result.hasPreviousPage, true);
38
+ assert.equal(result.hasNextPage, true);
39
+ assert.equal(result.currentPageItems[0].id, 750001);
40
+ assert.equal(result.currentPageItems[result.currentPageItems.length - 1].id, 751000);
41
+
42
+ const memory = process.memoryUsage();
43
+
44
+ console.log('Pagination benchmark: 10 lakh records');
45
+ console.log(`Total records: ${result.totalItems}`);
46
+ console.log(`Page size: ${result.pageSize}`);
47
+ console.log(`Total pages: ${result.totalPages}`);
48
+ console.log(`Current page: ${result.currentPage}`);
49
+ console.log(`Elapsed time: ${duration.toFixed(3)} ms`);
50
+ console.log(`Heap used: ${formatBytes(memory.heapUsed)}`);
51
+ console.log(`RSS: ${formatBytes(memory.rss)}`);
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { sortPagination } = require('../index');
4
+
5
+ const data = {
6
+ existingList: [
7
+ { id: 1, name: 'Alpha' },
8
+ { id: 2, name: 'Beta' },
9
+ { id: 3, name: 'Gamma' },
10
+ { id: 4, name: 'Delta' }
11
+ ],
12
+ newList: [
13
+ { id: 4, name: 'Delta duplicate' },
14
+ { id: 5, name: 'Epsilon' },
15
+ { id: 6, name: 'Zeta' },
16
+ { id: 7, name: 'Eta' }
17
+ ],
18
+ numberOfItems: 3,
19
+ filterField: 'id'
20
+ };
21
+
22
+ const pagination = sortPagination(data);
23
+
24
+ console.log(JSON.stringify(pagination, null, 2));
package/index.js ADDED
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ function assertArray(value, name) {
4
+ if (!Array.isArray(value)) {
5
+ throw new TypeError(`${name} must be an array.`);
6
+ }
7
+ }
8
+
9
+ function assertPageSize(value) {
10
+ if (!Number.isInteger(value) || value <= 0) {
11
+ throw new TypeError('numberOfItems must be a positive integer.');
12
+ }
13
+ }
14
+
15
+ function assertFilterField(value) {
16
+ if (typeof value !== 'string' || value.length === 0) {
17
+ throw new TypeError('filterField must be a non-empty string.');
18
+ }
19
+ }
20
+
21
+ function getPaginationItems(existingList, newList, filterField) {
22
+ const seenValues = new Set();
23
+ const combinedList = new Array(existingList.length + newList.length);
24
+ let writeIndex = 0;
25
+ let existingItemCount = 0;
26
+
27
+ for (let index = 0; index < existingList.length; index += 1) {
28
+ const item = existingList[index];
29
+
30
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
31
+ throw new TypeError(`existingList[${index}] must be an object.`);
32
+ }
33
+
34
+ const value = item[filterField];
35
+ const valueType = typeof value;
36
+
37
+ if (valueType !== 'string' && valueType !== 'number') {
38
+ throw new TypeError(
39
+ `existingList[${index}].${filterField} must be a string or number.`
40
+ );
41
+ }
42
+
43
+ if (!seenValues.has(value)) {
44
+ seenValues.add(value);
45
+ combinedList[writeIndex] = item;
46
+ writeIndex += 1;
47
+ existingItemCount += 1;
48
+ }
49
+ }
50
+
51
+ for (let index = 0; index < newList.length; index += 1) {
52
+ const item = newList[index];
53
+
54
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
55
+ throw new TypeError(`newList[${index}] must be an object.`);
56
+ }
57
+
58
+ const value = item[filterField];
59
+ const valueType = typeof value;
60
+
61
+ if (valueType !== 'string' && valueType !== 'number') {
62
+ throw new TypeError(
63
+ `newList[${index}].${filterField} must be a string or number.`
64
+ );
65
+ }
66
+
67
+ if (!seenValues.has(value)) {
68
+ seenValues.add(value);
69
+ combinedList[writeIndex] = item;
70
+ writeIndex += 1;
71
+ }
72
+ }
73
+
74
+ combinedList.length = writeIndex;
75
+ seenValues.clear();
76
+
77
+ return {
78
+ combinedList,
79
+ existingItemCount
80
+ };
81
+ }
82
+
83
+ function getCurrentPage(existingItemCount, totalPages, pageSize) {
84
+ if (totalPages === 0 || existingItemCount === 0) {
85
+ return 1;
86
+ }
87
+
88
+ const nextPage = Math.floor(existingItemCount / pageSize) + 1;
89
+ return Math.min(nextPage, totalPages);
90
+ }
91
+
92
+ function sortPagination(data) {
93
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
94
+ throw new TypeError('data must be an object.');
95
+ }
96
+
97
+ const { existingList, newList, numberOfItems, filterField } = data;
98
+
99
+ assertArray(existingList, 'existingList');
100
+ assertArray(newList, 'newList');
101
+ assertPageSize(numberOfItems);
102
+ assertFilterField(filterField);
103
+
104
+ const paginationItems = getPaginationItems(existingList, newList, filterField);
105
+ const combinedList = paginationItems.combinedList;
106
+ const totalItems = combinedList.length;
107
+ const pageSize = numberOfItems;
108
+ const totalPages = Math.ceil(totalItems / pageSize);
109
+ const currentPage = getCurrentPage(
110
+ paginationItems.existingItemCount,
111
+ totalPages,
112
+ pageSize
113
+ );
114
+ const startIndex = (currentPage - 1) * pageSize;
115
+ const currentPageItems = combinedList.slice(startIndex, startIndex + pageSize);
116
+ const hasPreviousPage = currentPage > 1;
117
+ const hasNextPage = currentPage < totalPages;
118
+
119
+ return {
120
+ combinedList,
121
+ currentPageItems,
122
+ totalItems,
123
+ pageSize,
124
+ totalPages,
125
+ currentPage,
126
+ nextPage: hasNextPage ? currentPage + 1 : null,
127
+ previousPage: hasPreviousPage ? currentPage - 1 : null,
128
+ hasNextPage,
129
+ hasPreviousPage
130
+ };
131
+ }
132
+
133
+ module.exports = {
134
+ sortPagination
135
+ };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@valliappan-periannan/pagination-sorter",
3
+ "version": "0.2.1",
4
+ "description": "Utility helper for deriving pagination metadata from existing and new lists.",
5
+ "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "README.md",
9
+ "examples/**",
10
+ "benchmark/**"
11
+ ],
12
+ "scripts": {
13
+ "test": "node --test",
14
+ "try-now": "node examples/try-now.js",
15
+ "benchmark": "node benchmark/ten-lakh-records.js",
16
+ "benchmark:report": "node benchmark/report.js"
17
+ },
18
+ "keywords": [
19
+ "pagination",
20
+ "list",
21
+ "metadata",
22
+ "utility"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT"
26
+ }