openvibers/openvibe.live Commits
Channel chat history: older scrollback in 25-row pieces, as many as fit in each idle moment (120-row pieces were still ~240 ms tasks on a phone CPU: TBT 2.6 s on /@goosely) (#209)
Changes
@@@ -2268,14 +2268,14 @@ function _renderHistoryOffscreen(chatEl, fill, widget = true) {}const _idle = (fn) => (window.requestIdleCallback ? requestIdleCallback(fn, { timeout: 1200 }) : setTimeout(fn, 60));--/** A payload as pieces of `n` rows in time order (as the renderer merges them), newest piece first. */--function _historyPieces(p, n) {++/** A payload as pieces in time order (as the renderer merges them), newest first: `first` rows, then `n` each. */++function _historyPieces(p, n, first = n) {if (!p || !p.data) return [p];const t = (m) => { const d = Date.parse(String(m.timestamp || '').replace(' ', 'T')); return isNaN(d) ? 0 : d; };const all = (p.data.messages || []).map((m) => [m, 0]).concat((p.globalMsgs || []).map((m) => [m, 1])).sort((a, b) => t(a[0]) - t(b[0]));const out = [];-- for (let end = all.length; end > 0; end -= n) {-- const part = all.slice(Math.max(0, end - n), end);++ for (let end = all.length, k = first; end > 0; end -= k, k = n) {++ const part = all.slice(Math.max(0, end - k), end);out.push({ ...p, data: { ...p.data, messages: part.filter((x) => !x[1]).map((x) => x[0]) }, globalMsgs: part.filter((x) => x[1]).map((x) => x[0]) });}return out.length ? out : [p];@@@ -2435,9 +2435,9 @@ async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {if (now.messages !== messages) return; // the page changed under usif (payload) {-- // The newest 120 rows first; older scrollback is put above them a piece per idle moment (all 1000 rows-- // in one go was a 2.7 s task on a phone), keeping the reader where they are.-- const pieces = _historyPieces(payload, 120);++ // The newest 120 rows first; older scrollback goes above them 25 rows at a time, as many as fit in each++ // idle moment (all 1000 rows at once was a 2.7 s task on a phone), keeping the reader where they are.++ const pieces = _historyPieces(payload, 25, 120);_swapChatHistory(messages, _renderHistoryOffscreen(now, () => _renderHistoryPayload(pieces[0])));messages.dataset.room = room;_chatCacheWrite(room, payload);@@@ -2445,13 +2445,13 @@ async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {const late = _chatLiveDuringHydrate;_chatLiveDuringHydrate = [];late.forEach((m) => addChatMessage(m));-- const older = (i) => i < pieces.length && _idle(() => {++ const older = (i) => i < pieces.length && _idle((dl) => {if (gen !== _chatHydrateGen || getChatEl().messages !== messages) return;-- const rows = _renderHistoryOffscreen(getChatEl(), () => _renderHistoryPayload(pieces[i]), false);const top = messages.scrollTop, h = messages.scrollHeight;-- messages.prepend(...rows.childNodes);++ do messages.prepend(..._renderHistoryOffscreen(getChatEl(), () => _renderHistoryPayload(pieces[i++]), false).childNodes);++ while (i < pieces.length && dl && dl.timeRemaining() > 15);messages.scrollTop = _chatPinned ? messages.scrollHeight : top + messages.scrollHeight - h;-- older(i + 1);++ older(i);});older(1);} else {@@@ -46,6 +46,9 @@ for (let i = 0; i + 1 < pieces.length; i++) {assert.ok(pieces[0].data.messages.some((m) => m.id === 'c299') && pieces[0].globalMsgs.some((m) => m.id === 'g249'));assert.deepStrictEqual([pieces[0].kind, pieces[0].data.channel, pieces[0].data.liveSlots], ['channel', 'goosely', [1]]);++// A bigger first piece (what fits on screen), then small ones.++assert.deepStrictEqual(_historyPieces(payload, 25, 120).map(rows), [120, ...Array(17).fill(25), 5], '550 rows: 120, then 17 × 25, then 5');++// The global room has no second feed; a small payload is one piece; an empty one is itself.const g = _historyPieces({ kind: 'global', data: { messages: global.slice(0, 50) } }, 120);assert.strictEqual(g.length, 1);
@@ -2268,14 +2268,14 @@ function _renderHistoryOffscreen(chatEl, fill, widget = true) {
@@ -2268,14 +2268,14 @@ function _renderHistoryOffscreen(chatEl, fill, widget = true) {
}
}
const _idle = (fn) => (window.requestIdleCallback ? requestIdleCallback(fn, { timeout: 1200 }) : setTimeout(fn, 60));
const _idle = (fn) => (window.requestIdleCallback ? requestIdleCallback(fn, { timeout: 1200 }) : setTimeout(fn, 60));
-/** A payload as pieces of `n` rows in time order (as the renderer merges them), newest piece first. */
-function _historyPieces(p, n) {
+/** A payload as pieces in time order (as the renderer merges them), newest first: `first` rows, then `n` each. */
+function _historyPieces(p, n, first = n) {
if (!p || !p.data) return [p];
if (!p || !p.data) return [p];
const t = (m) => { const d = Date.parse(String(m.timestamp || '').replace(' ', 'T')); return isNaN(d) ? 0 : d; };
const t = (m) => { const d = Date.parse(String(m.timestamp || '').replace(' ', 'T')); return isNaN(d) ? 0 : d; };
const all = (p.data.messages || []).map((m) => [m, 0]).concat((p.globalMsgs || []).map((m) => [m, 1])).sort((a, b) => t(a[0]) - t(b[0]));
const all = (p.data.messages || []).map((m) => [m, 0]).concat((p.globalMsgs || []).map((m) => [m, 1])).sort((a, b) => t(a[0]) - t(b[0]));
const out = [];
const out = [];
- for (let end = all.length; end > 0; end -= n) {
- const part = all.slice(Math.max(0, end - n), end);
+ for (let end = all.length, k = first; end > 0; end -= k, k = n) {
+ const part = all.slice(Math.max(0, end - k), end);
out.push({ ...p, data: { ...p.data, messages: part.filter((x) => !x[1]).map((x) => x[0]) }, globalMsgs: part.filter((x) => x[1]).map((x) => x[0]) });
out.push({ ...p, data: { ...p.data, messages: part.filter((x) => !x[1]).map((x) => x[0]) }, globalMsgs: part.filter((x) => x[1]).map((x) => x[0]) });
}
}
return out.length ? out : [p];
return out.length ? out : [p];
@@ -2435,9 +2435,9 @@ async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {
@@ -2435,9 +2435,9 @@ async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {
if (now.messages !== messages) return; // the page changed under us
if (now.messages !== messages) return; // the page changed under us
if (payload) {
if (payload) {
- // The newest 120 rows first; older scrollback is put above them a piece per idle moment (all 1000 rows
- // in one go was a 2.7 s task on a phone), keeping the reader where they are.
- const pieces = _historyPieces(payload, 120);
+ // The newest 120 rows first; older scrollback goes above them 25 rows at a time, as many as fit in each
+ // idle moment (all 1000 rows at once was a 2.7 s task on a phone), keeping the reader where they are.
+ const pieces = _historyPieces(payload, 25, 120);
_swapChatHistory(messages, _renderHistoryOffscreen(now, () => _renderHistoryPayload(pieces[0])));
_swapChatHistory(messages, _renderHistoryOffscreen(now, () => _renderHistoryPayload(pieces[0])));
messages.dataset.room = room;
messages.dataset.room = room;
_chatCacheWrite(room, payload);
_chatCacheWrite(room, payload);
@@ -2445,13 +2445,13 @@ async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {
@@ -2445,13 +2445,13 @@ async function hydrateActiveChatHistory(streamId, { clear = false } = {}) {
const late = _chatLiveDuringHydrate;
const late = _chatLiveDuringHydrate;
_chatLiveDuringHydrate = [];
_chatLiveDuringHydrate = [];
late.forEach((m) => addChatMessage(m));
late.forEach((m) => addChatMessage(m));
- const older = (i) => i < pieces.length && _idle(() => {
+ const older = (i) => i < pieces.length && _idle((dl) => {
if (gen !== _chatHydrateGen || getChatEl().messages !== messages) return;
if (gen !== _chatHydrateGen || getChatEl().messages !== messages) return;
- const rows = _renderHistoryOffscreen(getChatEl(), () => _renderHistoryPayload(pieces[i]), false);
const top = messages.scrollTop, h = messages.scrollHeight;
const top = messages.scrollTop, h = messages.scrollHeight;
- messages.prepend(...rows.childNodes);
+ do messages.prepend(..._renderHistoryOffscreen(getChatEl(), () => _renderHistoryPayload(pieces[i++]), false).childNodes);
+ while (i < pieces.length && dl && dl.timeRemaining() > 15);
messages.scrollTop = _chatPinned ? messages.scrollHeight : top + messages.scrollHeight - h;
messages.scrollTop = _chatPinned ? messages.scrollHeight : top + messages.scrollHeight - h;
- older(i + 1);
+ older(i);
});
});
older(1);
older(1);
} else {
} else {
@@ -46,6 +46,9 @@ for (let i = 0; i + 1 < pieces.length; i++) {
@@ -46,6 +46,9 @@ for (let i = 0; i + 1 < pieces.length; i++) {
assert.ok(pieces[0].data.messages.some((m) => m.id === 'c299') && pieces[0].globalMsgs.some((m) => m.id === 'g249'));
assert.ok(pieces[0].data.messages.some((m) => m.id === 'c299') && pieces[0].globalMsgs.some((m) => m.id === 'g249'));
assert.deepStrictEqual([pieces[0].kind, pieces[0].data.channel, pieces[0].data.liveSlots], ['channel', 'goosely', [1]]);
assert.deepStrictEqual([pieces[0].kind, pieces[0].data.channel, pieces[0].data.liveSlots], ['channel', 'goosely', [1]]);
+// A bigger first piece (what fits on screen), then small ones.
+assert.deepStrictEqual(_historyPieces(payload, 25, 120).map(rows), [120, ...Array(17).fill(25), 5], '550 rows: 120, then 17 × 25, then 5');
+
// The global room has no second feed; a small payload is one piece; an empty one is itself.
// The global room has no second feed; a small payload is one piece; an empty one is itself.
const g = _historyPieces({ kind: 'global', data: { messages: global.slice(0, 50) } }, 120);
const g = _historyPieces({ kind: 'global', data: { messages: global.slice(0, 50) } }, 120);
assert.strictEqual(g.length, 1);
assert.strictEqual(g.length, 1);