Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 55 additions & 41 deletions src/LatestValuesAccumulator.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
/**
* LatestValuesAccumulator - Accumulates Signal K delta values during backpressure,
* keeping only the latest value for each unique context:path:$source combination.
* Accumulates Signal K delta values during backpressure, keeping only the
* latest value for each unique context:path:$source combination.
*/

import {
Context,
Delta,
hasValues,
Path,
PathValue,
SourceRef,
Timestamp,
Update
Update,
Value
} from '@signalk/server-api'

export interface AccumulatedItem {
Expand All @@ -28,6 +30,8 @@ export interface BackpressureDelta extends Delta {
}
}

const UNKNOWN_SOURCE = 'unknown'

/**
* Accumulate latest value per context:path:$source during backpressure.
* Only keeps the most recent value for each unique combination, dropping intermediate updates.
Expand All @@ -40,17 +44,28 @@ export function accumulateLatestValue(
delta: Delta
): void {
if (!delta.updates) return
const context = delta.context as Context
for (const update of delta.updates) {
if (!hasValues(update)) continue
const $source = update.$source
const sourceKey = $source || UNKNOWN_SOURCE
const timestamp = update.timestamp
for (const pv of update.values) {
const key = `${delta.context}:${pv.path}:${update.$source || 'unknown'}`
accumulator.set(key, {
context: delta.context as Context,
path: pv.path,
value: pv.value,
$source: update.$source,
timestamp: update.timestamp
})
const key = `${context}:${pv.path}:${sourceKey}`
const existing = accumulator.get(key)
if (existing) {
existing.value = pv.value
existing.$source = $source
existing.timestamp = timestamp
} else {
accumulator.set(key, {
context,
path: pv.path,
value: pv.value,
$source,
timestamp
})
}
}
}
}
Expand All @@ -71,45 +86,44 @@ export function buildFlushDeltas(

const countBefore = accumulator.size

// Group by context
const byContext = new Map<Context, Map<string, Update>>()
for (const [, item] of accumulator) {
if (!byContext.has(item.context)) {
byContext.set(item.context, new Map())
const byContext = new Map<
Context,
Map<string, Update & { values: PathValue[] }>
>()
for (const item of accumulator.values()) {
let bySource = byContext.get(item.context)
if (!bySource) {
bySource = new Map()
byContext.set(item.context, bySource)
}
// Group by $source within context
const bySource = byContext.get(item.context)!
const sourceKey = item.$source || 'unknown'
if (!bySource.has(sourceKey)) {
bySource.set(sourceKey, {
$source: item.$source as SourceRef,
timestamp: item.timestamp as Timestamp,
const sourceKey = item.$source || UNKNOWN_SOURCE
let update = bySource.get(sourceKey)
if (!update) {
update = {
$source: item.$source,
timestamp: item.timestamp,
values: []
})
}
const update = bySource.get(sourceKey)!
if (hasValues(update)) {
update.values.push({
path: item.path as Path,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: item.value as any
})
// Use the most recent timestamp for this source
if (
item.timestamp &&
(!update.timestamp || item.timestamp > update.timestamp)
) {
update.timestamp = item.timestamp as Timestamp
}
bySource.set(sourceKey, update)
}
update.values.push({
path: item.path,
value: item.value as Value
})
// Keep the newest timestamp seen for this source so the flushed delta reflects the latest update.
if (
item.timestamp &&
(!update.timestamp || item.timestamp > update.timestamp)
) {
update.timestamp = item.timestamp
}
}

// Build one delta per context with backpressure indicator
const deltas: BackpressureDelta[] = []
for (const [context, bySourceTime] of byContext) {
for (const [context, bySource] of byContext) {
deltas.push({
context,
updates: Array.from(bySourceTime.values()),
updates: Array.from(bySource.values()),
$backpressure: {
accumulated: countBefore,
duration
Expand Down
17 changes: 17 additions & 0 deletions test/LatestValuesAccumulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,5 +326,22 @@ describe('LatestValuesAccumulator', function () {
expect(deltas[0].$backpressure.accumulated).to.equal(10)
expect(deltas[0].$backpressure.duration).to.equal(2000)
})

it('should propagate undefined timestamp when items have no timestamp', function () {
const accumulator = new Map()
accumulator.set('vessels.self:navigation.speedOverGround:gps', {
context: 'vessels.self',
path: 'navigation.speedOverGround',
value: 5.0,
$source: 'gps',
timestamp: undefined
})

const deltas = buildFlushDeltas(accumulator, 1000)

expect(deltas.length).to.equal(1)
expect(deltas[0].updates.length).to.equal(1)
expect(deltas[0].updates[0].timestamp).to.equal(undefined)
})
Comment on lines +330 to +345

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move this new test case to TypeScript.

This PR adds new test logic in a .js file; per repo policy, new code should be added as TypeScript.

As per coding guidelines: **/*.{js,jsx}: “All new code must be written in TypeScript, not JavaScript”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/LatestValuesAccumulator.js` around lines 330 - 345, The new JavaScript
test in LatestValuesAccumulator.js must be converted to TypeScript: create a new
file LatestValuesAccumulator.test.ts (or move the block) and port the
`it('should propagate undefined timestamp when items have no timestamp'...)`
test to TypeScript syntax, updating imports/requires to use ES modules or TS
imports consistent with the test suite, ensure types for the accumulator Map
entry (or use `as any`/typed helpers) and keep the call to
`buildFlushDeltas(accumulator, 1000)` and the same expectations
(`deltas[0].updates[0].timestamp` === undefined); remove the .js test file after
verifying the TS test runs.

})
})
Loading