@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 +136 -0
- package/benchmark/report.js +70 -0
- package/benchmark/ten-lakh-records.js +51 -0
- package/examples/try-now.js +24 -0
- package/index.js +135 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# @valliappan-periannan/pagination-sorter
|
|
2
|
+
|
|
3
|
+
[](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
|
+
}
|