nrel-spa/.github/wiki/Architecture.md

95 lines
4.8 KiB
Markdown

# Architecture
## Package Structure
```
nrel-spa/
├── src/
│ ├── index.ts <- Public API: getSpa, calcSpa, formatTime
│ └── types.ts <- TypeScript interfaces and SPA function code constants
├── lib/
│ └── spa.js <- Core SPA algorithm (JS port of NREL C source, tracked in git)
├── dist/ <- Built output (generated by tsup, gitignored)
│ ├── index.cjs <- CommonJS build
│ ├── index.mjs <- ESM build
│ ├── index.d.ts <- CJS type declarations
│ └── index.d.mts <- ESM type declarations
├── test.mjs <- ESM test suite (61 assertions)
└── test-cjs.cjs <- CJS test suite (17 assertions)
```
## Layers
**lib/spa.js** is the core mathematical engine. It is a direct JavaScript port of the NREL C source (`spa.c`), preserving function names, variable names, and the algorithm structure. The file is checked into git and shipped in the npm package.
**src/index.ts** is a thin TypeScript wrapper. It:
- Loads `lib/spa.js` at runtime
- Validates input parameters, throwing `TypeError` or `RangeError` for invalid values
- Validates `options.function` and throws `RangeError` for out-of-range codes
- Returns `NaN` for `sunrise`, `solarNoon`, and `sunset` when the function code does not include RTS (`SPA_ZA` or `SPA_ZA_INC`)
- Maps the flat SpaData structure to a clean output object
- Implements `adjustForCustomAngle()` internally for twilight calculations
- Provides `formatTime()` as a standalone export
**src/types.ts** exports all public TypeScript interfaces and constants:
- `SpaOptions`: atmospheric and calculation parameters
- `SpaResult` / `SpaFormattedResult`: base return types for `getSpa` / `calcSpa`
- `SpaAnglesResult` / `SpaFormattedAnglesResult`: per-angle entries in the `angles` array
- `SpaResultWithAngles` / `SpaFormattedResultWithAngles`: return types when `angles` is passed
- `SpaFunctionCode`: union type `0 | 1 | 2 | 3`
- `SPA_ZA`, `SPA_ZA_INC`, `SPA_ZA_RTS`, `SPA_ALL`: function code constants
**tsup** compiles `src/index.ts` to both CJS (`dist/index.cjs`) and ESM (`dist/index.mjs`) with TypeScript declarations. The `lib/spa.js` module is kept external (not bundled) and resolved at runtime via a `createRequire` shim in the ESM build.
## Loading Strategy
The ESM build uses a `createRequire` banner injected by tsup:
```javascript
import { createRequire as __cr } from 'node:module';
const __require = __cr(import.meta.url);
```
This anchors `require()` resolution to the file's own location rather than the calling context, so `../lib/spa.js` resolves correctly regardless of where the caller imports the package.
In the CJS build, standard `require()` is used directly.
## Why lib/spa.js Is External
Unlike a typical npm package where all source is compiled away, `lib/spa.js` ships as a separate file rather than being inlined into `dist/`. This is an intentional choice:
1. The algorithm is the canonical reference. Keeping it as a readable JS file allows inspection without source maps.
2. The file is large (989 lines). Bundling it into both CJS and ESM outputs would double its footprint for no functional gain.
3. It matches the structure of [solar-spa](https://www.npmjs.com/package/solar-spa), which keeps its Emscripten WASM module in a separate `wasm/` directory.
## Date Handling
The wrapper uses UTC components from the `Date` object (`getUTCFullYear()`, etc.) and accepts an explicit `timezone` offset. This avoids the ambiguity of `getFullYear()` which returns local time and can produce wrong results when the caller's timezone differs from the target location.
```javascript
// Always pass UTC date + explicit timezone
const date = new Date('2025-06-21T00:00:00Z');
getSpa(date, lat, lng, -4); // -4 for EDT
```
## Polar Conditions
When the sun does not rise or set (polar day/polar night), `lib/spa.js` sets sunrise, sunset, and suntransit to `-99999`. The wrapper propagates these raw values from `getSpa()`. `calcSpa()` passes them through `formatTime()`, which returns `"N/A"` for negative values.
Custom angle calculations (`adjustForCustomAngle`) return `NaN` sunrise/sunset when `cosH0 < -1 || cosH0 > 1`, which indicates no crossing of that zenith angle.
## Validation
Input validation runs before any SPA calculation:
- `date` must be a valid `Date` (not `Invalid Date`)
- `latitude` and `longitude` must be finite numbers in their valid ranges
- Invalid values throw before the SPA data structure is populated
The internal `spa_calculate()` function also validates its inputs and returns non-zero error codes for out-of-range values. The wrapper throws `Error` on any non-zero return.
---
[Home](Home) . [API Reference](API-Reference) . [Twilight Calculations](Twilight-Calculations) . [NREL SPA Algorithm](NREL-SPA-Algorithm)