Message history with delivery tracking (GET /api/messages) (#8)
CI / build + unit (push) Successful in 32s
CI / build + unit (push) Successful in 32s
This commit was merged in pull request #8.
This commit is contained in:
@@ -81,6 +81,63 @@ function positionPacketFrame(
|
||||
}
|
||||
|
||||
const KAMIL_NUM = 123456789;
|
||||
// inbound text message: TEXT_MESSAGE_APP packet with UTF-8 in Data.payload
|
||||
function textPacketFrame(
|
||||
from: number,
|
||||
to: number,
|
||||
text: string,
|
||||
channel = 0,
|
||||
): Uint8Array {
|
||||
const data = create(P.Mesh.DataSchema, {
|
||||
portnum: P.Portnums.PortNum.TEXT_MESSAGE_APP,
|
||||
payload: new TextEncoder().encode(text),
|
||||
});
|
||||
const pkt = create(P.Mesh.MeshPacketSchema, {
|
||||
from,
|
||||
to,
|
||||
id: 1,
|
||||
channel,
|
||||
payloadVariant: { case: 'decoded', value: data },
|
||||
});
|
||||
return frame(
|
||||
toBinary(
|
||||
P.Mesh.FromRadioSchema,
|
||||
create(P.Mesh.FromRadioSchema, {
|
||||
payloadVariant: { case: 'packet', value: pkt },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ACK/NACK for our sends: ROUTING_APP packet with Data.requestId = original id and a serialized Routing in the payload
|
||||
function routingFrame(
|
||||
requestId: number,
|
||||
errorReason: P.Mesh.Routing_Error,
|
||||
): Uint8Array {
|
||||
const data = create(P.Mesh.DataSchema, {
|
||||
portnum: P.Portnums.PortNum.ROUTING_APP,
|
||||
requestId,
|
||||
payload: toBinary(
|
||||
P.Mesh.RoutingSchema,
|
||||
create(P.Mesh.RoutingSchema, {
|
||||
variant: { case: 'errorReason', value: errorReason },
|
||||
}),
|
||||
),
|
||||
});
|
||||
const pkt = create(P.Mesh.MeshPacketSchema, {
|
||||
from: 1,
|
||||
id: 2,
|
||||
payloadVariant: { case: 'decoded', value: data },
|
||||
});
|
||||
return frame(
|
||||
toBinary(
|
||||
P.Mesh.FromRadioSchema,
|
||||
create(P.Mesh.FromRadioSchema, {
|
||||
payloadVariant: { case: 'packet', value: pkt },
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
function kamilNodeInfo(): Partial<P.Mesh.NodeInfo> {
|
||||
return {
|
||||
user: {
|
||||
@@ -437,3 +494,121 @@ describe('MeshService node database', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MeshService message history', () => {
|
||||
it('captures inbound text as rx records', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
bridge.sockets[0].write(textPacketFrame(KAMIL_NUM, 42, 'ping', 2));
|
||||
|
||||
await waitFor(() => svc.messages().length === 1);
|
||||
expect(svc.messages()).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
dir: 'rx',
|
||||
from: KAMIL_NUM,
|
||||
text: 'ping',
|
||||
channel: 2,
|
||||
at: expect.any(Number),
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('tracks a tx send pending -> delivered on the routing ACK', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
const id = await svc.sendText(42, 'hello testowy');
|
||||
expect(svc.messages()[0]).toMatchObject({
|
||||
id,
|
||||
dir: 'tx',
|
||||
to: 42,
|
||||
text: 'hello testowy',
|
||||
channel: 2,
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
bridge.sockets[0].write(routingFrame(id, P.Mesh.Routing_Error.NONE));
|
||||
|
||||
await waitFor(() => svc.messages()[0]?.status === 'delivered');
|
||||
expect(svc.messages()[0]).toMatchObject({
|
||||
id,
|
||||
dir: 'tx',
|
||||
status: 'delivered',
|
||||
});
|
||||
} finally {
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('marks a tx failed with the routing error reason on NACK', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
const id = await svc.sendText(42, 'will fail');
|
||||
expect(svc.messages()[0]?.status).toBe('pending');
|
||||
|
||||
bridge.sockets[0].write(routingFrame(id, P.Mesh.Routing_Error.TIMEOUT));
|
||||
|
||||
await waitFor(() => svc.messages()[0]?.status === 'failed');
|
||||
expect(svc.messages()[0]).toMatchObject({
|
||||
id,
|
||||
status: 'failed',
|
||||
ackError: 'TIMEOUT', // human-readable Routing.Error name
|
||||
});
|
||||
} finally {
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('caps history at 100 records, dropping the oldest', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
for (let i = 0; i < 105; i++)
|
||||
bridge.sockets[0].write(textPacketFrame(42, KAMIL_NUM, `m-${i}`));
|
||||
|
||||
await waitFor(() => svc.messages().length === 100, 10000);
|
||||
expect(svc.messages()[0]?.text).toBe('m-104'); // newest first
|
||||
expect(svc.messages()[99]?.text).toBe('m-5'); // m-0..m-4 dropped
|
||||
} finally {
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user