Skip to content

Instantly share code, notes, and snippets.

@cjea
Created October 20, 2020 20:35
Show Gist options
  • Save cjea/98b10dd232c15cff69d8e18e3346ec6c to your computer and use it in GitHub Desktop.
Save cjea/98b10dd232c15cff69d8e18e3346ec6c to your computer and use it in GitHub Desktop.
"use strict";
/**
* An example implementation of cursor-based pagination.
* The "database" is a singly-linked list of foods.
* The server uses the cursor to find the last returned record, so
* it can begin the next page after the cursor.
*
* The cursor is base64 encoded JSON with "type" and "id" fields.
* The "type" allows servers to tag pagination strategies, and change
* implementations later without users knowing about it.
* A "version" key would also make sense.
*/
let records = `{"1":{"id":"1","name":"eggs","tastiness":7,"next":"2"},"2":{"id":"2","name":"toast","tastiness":5,"next":"3"},"3":{"id":"3","name":"kale","tastiness":3,"next":"4"},"4":{"id":"4","name":"rice","tastiness":4,"next":"5"},"5":{"id":"5","name":"burrito","tastiness":9,"next":"6"},"6":{"id":"6","name":"hot pot","tastiness":8,"next":"7"},"7":{"id":"7","name":"reuben","tastiness":10,"next":"8"},"8":{"id":"8","name":"potato salad","tastiness":8,"next":null}}`;
Database.prototype = {
fetch(id) {
return this.records[id];
},
ids() {
return Object.keys(this.records).sort();
},
fetchFrom(id, limit, filters = []) {
let first = id === "undefined" ? this.ids()[0] : this.fetch(id).next;
let current = this.fetch(first);
let results = [];
while (current && results.length < limit) {
if (filters.every((f) => f(current))) results.push(current);
current = this.fetch(current.next);
}
return results;
},
};
function Database(records) {
this.records = records;
}
Server.prototype = {
encodeCursor(id) {
let str = `{"type":"direct", "id":"${id}"}`;
return Buffer.from(str).toString("base64");
},
fetchFrom(id, limit, filters = []) {
let data = this.db.fetchFrom(id, limit, filters);
if (data.length === 0) return { data, afterCursor: null };
let afterCursor = this.encodeCursor(data[data.length - 1].id);
return { data, afterCursor };
},
fetch(limit, filters = [], cursor = this.encodeCursor()) {
let data = JSON.parse(Buffer.from(cursor, "base64"));
switch (data.type) {
case "offset":
return this.fetchOffset(data.offset, limit, filters);
case "direct":
return this.fetchFrom(data.id, limit, filters);
default:
console.error(`unexpected cursor type: ${data.type}`);
}
},
};
function Server(db, defaultCursor = "direct") {
this.db = db;
this.defaultCursor = defaultCursor;
}
const PAGE_SIZE = 3;
const FILTERS = [(x) => x.tastiness > 3];
let server = new Server(new Database(JSON.parse(records)));
let currentPage = server.fetch(PAGE_SIZE, FILTERS);
let i = 1;
while (currentPage.afterCursor) {
console.log(`PAGE ${i++}`);
console.log(`--------`);
console.log(currentPage);
currentPage = server.fetch(PAGE_SIZE, FILTERS, currentPage.afterCursor);
}
console.log(currentPage); // the last page whose cursor is `null`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment