[WIP] Move page-related functionality into separate service

This commit is contained in:
gconsidine
2018-02-27 17:12:55 -05:00
committed by Jake McDermott
parent a5bd905f18
commit df84f822f6
5 changed files with 218 additions and 71 deletions

View File

@@ -5,6 +5,7 @@ let vm;
let ansi; let ansi;
let model; let model;
let resource; let resource;
let page;
let container; let container;
let $timeout; let $timeout;
let $sce; let $sce;
@@ -41,7 +42,7 @@ const TIME_EVENTS = [
function JobsIndexController ( function JobsIndexController (
_resource_, _resource_,
webSocketNamespace, _page_,
_$sce_, _$sce_,
_$timeout_, _$timeout_,
_$scope_, _$scope_,
@@ -56,6 +57,7 @@ function JobsIndexController (
$scope = _$scope_; $scope = _$scope_;
$q = _$q_; $q = _$q_;
resource = _resource_; resource = _resource_;
page = _page_;
model = resource.model; model = resource.model;
ansi = new Ansi(); ansi = new Ansi();
@@ -64,7 +66,9 @@ function JobsIndexController (
const parsed = parseEvents(events); const parsed = parseEvents(events);
const html = $sce.trustAsHtml(parsed.html); const html = $sce.trustAsHtml(parsed.html);
cache.push({ page: 1, lines: parsed.lines }); page.init(resource);
page.add({ number: 1, lines: parsed.lines });
// Development helper(s) // Development helper(s)
vm.clear = devClear; vm.clear = devClear;
@@ -86,10 +90,12 @@ function JobsIndexController (
vm.isExpanded = true; vm.isExpanded = true;
// Real-time (active between JOB_START and JOB_END events only) // Real-time (active between JOB_START and JOB_END events only)
$scope.$on(webSocketNamespace, processWebSocketEvents); $scope.$on(resource.ws.namespace, processWebSocketEvents);
vm.stream = { vm.stream = {
isActive: false, isActive: false,
isRendering: false, isRendering: false,
isPaused: false,
buffered: 0,
count: 0, count: 0,
page: 1 page: 1
}; };
@@ -105,7 +111,13 @@ function JobsIndexController (
}); });
} }
// TODO: Determine how to manage buffered events (store in page cache vs. separate)
// Leaning towards keeping separate (same as they come in over WS). On resume of scroll,
// Clear/reset cache, append buffered events, then back to normal render cycle
function processWebSocketEvents (scope, data) { function processWebSocketEvents (scope, data) {
let done;
if (data.event === JOB_START) { if (data.event === JOB_START) {
vm.scroll.isActive = true; vm.scroll.isActive = true;
vm.stream.isActive = true; vm.stream.isActive = true;
@@ -114,37 +126,28 @@ function processWebSocketEvents (scope, data) {
vm.stream.isActive = false; vm.stream.isActive = false;
} }
// TODO: Determine how to manage buffered events (store in page cache vs. separate) const pageAdded = page.addToBuffer(data);
// Leaning towards keeping separate (same as they come in over WS). On resume of scroll,
// Clear/reset cache, append buffered events, then back to normal render cycle
if (vm.stream.count % resource.page.size === 0) { if (pageAdded && !vm.scroll.isLocked) {
cache.push({ page: vm.stream.page }); vm.stream.isPaused = true;
vm.stream.page++;
if (buffer.length > (resource.page.resultLimit - resource.page.size)) {
buffer.splice(0, (buffer.length - resource.page.resultLimit) + resource.page.size);
}
} }
vm.stream.count++; if (vm.stream.isPaused && vm.scroll.isLocked) {
buffer.push(data); vm.stream.isPaused = false;
}
if (vm.stream.isRendering || !vm.scroll.isLocked) { if (vm.stream.isRendering || vm.stream.isPaused) {
return; return;
} }
vm.stream.isRendering = true; const events = page.emptyBuffer();
const events = buffer.slice(0, buffer.length);
buffer = [];
return render(events); return render(events);
} }
function render (events) { function render (events) {
vm.stream.isRendering = true;
return shift() return shift()
.then(() => append(events)) .then(() => append(events))
.then(() => { .then(() => {
@@ -154,16 +157,15 @@ function render (events) {
} }
if (!vm.stream.isActive) { if (!vm.stream.isActive) {
if (buffer.length) { const buffer = page.emptyBuffer();
events = buffer.slice(0, buffer.length);
buffer = [];
return render(events); if (buffer.length) {
} else { return render(buffer);
vm.stream.isRendering = false;
vm.scroll.isLocked = false;
vm.scroll.isActive = false;
} }
vm.stream.isRendering = false;
vm.scroll.isLocked = false;
vm.scroll.isActive = false;
} else { } else {
vm.stream.isRendering = false; vm.stream.isRendering = false;
} }
@@ -172,13 +174,14 @@ function render (events) {
function devClear () { function devClear () {
cache = []; cache = [];
page.init(resource);
clear(); clear();
} }
function next () { function next () {
const config = { const config = {
related: resource.related, related: resource.related,
page: cache[cache.length - 1].page + 1, page: vm.scroll.lastPage + 1,
params: { params: {
order_by: 'start_line' order_by: 'start_line'
} }
@@ -191,9 +194,9 @@ function next () {
return $q.resolve(); return $q.resolve();
} }
cache.push({ cache.push({ page: data.page, events: [] });
page: data.page
}); vm.scroll.lastPage = data.page;
return shift() return shift()
.then(() => append(data.results)); .then(() => append(data.results));
@@ -205,12 +208,13 @@ function prev () {
const config = { const config = {
related: resource.related, related: resource.related,
page: cache[0].page - 1, page: vm.scroll.firstPage - 1,
params: { params: {
order_by: 'start_line' order_by: 'start_line'
} }
}; };
console.log(cache);
// console.log('[2] getting previous page', config.page, cache); // console.log('[2] getting previous page', config.page, cache);
return model.goToPage(config) return model.goToPage(config)
.then(data => { .then(data => {
@@ -218,12 +222,13 @@ function prev () {
return $q.resolve(); return $q.resolve();
} }
cache.unshift({ cache.unshift({ page: data.page, events: [] });
page: data.page
}); vm.scroll.firstPage = data.page;
const previousHeight = container.scrollHeight; const previousHeight = container.scrollHeight;
console.log(cache);
return pop() return pop()
.then(() => prepend(data.results)) .then(() => prepend(data.results))
.then(lines => { .then(lines => {
@@ -241,13 +246,8 @@ function append (events) {
const parsed = parseEvents(events); const parsed = parseEvents(events);
const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html))); const rows = $($sce.getTrustedHtml($sce.trustAsHtml(parsed.html)));
const table = $(ELEMENT_TBODY); const table = $(ELEMENT_TBODY);
const index = cache.length - 1;
if (cache[index].lines) { page.updateLineCount('current', parsed.lines);
cache[index].lines += parsed.lines;
} else {
cache[index].lines = parsed.lines;
}
table.append(rows); table.append(rows);
$compile(rows.contents())($scope); $compile(rows.contents())($scope);
@@ -289,6 +289,8 @@ function pop () {
// console.log('[3.1] popping', ejected); // console.log('[3.1] popping', ejected);
const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines); const rows = $(ELEMENT_TBODY).children().slice(-ejected.lines);
vm.scroll.firstPage = cache[0].page;
rows.empty(); rows.empty();
rows.remove(); rows.remove();
@@ -298,17 +300,19 @@ function pop () {
} }
function shift () { function shift () {
console.log('[3] shifting old page', cache.length); // console.log('[3] shifting old page', cache.length);
return $q(resolve => { return $q(resolve => {
if (cache.length <= resource.page.pageLimit) { if (!page.isOverCapacity()) {
// console.log('[3.1] nothing to shift'); // console.log('[3.1] nothing to shift');
return resolve(); return resolve();
} }
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
const ejected = cache.shift(); const lines = page.trim();
console.log('[3.1] shifting', ejected); //console.log('[3.1] shifting', lines);
const rows = $(ELEMENT_TBODY).children().slice(0, ejected.lines); const rows = $(ELEMENT_TBODY).children().slice(0, lines);
vm.scroll.firstPage = page.getPageNumber('first');
rows.empty(); rows.empty();
rows.remove(); rows.remove();
@@ -637,17 +641,20 @@ function scrollHome () {
function scrollEnd () { function scrollEnd () {
if (vm.scroll.isLocked) { if (vm.scroll.isLocked) {
// Make note of current page when unlocked -- keep buffered events for that page for
// continuity
vm.scroll.firstPage = cache[0].page;
vm.scroll.lastPage = cache[cache.length - 1].page;
vm.scroll.isLocked = false; vm.scroll.isLocked = false;
vm.scroll.isActive = false; vm.scroll.isActive = false;
return; return;
} else if (!vm.scroll.isLocked && vm.stream.isActive) { } else if (!vm.scroll.isLocked && vm.stream.isActive) {
vm.scroll.isActive = true; vm.scroll.isActive = true;
vm.scroll.isLocked = true;
return clear() return;
.then(() => {
vm.scroll.isLocked = true;
});
} }
const config = { const config = {
@@ -698,7 +705,7 @@ function scrollPageDown () {
JobsIndexController.$inject = [ JobsIndexController.$inject = [
'resource', 'resource',
'webSocketNamespace', 'JobPageService',
'$sce', '$sce',
'$timeout', '$timeout',
'$scope', '$scope',

View File

@@ -3,15 +3,17 @@ import IndexController from '~features/output/index.controller';
import atLibModels from '~models'; import atLibModels from '~models';
import atLibComponents from '~components'; import atLibComponents from '~components';
import JobsStrings from '~features/output/jobs.strings'; import Strings from '~features/output/jobs.strings';
import IndexController from '~features/output/index.controller'; import Controller from '~features/output/index.controller';
import PageService from '~features/output/page.service';
const indexTemplate = require('~features/output/index.view.html'); const Template = require('~features/output/index.view.html');
const MODULE_NAME = 'at.features.output'; const MODULE_NAME = 'at.features.output';
const PAGE_CACHE = true; const PAGE_CACHE = true;
const PAGE_LIMIT = 3; const PAGE_LIMIT = 3;
const PAGE_SIZE = 100; const PAGE_SIZE = 100;
const WS_PREFIX = 'ws';
function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) { function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJob, $stateParams) {
const { id, type } = $stateParams; const { id, type } = $stateParams;
@@ -56,20 +58,20 @@ function resolveResource (Job, ProjectUpdate, AdHocCommand, SystemJob, WorkflowJ
type, type,
model, model,
related, related,
ws: getWebSocketResource(type), ws: {
namespace: `${WS_PREFIX}-${getWebSocketResource(type).key}-${id}`
},
page: { page: {
cache: PAGE_CACHE, cache: PAGE_CACHE,
size: PAGE_SIZE, size: PAGE_SIZE,
pageLimit: PAGE_LIMIT, pageLimit: PAGE_LIMIT
resultLimit: PAGE_SIZE * PAGE_LIMIT
} }
}; };
}); });
} }
function resolveWebSocket (SocketService, $stateParams) { function resolveWebSocketConnection (SocketService, $stateParams) {
const { type, id } = $stateParams; const { type, id } = $stateParams;
const prefix = 'ws';
const resource = getWebSocketResource(type); const resource = getWebSocketResource(type);
let name; let name;
@@ -87,8 +89,6 @@ function resolveWebSocket (SocketService, $stateParams) {
}; };
SocketService.addStateResolve(state, id); SocketService.addStateResolve(state, id);
return `${prefix}-${resource.key}-${id}`;
} }
function resolveBreadcrumb (strings) { function resolveBreadcrumb (strings) {
@@ -139,8 +139,8 @@ function JobsRun ($stateRegistry) {
}, },
views: { views: {
'@': { '@': {
templateUrl: indexTemplate, templateUrl: Template,
controller: IndexController, controller: Controller,
controllerAs: 'vm' controllerAs: 'vm'
} }
}, },
@@ -155,13 +155,13 @@ function JobsRun ($stateRegistry) {
resolveResource resolveResource
], ],
ncyBreadcrumb: [ ncyBreadcrumb: [
'JobsStrings', 'JobStrings',
resolveBreadcrumb resolveBreadcrumb
], ],
webSocketNamespace: [ webSocketConnection: [
'SocketService', 'SocketService',
'$stateParams', '$stateParams',
resolveWebSocket resolveWebSocketConnection
] ]
}, },
}; };
@@ -176,8 +176,8 @@ angular
atLibModels, atLibModels,
atLibComponents atLibComponents
]) ])
.controller('indexController', IndexController) .service('JobStrings', Strings)
.service('JobsStrings', JobsStrings) .service('JobPageService', PageService)
.run(JobsRun); .run(JobsRun);
export default MODULE_NAME; export default MODULE_NAME;

View File

@@ -0,0 +1,140 @@
function JobPageService () {
this.page = null;
this.resource = null;
this.result = null;
this.buffer = null;
this.cache = null;
this.init = resource => {
this.resource = resource;
this.page = {
limit: resource.page.pageLimit,
size: resource.page.size,
current: 0,
index: -1,
count: 0
};
this.result = {
limit: this.page.limit * this.page.size,
count: 0
};
this.buffer = {
count: 0
};
this.cache = [];
};
this.add = (page, position) => {
page.events = page.events || [];
page.lines = page.lines || 0;
if (!position) {
this.cache.push(page);
}
this.page.count++;
};
this.addToBuffer = event => {
let pageAdded = false;
if (this.result.count % this.page.size === 0) {
pageAdded = true;
this.add({ number: this.page.count + 1, events: [event] });
this.trimBuffer();
} else {
this.cache[this.cache.length - 1].events.push(event);
}
this.buffer.count++;
this.result.count++;
return pageAdded;
};
this.trimBuffer = () => {
const diff = this.cache.length - this.page.limit;
if (diff <= 0) {
return;
}
for (let i = 0; i < diff; i++) {
if (this.cache[i].events) {
this.buffer.count -= this.cache[i].events.length;
this.cache[i].events = [];
}
}
};
this.emptyBuffer = () => {
let data = [];
for (let i = 0; i < this.cache.length; i++) {
const events = this.cache[i].events;
if (events.length > 0) {
this.buffer.count -= events.length;
data = data.concat(this.cache[i].events.splice(0, events.length));
}
}
return data;
};
this.isOverCapacity = () => {
return (this.cache.length - this.page.limit) > 0;
};
this.trim = () => {
const count = this.cache.length - this.page.limit;
const ejected = this.cache.splice(0, count);
const linesRemoved = ejected.reduce((total, page) => total + page.lines, 0);
return linesRemoved;
};
this.getPageNumber = (page) => {
let index;
if (page === 'first') {
index = 0;
}
return this.cache[index].number;
};
this.updateLineCount = (page, lines) => {
let index;
if (page === 'current') {
index = this.cache.length - 1;
}
if (this.cache[index].lines) {
this.cache[index].lines += lines;
} else {
this.cache[index].lines = lines;
}
}
this.next = () => {
};
this.prev = () => {
};
this.current = () => {
return this.resource.model.get(`related.${this.resource.related}.results`);
};
}
export default JobPageService;