mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-07 09:22:53 +02:00
merge secondary channels by name (#468)
* merge secondary channels by name * cover missing unit test vectors
This commit is contained in:
@@ -108,7 +108,7 @@ test('buildChatTabModel returns sorted nodes and channel buckets', () => {
|
||||
|
||||
const secondaryChannel = channelByLabel.BerlinMesh;
|
||||
assert.equal(secondaryChannel.index, 1);
|
||||
assert.equal(secondaryChannel.id, 'channel-1');
|
||||
assert.equal(secondaryChannel.id, 'channel-secondary-berlinmesh');
|
||||
assert.equal(secondaryChannel.entries.length, 2);
|
||||
assert.deepEqual(secondaryChannel.entries.map(entry => entry.message.id), ['iso-ts', 'recent-alt']);
|
||||
});
|
||||
@@ -249,3 +249,80 @@ test('buildChatTabModel ignores plaintext log-only entries', () => {
|
||||
assert.equal(encryptedEntries.length, 1);
|
||||
assert.equal(encryptedEntries[0]?.message?.id, 'enc');
|
||||
});
|
||||
|
||||
test('buildChatTabModel merges secondary channels with matching labels regardless of index', () => {
|
||||
const primaryId = 'primary';
|
||||
const secondaryFirstId = 'secondary-one';
|
||||
const secondarySecondId = 'secondary-two';
|
||||
const label = 'MeshTown';
|
||||
const model = buildChatTabModel({
|
||||
nodes: [],
|
||||
messages: [
|
||||
{ id: secondaryFirstId, rx_time: NOW - 12, channel: 7, channel_name: label },
|
||||
{ id: primaryId, rx_time: NOW - 10, channel: 0, channel_name: label },
|
||||
{ id: secondarySecondId, rx_time: NOW - 8, channel: 3, channel_name: ` ${label} ` }
|
||||
],
|
||||
nowSeconds: NOW,
|
||||
windowSeconds: WINDOW
|
||||
});
|
||||
|
||||
const meshChannels = model.channels.filter(channel => channel.label === label);
|
||||
assert.equal(meshChannels.length, 2);
|
||||
|
||||
const primaryChannel = meshChannels.find(channel => channel.index === 0);
|
||||
assert.ok(primaryChannel);
|
||||
assert.equal(primaryChannel.entries.length, 1);
|
||||
assert.equal(primaryChannel.entries[0]?.message?.id, primaryId);
|
||||
|
||||
const secondaryChannel = meshChannels.find(channel => channel.index > 0);
|
||||
assert.ok(secondaryChannel);
|
||||
assert.equal(secondaryChannel.id, 'channel-secondary-meshtown');
|
||||
assert.equal(secondaryChannel.index, 3);
|
||||
assert.deepEqual(secondaryChannel.entries.map(entry => entry.message.id), [secondaryFirstId, secondarySecondId]);
|
||||
});
|
||||
|
||||
test('buildChatTabModel rekeys unnamed secondary buckets when a label later arrives', () => {
|
||||
const unnamedId = 'unnamed';
|
||||
const namedId = 'named';
|
||||
const label = 'SideMesh';
|
||||
const index = 4;
|
||||
const model = buildChatTabModel({
|
||||
nodes: [],
|
||||
messages: [
|
||||
{ id: unnamedId, rx_time: NOW - 15, channel: index },
|
||||
{ id: namedId, rx_time: NOW - 10, channel: index, channel_name: label }
|
||||
],
|
||||
nowSeconds: NOW,
|
||||
windowSeconds: WINDOW
|
||||
});
|
||||
|
||||
const secondaryChannels = model.channels.filter(channel => channel.index === index);
|
||||
assert.equal(secondaryChannels.length, 1);
|
||||
const [secondaryChannel] = secondaryChannels;
|
||||
assert.equal(secondaryChannel.id, 'channel-secondary-sidemesh');
|
||||
assert.equal(secondaryChannel.label, label);
|
||||
assert.deepEqual(secondaryChannel.entries.map(entry => entry.message.id), [unnamedId, namedId]);
|
||||
});
|
||||
|
||||
test('buildChatTabModel merges unlabeled secondary messages into existing named buckets by index', () => {
|
||||
const namedId = 'named';
|
||||
const unlabeledId = 'unlabeled';
|
||||
const label = 'MeshNorth';
|
||||
const index = 5;
|
||||
const model = buildChatTabModel({
|
||||
nodes: [],
|
||||
messages: [
|
||||
{ id: namedId, rx_time: NOW - 12, channel: index, channel_name: label },
|
||||
{ id: unlabeledId, rx_time: NOW - 8, channel: index }
|
||||
],
|
||||
nowSeconds: NOW,
|
||||
windowSeconds: WINDOW
|
||||
});
|
||||
|
||||
const secondaryChannels = model.channels.filter(channel => channel.index === index);
|
||||
assert.equal(secondaryChannels.length, 1);
|
||||
const [secondaryChannel] = secondaryChannels;
|
||||
assert.equal(secondaryChannel.id, 'channel-secondary-meshnorth');
|
||||
assert.equal(secondaryChannel.label, label);
|
||||
assert.deepEqual(secondaryChannel.entries.map(entry => entry.message.id), [namedId, unlabeledId]);
|
||||
});
|
||||
|
||||
@@ -189,8 +189,28 @@ export function buildChatTabModel({
|
||||
modemPreset,
|
||||
envFallbackLabel: primaryChannelEnvLabel
|
||||
});
|
||||
const bucketKey = buildChannelBucketKey(safeIndex, safeIndex === 0 && labelInfo.label !== '0' ? labelInfo.label : null);
|
||||
const nameBucketKey = safeIndex > 0 ? buildSecondaryNameBucketKey(labelInfo) : null;
|
||||
const primaryBucketKey = safeIndex === 0 && labelInfo.label !== '0' ? buildPrimaryBucketKey(labelInfo.label) : '0';
|
||||
|
||||
let bucketKey = safeIndex === 0 ? primaryBucketKey : nameBucketKey ?? String(safeIndex);
|
||||
let bucket = channelBuckets.get(bucketKey);
|
||||
|
||||
if (!bucket && safeIndex > 0) {
|
||||
const existingBucketKey = findExistingBucketKeyByIndex(channelBuckets, safeIndex);
|
||||
if (existingBucketKey) {
|
||||
bucketKey = existingBucketKey;
|
||||
bucket = channelBuckets.get(existingBucketKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (bucket && nameBucketKey && bucket.key !== nameBucketKey) {
|
||||
channelBuckets.delete(bucket.key);
|
||||
bucket.key = nameBucketKey;
|
||||
bucket.id = buildChannelTabId(nameBucketKey);
|
||||
channelBuckets.set(nameBucketKey, bucket);
|
||||
bucketKey = nameBucketKey;
|
||||
}
|
||||
|
||||
if (!bucket) {
|
||||
bucket = {
|
||||
key: bucketKey,
|
||||
@@ -208,6 +228,9 @@ export function buildChatTabModel({
|
||||
bucket.label = labelInfo.label;
|
||||
bucket.labelPriority = labelInfo.priority;
|
||||
}
|
||||
if (Number.isFinite(safeIndex)) {
|
||||
bucket.index = Math.min(bucket.index ?? safeIndex, safeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
bucket.entries.push({ ts, message });
|
||||
@@ -425,15 +448,47 @@ export function normaliseChannelName(value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildChannelBucketKey(index, primaryChannelLabel) {
|
||||
const safeIndex = Number.isFinite(index) ? Math.max(0, Math.trunc(index)) : 0;
|
||||
if (safeIndex === 0 && primaryChannelLabel) {
|
||||
function buildPrimaryBucketKey(primaryChannelLabel) {
|
||||
if (primaryChannelLabel) {
|
||||
const trimmed = primaryChannelLabel.trim();
|
||||
if (trimmed.length > 0 && trimmed !== '0') {
|
||||
return `0::${trimmed.toLowerCase()}`;
|
||||
}
|
||||
}
|
||||
return String(safeIndex);
|
||||
return '0';
|
||||
}
|
||||
|
||||
function buildSecondaryNameBucketKey(labelInfo) {
|
||||
const label = labelInfo?.label ?? null;
|
||||
const priority = labelInfo?.priority ?? CHANNEL_LABEL_PRIORITY.INDEX;
|
||||
if (priority !== CHANNEL_LABEL_PRIORITY.NAME || !label) {
|
||||
return null;
|
||||
}
|
||||
const trimmedLabel = label.trim().toLowerCase();
|
||||
if (!trimmedLabel.length) {
|
||||
return null;
|
||||
}
|
||||
return `secondary::${trimmedLabel}`;
|
||||
}
|
||||
|
||||
function findExistingBucketKeyByIndex(channelBuckets, targetIndex) {
|
||||
if (!channelBuckets || !Number.isFinite(targetIndex) || targetIndex <= 0) {
|
||||
return null;
|
||||
}
|
||||
const normalizedTarget = Math.trunc(targetIndex);
|
||||
for (const [key, bucket] of channelBuckets.entries()) {
|
||||
if (!bucket || !Number.isFinite(bucket.index)) {
|
||||
continue;
|
||||
}
|
||||
if (Math.trunc(bucket.index) !== normalizedTarget) {
|
||||
continue;
|
||||
}
|
||||
if (bucket.index === 0) {
|
||||
continue;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildChannelTabId(bucketKey) {
|
||||
|
||||
Reference in New Issue
Block a user