From fb0c14e76141f563849eae0508a3b9b640396378 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Wed, 25 Feb 2026 10:35:24 -0500 Subject: [PATCH] v2.0.0: TypeScript rewrite with WASM recompilation Complete rewrite of the package from plain JavaScript to TypeScript, compiled to dual CJS/ESM via tsup. The NREL SPA C source is recompiled to WASM with Emscripten using SINGLE_FILE base64 inlining, eliminating bundler path-resolution issues. Changes: - Rewrite JS wrapper in TypeScript with full type definitions - Recompile WASM with -O3 -flto, 1MB fixed memory, no filesystem - Add input validation with descriptive error messages - Add spaFormatted() for HH:MM:SS time strings - Add formatTime() utility and init() for eager WASM loading - Add SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL function code exports - Dual CJS/ESM output via tsup with proper exports map - Test suite: 68 ESM + 13 CJS assertions - 100-scenario validation suite across 7 categories - GitHub Wiki with 8 documentation pages - CI workflow: Node 20/22/24 matrix, typecheck, pack-check - NREL attribution in LICENSE and README per their license terms - Minimum Node.js 20 --- .editorconfig | 18 + .github/workflows/ci.yml | 78 ++ .github/workflows/wiki-sync.yml | 36 + .gitignore | 63 + .npmignore | 2 - .npmrc | 1 + .nvmrc | 1 + .wiki/API-Reference.md | 155 +++ .wiki/Architecture.md | 140 ++ .wiki/Bundler-Compatibility.md | 125 ++ .wiki/Contributing.md | 91 ++ .wiki/Home.md | 55 + .wiki/NREL-SPA-Algorithm.md | 114 ++ .wiki/Performance.md | 87 ++ .wiki/Validation-and-Benchmarks.md | 230 ++++ .wiki/WebAssembly-in-npm-Packages.md | 132 ++ CHANGELOG.md | 94 ++ LICENSE | 45 +- README.md | 196 ++- index.d.ts | 21 - package.json | 74 +- pnpm-lock.yaml | 925 +++++++++++++ pnpm-workspace.yaml | 2 + solar-spa.js | 62 - spa.js | 1781 -------------------------- spa.wasm | Bin 64708 -> 0 bytes src/index.ts | 217 ++++ src/spa_wrapper.c | 111 +- src/types.ts | 74 ++ test-cjs.cjs | 58 + test.js | 35 - test.mjs | 258 ++++ tsconfig.json | 20 + tsup.config.ts | 28 + validate.mjs | 854 ++++++++++++ wasm/spa-module.js | Bin 0 -> 60004 bytes 36 files changed, 4177 insertions(+), 2006 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/wiki-sync.yml create mode 100644 .gitignore delete mode 100644 .npmignore create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .wiki/API-Reference.md create mode 100644 .wiki/Architecture.md create mode 100644 .wiki/Bundler-Compatibility.md create mode 100644 .wiki/Contributing.md create mode 100644 .wiki/Home.md create mode 100644 .wiki/NREL-SPA-Algorithm.md create mode 100644 .wiki/Performance.md create mode 100644 .wiki/Validation-and-Benchmarks.md create mode 100644 .wiki/WebAssembly-in-npm-Packages.md create mode 100644 CHANGELOG.md delete mode 100644 index.d.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml delete mode 100644 solar-spa.js delete mode 100644 spa.js delete mode 100755 spa.wasm create mode 100644 src/index.ts create mode 100644 src/types.ts create mode 100644 test-cjs.cjs delete mode 100644 test.js create mode 100644 test.mjs create mode 100644 tsconfig.json create mode 100644 tsup.config.ts create mode 100644 validate.mjs create mode 100644 wasm/spa-module.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0c2fa63 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{js,mjs,cjs,ts,mts,cts,json,yml,yaml,md}] +indent_style = space +indent_size = 2 + +[*.{c,h}] +indent_style = space +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a78e7fe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22, 24] + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Build TypeScript + run: pnpm run build:ts + + - name: Run tests (ESM) + run: node test.mjs + + - name: Run tests (CJS) + run: node test-cjs.cjs + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm run typecheck + + pack-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm run build:ts + + - name: Verify package contents + run: | + npm pack --dry-run 2>&1 | tee pack-output.txt + for f in dist/index.cjs dist/index.mjs dist/index.d.ts dist/index.d.mts wasm/spa-module.js README.md CHANGELOG.md LICENSE; do + grep -q "$f" pack-output.txt || { echo "MISSING: $f"; exit 1; } + done + echo "All expected files present in package" diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml new file mode 100644 index 0000000..50ef085 --- /dev/null +++ b/.github/workflows/wiki-sync.yml @@ -0,0 +1,36 @@ +name: Sync Wiki + +on: + push: + branches: [main] + paths: ['.wiki/**'] + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Checkout wiki + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }}.wiki + path: .wiki-remote + + - name: Sync wiki pages + run: | + cp .wiki/*.md .wiki-remote/ + cd .wiki-remote + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + echo "No wiki changes to commit" + else + git commit -m "Sync wiki from repo" + git push + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..824a1cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# ─── Dependencies ─── +node_modules/ +.pnp +.pnp.js + +# ─── Build ─── +dist/ +build/ +out/ +*.tsbuildinfo + +# ─── Environment ─── +.env +.env.* +!.env.example + +# ─── OS ─── +.DS_Store +Thumbs.db +._* +Desktop.ini +$RECYCLE.BIN/ + +# ─── IDE / Editor ─── +.vscode/ +.idea/ +*.swp +*.swo +*~ +*.sublime-project +*.sublime-workspace + +# ─── Logs ─── +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# ─── Testing / Coverage ─── +coverage/ +.nyc_output/ + +# ─── AI Agents ─── +.claude/ +.cursor/ +.copilot/ +.github/copilot/ +.aider* +.codeium/ +.tabnine/ +.windsurf/ +.cody/ +.sourcegraph/ + +# ─── Packages ─── +*.tgz + +# ─── Custom ─── +# C compilation artifacts (WASM binary is pre-compiled and tracked) +*.o +*.a diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 00fc139..0000000 --- a/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -src/ -test.js diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..391eb15 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-import-method=hardlink diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.wiki/API-Reference.md b/.wiki/API-Reference.md new file mode 100644 index 0000000..ef15461 --- /dev/null +++ b/.wiki/API-Reference.md @@ -0,0 +1,155 @@ +# API Reference + +## `spa(date, latitude, longitude, options?)` + +Returns a `Promise` with raw numeric values. + +### Parameters + +| Name | Type | Description | +| --- | --- | --- | +| `date` | `Date` | Date and time for the calculation | +| `latitude` | `number` | Observer latitude, -90 to 90 (negative = south) | +| `longitude` | `number` | Observer longitude, -180 to 180 (negative = west) | +| `options` | `object` | Optional. See below | + +### Options + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `timezone` | `number` | auto | Hours from UTC. Auto-detected from the Date object if omitted | +| `elevation` | `number` | `0` | Meters above sea level | +| `pressure` | `number` | `1013.25` | Atmospheric pressure in millibars | +| `temperature` | `number` | `15` | Temperature in Celsius | +| `delta_ut1` | `number` | `0` | UT1-UTC correction in seconds | +| `delta_t` | `number` | `67` | TT-UTC difference in seconds | +| `slope` | `number` | `0` | Surface slope in degrees from horizontal | +| `azm_rotation` | `number` | `0` | Surface azimuth rotation in degrees from south | +| `atmos_refract` | `number` | `0.5667` | Atmospheric refraction in degrees | +| `function` | `number` | `3` | SPA function code (see below) | + +### Result Fields + +| Field | Type | Unit | Description | +| --- | --- | --- | --- | +| `zenith` | `number` | degrees | Topocentric zenith angle (0 = directly overhead) | +| `azimuth` | `number` | degrees | Topocentric azimuth, eastward from north (navigational convention) | +| `azimuth_astro` | `number` | degrees | Topocentric azimuth, westward from south (astronomical convention) | +| `incidence` | `number` | degrees | Surface incidence angle | +| `sunrise` | `number` | fractional hours | Local sunrise time | +| `sunset` | `number` | fractional hours | Local sunset time | +| `suntransit` | `number` | fractional hours | Solar noon (sun transit) | +| `sun_transit_alt` | `number` | degrees | Sun altitude at transit | +| `eot` | `number` | minutes | Equation of time | +| `error_code` | `number` | integer | 0 on success | + +### Timezone Auto-detection + +When `timezone` is omitted, the value is derived from the `Date` object's local timezone offset: + +```js +timezone = -(date.getTimezoneOffset() / 60) +``` + +This works correctly in most cases. Provide an explicit value when computing for a location whose timezone differs from the machine's local timezone. + +### Error Handling + +The SPA validates all inputs against physical bounds (latitude -90 to 90, timezone -18 to 18, etc.). If validation fails, `spa()` throws an `Error` with the SPA error code in the message: + +```js +try { + await spa(new Date(), 40, -74, { timezone: 100 }); +} catch (e) { + // "SPA: calculation failed (error code 8)" +} +``` + +A null result pointer (WASM memory allocation failure) also throws. + +## `spaFormatted(date, latitude, longitude, options?)` + +Same parameters and behavior as `spa()`. Returns a result object with the same fields, but `sunrise`, `sunset`, and `suntransit` are `HH:MM:SS` strings instead of fractional hours. During polar day or polar night, these strings are `"N/A"`: + +```js +const result = await spaFormatted( + new Date(2025, 5, 21, 12, 0, 0), + 40.7128, -74.006, + { timezone: -4 } +); + +console.log(result.sunrise); // "05:25:12" +console.log(result.sunset); // "20:30:42" +console.log(result.suntransit); // "12:57:54" +console.log(result.zenith); // 27.08 (still a number) +``` + +## `formatTime(hours)` + +Converts fractional hours to an `HH:MM:SS` string. Returns `"N/A"` for non-finite or negative values, which occur during polar day or polar night when sunrise or sunset does not happen. Values at or above 24 hours wrap to the next day (e.g., 24.5 becomes `"00:30:00"`). + +```js +formatTime(6.5); // "06:30:00" +formatTime(12); // "12:00:00" +formatTime(Infinity); // "N/A" +``` + +## `init()` + +Pre-initializes the WASM module. Returns a `Promise` that resolves when the module is ready. + +This is optional. The module initializes automatically on the first `spa()` call. Use `init()` if you want to pay the initialization cost at application startup rather than on the first calculation. + +```js +import { init, spa } from 'solar-spa'; + +// Somewhere during app bootstrap +await init(); + +// Later, this call has no initialization overhead +const result = await spa(new Date(), 40, -74); +``` + +Calling `init()` multiple times is safe. The second and subsequent calls return immediately. + +## Function Codes + +The `function` option controls which outputs the SPA computes. Lower codes skip the rise/transit/set and incidence calculations, which are the most expensive part. + +| Constant | Value | Computes | +| --- | --- | --- | +| `SPA_ZA` | `0` | Zenith and azimuth only | +| `SPA_ZA_INC` | `1` | Zenith, azimuth, and incidence angle | +| `SPA_ZA_RTS` | `2` | Zenith, azimuth, and rise/transit/set times | +| `SPA_ALL` | `3` | All output values (default) | + +```js +import { spa, SPA_ZA } from 'solar-spa'; + +// When you only need zenith and azimuth (fastest) +const result = await spa(new Date(), 40, -74, { function: SPA_ZA }); +console.log(result.zenith, result.azimuth); +``` + +When using `SPA_ZA` or `SPA_ZA_INC`, the rise/transit/set fields will contain zeros. + +## TypeScript + +Full type definitions are included. Import types directly: + +```ts +import { spa, SPA_ALL } from 'solar-spa'; +import type { SpaResult, SpaOptions } from 'solar-spa'; + +const options: SpaOptions = { + timezone: -4, + elevation: 10, + function: SPA_ALL, +}; + +const result: SpaResult = await spa(new Date(), 40.7128, -74.006, options); +``` + +--- + +[Home](Home) · [Architecture](Architecture) · [NREL SPA Algorithm](NREL-SPA-Algorithm) · [Changelog](https://github.com/acamarata/solar-spa/blob/main/CHANGELOG.md) diff --git a/.wiki/Architecture.md b/.wiki/Architecture.md new file mode 100644 index 0000000..96ba1ff --- /dev/null +++ b/.wiki/Architecture.md @@ -0,0 +1,140 @@ +# Architecture + +solar-spa has three layers. Each solves a specific problem. + +## Layer 1: C (NREL SPA) + +**Files:** `src/spa.c`, `src/spa.h`, `src/spa_wrapper.c` + +The core algorithm is the [NREL Solar Position Algorithm](https://midcdmz.nrel.gov/spa/) by Ibrahim Reda and Afshin Andreas. It is a direct C implementation of the algorithm described in the paper "Solar Position Algorithm for Solar Radiation Applications" (Solar Energy, Vol. 76, Issue 5, 2004, pp. 577-589). The code is unmodified from the NREL distribution. + +`spa_wrapper.c` is a thin adapter. The original `spa_calculate()` function takes a pointer to a `spa_data` struct with 30+ fields. That struct layout is not accessible from JavaScript via Emscripten's `cwrap()`. The wrapper provides a flat function signature that accepts each input as a separate argument, calls `spa_calculate()`, and copies the output fields into a compact result struct allocated on the heap. + +The result struct occupies 80 bytes in memory: nine `double` fields (72 bytes) followed by one `int` (4 bytes), plus 4 bytes of trailing padding for struct alignment. The caller reads the fields by offset using `getValue()` and then calls `spa_free_result()` to release the allocation. + +This design avoids the complexity of passing structs across the WASM boundary. The flat signature maps directly to `cwrap()` type arrays, and reading by fixed byte offset is the fastest way to extract results from WASM memory. + +## Layer 2: WASM (Emscripten output) + +**Files:** `wasm/spa-module.js` + +The C source is compiled with Emscripten to produce a single JavaScript file that contains the WASM binary encoded as base64. No separate `.wasm` file exists. + +### Build flags + +| Flag | Purpose | +| --- | --- | +| `-O3 -flto` | Maximum optimization with link-time optimization. The compiler inlines across translation units and eliminates dead code | +| `--no-entry` | No `main()` function exists. The module exposes only the exported wrapper functions | +| `-sSINGLE_FILE=1` | Inlines the WASM binary as a base64 string inside the JavaScript file. Eliminates the `.wasm` file entirely | +| `-sMODULARIZE=1` | Wraps the output in a factory function (`createSpaModule()`) instead of executing immediately. Prevents global `Module` pollution | +| `-sEXPORT_NAME=createSpaModule` | Names the factory function | +| `-sEXPORTED_FUNCTIONS` | Exposes `_spa_calculate_wrapper`, `_spa_free_result`, `_malloc`, and `_free` to JavaScript | +| `-sEXPORTED_RUNTIME_METHODS` | Makes `cwrap` and `getValue` available on the module instance | +| `-sNO_FILESYSTEM=1` | Excludes the virtual filesystem API. SPA does not read files. Saves ~15KB | +| `-sINITIAL_MEMORY=1048576` | 1MB fixed memory. SPA allocates one 80-byte struct per call, so this is more than sufficient | +| `-sALLOW_MEMORY_GROWTH=0` | Disables dynamic memory growth. Fixed memory avoids the overhead of growable ArrayBuffers and detached buffer checks | +| `-sSTACK_SIZE=65536` | 64KB stack. Default is 5MB, which is wasteful for a pure computation | +| `-sENVIRONMENT='node,web,worker'` | Includes runtime support for Node.js, browsers, and web workers | +| `-sASSERTIONS=0` | Removes debug assertions. Smaller output, no runtime checks | +| `-sDISABLE_EXCEPTION_CATCHING=1` | Disables C++ exception support. SPA is plain C, so this strips dead code | +| `-sWASM_BIGINT=0` | Disables BigInt integration for 64-bit integers. SPA uses only doubles and 32-bit ints | + +The `SINGLE_FILE` flag is the critical one. Most WASM packages ship a separate `.wasm` file and resolve its path at runtime using `__dirname`, `import.meta.url`, or `URL` constructors. This breaks in bundlers (Webpack rewrites paths), edge runtimes (no filesystem), and testing environments (different module resolution). By inlining the binary, the module is self-contained. It works anywhere JavaScript runs. + +The output is ~60KB. The base64-encoded WASM accounts for most of that. The JavaScript glue code is minimal because we disabled the filesystem, exception handling, and assertions. + +### Why MODULARIZE? + +Without `MODULARIZE`, Emscripten emits code that creates or mutates a global `Module` object. If two packages in the same application use Emscripten, they clobber each other's `Module`. With `MODULARIZE`, each call to `createSpaModule()` returns an independent instance. solar-spa creates exactly one instance (the singleton) and caches it. + +## Layer 3: TypeScript wrapper + +**Source:** `src/index.ts`, `src/types.ts` +**Output:** `dist/index.cjs`, `dist/index.mjs`, `dist/index.d.ts` (CJS), `dist/index.d.mts` (ESM) + +The wrapper is written in TypeScript and compiled by [tsup](https://tsup.egoist.dev/) to both CJS and native ESM, with generated declaration files for each format. The source handles four concerns: initialization, input validation, calling convention, and struct reading. + +### Singleton initialization + +The WASM module initializes once. A module-level variable `_module` holds the cached instance. A second variable `_pending` holds the in-flight initialization promise to prevent duplicate init when multiple `spa()` calls arrive before the first init completes. If initialization fails, `_pending` is cleared so the next call retries. + +```text +spa() called + |-> init() called + |-- _module exists? Return immediately + |-- _pending exists? Return existing promise + |-- Neither? Call createSpaModule(), cache promise in _pending + |-- On resolve: cache module in _module, cache cwrap bindings, clear _pending + |-- On reject: clear _pending, re-throw (allows retry) +``` + +The `cwrap()` bindings for `spa_calculate_wrapper` and `spa_free_result` are created once during initialization and stored in `_calculate` and `_free`. This avoids the overhead of re-wrapping on every call. + +### Input validation + +Before reaching the WASM layer, `spa()` validates that `date` is a valid `Date` object, that `latitude` and `longitude` are finite numbers within their valid ranges, and throws `TypeError` or `RangeError` with a clear message. The C layer performs its own validation and returns error codes, but the TypeScript validation provides better developer experience with descriptive error types. + +### Struct offsets + +The byte offsets for reading the result struct are stored in a named constant object (`OFFSET`) rather than as magic numbers. This makes the layout self-documenting and reduces the risk of an offset error when the struct is modified. + +### Dual CJS/ESM + +tsup compiles the TypeScript source to both CJS (`.cjs`) and native ESM (`.mjs`). The ESM build uses a `createRequire` shim (injected via tsup's banner option) to load the Emscripten WASM module, which is CJS. The CJS build uses native `require()`. + +Both builds share the same WASM singleton within a single Node.js process because CJS module caching ensures the Emscripten module is loaded only once regardless of which entry point is used. + +The `package.json` exports map routes `require()` to the CJS file and `import` to the ESM file, with per-format TypeScript declaration files: + +```json +{ + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs" + } + } + } +} +``` + +## File layout + +```text +solar-spa/ +|-- src/ +| |-- index.ts # Main implementation (TypeScript) +| |-- types.ts # Interfaces, constants, WASM module type +| |-- spa.c # NREL SPA algorithm (unmodified) +| |-- spa.h # NREL SPA header (unmodified) +| |-- spa_wrapper.c # Flat wrapper for WASM boundary +|-- wasm/ +| |-- spa-module.js # Compiled output (WASM inlined as base64) +|-- dist/ # Generated by tsup +| |-- index.cjs # CommonJS output +| |-- index.mjs # ESM output +| |-- index.d.ts # CJS declarations +| |-- index.d.mts # ESM declarations +|-- test.mjs # ESM test suite +|-- test-cjs.cjs # CJS smoke test +|-- tsup.config.ts # Build configuration +|-- tsconfig.json # TypeScript configuration +|-- package.json +|-- CHANGELOG.md +|-- README.md +|-- LICENSE +``` + +Published files (the `files` field in `package.json`): `dist/`, `wasm/`, `README.md`, `CHANGELOG.md`, `LICENSE`. + +Source files (`src/`, `tsup.config.ts`, `tsconfig.json`) are not published. They are in the repository for building from source but are not needed at runtime. + +--- + +[Home](Home) · [API Reference](API-Reference) · [Performance](Performance) · [Contributing](Contributing) diff --git a/.wiki/Bundler-Compatibility.md b/.wiki/Bundler-Compatibility.md new file mode 100644 index 0000000..eae1e90 --- /dev/null +++ b/.wiki/Bundler-Compatibility.md @@ -0,0 +1,125 @@ +# Bundler Compatibility + +solar-spa works out of the box in every tested environment. No special configuration is required. + +## How it works + +The WASM binary is inlined as base64 inside `wasm/spa-module.js` (compiled with Emscripten's `SINGLE_FILE` flag). There is no external `.wasm` file to resolve, fetch, or copy. The package is just JavaScript files, and every bundler knows how to handle JavaScript. See [WebAssembly in npm Packages](WebAssembly-in-npm-Packages) for a deeper discussion of why this approach works. + +## Tested environments + +### Node.js (18+) + +Both ES module and CommonJS imports work: + +```js +// ESM +import { spa } from 'solar-spa'; + +// CommonJS +const { spa } = require('solar-spa'); +``` + +The `package.json` exports map routes `import` to `dist/index.mjs` and `require()` to `dist/index.cjs`. Full TypeScript declarations are provided for both formats. + +### Webpack 5 + +No configuration needed. Webpack resolves the package through the exports map and bundles the JavaScript files normally. The base64-encoded WASM is just a string literal inside the bundle. + +```js +// webpack project +import { spa } from 'solar-spa'; +const result = await spa(new Date(), 40.7128, -74.006); +``` + +If your Webpack config includes rules for `.wasm` files, they will not interfere because solar-spa does not import or reference any `.wasm` files. + +### Vite + +Works without configuration. Vite handles the CJS-to-ESM interop through its pre-bundling step (esbuild), and the inlined WASM is transparent to the build pipeline. + +```js +// vite project +import { spa } from 'solar-spa'; +``` + +### Next.js (Pages Router) + +Works in both API routes (server-side) and `getServerSideProps`. The WASM initializes on the server without filesystem access issues because the binary is inlined. + +```js +// pages/api/sun.js +import { spa } from 'solar-spa'; + +export default async function handler(req, res) { + const result = await spa(new Date(), 40.7128, -74.006, { timezone: -4 }); + res.json(result); +} +``` + +### Next.js (App Router) + +Works in server components, route handlers, and middleware. The same inlining approach avoids the edge runtime file resolution problems that affect packages with separate `.wasm` files. + +```js +// app/api/sun/route.js +import { spa } from 'solar-spa'; + +export async function GET() { + const result = await spa(new Date(), 40.7128, -74.006, { timezone: -4 }); + return Response.json(result); +} +``` + +### Browser (direct) + +If you are not using a bundler, use a CDN that provides ESM builds: + +```html + +``` + +### Web Workers + +The Emscripten output includes `worker` in its environment list. WASM initialization works inside a Web Worker without modification: + +```js +// worker.js +import { spa } from 'solar-spa'; + +self.addEventListener('message', async (event) => { + const { date, lat, lon } = event.data; + const result = await spa(new Date(date), lat, lon); + self.postMessage(result); +}); +``` + +## Common questions + +### Do I need to configure `wasm` asset handling in my bundler? + +No. There is no `.wasm` file in the package. The binary is a base64 string inside a `.js` file. + +### Will this work with tree-shaking? + +The package exports individual named functions. Bundlers that support tree-shaking will include only the functions you import. However, the WASM module is initialized as a whole, so the binary size is fixed regardless of which functions you use. + +### Is there a browser-only build? + +The same build works in both Node.js and browsers. The Emscripten output detects the environment at runtime (`ENVIRONMENT='node,web,worker'`). + +### What about Content Security Policy (CSP)? + +The WASM binary is decoded from base64 and compiled via `WebAssembly.instantiate()`. If your CSP restricts `wasm-eval` or `wasm-unsafe-eval`, you will need to add the appropriate directive. This is a general WASM requirement, not specific to this package. + +### What is the minimum browser version? + +Any browser that supports WebAssembly: Chrome 57+, Firefox 52+, Safari 11+, Edge 16+. This covers all browsers released since 2017. + +--- + +[Home](Home) · [WebAssembly in npm Packages](WebAssembly-in-npm-Packages) · [Architecture](Architecture) diff --git a/.wiki/Contributing.md b/.wiki/Contributing.md new file mode 100644 index 0000000..9423121 --- /dev/null +++ b/.wiki/Contributing.md @@ -0,0 +1,91 @@ +# Contributing + +## Prerequisites + +- Node.js 18 or later +- pnpm 10 or later +- [Emscripten](https://emscripten.org/docs/getting_started/downloads.html) (only for recompiling the WASM module) + +On macOS, Emscripten is available via Homebrew: + +```sh +brew install emscripten +``` + +## Building from source + +Clone the repository, install dependencies, and build the TypeScript: + +```sh +git clone https://github.com/acamarata/solar-spa.git +cd solar-spa +pnpm install +pnpm run build:ts +``` + +To recompile the WASM module (requires Emscripten): + +```sh +pnpm run build:wasm +``` + +Or build everything at once: + +```sh +pnpm run build +``` + +## Running tests + +```sh +pnpm test +``` + +This runs both the ESM test suite (`test.mjs`, 66 assertions) and the CJS smoke test (`test-cjs.cjs`). Tests cover multiple geographic locations, seasons, input validation, error handling, concurrent initialization, polar regions, boundary coordinates, and all SPA function codes. + +## Type checking + +```sh +pnpm run typecheck +``` + +## Project structure + +``` +src/ + index.ts # Main implementation (TypeScript source) + types.ts # Type definitions and constants + spa.c # NREL SPA algorithm (do not modify) + spa.h # NREL SPA header (do not modify) + spa_wrapper.c # Flat wrapper for WASM boundary +wasm/ + spa-module.js # Compiled WASM output (~60KB, inlined as base64) +dist/ # Generated by tsup (CJS + ESM + declarations) + index.cjs + index.mjs + index.d.ts # CJS declarations + index.d.mts # ESM declarations +test.mjs # ESM test suite +test-cjs.cjs # CJS smoke test +tsup.config.ts # Build configuration +tsconfig.json # TypeScript configuration +``` + +## Guidelines + +- Do not modify `spa.c` or `spa.h`. These are the original NREL source files and should remain identical to the NREL distribution. +- Changes to `spa_wrapper.c` that add new output fields require corresponding changes in `src/index.ts` (the OFFSET map and `readResult` function) and `src/types.ts` (the result interfaces). +- Run `pnpm test` and `pnpm run typecheck` before submitting. All tests and type checks should pass. +- The test suite uses approximate comparisons (`approx` with a tolerance) because floating-point results vary slightly across platforms and Emscripten versions. + +## Reporting bugs + +Open an issue at [github.com/acamarata/solar-spa/issues](https://github.com/acamarata/solar-spa/issues). Include: + +- Node.js version (`node --version`) +- The input parameters that produced the unexpected result +- Expected vs actual output + +--- + +[Home](Home) · [Architecture](Architecture) · [API Reference](API-Reference) diff --git a/.wiki/Home.md b/.wiki/Home.md new file mode 100644 index 0000000..f4e86ca --- /dev/null +++ b/.wiki/Home.md @@ -0,0 +1,55 @@ +# solar-spa + +NREL Solar Position Algorithm compiled to WebAssembly. Calculates solar zenith, azimuth, incidence angle, sunrise, sunset, solar noon, and equation of time for any location and date. + +The algorithm is the [NREL SPA](https://midcdmz.nrel.gov/spa/) by Ibrahim Reda and Afshin Andreas, originally written in C. This package compiles the original C source to WASM via Emscripten and provides a TypeScript/JavaScript interface on top. + +## Why WASM? + +The SPA involves thousands of floating-point operations per call: trigonometric series, Julian date conversions, nutation corrections, and iterative sunrise/sunset bracketing. WASM executes these at near-native speed, which matters when computing positions for thousands of locations or running tight animation loops. + +For single calls, the difference is negligible. See [Performance](Performance) for benchmarks. + +## Pages + +- [API Reference](API-Reference): Full function signatures, option fields, and return types. +- [Architecture](Architecture): How the three layers (C, WASM, JS) fit together and why each design choice was made. +- [Performance](Performance): Benchmarks, memory footprint, and optimization notes. +- [NREL SPA Algorithm](NREL-SPA-Algorithm): Background on the algorithm itself, its accuracy, and valid date range. +- [WebAssembly in npm Packages](WebAssembly-in-npm-Packages): Practical notes on shipping WASM in npm packages, common pitfalls, and how this package avoids them. +- [Bundler Compatibility](Bundler-Compatibility): Tested environments and configuration notes for Webpack, Vite, Next.js, and web workers. +- [Validation and Benchmarks](Validation-and-Benchmarks): Accuracy validation against NREL reference values and performance benchmarks. +- [Contributing](Contributing): How to build from source, run tests, and submit changes. +- [Changelog](https://github.com/acamarata/solar-spa/blob/main/CHANGELOG.md): Version history and breaking changes. + +## Quick Start + +```js +import { spa } from 'solar-spa'; + +const result = await spa( + new Date(2025, 5, 21, 12, 0, 0), // June 21, 2025 at noon + 40.7128, // latitude (NYC) + -74.0060, // longitude + { timezone: -4, elevation: 10 } // EDT (UTC-4), 10m elevation +); + +console.log(result.zenith); // ~27 (degrees from vertical) +console.log(result.azimuth); // ~179 (degrees from north) +console.log(result.sunrise); // ~5.4 (fractional hours) +console.log(result.sunset); // ~20.5 (fractional hours) +``` + +CommonJS works too: + +```js +const { spa } = require('solar-spa'); +``` + +## License + +MIT + +--- + +[GitHub](https://github.com/acamarata/solar-spa) · [npm](https://www.npmjs.com/package/solar-spa) · [Changelog](https://github.com/acamarata/solar-spa/blob/main/CHANGELOG.md) diff --git a/.wiki/NREL-SPA-Algorithm.md b/.wiki/NREL-SPA-Algorithm.md new file mode 100644 index 0000000..de004eb --- /dev/null +++ b/.wiki/NREL-SPA-Algorithm.md @@ -0,0 +1,114 @@ +# NREL SPA Algorithm + +## Background + +The Solar Position Algorithm (SPA) was developed at the National Renewable Energy Laboratory (NREL) by Ibrahim Reda and Afshin Andreas. It calculates the solar zenith and azimuth angles for any location on Earth, for any date between -2000 and 6000, with an uncertainty of +/- 0.0003 degrees. + +The algorithm is described in the paper: + +> Ibrahim Reda, Afshin Andreas, "Solar Position Algorithm for Solar Radiation Applications," Solar Energy, Vol. 76, Issue 5, 2004, pp. 577-589. + +The C reference implementation is available from NREL at [midcdmz.nrel.gov/spa/](https://midcdmz.nrel.gov/spa/). + +## What it computes + +Given a date, time, observer location, and atmospheric conditions, the SPA produces: + +- **Zenith angle**: the angle between the sun and the point directly overhead. 0 degrees means the sun is at the zenith. 90 degrees means the sun is on the horizon. Values above 90 indicate the sun is below the horizon. +- **Azimuth angle**: the compass direction of the sun. Two conventions are provided: eastward from north (navigational) and westward from south (astronomical). +- **Incidence angle**: the angle between the sun and a surface with a given slope and orientation. +- **Sunrise, sunset, and solar transit**: the times at which the sun crosses the horizon and reaches its highest point. +- **Equation of time**: the difference between apparent solar time and mean solar time, in minutes. + +## How it works + +The algorithm proceeds through several stages: + +### 1. Julian date calculation + +The input date is converted to a Julian Date (JD), which is a continuous count of days since January 1, 4713 BC. This simplifies all subsequent time-dependent calculations. The Julian Ephemeris Day (JDE) is then computed by adding the delta_t correction (the difference between Terrestrial Time and UTC). + +### 2. Earth heliocentric position + +The Earth's position relative to the Sun is calculated using the VSOP87 theory (Variations Seculaires des Orbites Planetaires). This involves evaluating trigonometric series with over 60 terms for heliocentric longitude, plus smaller series for heliocentric latitude and radius vector. + +The heliocentric longitude uses five polynomial coefficients (L0 through L4), each of which is a sum of terms of the form `A * cos(B + C * JME)` where JME is the Julian Ephemeris Millennium. + +### 3. Geocentric position + +The heliocentric coordinates (Earth's position relative to the Sun) are converted to geocentric coordinates (Sun's position as seen from Earth) by adding 180 degrees to the longitude and negating the latitude. The geocentric right ascension and declination are then computed. + +### 4. Nutation and obliquity + +Nutation (the periodic wobble of Earth's axis) is computed from a series of 63 terms based on five fundamental arguments: the mean elongation of the Moon, the mean anomaly of the Sun, the mean anomaly of the Moon, the Moon's argument of latitude, and the longitude of the ascending node of the Moon's orbit. + +The true obliquity of the ecliptic (the tilt of Earth's axis) is the mean obliquity plus the nutation in obliquity. + +### 5. Aberration and apparent position + +Aberration is the apparent shift in a star's position caused by Earth's orbital motion and the finite speed of light. The correction is small (about 20 arc-seconds) but necessary for the stated accuracy. + +### 6. Topocentric correction + +The geocentric position is adjusted for the observer's actual location on Earth's surface (parallax correction). An observer at sea level sees a slightly different sun position than one at high elevation, and both differ from the geocentric center-of-Earth viewpoint. + +### 7. Atmospheric refraction + +The atmosphere bends sunlight, making the sun appear higher than its geometric position. The refraction correction is most significant near the horizon (about 0.57 degrees at sunrise/sunset) and negligible when the sun is high. + +### 8. Rise, transit, and set + +Sunrise, sunset, and solar transit are calculated by evaluating the sun's position at midnight, then using iterative approximation to find the exact times when the zenith angle crosses 90.8333 degrees (the standard value accounting for the sun's apparent diameter and atmospheric refraction at the horizon). + +## Valid date range + +The algorithm is valid for dates between -2000 and 6000 (i.e., 2000 BC to 6000 AD). Outside this range, the VSOP87 series lose accuracy and the SPA returns an error code. + +## Accuracy + +The stated uncertainty is +/- 0.0003 degrees for the period from -2000 to 6000. This is well within the requirements of any solar energy, astronomical, or navigational application. + +For comparison: + +- The sun's apparent diameter is about 0.53 degrees +- A solar panel tracking error of 1 degree reduces energy capture by roughly 0.015% +- Consumer GPS accuracy is typically 3-5 meters, which corresponds to roughly 0.00005 degrees of latitude + +The SPA's accuracy far exceeds the precision of any practical measurement system it would be paired with. + +## Input validation + +The SPA validates all inputs and returns a non-zero error code if any are out of range: + +| Parameter | Valid range | +| --- | --- | +| Year | -2000 to 6000 | +| Month | 1 to 12 | +| Day | 1 to 31 | +| Hour | 0 to 24 | +| Minute | 0 to 59 | +| Second | 0 to less than 60 | +| Timezone | -18 to 18 | +| Latitude | -90 to 90 | +| Longitude | -180 to 180 | +| Elevation | -6500000 or higher (meters) | +| Pressure | 0 to 5000 | +| Temperature | -273 to 6000 | +| Delta UT1 | -1 to 1 | +| Delta T | -8000 to 8000 | +| Slope | -360 to 360 | +| Azimuth rotation | -360 to 360 | +| Atmospheric refraction | -5 to 5 | + +The solar-spa package propagates these error codes as thrown JavaScript errors. + +## References + +- Reda, I., Andreas, A. (2004). "Solar Position Algorithm for Solar Radiation Applications." Solar Energy, 76(5), 577-589. +- Meeus, J. (1998). "Astronomical Algorithms." 2nd ed. Willmann-Bell. +- Bretagnon, P., Francou, G. (1988). "Planetary Theories in Rectangular and Spherical Variables: VSOP87 Solutions." Astronomy and Astrophysics, 202, 309-315. +- [NREL SPA Calculator](https://midcdmz.nrel.gov/spa/): Online calculator and C source distribution. + +--- + +[Home](Home) · [Architecture](Architecture) · [API Reference](API-Reference) · [Validation and Benchmarks](Validation-and-Benchmarks) diff --git a/.wiki/Performance.md b/.wiki/Performance.md new file mode 100644 index 0000000..2d94f0c --- /dev/null +++ b/.wiki/Performance.md @@ -0,0 +1,87 @@ +# Performance + +## What WASM buys you + +The SPA algorithm involves: + +- Julian date conversion (integer and fractional components) +- Earth heliocentric longitude, latitude, and radius vector via trigonometric series (over 60 terms for longitude alone) +- Nutation in longitude and obliquity (63 terms each) +- Aberration correction +- Topocentric adjustments for observer position +- Atmospheric refraction correction +- Sunrise/sunset via iterative bisection (three full position calculations per call when computing rise/transit/set) + +A single `SPA_ALL` call executes roughly 4,000 floating-point operations. WASM runs these at near-native speed because the code compiles to hardware-optimized floating-point instructions without JavaScript's JIT warm-up or type-checking overhead. + +For a single call, the difference between WASM and a pure JavaScript implementation is small (both are fast enough). The gap widens with volume. Computing solar positions for 10,000 locations, or running a sun-tracking animation at 60fps, is where WASM execution speed becomes measurable. + +## Initialization cost + +The first `spa()` call pays a one-time initialization cost: decoding ~40KB of base64 WASM, compiling the module, and instantiating it. This takes approximately 5 to 15 milliseconds depending on the runtime and hardware. + +Subsequent calls skip initialization entirely and go straight to the C function via the cached `cwrap` binding. Each call allocates an 80-byte struct, runs the computation, reads the result, and frees the struct. The per-call overhead from the JavaScript wrapper is negligible. + +Use `init()` at application startup to pay the initialization cost early: + +```js +import { init } from 'solar-spa'; +await init(); // ~5-15ms, happens once +``` + +## Memory footprint + +The WASM module uses 1MB of fixed memory (`INITIAL_MEMORY=1048576`). This includes: + +- 64KB stack +- The compiled code segment +- Heap for `malloc`/`free` of result structs (80 bytes each, freed immediately after reading) + +Memory growth is disabled (`ALLOW_MEMORY_GROWTH=0`). This means the ArrayBuffer backing WASM memory is never detached or reallocated, which avoids a class of subtle bugs in long-running applications and allows the engine to optimize memory access patterns. + +1MB is conservative. SPA does not accumulate state. Each call allocates one struct, reads it, and frees it. The heap utilization at any point is a few hundred bytes at most. + +## Function code optimization + +Not all callers need every output. The `function` option controls how much work the SPA does: + +| Code | Computation | Relative cost | +| --- | --- | --- | +| `SPA_ZA` (0) | Zenith and azimuth | ~1x | +| `SPA_ZA_INC` (1) | + incidence angle | ~1x (incidence is cheap) | +| `SPA_ZA_RTS` (2) | + rise/transit/set | ~3x (three position evaluations) | +| `SPA_ALL` (3) | All outputs | ~3x | + +The sunrise/sunset calculation is the expensive part. It evaluates the full position algorithm three times (for transit, sunrise, and sunset). If you only need the current sun position, use `SPA_ZA` for a roughly 3x speed improvement. + +## Build optimizations + +The Emscripten build uses: + +- `-O3`: Highest optimization level. Aggressive inlining, loop unrolling, vectorization. +- `-flto`: Link-time optimization. The compiler sees both `spa.c` and `spa_wrapper.c` as a single compilation unit, enabling cross-file inlining and dead code elimination. +- `-sASSERTIONS=0`: Strips all runtime assertions from the Emscripten glue code. +- `-sDISABLE_EXCEPTION_CATCHING=1`: Removes C++ exception handling support. SPA is pure C. +- `-sNO_FILESYSTEM=1`: Removes the virtual filesystem API (~15KB of JavaScript). +- `-sSTACK_SIZE=65536`: Reduces the stack from the default 5MB to 64KB. SPA is not recursive and uses minimal stack space. + +These flags together produce a ~60KB output file, down from the ~150KB that a default Emscripten build would generate. + +## When to use solar-spa vs nrel-spa + +| Scenario | Recommended | +| --- | --- | +| Single position lookup (e.g., sunrise for today) | Either. Both are fast enough | +| Batch computation (hundreds or thousands of positions) | solar-spa (WASM) | +| Animation or real-time tracking | solar-spa (WASM) | +| Synchronous API required | [nrel-spa](https://github.com/acamarata/nrel-spa) (pure JS, sync) | +| Environments without WASM support | [nrel-spa](https://github.com/acamarata/nrel-spa) | +| Minimal dependency footprint | [nrel-spa](https://github.com/acamarata/nrel-spa) (zero deps, ~30KB) | + +Both packages implement the same NREL algorithm and produce identical results within floating-point rounding tolerance. + +For measured benchmark numbers, see [Validation and Benchmarks](Validation-and-Benchmarks). + +--- + +[Home](Home) · [Architecture](Architecture) · [API Reference](API-Reference) · [Validation and Benchmarks](Validation-and-Benchmarks) diff --git a/.wiki/Validation-and-Benchmarks.md b/.wiki/Validation-and-Benchmarks.md new file mode 100644 index 0000000..901b2ad --- /dev/null +++ b/.wiki/Validation-and-Benchmarks.md @@ -0,0 +1,230 @@ +# Validation and Benchmarks + +Pre-release validation of the solar-spa v2.0.0 WASM implementation. All results were generated by `validate.mjs` in the repository root. + +## Overview + +100 scenarios covering seven categories: + +| Category | Scenarios | Description | +| --- | --- | --- | +| Cities worldwide | 1-40 | 20 cities across every continent, summer and winter solstice | +| Boundary conditions | 41-55 | Poles, equator, date line, extreme elevation, date range limits | +| Polar regions | 56-65 | Polar day, polar night, midnight sun, Antarctic stations | +| Time edge cases | 66-75 | Midnight, dawn, dusk, leap year, fractional seconds | +| Function code consistency | 76-80 | All four function codes produce identical zenith/azimuth | +| Atmospheric conditions | 81-90 | Pressure, temperature, refraction, vacuum, high altitude | +| Historical/future dates | 91-100 | Year -2000 to 6000, Gregorian switch, Apollo era | + +**Result: 100/100 passed.** + +## Category 1: Cities Worldwide (1-40) + +20 major cities tested at both the June 21 and December 21 solstices, 2025, local noon. Each scenario validates that the zenith angle falls within a physically reasonable range for the latitude and season. + +| # | City | Season | Zenith | Azimuth | Time | +| --- | --- | --- | --- | --- | --- | +| 1 | New York | Summer | 21.12 | 140.47 | 242us | +| 2 | New York | Winter | 65.35 | 166.30 | 168us | +| 3 | London | Summer | 30.52 | 150.96 | 110us | +| 4 | London | Winter | 75.98 | 166.15 | 110us | +| 5 | Tokyo | Summer | 12.77 | 197.73 | 102us | +| 6 | Tokyo | Winter | 59.29 | 185.50 | 99us | +| 7 | Sydney | Summer | 57.29 | 359.16 | 100us | +| 8 | Sydney | Winter | 10.54 | 351.37 | 112us | +| 9 | Cairo | Summer | 6.64 | 186.17 | 115us | +| 10 | Cairo | Winter | 53.49 | 181.94 | 101us | +| 11 | Mumbai | Summer | 10.35 | 63.31 | 109us | +| 12 | Mumbai | Winter | 43.43 | 167.76 | 96us | +| 13 | Sao Paulo | Summer | 47.02 | 2.64 | 94us | +| 14 | Sao Paulo | Winter | 1.10 | 84.39 | 137us | +| 15 | Moscow | Summer | 32.82 | 166.65 | 71us | +| 16 | Moscow | Winter | 79.33 | 173.55 | 56us | +| 17 | Beijing | Summer | 16.81 | 167.09 | 54us | +| 18 | Beijing | Winter | 63.38 | 176.82 | 50us | +| 19 | Nairobi | Summer | 26.11 | 18.24 | 56us | +| 20 | Nairobi | Winter | 23.37 | 161.93 | 55us | +| 21 | Reykjavik | Summer | 43.28 | 149.34 | 49us | +| 22 | Reykjavik | Winter | 88.81 | 160.36 | 47us | +| 23 | Singapore | Summer | 27.34 | 34.86 | 34us | +| 24 | Singapore | Winter | 29.10 | 149.34 | 25us | +| 25 | Cape Town | Summer | 58.47 | 12.97 | 22us | +| 26 | Cape Town | Winter | 14.29 | 45.72 | 22us | +| 27 | Buenos Aires | Summer | 59.49 | 14.77 | 22us | +| 28 | Buenos Aires | Winter | 15.87 | 48.72 | 21us | +| 29 | Dubai | Summer | 5.04 | 109.42 | 22us | +| 30 | Dubai | Winter | 48.80 | 174.81 | 25us | +| 31 | Toronto | Summer | 25.98 | 134.65 | 22us | +| 32 | Toronto | Winter | 69.27 | 161.43 | 24us | +| 33 | Mexico City | Summer | 9.80 | 64.18 | 23us | +| 34 | Mexico City | Winter | 43.69 | 168.40 | 23us | +| 35 | Seoul | Summer | 15.88 | 150.42 | 46us | +| 36 | Seoul | Winter | 61.38 | 172.14 | 31us | +| 37 | Rome | Summer | 23.76 | 135.39 | 26us | +| 38 | Rome | Winter | 67.18 | 163.05 | 24us | +| 39 | Anchorage | Summer | 43.13 | 137.26 | 25us | +| 40 | Anchorage | Winter | 87.67 | 153.13 | 23us | + +Note: "Summer" and "Winter" refer to Northern Hemisphere seasons. For Southern Hemisphere cities (Sydney, Sao Paulo, Cape Town, Buenos Aires, Nairobi), the seasons are reversed. A high zenith in "summer" (June) for Sydney is correct because June is winter there. + +## Category 2: Boundary Conditions (41-55) + +Tests at the mathematical limits of the algorithm's input domain. + +| # | Scenario | Zenith | Note | +| --- | --- | --- | --- | +| 41 | North Pole, June solstice | 66.53 | Midnight sun: zenith < 90 | +| 42 | North Pole, Dec solstice | 113.44 | Polar night: zenith > 90 | +| 43 | South Pole, Dec solstice | 66.53 | Midnight sun (southern summer) | +| 44 | South Pole, June solstice | 113.44 | Polar night (southern winter) | +| 45 | Equator, March equinox | 1.84 | Near-overhead sun | +| 46 | Equator, Sept equinox | 1.84 | Near-overhead sun | +| 47 | Equator, June solstice | 23.44 | Sun 23.44 north of equator | +| 48 | Equator, Dec solstice | 23.44 | Sun 23.44 south of equator | +| 49 | Date line +180 | 23.44 | Longitude boundary | +| 50 | Date line -180 | 23.43 | Longitude boundary | +| 51 | Mt Everest (8849m) | 4.55 | Thin atmosphere (314 mbar, -20C) | +| 52 | Dead Sea (-430m) | 11.95 | Dense atmosphere (1065 mbar, 40C) | +| 53 | Year -2000 | 7.90 | Earliest valid date | +| 54 | Year 6000 | 7.27 | Latest valid date | +| 55 | Year 6001 | throws | Correctly rejects out-of-range year | + +The equinox results confirm the algorithm's accuracy: a zenith of 1.84 degrees at the equator on the March equinox at solar noon on the prime meridian is consistent with the small angular offset between the vernal equinox and the actual date (March 20 vs. the instant the sun crosses the celestial equator). + +## Category 3: Polar Regions (56-65) + +Polar day/night conditions test the algorithm's handling of non-standard sunrise/sunset scenarios. + +| # | Location | Condition | Zenith | +| --- | --- | --- | --- | +| 56 | Tromso, Norway (69.6N) | Polar day (June) | 46.70 | +| 57 | Tromso, Norway | Polar night (Dec) | 93.14 | +| 58 | Murmansk, Russia (69.0N) | Polar day (June) | 46.12 | +| 59 | Murmansk, Russia | Polar night (Dec) | 92.77 | +| 60 | Utqiagvik, AK (71.3N) | Polar day (June) | 52.32 | +| 61 | Utqiagvik, AK | Polar night (Dec) | 95.90 | +| 62 | McMurdo Station (-77.9S) | Summer (Dec) | 55.95 | +| 63 | McMurdo Station | Winter (June) | 101.62 | +| 64 | Svalbard (78.2N) | Midnight sun (June, 00:00) | 77.90 | +| 65 | South Pole Station (-90S) | Summer (Jan) | 67.01 | + +Scenario 64 is notable: at Svalbard at midnight on the June solstice, the sun is still 12.1 degrees above the horizon (90 - 77.9 = 12.1). This is correct for 78N latitude during continuous polar daylight. + +## Category 4: Time Edge Cases (66-75) + +| # | Scenario | Zenith | Note | +| --- | --- | --- | --- | +| 66 | Exact midnight UTC (London, June) | 105.05 | Sun well below horizon | +| 67 | Dawn, 5 AM summer London | 88.47 | Near horizon | +| 68 | Dusk, 9 PM summer London | 87.93 | Near horizon | +| 69 | Solar noon NYC (13:00 EDT) | 17.28 | Azimuth 181.6 (nearly due south) | +| 70 | UTC midnight Jan 1, equator | 156.99 | Deep below horizon | +| 71 | Fractional seconds | 18.24 | Handles sub-minute times | +| 72 | End of day 23:59 | 114.40 | Late night | +| 73 | Feb 29 leap year (2024) | 48.33 | Leap day handled correctly | +| 74 | Prime meridian equator noon | 1.84 | Most symmetric case | +| 75 | New Year's Eve midnight | 162.37 | Deep below horizon | + +## Category 5: Function Code Consistency (76-80) + +All four function codes (`SPA_ZA`, `SPA_ZA_INC`, `SPA_ZA_RTS`, `SPA_ALL`) produce identical zenith and azimuth values within 0.01 degree tolerance. This confirms that the function code parameter only affects which outputs are computed, not the core position algorithm. + +| # | Test | Result | +| --- | --- | --- | +| 76 | SPA_ZA matches SPA_ALL | zenith identical | +| 77 | SPA_ZA_INC matches SPA_ALL | zenith, azimuth, incidence identical | +| 78 | SPA_ZA_RTS matches SPA_ALL | zenith, azimuth identical | +| 79 | SPA_ALL fields all finite | All 9 numeric fields populated | +| 80 | azimuth = (azimuth_astro + 180) % 360 | Navigational/astronomical consistency | + +## Category 6: Atmospheric Conditions (81-90) + +| # | Condition | Zenith | Note | +| --- | --- | --- | --- | +| 81 | Standard atmosphere (1013.25 mbar, 15C) | 21.12 | Reference baseline | +| 82 | Low pressure (300 mbar, -30C, 9000m) | 21.12 | High altitude conditions | +| 83 | High pressure (1100 mbar) | 21.12 | Dense atmosphere | +| 84 | Extreme cold (-40C) | 43.28 | Reykjavik test | +| 85 | Extreme heat (+50C) | 5.04 | Dubai test | +| 86 | Zero pressure (vacuum) | 21.13 | No refraction correction | +| 87 | Custom refraction (0 deg) | 21.12 | Override default 0.5667 | +| 88 | Custom refraction (2 deg) | 21.12 | Large refraction override | +| 89 | Pressure effect on zenith | varies | Low != high pressure confirmed | +| 90 | High elevation + low pressure | 4.55 | Everest base camp | + +Scenario 89 confirms that different atmospheric pressures produce measurably different zenith values. The difference is small (sub-arc-second at high solar elevations) but present, which validates that the atmospheric refraction correction is active and working. + +## Category 7: Historical and Future Dates (91-100) + +The SPA is valid for years -2000 to 6000. These scenarios confirm correct behavior across the full range, with era-appropriate delta_t values. + +| # | Year | Context | Zenith | delta_t | +| --- | --- | --- | --- | --- | +| 91 | 1000 CE | Medieval era | 17.25 | 1574s | +| 92 | 1582 | Gregorian calendar switch | 50.37 | 120s | +| 93 | 1900 | Turn of century | 25.45 | -3s | +| 94 | 1969 | Apollo 11 era, Cape Canaveral | 10.31 | 40s | +| 95 | 2050 | Near future | 21.13 | 93s | +| 96 | 2100 | Far future | 74.88 | 200s | +| 97 | 3000 | Distant future | 12.77 | 0s | +| 98 | 5000 | Deep future | 0.67 | 0s | +| 99 | -1000 (1001 BCE) | Ancient Athens | 15.07 | 0s | +| 100 | -2000 (earliest valid) | Ancient Cairo, winter solstice | 53.00 | 0s | + +Delta_t values for historical dates follow published estimates. For dates beyond ~2050, delta_t is not well predicted. The scenarios use conservative values to avoid introducing error from the correction itself. + +## Performance Benchmarks + +Measured on Apple Silicon (Node.js), single-threaded. + +### Per-Call Latency + +From the 100 validation scenarios: + +| Metric | Value | +| --- | --- | +| Min | 7us | +| Max | 242us | +| Mean | 46us | +| Median | 33us | +| P95 | 110us | +| P99 | 168us | + +The first call is slowest (242us) because it includes WASM module initialization. Subsequent calls settle into the 20-40us range for `SPA_ALL` computations. + +### Batch Throughput + +10,000 consecutive calls to the same location and date: + +| Function Code | Time | Throughput | +| --- | --- | --- | +| SPA_ALL | 201ms | ~50,000 calls/sec | +| SPA_ZA | 46ms | ~219,000 calls/sec | + +`SPA_ZA` is roughly 4x faster than `SPA_ALL` because it skips the sunrise/sunset iterative solver, which requires three full position evaluations per call. + +### Initialization + +First-time WASM module initialization takes approximately 5-15ms depending on hardware and Node.js version. Subsequent `init()` calls return immediately (0.0ms). The module is initialized lazily on the first `spa()` call, or eagerly via `init()`. + +### Comparison to Native C + +The NREL reference C implementation, compiled natively with `-O2`, runs a single `SPA_ALL` call in approximately 5-10us on comparable hardware. The WASM version at ~20us (steady state, warm cache) represents roughly a 2-3x overhead from the WASM sandbox, JavaScript FFI, and memory copy for the result struct. + +For most applications, the absolute latency difference (10-15us per call) is not measurable. The overhead becomes relevant only at extreme volumes (millions of calls per second), at which point native C would be the correct choice regardless. + +## Running the Validation Suite + +```bash +git clone https://github.com/acamarata/solar-spa.git +cd solar-spa +npm install +npm run build +node validate.mjs +``` + +The suite takes approximately 3 seconds to run, including the 20,000-call throughput benchmark. + +--- + +[Home](Home) · [Performance](Performance) · [API Reference](API-Reference) · [NREL SPA Algorithm](NREL-SPA-Algorithm) diff --git a/.wiki/WebAssembly-in-npm-Packages.md b/.wiki/WebAssembly-in-npm-Packages.md new file mode 100644 index 0000000..cea6f5c --- /dev/null +++ b/.wiki/WebAssembly-in-npm-Packages.md @@ -0,0 +1,132 @@ +# WebAssembly in npm Packages + +Shipping WASM inside an npm package is surprisingly difficult. This page documents the common problems and how solar-spa avoids them. + +## The core problem: file resolution + +A typical Emscripten build produces two files: a `.js` loader and a `.wasm` binary. At runtime, the JavaScript loader must locate the `.wasm` file to fetch and compile it. Emscripten's default behavior is to construct a URL or file path relative to the JavaScript file's location. + +This breaks in almost every real-world environment: + +**Node.js CommonJS:** Works by default. `__dirname` resolves correctly, and `fs.readFileSync` loads the binary. This is the only environment where the default works. + +**Node.js ESM:** `__dirname` is not defined in ES modules. Emscripten's loader either crashes or falls back to a relative URL that does not resolve. + +**Webpack:** Rewrites `require()` calls and moves files to the output bundle. The `.wasm` file might end up at a completely different path than the JavaScript expects. Webpack 5 has a `webassembly/async` experiment, but it does not help with Emscripten-generated loaders. + +**Vite:** Serves files through its dev server with transformed URLs. The `.wasm` file's path at build time does not match its URL at runtime. + +**Next.js (Pages Router):** Bundles server code with Webpack. The `.wasm` file needs to be copied to the correct output directory, which varies between `next build`, `next dev`, and `next export`. + +**Next.js (App Router):** The server component runtime environment has different file resolution behavior than the pages router. A `.wasm` path that works in pages may not work in app. + +**Web Workers:** No access to `__dirname`. The `import.meta.url` base may differ from the main thread. + +**Cloudflare Workers / Edge runtimes:** No filesystem. No Node.js built-ins. WASM must be imported as an ES module binding or provided inline. + +## The SINGLE_FILE solution + +Emscripten's `-sSINGLE_FILE=1` flag eliminates the problem entirely. Instead of writing a separate `.wasm` file, the compiler encodes the WASM binary as a base64 string inside the JavaScript file. At runtime, the loader decodes the string to an `ArrayBuffer` and passes it to `WebAssembly.instantiate()`. + +One file. No paths to resolve. No file to fetch. It works everywhere JavaScript runs. + +The tradeoff is size: base64 encoding adds ~33% overhead. A 30KB WASM binary becomes ~40KB of base64 text inside the JavaScript file. For solar-spa, the total output is ~60KB. This is acceptable for a computational library, and considerably smaller than the debugging headaches it prevents. + +## Other approaches and why they fall short + +### Manual `locateFile` override + +Emscripten supports a `locateFile` callback that lets the consumer specify where to find the `.wasm` file: + +```js +const Module = await createModule({ + locateFile: (path) => '/static/wasm/' + path +}); +``` + +This works but pushes the problem onto every consumer. Each bundler and deployment environment needs a different `locateFile` implementation. Library authors cannot control how their package is bundled. + +### Webpack `file-loader` or `asset/resource` + +Webpack can be configured to copy `.wasm` files to the output directory and rewrite the URL: + +```js +// webpack.config.js +module.exports = { + module: { + rules: [ + { test: /\.wasm$/, type: 'asset/resource' } + ] + } +}; +``` + +This works for Webpack but requires the consumer to modify their build configuration. It does nothing for Vite, Node.js, or non-bundled environments. + +### Vite `?url` imports + +Vite can import a WASM file as a URL: + +```js +import wasmUrl from './spa.wasm?url'; +``` + +This is Vite-specific syntax and does not work in any other environment. + +### `import.meta.url`-based resolution + +Some packages use `new URL('./spa.wasm', import.meta.url)` to resolve the WASM file relative to the JavaScript module. This works in browsers with native ES modules and in Node.js 20+ with ESM. It does not work in CommonJS, in older Node.js versions, or when bundlers transform the URL. + +## MODULARIZE prevents global pollution + +Without the `-sMODULARIZE=1` flag, Emscripten emits code that creates or mutates a global `Module` object. If two Emscripten-based packages exist in the same application, they overwrite each other's `Module`. + +With `MODULARIZE`, the output is a factory function. Each call returns an independent instance. solar-spa calls the factory once and caches the result: + +```ts +let _module: SpaWasmModule | null = null; +let _pending: Promise | null = null; + +export function init(): Promise { + if (_module) return Promise.resolve(); + if (_pending) return _pending; + _pending = createSpaModule().then((mod) => { + _module = mod; + _pending = null; + }).catch((err) => { + _pending = null; // Allow retry on next call + throw err; + }); + return _pending; +} +``` + +## Other WASM packaging strategies + +### wasm-pack (Rust) + +The Rust ecosystem uses `wasm-pack`, which generates JavaScript bindings from Rust code. It produces `pkg/` directories with `.wasm` files and JavaScript glue. The same file resolution problems apply, though `wasm-pack` supports a `--target bundler` mode that assumes Webpack-compatible resolution. + +### AssemblyScript + +AssemblyScript compiles TypeScript-like code to WASM. It has its own loader that suffers from the same path resolution issues. The community recommendation is to use `fetch()` in browsers and `fs.readFileSync()` in Node.js, with the consumer responsible for the correct path. + +### Inline approach (what solar-spa does) + +For small to medium WASM binaries (under a few hundred KB), inlining as base64 is the most practical approach. It trades a 33% size increase for universal compatibility. The tradeoff stops making sense for large binaries (several MB), where the base64 overhead and parsing cost become significant. + +## Summary + +| Approach | Universal? | Consumer config? | Size overhead | +| --- | --- | --- | --- | +| Separate `.wasm` + default resolution | No | No | None | +| `locateFile` callback | Yes* | Yes (per-bundler) | None | +| Bundler-specific config | Per-bundler | Yes | None | +| `import.meta.url` | Partial | No | None | +| **SINGLE_FILE (base64 inline)** | **Yes** | **No** | **~33%** | + +solar-spa uses the last approach. It works in every tested environment without any consumer configuration. + +--- + +[Home](Home) · [Architecture](Architecture) · [Bundler Compatibility](Bundler-Compatibility) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c01e216 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,94 @@ +# Changelog + +## 2.0.0 + +Complete rewrite. The source is now TypeScript, compiled by tsup to dual CJS/ESM with generated declarations. The WASM binary and build pipeline are all new. + +### Breaking Changes + +- The default export is now an async function named `spa()` with a different signature. The old `spa(date, lat, lon, elev, temp, pressure, refraction)` positional API is replaced by `spa(date, lat, lon, options)` where options is an object. +- Return field `solar_noon` is now `suntransit` (matches the NREL SPA spec naming). +- Return field `azimuth` is now the navigational azimuth (eastward from north). The astronomical convention (westward from south) is available as `azimuth_astro`. +- New return fields: `azimuth_astro`, `eot`, `error_code`. +- Non-zero SPA error codes now throw an `Error` instead of returning silently. +- Invalid inputs (non-Date, non-finite coordinates, out-of-range values) now throw `TypeError` or `RangeError` before reaching the WASM layer. +- CJS entry point moved from `./solar-spa.js` to `./dist/index.cjs`. +- Minimum Node.js version is now 20. + +### Added + +- TypeScript source (`src/index.ts`, `src/types.ts`) with full type safety. +- Dual CJS/ESM output via tsup: `dist/index.cjs` and `dist/index.mjs`. +- Generated TypeScript declarations for both formats: `dist/index.d.ts` (CJS) and `dist/index.d.mts` (ESM). +- Input validation with descriptive `TypeError` and `RangeError` messages. +- `spaFormatted()` function that returns sunrise, sunset, and transit as `HH:MM:SS` strings. Returns `"N/A"` during polar day or polar night. +- `formatTime()` utility for converting fractional hours to time strings. Handles negative values and 24-hour overflow. +- `init()` function for optional eager WASM initialization. Retries on failure instead of caching a rejected promise. +- SPA function code constants (`SPA_ZA`, `SPA_ZA_INC`, `SPA_ZA_RTS`, `SPA_ALL`) exported as `const` types. +- `SpaFunctionCode` union type for the function option. +- Options object with named parameters: `timezone`, `elevation`, `pressure`, `temperature`, `delta_ut1`, `delta_t`, `slope`, `azm_rotation`, `atmos_refract`, `function`. +- Automatic timezone detection from the `Date` object when `timezone` is omitted. +- Named `OFFSET` constant for WASM struct byte offsets (replaces magic numbers). +- Test suite with 68 ESM assertions and 13 CJS assertions covering multiple locations, seasons, input validation, concurrent calls, boundary coordinates, polar regions, all function codes, and `formatTime` edge cases. +- 100-scenario validation suite (`validate.mjs`) covering 20 cities worldwide, boundary conditions, polar regions, time edge cases, all function codes, atmospheric variations, and historical dates from 2000 BCE to 6000 CE. Includes throughput benchmarks. +- GitHub Actions CI workflow (Node 20/22/24 matrix with type checking). + +### Fixed + +- `onRuntimeInitialized` race condition that caused the module to break after the first call. The v1 wrapper re-assigned the one-shot callback on every invocation, so only the first call ever resolved. +- WASM file path resolution failures in Webpack, Vite, and Next.js. The binary is now inlined as base64 via Emscripten's `SINGLE_FILE` flag, so there is no `.wasm` file to resolve at runtime. +- `cwrap` bindings are now created once during initialization, not on every call. +- Global `Module` object pollution. The Emscripten output is now modularized (`MODULARIZE=1`) and returns a factory function instead of mutating a global. +- `formatTime()` now returns `"N/A"` for negative values and wraps correctly at 24 hours. +- `init()` clears the pending promise on failure, allowing retry on subsequent calls. + +### Changed + +- Source rewritten in TypeScript, compiled by tsup to CJS + ESM with source maps. +- Recompiled WASM with Emscripten using `-O3 -flto`, `SINGLE_FILE`, `MODULARIZE`, `NO_FILESYSTEM`, and fixed 1MB memory. Output is ~60KB. +- C wrapper (`spa_wrapper.c`) extended to accept all SPA input parameters and return all output fields. +- Package exports map uses `types`-first ordering per TypeScript documentation. +- `sideEffects: false` declared for tree-shaking support. + +### Removed + +- `solar-spa.js` (old entry point). +- `lib/solar-spa.cjs` and `lib/solar-spa.mjs` (replaced by `dist/index.cjs` and `dist/index.mjs`). +- `index.d.ts` (hand-written declarations replaced by generated output in `dist/`). +- `spa.js` and `spa.wasm` (old Emscripten output, replaced by `wasm/spa-module.js`). +- `.npmignore` (replaced by the `files` field in `package.json`). + +## 1.2.5 + +- Updated package.json repository field for npm listing. +- Updated README. + +## 1.2.4 + +- Reverted bug fix from 1.2.1 that introduced a regression. + +## 1.2.3 + +- Bug fix for `onRuntimeInitialized` callback timing. + +## 1.2.2 + +- Rebuilt WASM targeting both web and Node.js environments. +- Removed dependency on `fs` module. + +## 1.2.1 + +- Bug fix (reverted in 1.2.4). + +## 1.2.0 + +- Directly linked WASM file to resolve path resolution bug. + +## 1.1.0 + +- Added TypeScript declaration file. +- Added explicit WASM file path. + +## 1.0.0 + +- Initial release. NREL SPA compiled to WebAssembly with a JavaScript wrapper. diff --git a/LICENSE b/LICENSE index 11c9c33..6e43aa1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Aric Camarata +Copyright (c) 2023-2026 Aric Camarata Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +19,46 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Third-Party Notice: NREL Solar Position Algorithm +-------------------------------------------------- + +The files src/spa.c and src/spa.h contain the Solar Position Algorithm (SPA) +developed at the National Renewable Energy Laboratory (NREL). These files are +subject to their own license terms, reproduced below from the source: + + Copyright (C) 2008-2011 Alliance for Sustainable Energy, LLC, + All Rights Reserved + + The Solar Position Algorithm ("Software") is code in development prepared + by employees of the Alliance for Sustainable Energy, LLC, (hereinafter the + "Contractor"), under Contract No. DE-AC36-08GO28308 ("Contract") with the + U.S. Department of Energy (the "DOE"). The United States Government has + been granted for itself and others acting on its behalf a paid-up, non- + exclusive, irrevocable, worldwide license in the Software to reproduce, + prepare derivative works, and perform publicly and display publicly. + Beginning five (5) years after the date permission to assert copyright is + obtained from the DOE, and subject to any subsequent five (5) year + renewals, the United States Government is granted for itself and others + acting on its behalf a paid-up, non-exclusive, irrevocable, worldwide + license in the Software to reproduce, prepare derivative works, distribute + copies to the public, perform publicly and display publicly, and to permit + others to do so. If the Contractor ceases to make this computer software + available, it may be obtained from DOE's Office of Scientific and Technical + Information's Energy Science and Technology Software Center (ESTSC) at + P.O. Box 1020, Oak Ridge, TN 37831-1020. + + THIS SOFTWARE IS PROVIDED BY THE CONTRACTOR "AS IS" AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE CONTRACTOR OR THE U.S. GOVERNMENT BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER, + INCLUDING BUT NOT LIMITED TO CLAIMS ASSOCIATED WITH THE LOSS OF DATA OR + PROFITS, WHICH MAY RESULT FROM AN ACTION IN CONTRACT, NEGLIGENCE OR OTHER + TORTIOUS CLAIM THAT ARISES OUT OF OR IN CONNECTION WITH THE ACCESS, USE OR + PERFORMANCE OF THIS SOFTWARE. + + Reference: Reda, I., Andreas, A. (2004). "Solar Position Algorithm for + Solar Radiation Applications." Solar Energy, 76(5), 577-589. + + Original source: https://midcdmz.nrel.gov/spa/ diff --git a/README.md b/README.md index 0075702..29e510c 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,171 @@ # solar-spa -The solar-spa package provides a Node.js module for calculating solar position and related parameters using the National Renewable Energy Laboratory (NREL) Solar Position Algorithm (SPA). This implementation uses WebAssembly (WASM) to achieve high performance and accuracy. +[![npm version](https://img.shields.io/npm/v/solar-spa.svg)](https://www.npmjs.com/package/solar-spa) +[![CI](https://github.com/acamarata/solar-spa/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/solar-spa/actions/workflows/ci.yml) +[![license](https://img.shields.io/npm/l/solar-spa.svg)](https://github.com/acamarata/solar-spa/blob/main/LICENSE) -The SPA calculates the solar zenith angle, azimuth angle, incidence angle, sunrise time, sunset time, solar noon time, and sun transit altitude for a given date, time, and location. +NREL Solar Position Algorithm compiled to WebAssembly. Calculates solar zenith, azimuth, incidence angle, sunrise, sunset, solar noon, and equation of time for any location and date. -This is a direct WASM conversion, which has some limitations and bugs in real-world usage, especially when used as an NPM package or within frameworks like Next.js. This repository will remain available for those who need or want it, but it will no longer be maintained by me in favor of the newer **[nrel-spa](https://github.com/acamarata/nrel-spa/)** package going forward. +The algorithm is the [NREL SPA](https://midcdmz.nrel.gov/spa/) by Ibrahim Reda and Afshin Andreas, originally written in C. This package compiles the original C source to WASM via Emscripten and provides a TypeScript/JavaScript interface on top. ## Installation -To install the solar-spa package, use the following command: - ```sh npm install solar-spa ``` -## Usage +## Quick Start -``` -const spa = require('solar-spa'); +```js +import { spa } from 'solar-spa'; -// Define input parameters for a specific date, time, and location -const date = new Date(2023, 3, 1, 0, 0, 0); // April 1, 2023 at Midnight -const latitude = 40.7128; // Latitude of New York City, USA -const longitude = -74.0060; // Longitude of New York City, USA +const result = await spa( + new Date(2025, 5, 21, 12, 0, 0), // June 21, 2025 at noon + 40.7128, // latitude (NYC) + -74.0060, // longitude + { timezone: -4, elevation: 10 } // EDT (UTC-4), 10m elevation +); -// Optional input parameters (default values provided if not specified) -const elevation = 10; // Elevation in meters (approximately) -const temperature = 20; // Temperature (degrees Celsius) -const pressure = 1013.25; // Atmospheric pressure (millibars) -const refraction = 0.5667; // Atmospheric refraction (degrees) - -// Call the 'spa' function and log the results -spa(date, latitude, longitude, elevation, temperature, pressure, refraction) - .then(result => { - console.log(result); - }) - .catch(error => { - console.error(error); - }); +console.log(result.zenith); // ~27 (degrees from vertical) +console.log(result.azimuth); // ~179 (degrees from north) +console.log(result.sunrise); // ~5.4 (fractional hours) +console.log(result.sunset); // ~20.5 (fractional hours) ``` -## Example Output +CommonJS works too: -``` -{ - zenith: 132.82035808538367, - azimuth: 339.3841959764823, - incidence: 132.82035808538367, - sun_transit_alt: 53.91557045916343, - sunrise: 6.665306569794356, - solar_noon: 12.99818246332967, - sunset: 19.342862135890314 -} +```js +const { spa } = require('solar-spa'); ``` -## Helper (function not included) +## API -If you would like to translate the sunrise, solar_noon, or sunset to a normal time output you can convert fractional hours to formatted time string like below: +### `spa(date, latitude, longitude, options?)` -``` -function formatTime(hours) { - const milliseconds = hours * 60 * 60 * 1000; - const date = new Date(milliseconds); - return date.toISOString().substr(11, 12); -} +Returns a `Promise` with raw numeric values. + +**Parameters:** + +| Name | Type | Description | +| --- | --- | --- | +| `date` | `Date` | Date and time for the calculation | +| `latitude` | `number` | Observer latitude, -90 to 90 (negative = south) | +| `longitude` | `number` | Observer longitude, -180 to 180 (negative = west) | +| `options` | `object` | Optional. See below | + +**Options:** + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `timezone` | `number` | auto | Hours from UTC. Auto-detected from the Date object if omitted | +| `elevation` | `number` | `0` | Meters above sea level | +| `pressure` | `number` | `1013.25` | Atmospheric pressure in millibars | +| `temperature` | `number` | `15` | Temperature in Celsius | +| `delta_ut1` | `number` | `0` | UT1-UTC correction in seconds | +| `delta_t` | `number` | `67` | TT-UTC difference in seconds | +| `slope` | `number` | `0` | Surface slope in degrees | +| `azm_rotation` | `number` | `0` | Surface azimuth rotation in degrees | +| `atmos_refract` | `number` | `0.5667` | Atmospheric refraction in degrees | +| `function` | `number` | `3` | SPA function code (see below) | + +**Result fields:** + +| Field | Unit | Description | +| --- | --- | --- | +| `zenith` | degrees | Topocentric zenith angle | +| `azimuth` | degrees | Topocentric azimuth, eastward from north | +| `azimuth_astro` | degrees | Topocentric azimuth, westward from south | +| `incidence` | degrees | Surface incidence angle | +| `sunrise` | fractional hours | Local sunrise time | +| `sunset` | fractional hours | Local sunset time | +| `suntransit` | fractional hours | Solar noon | +| `sun_transit_alt` | degrees | Sun transit altitude | +| `eot` | minutes | Equation of time | +| `error_code` | integer | 0 on success | + +### `spaFormatted(date, latitude, longitude, options?)` + +Same as `spa()`, but `sunrise`, `sunset`, and `suntransit` are returned as `HH:MM:SS` strings. Returns `"N/A"` for these fields during polar day or polar night. + +### `formatTime(hours)` + +Converts fractional hours to an `HH:MM:SS` string. Returns `"N/A"` for non-finite or negative values (polar night/day scenarios). + +### `init()` + +Pre-initializes the WASM module. Optional. The module initializes automatically on the first `spa()` call. Useful if you want to pay the initialization cost at application startup rather than on the first calculation. + +### Function Codes + +| Constant | Value | Computes | +| --- | --- | --- | +| `SPA_ZA` | `0` | Zenith and azimuth | +| `SPA_ZA_INC` | `1` | Zenith, azimuth, and incidence | +| `SPA_ZA_RTS` | `2` | Zenith, azimuth, and rise/transit/set | +| `SPA_ALL` | `3` | All output values | + +## Architecture + +The package has three layers: + +1. **C layer** (`src/spa.c`, `src/spa_wrapper.c`): The original NREL SPA algorithm with a thin wrapper that exposes a flat function signature suitable for WASM. + +2. **WASM layer** (`wasm/spa-module.js`): Compiled with Emscripten using `-sSINGLE_FILE=1`, which inlines the WASM binary as base64. No external `.wasm` file to resolve. This eliminates the bundler path-resolution issues that plague most WASM packages. + +3. **TypeScript layer** (`src/index.ts`, `src/types.ts`): Written in TypeScript and compiled by tsup to both CJS and native ESM with generated declaration files. Singleton WASM initialization, cached `cwrap` bindings, input validation, and named struct offset constants. + +### Build Flags + +The WASM binary is compiled with: + +- `-O3 -flto`: Maximum optimization with link-time optimization +- `-sSINGLE_FILE=1`: WASM inlined as base64 (no file path resolution) +- `-sMODULARIZE=1`: No global `Module` pollution +- `-sNO_FILESYSTEM=1`: No filesystem API bundled (saves ~15KB) +- `-sINITIAL_MEMORY=1048576`: 1MB fixed memory (SPA needs very little) +- `-sASSERTIONS=0`: No debug assertions in production +- `-sENVIRONMENT='node,web,worker'`: Works in Node.js, browsers, and web workers + +## Compatibility + +Tested in: + +- Node.js 20+ +- Modern browsers (Chrome, Firefox, Safari, Edge) +- Webpack 5 +- Vite +- Next.js (both Pages and App Router) +- Web Workers + +The `SINGLE_FILE` approach eliminates the `.wasm` file resolution problem that breaks most WASM packages in bundlers. There is no external binary to locate at runtime. + +## TypeScript + +Full type definitions are generated from the TypeScript source and included with the package. Import types directly: + +```ts +import { spa, SPA_ALL } from 'solar-spa'; +import type { SpaResult, SpaOptions } from 'solar-spa'; ``` -# Repository -The source code for this package is available on GitHub: github.com/acamarata/solar-spa +## Documentation -# License -MIT +See the [Wiki](https://github.com/acamarata/solar-spa/wiki) for detailed documentation: API reference, architecture, performance, bundler compatibility, validation benchmarks, and more. + +## Related + +- [nrel-spa](https://github.com/acamarata/nrel-spa): Pure JavaScript port of the same algorithm. No WASM dependency, synchronous API. Better choice if you do not need WASM-level performance. +- [NREL SPA](https://midcdmz.nrel.gov/spa/): The original algorithm specification and C source. +- [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer time calculator built on nrel-spa. + +## Acknowledgments + +This package includes the Solar Position Algorithm (SPA) developed at the National Renewable Energy Laboratory (NREL) by Ibrahim Reda and Afshin Andreas. The C source files `spa.c` and `spa.h` are copyright Alliance for Sustainable Energy, LLC (2008-2011) and are distributed under the terms included in those files. + +> Reda, I., Andreas, A. (2004). "Solar Position Algorithm for Solar Radiation Applications." *Solar Energy*, 76(5), 577-589. [doi:10.1016/j.solener.2003.12.003](https://doi.org/10.1016/j.solener.2003.12.003) + +The original C source and an online calculator are available at [midcdmz.nrel.gov/spa](https://midcdmz.nrel.gov/spa/). + +## License + +MIT (wrapper, TypeScript source, and build tooling). The NREL SPA C source (`src/spa.c`, `src/spa.h`) is subject to its own terms; see the notice in those files. diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index b7af8bd..0000000 --- a/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// index.d.ts -declare module 'solar-spa' { - export default function spa( - date: Date, - latitude: number, - longitude: number, - elevation?: number, - temperature?: number, - pressure?: number, - refraction?: number - ): Promise<{ - zenith: number; - azimuth: number; - incidence: number; - sunrise: number; - sunset: number; - solar_noon: number; - sun_transit_alt: number; - }>; - } - \ No newline at end of file diff --git a/package.json b/package.json index 4bbdcbc..83a3e0c 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,74 @@ { "name": "solar-spa", - "version": "1.2.5", - "description": "NREL Solar Position Algorithm (SPA) in WebAssembly", - "main": "solar-spa.js", - "types": "index.d.ts", - "scripts": { - "test": "node test.js" - }, - "author": "Ali Camarata", + "version": "2.0.0", + "description": "NREL Solar Position Algorithm (SPA) compiled to WebAssembly. High-performance solar position, sunrise, sunset, and solar noon calculations.", + "author": "Aric Camarata", "license": "MIT", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.cjs" + } + } + }, + "sideEffects": false, + "files": [ + "dist/", + "wasm/", + "README.md", + "CHANGELOG.md", + "LICENSE" + ], + "scripts": { + "build:wasm": "emcc src/spa.c src/spa_wrapper.c -O3 -flto --no-entry -sMODULARIZE=1 -sEXPORT_NAME=createSpaModule -sSINGLE_FILE=1 -sEXPORTED_FUNCTIONS='[\"_spa_calculate_wrapper\",\"_spa_free_result\",\"_malloc\",\"_free\"]' -sEXPORTED_RUNTIME_METHODS='[\"cwrap\",\"getValue\"]' -sALLOW_MEMORY_GROWTH=0 -sINITIAL_MEMORY=1048576 -sSTACK_SIZE=65536 -sENVIRONMENT='node,web,worker' -sNO_FILESYSTEM=1 -sASSERTIONS=0 -sDISABLE_EXCEPTION_CATCHING=1 -sWASM_BIGINT=0 -o wasm/spa-module.js", + "build:ts": "tsup", + "build": "pnpm run build:wasm && pnpm run build:ts", + "typecheck": "tsc --noEmit", + "pretest": "pnpm run build:ts", + "test": "node test.mjs && node test-cjs.cjs", + "validate": "node validate.mjs", + "prepublishOnly": "pnpm run build:ts" + }, + "keywords": [ + "solar", + "spa", + "nrel", + "wasm", + "webassembly", + "sunrise", + "sunset", + "solar-noon", + "zenith", + "azimuth", + "solar-position", + "astronomy" + ], "repository": { "type": "git", "url": "https://github.com/acamarata/solar-spa.git" + }, + "homepage": "https://github.com/acamarata/solar-spa#readme", + "bugs": { + "url": "https://github.com/acamarata/solar-spa/issues" + }, + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@types/node": "^25.3.0", + "tsup": "^8.5.1", + "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6f13fe1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,925 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^25.3.0 + version: 25.3.0 + tsup: + specifier: ^8.5.1 + version: 8.5.1(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@25.3.0': + resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@25.3.0': + dependencies: + undici-types: 7.18.2 + + acorn@8.16.0: {} + + any-promise@1.3.0: {} + + bundle-require@5.1.0(esbuild@0.27.3): + dependencies: + esbuild: 0.27.3 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.59.0 + + fsevents@2.3.3: + optional: true + + joycon@3.1.1: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + object-assign@4.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + postcss-load-config@6.0.1: + dependencies: + lilconfig: 3.1.3 + + readdirp@4.1.2: {} + + resolve-from@5.0.0: {} + + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + source-map@0.7.6: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1 + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + undici-types@7.18.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/solar-spa.js b/solar-spa.js deleted file mode 100644 index a59ccde..0000000 --- a/solar-spa.js +++ /dev/null @@ -1,62 +0,0 @@ -// solar-spa.js -const spaModule = require('./spa.js'); - -module.exports = function spa( - date, - latitude, - longitude, - elevation = 0, - temperature = 20, - pressure = 1013.25, - refraction = 0.5667 -) { - return new Promise((resolve) => { - spaModule.onRuntimeInitialized = function () { - const spa_calculate = spaModule.cwrap( - 'spa_calculate_wrapper', - 'number', - [ - 'number', 'number', 'number', 'number', 'number', - 'number', 'number', 'number', 'number', 'number', - 'number', 'number', 'number', 'number', 'number' - ] - ); - - const spa_free_result = spaModule.cwrap( - 'spa_free_result', - null, - ['number'] - ); - - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - const hour = date.getHours(); - const minute = date.getMinutes(); - const second = date.getSeconds(); - const timezone = -date.getTimezoneOffset() / 60; - const slope = 0; - const azm_rotation = 0; - - const resultPtr = spa_calculate( - year, month, day, hour, minute, second, timezone, - latitude, longitude, elevation, pressure, temperature, - slope, azm_rotation, refraction - ); - - const result = { - zenith: spaModule.getValue(resultPtr, 'double'), - azimuth: spaModule.getValue(resultPtr + 8, 'double'), - incidence: spaModule.getValue(resultPtr + 16, 'double'), - sunrise: spaModule.getValue(resultPtr + 24, 'double'), - sunset: spaModule.getValue(resultPtr + 32, 'double'), - solar_noon: spaModule.getValue(resultPtr + 40, 'double'), - sun_transit_alt: spaModule.getValue(resultPtr + 48, 'double'), - }; - - spa_free_result(resultPtr); - - resolve(result); - }; - }); -}; diff --git a/spa.js b/spa.js deleted file mode 100644 index d15f6c0..0000000 --- a/spa.js +++ /dev/null @@ -1,1781 +0,0 @@ -// include: shell.js -// The Module object: Our interface to the outside world. We import -// and export values on it. There are various ways Module can be used: -// 1. Not defined. We create it here -// 2. A function parameter, function(Module) { ..generated code.. } -// 3. pre-run appended it, var Module = {}; ..generated code.. -// 4. External script tag defines var Module. -// We need to check if Module already exists (e.g. case 3 above). -// Substitution will be replaced with actual code on later stage of the build, -// this way Closure Compiler will not mangle it (e.g. case 4. above). -// Note that if you want to run closure, and also to use Module -// after the generated code, you will need to define var Module = {}; -// before the code. Then that object will be used in the code, and you -// can continue to use Module afterwards as well. -var Module = typeof Module != 'undefined' ? Module : {}; - -// --pre-jses are emitted after the Module integration code, so that they can -// refer to Module (if they choose; they can also define Module) - - -// Sometimes an existing Module object exists with properties -// meant to overwrite the default module functionality. Here -// we collect those properties and reapply _after_ we configure -// the current environment's defaults to avoid having to be so -// defensive during initialization. -var moduleOverrides = Object.assign({}, Module); - -var arguments_ = []; -var thisProgram = './this.program'; -var quit_ = (status, toThrow) => { - throw toThrow; -}; - -// Determine the runtime environment we are in. You can customize this by -// setting the ENVIRONMENT setting at compile time (see settings.js). - -// Attempt to auto-detect the environment -var ENVIRONMENT_IS_WEB = typeof window == 'object'; -var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function'; -// N.b. Electron.js environment is simultaneously a NODE-environment, but -// also a web environment. -var ENVIRONMENT_IS_NODE = typeof process == 'object' && typeof process.versions == 'object' && typeof process.versions.node == 'string'; -var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; - -if (Module['ENVIRONMENT']) { - throw new Error('Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)'); -} - -// `/` should be present at the end if `scriptDirectory` is not empty -var scriptDirectory = ''; -function locateFile(path) { - if (Module['locateFile']) { - return Module['locateFile'](path, scriptDirectory); - } - return scriptDirectory + path; -} - -// Hooks that are implemented differently in different runtime environments. -var read_, - readAsync, - readBinary, - setWindowTitle; - -if (ENVIRONMENT_IS_NODE) { - if (typeof process == 'undefined' || !process.release || process.release.name !== 'node') throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - var nodeVersion = process.versions.node; - var numericVersion = nodeVersion.split('.').slice(0, 3); - numericVersion = (numericVersion[0] * 10000) + (numericVersion[1] * 100) + numericVersion[2] * 1; - var minVersion = 101900; - if (numericVersion < 101900) { - throw new Error('This emscripten-generated code requires node v10.19.19.0 (detected v' + nodeVersion + ')'); - } - - // `require()` is no-op in an ESM module, use `createRequire()` to construct - // the require()` function. This is only necessary for multi-environment - // builds, `-sENVIRONMENT=node` emits a static import declaration instead. - // TODO: Swap all `require()`'s with `import()`'s? - // These modules will usually be used on Node.js. Load them eagerly to avoid - // the complexity of lazy-loading. - var fs = require('fs'); - var nodePath = require('path'); - - if (ENVIRONMENT_IS_WORKER) { - scriptDirectory = nodePath.dirname(scriptDirectory) + '/'; - } else { - scriptDirectory = __dirname + '/'; - } - -// include: node_shell_read.js -read_ = (filename, binary) => { - // We need to re-wrap `file://` strings to URLs. Normalizing isn't - // necessary in that case, the path should already be absolute. - filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename); - return fs.readFileSync(filename, binary ? undefined : 'utf8'); -}; - -readBinary = (filename) => { - var ret = read_(filename, true); - if (!ret.buffer) { - ret = new Uint8Array(ret); - } - assert(ret.buffer); - return ret; -}; - -readAsync = (filename, onload, onerror) => { - // See the comment in the `read_` function. - filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename); - fs.readFile(filename, function(err, data) { - if (err) onerror(err); - else onload(data.buffer); - }); -}; - -// end include: node_shell_read.js - if (process.argv.length > 1) { - thisProgram = process.argv[1].replace(/\\/g, '/'); - } - - arguments_ = process.argv.slice(2); - - if (typeof module != 'undefined') { - module['exports'] = Module; - } - - process.on('uncaughtException', function(ex) { - // suppress ExitStatus exceptions from showing an error - if (ex !== 'unwind' && !(ex instanceof ExitStatus) && !(ex.context instanceof ExitStatus)) { - throw ex; - } - }); - - // Without this older versions of node (< v15) will log unhandled rejections - // but return 0, which is not normally the desired behaviour. This is - // not be needed with node v15 and about because it is now the default - // behaviour: - // See https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode - var nodeMajor = process.versions.node.split(".")[0]; - if (nodeMajor < 15) { - process.on('unhandledRejection', function(reason) { throw reason; }); - } - - quit_ = (status, toThrow) => { - process.exitCode = status; - throw toThrow; - }; - - Module['inspect'] = function () { return '[Emscripten Module object]'; }; - -} else -if (ENVIRONMENT_IS_SHELL) { - - if ((typeof process == 'object' && typeof require === 'function') || typeof window == 'object' || typeof importScripts == 'function') throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - if (typeof read != 'undefined') { - read_ = function shell_read(f) { - return read(f); - }; - } - - readBinary = function readBinary(f) { - let data; - if (typeof readbuffer == 'function') { - return new Uint8Array(readbuffer(f)); - } - data = read(f, 'binary'); - assert(typeof data == 'object'); - return data; - }; - - readAsync = function readAsync(f, onload, onerror) { - setTimeout(() => onload(readBinary(f)), 0); - }; - - if (typeof clearTimeout == 'undefined') { - globalThis.clearTimeout = (id) => {}; - } - - if (typeof scriptArgs != 'undefined') { - arguments_ = scriptArgs; - } else if (typeof arguments != 'undefined') { - arguments_ = arguments; - } - - if (typeof quit == 'function') { - quit_ = (status, toThrow) => { - // Unlike node which has process.exitCode, d8 has no such mechanism. So we - // have no way to set the exit code and then let the program exit with - // that code when it naturally stops running (say, when all setTimeouts - // have completed). For that reason, we must call `quit` - the only way to - // set the exit code - but quit also halts immediately. To increase - // consistency with node (and the web) we schedule the actual quit call - // using a setTimeout to give the current stack and any exception handlers - // a chance to run. This enables features such as addOnPostRun (which - // expected to be able to run code after main returns). - setTimeout(() => { - if (!(toThrow instanceof ExitStatus)) { - let toLog = toThrow; - if (toThrow && typeof toThrow == 'object' && toThrow.stack) { - toLog = [toThrow, toThrow.stack]; - } - err('exiting due to exception: ' + toLog); - } - quit(status); - }); - throw toThrow; - }; - } - - if (typeof print != 'undefined') { - // Prefer to use print/printErr where they exist, as they usually work better. - if (typeof console == 'undefined') console = /** @type{!Console} */({}); - console.log = /** @type{!function(this:Console, ...*): undefined} */ (print); - console.warn = console.error = /** @type{!function(this:Console, ...*): undefined} */ (typeof printErr != 'undefined' ? printErr : print); - } - -} else - -// Note that this includes Node.js workers when relevant (pthreads is enabled). -// Node.js workers are detected as a combination of ENVIRONMENT_IS_WORKER and -// ENVIRONMENT_IS_NODE. -if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { - if (ENVIRONMENT_IS_WORKER) { // Check worker, not web, since window could be polyfilled - scriptDirectory = self.location.href; - } else if (typeof document != 'undefined' && document.currentScript) { // web - scriptDirectory = document.currentScript.src; - } - // blob urls look like blob:http://site.com/etc/etc and we cannot infer anything from them. - // otherwise, slice off the final part of the url to find the script directory. - // if scriptDirectory does not contain a slash, lastIndexOf will return -1, - // and scriptDirectory will correctly be replaced with an empty string. - // If scriptDirectory contains a query (starting with ?) or a fragment (starting with #), - // they are removed because they could contain a slash. - if (scriptDirectory.indexOf('blob:') !== 0) { - scriptDirectory = scriptDirectory.substr(0, scriptDirectory.replace(/[?#].*/, "").lastIndexOf('/')+1); - } else { - scriptDirectory = ''; - } - - if (!(typeof window == 'object' || typeof importScripts == 'function')) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)'); - - // Differentiate the Web Worker from the Node Worker case, as reading must - // be done differently. - { -// include: web_or_worker_shell_read.js -read_ = (url) => { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.send(null); - return xhr.responseText; - } - - if (ENVIRONMENT_IS_WORKER) { - readBinary = (url) => { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.responseType = 'arraybuffer'; - xhr.send(null); - return new Uint8Array(/** @type{!ArrayBuffer} */(xhr.response)); - }; - } - - readAsync = (url, onload, onerror) => { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.onload = () => { - if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 - onload(xhr.response); - return; - } - onerror(); - }; - xhr.onerror = onerror; - xhr.send(null); - } - -// end include: web_or_worker_shell_read.js - } - - setWindowTitle = (title) => document.title = title; -} else -{ - throw new Error('environment detection error'); -} - -var out = Module['print'] || console.log.bind(console); -var err = Module['printErr'] || console.warn.bind(console); - -// Merge back in the overrides -Object.assign(Module, moduleOverrides); -// Free the object hierarchy contained in the overrides, this lets the GC -// reclaim data used e.g. in memoryInitializerRequest, which is a large typed array. -moduleOverrides = null; -checkIncomingModuleAPI(); - -// Emit code to handle expected values on the Module object. This applies Module.x -// to the proper local x. This has two benefits: first, we only emit it if it is -// expected to arrive, and second, by using a local everywhere else that can be -// minified. - -if (Module['arguments']) arguments_ = Module['arguments'];legacyModuleProp('arguments', 'arguments_'); - -if (Module['thisProgram']) thisProgram = Module['thisProgram'];legacyModuleProp('thisProgram', 'thisProgram'); - -if (Module['quit']) quit_ = Module['quit'];legacyModuleProp('quit', 'quit_'); - -// perform assertions in shell.js after we set up out() and err(), as otherwise if an assertion fails it cannot print the message -// Assertions on removed incoming Module JS APIs. -assert(typeof Module['memoryInitializerPrefixURL'] == 'undefined', 'Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['pthreadMainPrefixURL'] == 'undefined', 'Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['cdInitializerPrefixURL'] == 'undefined', 'Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['filePackagePrefixURL'] == 'undefined', 'Module.filePackagePrefixURL option was removed, use Module.locateFile instead'); -assert(typeof Module['read'] == 'undefined', 'Module.read option was removed (modify read_ in JS)'); -assert(typeof Module['readAsync'] == 'undefined', 'Module.readAsync option was removed (modify readAsync in JS)'); -assert(typeof Module['readBinary'] == 'undefined', 'Module.readBinary option was removed (modify readBinary in JS)'); -assert(typeof Module['setWindowTitle'] == 'undefined', 'Module.setWindowTitle option was removed (modify setWindowTitle in JS)'); -assert(typeof Module['TOTAL_MEMORY'] == 'undefined', 'Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY'); -legacyModuleProp('read', 'read_'); -legacyModuleProp('readAsync', 'readAsync'); -legacyModuleProp('readBinary', 'readBinary'); -legacyModuleProp('setWindowTitle', 'setWindowTitle'); -var IDBFS = 'IDBFS is no longer included by default; build with -lidbfs.js'; -var PROXYFS = 'PROXYFS is no longer included by default; build with -lproxyfs.js'; -var WORKERFS = 'WORKERFS is no longer included by default; build with -lworkerfs.js'; -var NODEFS = 'NODEFS is no longer included by default; build with -lnodefs.js'; - -assert(!ENVIRONMENT_IS_WORKER, "worker environment detected but not enabled at build time. Add 'worker' to `-sENVIRONMENT` to enable."); - -assert(!ENVIRONMENT_IS_SHELL, "shell environment detected but not enabled at build time. Add 'shell' to `-sENVIRONMENT` to enable."); - - -// end include: shell.js -// include: preamble.js -// === Preamble library stuff === - -// Documentation for the public APIs defined in this file must be updated in: -// site/source/docs/api_reference/preamble.js.rst -// A prebuilt local version of the documentation is available at: -// site/build/text/docs/api_reference/preamble.js.txt -// You can also build docs locally as HTML or other formats in site/ -// An online HTML version (which may be of a different version of Emscripten) -// is up at http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html - -var wasmBinary; -if (Module['wasmBinary']) wasmBinary = Module['wasmBinary'];legacyModuleProp('wasmBinary', 'wasmBinary'); -var noExitRuntime = Module['noExitRuntime'] || true;legacyModuleProp('noExitRuntime', 'noExitRuntime'); - -if (typeof WebAssembly != 'object') { - abort('no native wasm support detected'); -} - -// Wasm globals - -var wasmMemory; - -//======================================== -// Runtime essentials -//======================================== - -// whether we are quitting the application. no code should run after this. -// set in exit() and abort() -var ABORT = false; - -// set by exit() and abort(). Passed to 'onExit' handler. -// NOTE: This is also used as the process return code code in shell environments -// but only when noExitRuntime is false. -var EXITSTATUS; - -/** @type {function(*, string=)} */ -function assert(condition, text) { - if (!condition) { - abort('Assertion failed' + (text ? ': ' + text : '')); - } -} - -// We used to include malloc/free by default in the past. Show a helpful error in -// builds with assertions. -function _malloc() { - abort("malloc() called but not included in the build - add '_malloc' to EXPORTED_FUNCTIONS"); -} -function _free() { - // Show a helpful error since we used to include free by default in the past. - abort("free() called but not included in the build - add '_free' to EXPORTED_FUNCTIONS"); -} - -// include: runtime_strings.js -// runtime_strings.js: String related runtime functions that are part of both -// MINIMAL_RUNTIME and regular runtime. - -var UTF8Decoder = typeof TextDecoder != 'undefined' ? new TextDecoder('utf8') : undefined; - -/** - * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given - * array that contains uint8 values, returns a copy of that string as a - * Javascript String object. - * heapOrArray is either a regular array, or a JavaScript typed array view. - * @param {number} idx - * @param {number=} maxBytesToRead - * @return {string} - */ -function UTF8ArrayToString(heapOrArray, idx, maxBytesToRead) { - var endIdx = idx + maxBytesToRead; - var endPtr = idx; - // TextDecoder needs to know the byte length in advance, it doesn't stop on - // null terminator by itself. Also, use the length info to avoid running tiny - // strings through TextDecoder, since .subarray() allocates garbage. - // (As a tiny code save trick, compare endPtr against endIdx using a negation, - // so that undefined means Infinity) - while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr; - - if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { - return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); - } - var str = ''; - // If building with TextDecoder, we have already computed the string length - // above, so test loop end condition against that - while (idx < endPtr) { - // For UTF8 byte structure, see: - // http://en.wikipedia.org/wiki/UTF-8#Description - // https://www.ietf.org/rfc/rfc2279.txt - // https://tools.ietf.org/html/rfc3629 - var u0 = heapOrArray[idx++]; - if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; } - var u1 = heapOrArray[idx++] & 63; - if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; } - var u2 = heapOrArray[idx++] & 63; - if ((u0 & 0xF0) == 0xE0) { - u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; - } else { - if ((u0 & 0xF8) != 0xF0) warnOnce('Invalid UTF-8 leading byte ' + ptrToString(u0) + ' encountered when deserializing a UTF-8 string in wasm memory to a JS string!'); - u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63); - } - - if (u0 < 0x10000) { - str += String.fromCharCode(u0); - } else { - var ch = u0 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } - } - return str; -} - -/** - * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the - * emscripten HEAP, returns a copy of that string as a Javascript String object. - * - * @param {number} ptr - * @param {number=} maxBytesToRead - An optional length that specifies the - * maximum number of bytes to read. You can omit this parameter to scan the - * string until the first \0 byte. If maxBytesToRead is passed, and the string - * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the - * string will cut short at that byte index (i.e. maxBytesToRead will not - * produce a string of exact length [ptr, ptr+maxBytesToRead[) N.B. mixing - * frequent uses of UTF8ToString() with and without maxBytesToRead may throw - * JS JIT optimizations off, so it is worth to consider consistently using one - * @return {string} - */ -function UTF8ToString(ptr, maxBytesToRead) { - assert(typeof ptr == 'number'); - return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : ''; -} - -/** - * Copies the given Javascript String object 'str' to the given byte array at - * address 'outIdx', encoded in UTF8 form and null-terminated. The copy will - * require at most str.length*4+1 bytes of space in the HEAP. Use the function - * lengthBytesUTF8 to compute the exact number of bytes (excluding null - * terminator) that this function will write. - * - * @param {string} str - The Javascript string to copy. - * @param {ArrayBufferView|Array} heap - The array to copy to. Each - * index in this array is assumed - * to be one 8-byte element. - * @param {number} outIdx - The starting offset in the array to begin the copying. - * @param {number} maxBytesToWrite - The maximum number of bytes this function - * can write to the array. This count should - * include the null terminator, i.e. if - * maxBytesToWrite=1, only the null terminator - * will be written and nothing else. - * maxBytesToWrite=0 does not write any bytes - * to the output, not even the null - * terminator. - * @return {number} The number of bytes written, EXCLUDING the null terminator. - */ -function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) { - // Parameter maxBytesToWrite is not optional. Negative values, 0, null, - // undefined and false each don't write out any bytes. - if (!(maxBytesToWrite > 0)) - return 0; - - var startIdx = outIdx; - var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code - // unit, not a Unicode code point of the character! So decode - // UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - // For UTF8 byte structure, see http://en.wikipedia.org/wiki/UTF-8#Description - // and https://www.ietf.org/rfc/rfc2279.txt - // and https://tools.ietf.org/html/rfc3629 - var u = str.charCodeAt(i); // possibly a lead surrogate - if (u >= 0xD800 && u <= 0xDFFF) { - var u1 = str.charCodeAt(++i); - u = 0x10000 + ((u & 0x3FF) << 10) | (u1 & 0x3FF); - } - if (u <= 0x7F) { - if (outIdx >= endIdx) break; - heap[outIdx++] = u; - } else if (u <= 0x7FF) { - if (outIdx + 1 >= endIdx) break; - heap[outIdx++] = 0xC0 | (u >> 6); - heap[outIdx++] = 0x80 | (u & 63); - } else if (u <= 0xFFFF) { - if (outIdx + 2 >= endIdx) break; - heap[outIdx++] = 0xE0 | (u >> 12); - heap[outIdx++] = 0x80 | ((u >> 6) & 63); - heap[outIdx++] = 0x80 | (u & 63); - } else { - if (outIdx + 3 >= endIdx) break; - if (u > 0x10FFFF) warnOnce('Invalid Unicode code point ' + ptrToString(u) + ' encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).'); - heap[outIdx++] = 0xF0 | (u >> 18); - heap[outIdx++] = 0x80 | ((u >> 12) & 63); - heap[outIdx++] = 0x80 | ((u >> 6) & 63); - heap[outIdx++] = 0x80 | (u & 63); - } - } - // Null-terminate the pointer to the buffer. - heap[outIdx] = 0; - return outIdx - startIdx; -} - -/** - * Copies the given Javascript String object 'str' to the emscripten HEAP at - * address 'outPtr', null-terminated and encoded in UTF8 form. The copy will - * require at most str.length*4+1 bytes of space in the HEAP. - * Use the function lengthBytesUTF8 to compute the exact number of bytes - * (excluding null terminator) that this function will write. - * - * @return {number} The number of bytes written, EXCLUDING the null terminator. - */ -function stringToUTF8(str, outPtr, maxBytesToWrite) { - assert(typeof maxBytesToWrite == 'number', 'stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); - return stringToUTF8Array(str, HEAPU8,outPtr, maxBytesToWrite); -} - -/** - * Returns the number of bytes the given Javascript string takes if encoded as a - * UTF8 byte array, EXCLUDING the null terminator byte. - * - * @param {string} str - JavaScript string to operator on - * @return {number} Length, in bytes, of the UTF8 encoded string. - */ -function lengthBytesUTF8(str) { - var len = 0; - for (var i = 0; i < str.length; ++i) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code - // unit, not a Unicode code point of the character! So decode - // UTF16->UTF32->UTF8. - // See http://unicode.org/faq/utf_bom.html#utf16-3 - var c = str.charCodeAt(i); // possibly a lead surrogate - if (c <= 0x7F) { - len++; - } else if (c <= 0x7FF) { - len += 2; - } else if (c >= 0xD800 && c <= 0xDFFF) { - len += 4; ++i; - } else { - len += 3; - } - } - return len; -} - -// end include: runtime_strings.js -// Memory management - -var HEAP, -/** @type {!Int8Array} */ - HEAP8, -/** @type {!Uint8Array} */ - HEAPU8, -/** @type {!Int16Array} */ - HEAP16, -/** @type {!Uint16Array} */ - HEAPU16, -/** @type {!Int32Array} */ - HEAP32, -/** @type {!Uint32Array} */ - HEAPU32, -/** @type {!Float32Array} */ - HEAPF32, -/** @type {!Float64Array} */ - HEAPF64; - -function updateMemoryViews() { - var b = wasmMemory.buffer; - Module['HEAP8'] = HEAP8 = new Int8Array(b); - Module['HEAP16'] = HEAP16 = new Int16Array(b); - Module['HEAP32'] = HEAP32 = new Int32Array(b); - Module['HEAPU8'] = HEAPU8 = new Uint8Array(b); - Module['HEAPU16'] = HEAPU16 = new Uint16Array(b); - Module['HEAPU32'] = HEAPU32 = new Uint32Array(b); - Module['HEAPF32'] = HEAPF32 = new Float32Array(b); - Module['HEAPF64'] = HEAPF64 = new Float64Array(b); -} - -assert(!Module['STACK_SIZE'], 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time') - -assert(typeof Int32Array != 'undefined' && typeof Float64Array !== 'undefined' && Int32Array.prototype.subarray != undefined && Int32Array.prototype.set != undefined, - 'JS engine does not provide full typed array support'); - -// If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY -assert(!Module['wasmMemory'], 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally'); -assert(!Module['INITIAL_MEMORY'], 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically'); - -// include: runtime_init_table.js -// In regular non-RELOCATABLE mode the table is exported -// from the wasm module and this will be assigned once -// the exports are available. -var wasmTable; - -// end include: runtime_init_table.js -// include: runtime_stack_check.js -// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode. -function writeStackCookie() { - var max = _emscripten_stack_get_end(); - assert((max & 3) == 0); - // If the stack ends at address zero we write our cookies 4 bytes into the - // stack. This prevents interference with the (separate) address-zero check - // below. - if (max == 0) { - max += 4; - } - // The stack grow downwards towards _emscripten_stack_get_end. - // We write cookies to the final two words in the stack and detect if they are - // ever overwritten. - HEAPU32[((max)>>2)] = 0x02135467; - HEAPU32[(((max)+(4))>>2)] = 0x89BACDFE; - // Also test the global address 0 for integrity. - HEAPU32[0] = 0x63736d65; /* 'emsc' */ -} - -function checkStackCookie() { - if (ABORT) return; - var max = _emscripten_stack_get_end(); - // See writeStackCookie(). - if (max == 0) { - max += 4; - } - var cookie1 = HEAPU32[((max)>>2)]; - var cookie2 = HEAPU32[(((max)+(4))>>2)]; - if (cookie1 != 0x02135467 || cookie2 != 0x89BACDFE) { - abort('Stack overflow! Stack cookie has been overwritten at ' + ptrToString(max) + ', expected hex dwords 0x89BACDFE and 0x2135467, but received ' + ptrToString(cookie2) + ' ' + ptrToString(cookie1)); - } - // Also test the global address 0 for integrity. - if (HEAPU32[0] !== 0x63736d65 /* 'emsc' */) { - abort('Runtime error: The application has corrupted its heap memory area (address zero)!'); - } -} - -// end include: runtime_stack_check.js -// include: runtime_assertions.js -// Endianness check -(function() { - var h16 = new Int16Array(1); - var h8 = new Int8Array(h16.buffer); - h16[0] = 0x6373; - if (h8[0] !== 0x73 || h8[1] !== 0x63) throw 'Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)'; -})(); - -// end include: runtime_assertions.js -var __ATPRERUN__ = []; // functions called before the runtime is initialized -var __ATINIT__ = []; // functions called during startup -var __ATEXIT__ = []; // functions called during shutdown -var __ATPOSTRUN__ = []; // functions called after the main() is called - -var runtimeInitialized = false; - -var runtimeKeepaliveCounter = 0; - -function keepRuntimeAlive() { - return noExitRuntime || runtimeKeepaliveCounter > 0; -} - -function preRun() { - if (Module['preRun']) { - if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; - while (Module['preRun'].length) { - addOnPreRun(Module['preRun'].shift()); - } - } - callRuntimeCallbacks(__ATPRERUN__); -} - -function initRuntime() { - assert(!runtimeInitialized); - runtimeInitialized = true; - - checkStackCookie(); - - - callRuntimeCallbacks(__ATINIT__); -} - -function postRun() { - checkStackCookie(); - - if (Module['postRun']) { - if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; - while (Module['postRun'].length) { - addOnPostRun(Module['postRun'].shift()); - } - } - - callRuntimeCallbacks(__ATPOSTRUN__); -} - -function addOnPreRun(cb) { - __ATPRERUN__.unshift(cb); -} - -function addOnInit(cb) { - __ATINIT__.unshift(cb); -} - -function addOnExit(cb) { -} - -function addOnPostRun(cb) { - __ATPOSTRUN__.unshift(cb); -} - -// include: runtime_math.js -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc - -assert(Math.imul, 'This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.fround, 'This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.clz32, 'This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); -assert(Math.trunc, 'This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill'); - -// end include: runtime_math.js -// A counter of dependencies for calling run(). If we need to -// do asynchronous work before running, increment this and -// decrement it. Incrementing must happen in a place like -// Module.preRun (used by emcc to add file preloading). -// Note that you can add dependencies in preRun, even though -// it happens right before run - run will be postponed until -// the dependencies are met. -var runDependencies = 0; -var runDependencyWatcher = null; -var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled -var runDependencyTracking = {}; - -function getUniqueRunDependency(id) { - var orig = id; - while (1) { - if (!runDependencyTracking[id]) return id; - id = orig + Math.random(); - } -} - -function addRunDependency(id) { - runDependencies++; - - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - - if (id) { - assert(!runDependencyTracking[id]); - runDependencyTracking[id] = 1; - if (runDependencyWatcher === null && typeof setInterval != 'undefined') { - // Check for missing dependencies every few seconds - runDependencyWatcher = setInterval(function() { - if (ABORT) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - return; - } - var shown = false; - for (var dep in runDependencyTracking) { - if (!shown) { - shown = true; - err('still waiting on run dependencies:'); - } - err('dependency: ' + dep); - } - if (shown) { - err('(end of list)'); - } - }, 10000); - } - } else { - err('warning: run dependency added without ID'); - } -} - -function removeRunDependency(id) { - runDependencies--; - - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - - if (id) { - assert(runDependencyTracking[id]); - delete runDependencyTracking[id]; - } else { - err('warning: run dependency removed without ID'); - } - if (runDependencies == 0) { - if (runDependencyWatcher !== null) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } - } -} - -/** @param {string|number=} what */ -function abort(what) { - if (Module['onAbort']) { - Module['onAbort'](what); - } - - what = 'Aborted(' + what + ')'; - // TODO(sbc): Should we remove printing and leave it up to whoever - // catches the exception? - err(what); - - ABORT = true; - EXITSTATUS = 1; - - // Use a wasm runtime error, because a JS error might be seen as a foreign - // exception, which means we'd run destructors on it. We need the error to - // simply make the program stop. - // FIXME This approach does not work in Wasm EH because it currently does not assume - // all RuntimeErrors are from traps; it decides whether a RuntimeError is from - // a trap or not based on a hidden field within the object. So at the moment - // we don't have a way of throwing a wasm trap from JS. TODO Make a JS API that - // allows this in the wasm spec. - - // Suppress closure compiler warning here. Closure compiler's builtin extern - // defintion for WebAssembly.RuntimeError claims it takes no arguments even - // though it can. - // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure gets fixed. - /** @suppress {checkTypes} */ - var e = new WebAssembly.RuntimeError(what); - - // Throw the error whether or not MODULARIZE is set because abort is used - // in code paths apart from instantiation where an exception is expected - // to be thrown when abort is called. - throw e; -} - -// include: memoryprofiler.js -// end include: memoryprofiler.js -// show errors on likely calls to FS when it was not included -var FS = { - error: function() { - abort('Filesystem support (FS) was not included. The problem is that you are using files from JS, but files were not used from C/C++, so filesystem support was not auto-included. You can force-include filesystem support with -sFORCE_FILESYSTEM'); - }, - init: function() { FS.error() }, - createDataFile: function() { FS.error() }, - createPreloadedFile: function() { FS.error() }, - createLazyFile: function() { FS.error() }, - open: function() { FS.error() }, - mkdev: function() { FS.error() }, - registerDevice: function() { FS.error() }, - analyzePath: function() { FS.error() }, - loadFilesFromDB: function() { FS.error() }, - - ErrnoError: function ErrnoError() { FS.error() }, -}; -Module['FS_createDataFile'] = FS.createDataFile; -Module['FS_createPreloadedFile'] = FS.createPreloadedFile; - -// include: URIUtils.js -// Prefix of data URIs emitted by SINGLE_FILE and related options. -var dataURIPrefix = 'data:application/octet-stream;base64,'; - -// Indicates whether filename is a base64 data URI. -function isDataURI(filename) { - // Prefix of data URIs emitted by SINGLE_FILE and related options. - return filename.startsWith(dataURIPrefix); -} - -// Indicates whether filename is delivered via file protocol (as opposed to http/https) -function isFileURI(filename) { - return filename.startsWith('file://'); -} - -// end include: URIUtils.js -/** @param {boolean=} fixedasm */ -function createExportWrapper(name, fixedasm) { - return function() { - var displayName = name; - var asm = fixedasm; - if (!fixedasm) { - asm = Module['asm']; - } - assert(runtimeInitialized, 'native function `' + displayName + '` called before runtime initialization'); - if (!asm[name]) { - assert(asm[name], 'exported native function `' + displayName + '` not found'); - } - return asm[name].apply(null, arguments); - }; -} - -// include: runtime_exceptions.js -// end include: runtime_exceptions.js -var wasmBinaryFile; - wasmBinaryFile = 'spa.wasm'; - if (!isDataURI(wasmBinaryFile)) { - wasmBinaryFile = locateFile(wasmBinaryFile); - } - -function getBinary(file) { - try { - if (file == wasmBinaryFile && wasmBinary) { - return new Uint8Array(wasmBinary); - } - if (readBinary) { - return readBinary(file); - } - throw "both async and sync fetching of the wasm failed"; - } - catch (err) { - abort(err); - } -} - -function getBinaryPromise(binaryFile) { - // If we don't have the binary yet, try to to load it asynchronously. - // Fetch has some additional restrictions over XHR, like it can't be used on a file:// url. - // See https://github.com/github/fetch/pull/92#issuecomment-140665932 - // Cordova or Electron apps are typically loaded from a file:// url. - // So use fetch if it is available and the url is not a file, otherwise fall back to XHR. - if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) { - if (typeof fetch == 'function' - ) { - return fetch(binaryFile, { credentials: 'same-origin' }).then(function(response) { - if (!response['ok']) { - throw "failed to load wasm binary file at '" + binaryFile + "'"; - } - return response['arrayBuffer'](); - }).catch(function () { - return getBinary(binaryFile); - }); - } - } - - // Otherwise, getBinary should be able to get it synchronously - return Promise.resolve().then(function() { return getBinary(binaryFile); }); -} - -function instantiateArrayBuffer(binaryFile, imports, receiver) { - return getBinaryPromise(binaryFile).then(function(binary) { - return WebAssembly.instantiate(binary, imports); - }).then(function (instance) { - return instance; - }).then(receiver, function(reason) { - err('failed to asynchronously prepare wasm: ' + reason); - - // Warn on some common problems. - if (isFileURI(wasmBinaryFile)) { - err('warning: Loading from a file URI (' + wasmBinaryFile + ') is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing'); - } - abort(reason); - }); -} - -function instantiateAsync(binary, binaryFile, imports, callback) { - if (!binary && - typeof WebAssembly.instantiateStreaming == 'function' && - !isDataURI(binaryFile) && - // Avoid instantiateStreaming() on Node.js environment for now, as while - // Node.js v18.1.0 implements it, it does not have a full fetch() - // implementation yet. - // - // Reference: - // https://github.com/emscripten-core/emscripten/pull/16917 - !ENVIRONMENT_IS_NODE && - typeof fetch == 'function') { - return fetch(binaryFile, { credentials: 'same-origin' }).then(function(response) { - // Suppress closure warning here since the upstream definition for - // instantiateStreaming only allows Promise rather than - // an actual Response. - // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure is fixed. - /** @suppress {checkTypes} */ - var result = WebAssembly.instantiateStreaming(response, imports); - - return result.then( - callback, - function(reason) { - // We expect the most common failure cause to be a bad MIME type for the binary, - // in which case falling back to ArrayBuffer instantiation should work. - err('wasm streaming compile failed: ' + reason); - err('falling back to ArrayBuffer instantiation'); - return instantiateArrayBuffer(binaryFile, imports, callback); - }); - }); - } else { - return instantiateArrayBuffer(binaryFile, imports, callback); - } -} - -// Create the wasm instance. -// Receives the wasm imports, returns the exports. -function createWasm() { - // prepare imports - var info = { - 'env': wasmImports, - 'wasi_snapshot_preview1': wasmImports, - }; - // Load the wasm module and create an instance of using native support in the JS engine. - // handle a generated wasm instance, receiving its exports and - // performing other necessary setup - /** @param {WebAssembly.Module=} module*/ - function receiveInstance(instance, module) { - var exports = instance.exports; - - Module['asm'] = exports; - - wasmMemory = Module['asm']['memory']; - assert(wasmMemory, "memory not found in wasm exports"); - // This assertion doesn't hold when emscripten is run in --post-link - // mode. - // TODO(sbc): Read INITIAL_MEMORY out of the wasm file in post-link mode. - //assert(wasmMemory.buffer.byteLength === 16777216); - updateMemoryViews(); - - wasmTable = Module['asm']['__indirect_function_table']; - assert(wasmTable, "table not found in wasm exports"); - - addOnInit(Module['asm']['__wasm_call_ctors']); - - removeRunDependency('wasm-instantiate'); - - return exports; - } - // wait for the pthread pool (if any) - addRunDependency('wasm-instantiate'); - - // Prefer streaming instantiation if available. - // Async compilation can be confusing when an error on the page overwrites Module - // (for example, if the order of elements is wrong, and the one defining Module is - // later), so we save Module and check it later. - var trueModule = Module; - function receiveInstantiationResult(result) { - // 'result' is a ResultObject object which has both the module and instance. - // receiveInstance() will swap in the exports (to Module.asm) so they can be called - assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?'); - trueModule = null; - // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line. - // When the regression is fixed, can restore the above PTHREADS-enabled path. - receiveInstance(result['instance']); - } - - // User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback - // to manually instantiate the Wasm module themselves. This allows pages to run the instantiation parallel - // to any other async startup actions they are performing. - // Also pthreads and wasm workers initialize the wasm instance through this path. - if (Module['instantiateWasm']) { - try { - return Module['instantiateWasm'](info, receiveInstance); - } catch(e) { - err('Module.instantiateWasm callback failed with error: ' + e); - return false; - } - } - - instantiateAsync(wasmBinary, wasmBinaryFile, info, receiveInstantiationResult); - return {}; // no exports yet; we'll fill them in later -} - -// Globals used by JS i64 conversions (see makeSetValue) -var tempDouble; -var tempI64; - -// include: runtime_debug.js -function legacyModuleProp(prop, newName) { - if (!Object.getOwnPropertyDescriptor(Module, prop)) { - Object.defineProperty(Module, prop, { - configurable: true, - get: function() { - abort('Module.' + prop + ' has been replaced with plain ' + newName + ' (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)'); - } - }); - } -} - -function ignoredModuleProp(prop) { - if (Object.getOwnPropertyDescriptor(Module, prop)) { - abort('`Module.' + prop + '` was supplied but `' + prop + '` not included in INCOMING_MODULE_JS_API'); - } -} - -// forcing the filesystem exports a few things by default -function isExportedByForceFilesystem(name) { - return name === 'FS_createPath' || - name === 'FS_createDataFile' || - name === 'FS_createPreloadedFile' || - name === 'FS_unlink' || - name === 'addRunDependency' || - // The old FS has some functionality that WasmFS lacks. - name === 'FS_createLazyFile' || - name === 'FS_createDevice' || - name === 'removeRunDependency'; -} - -function missingGlobal(sym, msg) { - if (typeof globalThis !== 'undefined') { - Object.defineProperty(globalThis, sym, { - configurable: true, - get: function() { - warnOnce('`' + sym + '` is not longer defined by emscripten. ' + msg); - return undefined; - } - }); - } -} - -missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer'); - -function missingLibrarySymbol(sym) { - if (typeof globalThis !== 'undefined' && !Object.getOwnPropertyDescriptor(globalThis, sym)) { - Object.defineProperty(globalThis, sym, { - configurable: true, - get: function() { - // Can't `abort()` here because it would break code that does runtime - // checks. e.g. `if (typeof SDL === 'undefined')`. - var msg = '`' + sym + '` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line'; - // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in - // library.js, which means $name for a JS name with no prefix, or name - // for a JS name like _name. - var librarySymbol = sym; - if (!librarySymbol.startsWith('_')) { - librarySymbol = '$' + sym; - } - msg += " (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=" + librarySymbol + ")"; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - warnOnce(msg); - return undefined; - } - }); - } - // Any symbol that is not included from the JS libary is also (by definition) - // not exported on the Module object. - unexportedRuntimeSymbol(sym); -} - -function unexportedRuntimeSymbol(sym) { - if (!Object.getOwnPropertyDescriptor(Module, sym)) { - Object.defineProperty(Module, sym, { - configurable: true, - get: function() { - var msg = "'" + sym + "' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the FAQ)"; - if (isExportedByForceFilesystem(sym)) { - msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you'; - } - abort(msg); - } - }); - } -} - -// Used by XXXXX_DEBUG settings to output debug messages. -function dbg(text) { - // TODO(sbc): Make this configurable somehow. Its not always convenient for - // logging to show up as errors. - console.error(text); -} - -// end include: runtime_debug.js -// === Body === - - -// end include: preamble.js - - /** @constructor */ - function ExitStatus(status) { - this.name = 'ExitStatus'; - this.message = 'Program terminated with exit(' + status + ')'; - this.status = status; - } - - function callRuntimeCallbacks(callbacks) { - while (callbacks.length > 0) { - // Pass the module as the first argument. - callbacks.shift()(Module); - } - } - - - /** - * @param {number} ptr - * @param {string} type - */ - function getValue(ptr, type = 'i8') { - if (type.endsWith('*')) type = '*'; - switch (type) { - case 'i1': return HEAP8[((ptr)>>0)]; - case 'i8': return HEAP8[((ptr)>>0)]; - case 'i16': return HEAP16[((ptr)>>1)]; - case 'i32': return HEAP32[((ptr)>>2)]; - case 'i64': return HEAP32[((ptr)>>2)]; - case 'float': return HEAPF32[((ptr)>>2)]; - case 'double': return HEAPF64[((ptr)>>3)]; - case '*': return HEAPU32[((ptr)>>2)]; - default: abort('invalid type for getValue: ' + type); - } - } - - function ptrToString(ptr) { - assert(typeof ptr === 'number'); - return '0x' + ptr.toString(16).padStart(8, '0'); - } - - - /** - * @param {number} ptr - * @param {number} value - * @param {string} type - */ - function setValue(ptr, value, type = 'i8') { - if (type.endsWith('*')) type = '*'; - switch (type) { - case 'i1': HEAP8[((ptr)>>0)] = value; break; - case 'i8': HEAP8[((ptr)>>0)] = value; break; - case 'i16': HEAP16[((ptr)>>1)] = value; break; - case 'i32': HEAP32[((ptr)>>2)] = value; break; - case 'i64': (tempI64 = [value>>>0,(tempDouble=value,(+(Math.abs(tempDouble))) >= 1.0 ? (tempDouble > 0.0 ? ((Math.min((+(Math.floor((tempDouble)/4294967296.0))), 4294967295.0))|0)>>>0 : (~~((+(Math.ceil((tempDouble - +(((~~(tempDouble)))>>>0))/4294967296.0)))))>>>0) : 0)],HEAP32[((ptr)>>2)] = tempI64[0],HEAP32[(((ptr)+(4))>>2)] = tempI64[1]); break; - case 'float': HEAPF32[((ptr)>>2)] = value; break; - case 'double': HEAPF64[((ptr)>>3)] = value; break; - case '*': HEAPU32[((ptr)>>2)] = value; break; - default: abort('invalid type for setValue: ' + type); - } - } - - function warnOnce(text) { - if (!warnOnce.shown) warnOnce.shown = {}; - if (!warnOnce.shown[text]) { - warnOnce.shown[text] = 1; - if (ENVIRONMENT_IS_NODE) text = 'warning: ' + text; - err(text); - } - } - - function _emscripten_memcpy_big(dest, src, num) { - HEAPU8.copyWithin(dest, src, src + num); - } - - function getHeapMax() { - return HEAPU8.length; - } - - function abortOnCannotGrowMemory(requestedSize) { - abort('Cannot enlarge memory arrays to size ' + requestedSize + ' bytes (OOM). Either (1) compile with -sINITIAL_MEMORY=X with X higher than the current value ' + HEAP8.length + ', (2) compile with -sALLOW_MEMORY_GROWTH which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -sABORTING_MALLOC=0'); - } - function _emscripten_resize_heap(requestedSize) { - var oldSize = HEAPU8.length; - requestedSize = requestedSize >>> 0; - abortOnCannotGrowMemory(requestedSize); - } - - function getCFunc(ident) { - var func = Module['_' + ident]; // closure exported function - assert(func, 'Cannot call unknown function ' + ident + ', make sure it is exported'); - return func; - } - - function writeArrayToMemory(array, buffer) { - assert(array.length >= 0, 'writeArrayToMemory array must have a length (should be an array or typed array)') - HEAP8.set(array, buffer); - } - - /** - * @param {string|null=} returnType - * @param {Array=} argTypes - * @param {Arguments|Array=} args - * @param {Object=} opts - */ - function ccall(ident, returnType, argTypes, args, opts) { - // For fast lookup of conversion functions - var toC = { - 'string': (str) => { - var ret = 0; - if (str !== null && str !== undefined && str !== 0) { // null string - // at most 4 bytes per UTF-8 code point, +1 for the trailing '\0' - var len = (str.length << 2) + 1; - ret = stackAlloc(len); - stringToUTF8(str, ret, len); - } - return ret; - }, - 'array': (arr) => { - var ret = stackAlloc(arr.length); - writeArrayToMemory(arr, ret); - return ret; - } - }; - - function convertReturnValue(ret) { - if (returnType === 'string') { - - return UTF8ToString(ret); - } - if (returnType === 'boolean') return Boolean(ret); - return ret; - } - - var func = getCFunc(ident); - var cArgs = []; - var stack = 0; - assert(returnType !== 'array', 'Return type should not be "array".'); - if (args) { - for (var i = 0; i < args.length; i++) { - var converter = toC[argTypes[i]]; - if (converter) { - if (stack === 0) stack = stackSave(); - cArgs[i] = converter(args[i]); - } else { - cArgs[i] = args[i]; - } - } - } - var ret = func.apply(null, cArgs); - function onDone(ret) { - if (stack !== 0) stackRestore(stack); - return convertReturnValue(ret); - } - - ret = onDone(ret); - return ret; - } - - - - /** - * @param {string=} returnType - * @param {Array=} argTypes - * @param {Object=} opts - */ - function cwrap(ident, returnType, argTypes, opts) { - return function() { - return ccall(ident, returnType, argTypes, arguments, opts); - } - } - -function checkIncomingModuleAPI() { - ignoredModuleProp('fetchSettings'); -} -var wasmImports = { - "emscripten_memcpy_big": _emscripten_memcpy_big, - "emscripten_resize_heap": _emscripten_resize_heap -}; -var asm = createWasm(); -/** @type {function(...*):?} */ -var ___wasm_call_ctors = createExportWrapper("__wasm_call_ctors"); -/** @type {function(...*):?} */ -var _spa_calculate_wrapper = Module["_spa_calculate_wrapper"] = createExportWrapper("spa_calculate_wrapper"); -/** @type {function(...*):?} */ -var _spa_free_result = Module["_spa_free_result"] = createExportWrapper("spa_free_result"); -/** @type {function(...*):?} */ -var ___errno_location = createExportWrapper("__errno_location"); -/** @type {function(...*):?} */ -var _fflush = Module["_fflush"] = createExportWrapper("fflush"); -/** @type {function(...*):?} */ -var _emscripten_stack_init = function() { - return (_emscripten_stack_init = Module["asm"]["emscripten_stack_init"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _emscripten_stack_get_free = function() { - return (_emscripten_stack_get_free = Module["asm"]["emscripten_stack_get_free"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _emscripten_stack_get_base = function() { - return (_emscripten_stack_get_base = Module["asm"]["emscripten_stack_get_base"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var _emscripten_stack_get_end = function() { - return (_emscripten_stack_get_end = Module["asm"]["emscripten_stack_get_end"]).apply(null, arguments); -}; - -/** @type {function(...*):?} */ -var stackSave = createExportWrapper("stackSave"); -/** @type {function(...*):?} */ -var stackRestore = createExportWrapper("stackRestore"); -/** @type {function(...*):?} */ -var stackAlloc = createExportWrapper("stackAlloc"); -/** @type {function(...*):?} */ -var _emscripten_stack_get_current = function() { - return (_emscripten_stack_get_current = Module["asm"]["emscripten_stack_get_current"]).apply(null, arguments); -}; - - - -// include: postamble.js -// === Auto-generated postamble setup entry stuff === - -Module["ccall"] = ccall; -Module["cwrap"] = cwrap; -Module["getValue"] = getValue; -var missingLibrarySymbols = [ - 'zeroMemory', - 'stringToNewUTF8', - 'exitJS', - 'emscripten_realloc_buffer', - 'setErrNo', - 'inetPton4', - 'inetNtop4', - 'inetPton6', - 'inetNtop6', - 'readSockaddr', - 'writeSockaddr', - 'getHostByName', - 'getRandomDevice', - 'traverseStack', - 'convertPCtoSourceLocation', - 'readEmAsmArgs', - 'jstoi_q', - 'jstoi_s', - 'getExecutableName', - 'listenOnce', - 'autoResumeAudioContext', - 'dynCallLegacy', - 'getDynCaller', - 'dynCall', - 'handleException', - 'runtimeKeepalivePush', - 'runtimeKeepalivePop', - 'callUserCallback', - 'maybeExit', - 'safeSetTimeout', - 'asmjsMangle', - 'asyncLoad', - 'alignMemory', - 'mmapAlloc', - 'HandleAllocator', - 'getNativeTypeSize', - 'STACK_SIZE', - 'STACK_ALIGN', - 'POINTER_SIZE', - 'ASSERTIONS', - 'writeI53ToI64', - 'writeI53ToI64Clamped', - 'writeI53ToI64Signaling', - 'writeI53ToU64Clamped', - 'writeI53ToU64Signaling', - 'readI53FromI64', - 'readI53FromU64', - 'convertI32PairToI53', - 'convertI32PairToI53Checked', - 'convertU32PairToI53', - 'uleb128Encode', - 'sigToWasmTypes', - 'generateFuncType', - 'convertJsFunctionToWasm', - 'getEmptyTableSlot', - 'updateTableMap', - 'getFunctionAddress', - 'addFunction', - 'removeFunction', - 'reallyNegative', - 'unSign', - 'strLen', - 'reSign', - 'formatString', - 'intArrayFromString', - 'intArrayToString', - 'AsciiToString', - 'stringToAscii', - 'UTF16ToString', - 'stringToUTF16', - 'lengthBytesUTF16', - 'UTF32ToString', - 'stringToUTF32', - 'lengthBytesUTF32', - 'allocateUTF8', - 'allocateUTF8OnStack', - 'writeStringToMemory', - 'writeAsciiToMemory', - 'getSocketFromFD', - 'getSocketAddress', - 'registerKeyEventCallback', - 'maybeCStringToJsString', - 'findEventTarget', - 'findCanvasEventTarget', - 'getBoundingClientRect', - 'fillMouseEventData', - 'registerMouseEventCallback', - 'registerWheelEventCallback', - 'registerUiEventCallback', - 'registerFocusEventCallback', - 'fillDeviceOrientationEventData', - 'registerDeviceOrientationEventCallback', - 'fillDeviceMotionEventData', - 'registerDeviceMotionEventCallback', - 'screenOrientation', - 'fillOrientationChangeEventData', - 'registerOrientationChangeEventCallback', - 'fillFullscreenChangeEventData', - 'registerFullscreenChangeEventCallback', - 'JSEvents_requestFullscreen', - 'JSEvents_resizeCanvasForFullscreen', - 'registerRestoreOldStyle', - 'hideEverythingExceptGivenElement', - 'restoreHiddenElements', - 'setLetterbox', - 'softFullscreenResizeWebGLRenderTarget', - 'doRequestFullscreen', - 'fillPointerlockChangeEventData', - 'registerPointerlockChangeEventCallback', - 'registerPointerlockErrorEventCallback', - 'requestPointerLock', - 'fillVisibilityChangeEventData', - 'registerVisibilityChangeEventCallback', - 'registerTouchEventCallback', - 'fillGamepadEventData', - 'registerGamepadEventCallback', - 'registerBeforeUnloadEventCallback', - 'fillBatteryEventData', - 'battery', - 'registerBatteryEventCallback', - 'setCanvasElementSize', - 'getCanvasElementSize', - 'demangle', - 'demangleAll', - 'jsStackTrace', - 'stackTrace', - 'getEnvStrings', - 'checkWasiClock', - 'flush_NO_FILESYSTEM', - 'createDyncallWrapper', - 'setImmediateWrapped', - 'clearImmediateWrapped', - 'polyfillSetImmediate', - 'getPromise', - 'makePromise', - 'makePromiseCallback', - 'ExceptionInfo', - 'exception_addRef', - 'exception_decRef', - 'setMainLoop', - '_setNetworkCallback', - 'heapObjectForWebGLType', - 'heapAccessShiftForWebGLHeap', - 'emscriptenWebGLGet', - 'computeUnpackAlignedImageSize', - 'emscriptenWebGLGetTexPixelData', - 'emscriptenWebGLGetUniform', - 'webglGetUniformLocation', - 'webglPrepareUniformLocationsBeforeFirstUse', - 'webglGetLeftBracePos', - 'emscriptenWebGLGetVertexAttrib', - 'writeGLArray', - 'SDL_unicode', - 'SDL_ttfContext', - 'SDL_audio', - 'GLFW_Window', - 'runAndAbortIfError', - 'ALLOC_NORMAL', - 'ALLOC_STACK', - 'allocate', -]; -missingLibrarySymbols.forEach(missingLibrarySymbol) - -var unexportedSymbols = [ - 'run', - 'UTF8ArrayToString', - 'UTF8ToString', - 'stringToUTF8Array', - 'stringToUTF8', - 'lengthBytesUTF8', - 'addOnPreRun', - 'addOnInit', - 'addOnPreMain', - 'addOnExit', - 'addOnPostRun', - 'addRunDependency', - 'removeRunDependency', - 'FS_createFolder', - 'FS_createPath', - 'FS_createDataFile', - 'FS_createPreloadedFile', - 'FS_createLazyFile', - 'FS_createLink', - 'FS_createDevice', - 'FS_unlink', - 'out', - 'err', - 'callMain', - 'abort', - 'keepRuntimeAlive', - 'wasmMemory', - 'stackAlloc', - 'stackSave', - 'stackRestore', - 'getTempRet0', - 'setTempRet0', - 'writeStackCookie', - 'checkStackCookie', - 'ptrToString', - 'getHeapMax', - 'abortOnCannotGrowMemory', - 'ENV', - 'ERRNO_CODES', - 'ERRNO_MESSAGES', - 'DNS', - 'Protocols', - 'Sockets', - 'timers', - 'warnOnce', - 'UNWIND_CACHE', - 'readEmAsmArgsArray', - 'getCFunc', - 'freeTableIndexes', - 'functionsInTableMap', - 'setValue', - 'PATH', - 'PATH_FS', - 'UTF16Decoder', - 'writeArrayToMemory', - 'SYSCALLS', - 'JSEvents', - 'specialHTMLTargets', - 'currentFullscreenStrategy', - 'restoreOldWindowedStyle', - 'ExitStatus', - 'dlopenMissingError', - 'promiseMap', - 'uncaughtExceptionCount', - 'exceptionLast', - 'exceptionCaught', - 'Browser', - 'wget', - 'FS', - 'MEMFS', - 'TTY', - 'PIPEFS', - 'SOCKFS', - 'tempFixedLengthArray', - 'miniTempWebGLFloatBuffers', - 'GL', - 'AL', - 'SDL', - 'SDL_gfx', - 'GLUT', - 'EGL', - 'GLFW', - 'GLEW', - 'IDBStore', -]; -unexportedSymbols.forEach(unexportedRuntimeSymbol); - - - -var calledRun; - -dependenciesFulfilled = function runCaller() { - // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false) - if (!calledRun) run(); - if (!calledRun) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled -}; - -function stackCheckInit() { - // This is normally called automatically during __wasm_call_ctors but need to - // get these values before even running any of the ctors so we call it redundantly - // here. - _emscripten_stack_init(); - // TODO(sbc): Move writeStackCookie to native to to avoid this. - writeStackCookie(); -} - -function run() { - - if (runDependencies > 0) { - return; - } - - stackCheckInit(); - - preRun(); - - // a preRun added a dependency, run will be called later - if (runDependencies > 0) { - return; - } - - function doRun() { - // run may have just been called through dependencies being fulfilled just in this very frame, - // or while the async setStatus time below was happening - if (calledRun) return; - calledRun = true; - Module['calledRun'] = true; - - if (ABORT) return; - - initRuntime(); - - if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized'](); - - assert(!Module['_main'], 'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]'); - - postRun(); - } - - if (Module['setStatus']) { - Module['setStatus']('Running...'); - setTimeout(function() { - setTimeout(function() { - Module['setStatus'](''); - }, 1); - doRun(); - }, 1); - } else - { - doRun(); - } - checkStackCookie(); -} - -function checkUnflushedContent() { - // Compiler settings do not allow exiting the runtime, so flushing - // the streams is not possible. but in ASSERTIONS mode we check - // if there was something to flush, and if so tell the user they - // should request that the runtime be exitable. - // Normally we would not even include flush() at all, but in ASSERTIONS - // builds we do so just for this check, and here we see if there is any - // content to flush, that is, we check if there would have been - // something a non-ASSERTIONS build would have not seen. - // How we flush the streams depends on whether we are in SYSCALLS_REQUIRE_FILESYSTEM=0 - // mode (which has its own special function for this; otherwise, all - // the code is inside libc) - var oldOut = out; - var oldErr = err; - var has = false; - out = err = (x) => { - has = true; - } - try { // it doesn't matter if it fails - _fflush(0); - } catch(e) {} - out = oldOut; - err = oldErr; - if (has) { - warnOnce('stdio streams had content in them that was not flushed. you should set EXIT_RUNTIME to 1 (see the FAQ), or make sure to emit a newline when you printf etc.'); - warnOnce('(this may also be due to not including full filesystem support - try building with -sFORCE_FILESYSTEM)'); - } -} - -if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].pop()(); - } -} - -run(); - - -// end include: postamble.js diff --git a/spa.wasm b/spa.wasm deleted file mode 100755 index 8d8f224c092d429fb16b867b4894437efbe59ac5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64708 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 diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..5b5bee2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,217 @@ +import type { SpaWasmModule, SpaResult, SpaFormattedResult, SpaOptions } from './types.js'; + +export type { SpaOptions, SpaResult, SpaFormattedResult } from './types.js'; +export { SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from './types.js'; +export type { SpaFunctionCode } from './types.js'; + +import { SPA_ALL } from './types.js'; + +// The WASM module is Emscripten CJS output. In ESM builds, tsup injects a +// createRequire-based __require shim via the banner option (see tsup.config.ts). +// In CJS builds, require() is natively available. +declare const __require: NodeRequire; +const _loadModule = typeof __require === 'function' ? __require : require; +const createSpaModule: () => Promise = _loadModule('../wasm/spa-module.js'); + +// Singleton: the WASM module initializes once, all calls share it. +let _module: SpaWasmModule | null = null; +let _pending: Promise | null = null; +let _calculate: ((...args: number[]) => number) | null = null; +let _free: ((ptr: number) => void) | null = null; + +// Result struct layout (10 fields, 9 doubles + 1 int32): +// offset 0: zenith (f64) +// offset 8: azimuth_astro (f64) +// offset 16: azimuth (f64) +// offset 24: incidence (f64) +// offset 32: sunrise (f64) +// offset 40: sunset (f64) +// offset 48: suntransit (f64) +// offset 56: sun_transit_alt (f64) +// offset 64: eot (f64) +// offset 72: error_code (i32) +const OFFSET = { + zenith: 0, + azimuth_astro: 8, + azimuth: 16, + incidence: 24, + sunrise: 32, + sunset: 40, + suntransit: 48, + sun_transit_alt: 56, + eot: 64, + error_code: 72, +} as const; + +/** + * Initialize the WASM module. Returns a cached promise on repeat calls. + * Safe to call multiple times. If initialization fails, subsequent calls + * will retry rather than returning the failed promise. + */ +export function init(): Promise { + if (_module) return Promise.resolve(); + if (_pending) return _pending; + + _pending = createSpaModule().then((mod: SpaWasmModule) => { + _module = mod; + _calculate = mod.cwrap('spa_calculate_wrapper', 'number', [ + 'number', 'number', 'number', 'number', 'number', 'number', + 'number', 'number', 'number', 'number', 'number', 'number', + 'number', 'number', 'number', 'number', 'number', 'number', + ]) as (...args: number[]) => number; + _free = mod.cwrap('spa_free_result', null, ['number']) as (ptr: number) => void; + _pending = null; + }).catch((err: unknown) => { + _pending = null; + throw err; + }); + + return _pending; +} + +/** + * Format fractional hours to HH:MM:SS string. + * Returns "N/A" for non-finite or negative values (polar night/day scenarios). + */ +export function formatTime(hours: number): string { + if (!isFinite(hours) || hours < 0) return 'N/A'; + + const totalSec = Math.round(hours * 3600); + // Wrap at 24h: values near midnight can round to 24:00:00 + const h = Math.floor(totalSec / 3600) % 24; + const rem = totalSec - Math.floor(totalSec / 3600) * 3600; + const m = Math.floor(rem / 60); + const s = rem - m * 60; + + return ( + String(h).padStart(2, '0') + ':' + + String(m).padStart(2, '0') + ':' + + String(s).padStart(2, '0') + ); +} + +/** Read the result struct from WASM memory and free it. */ +function readResult(ptr: number): SpaResult { + const m = _module!; + const result: SpaResult = { + zenith: m.getValue(ptr + OFFSET.zenith, 'double'), + azimuth_astro: m.getValue(ptr + OFFSET.azimuth_astro, 'double'), + azimuth: m.getValue(ptr + OFFSET.azimuth, 'double'), + incidence: m.getValue(ptr + OFFSET.incidence, 'double'), + sunrise: m.getValue(ptr + OFFSET.sunrise, 'double'), + sunset: m.getValue(ptr + OFFSET.sunset, 'double'), + suntransit: m.getValue(ptr + OFFSET.suntransit, 'double'), + sun_transit_alt: m.getValue(ptr + OFFSET.sun_transit_alt, 'double'), + eot: m.getValue(ptr + OFFSET.eot, 'double'), + error_code: m.getValue(ptr + OFFSET.error_code, 'i32'), + }; + _free!(ptr); + return result; +} + +/** + * Validate that a value is a finite number, throwing a clear error if not. + * @internal + */ +function assertFiniteNumber(value: unknown, name: string): asserts value is number { + if (typeof value !== 'number' || !isFinite(value)) { + throw new TypeError(`SPA: ${name} must be a finite number, got ${typeof value === 'number' ? value : typeof value}`); + } +} + +/** + * Compute solar position for the given parameters. + * + * @param date - Date and time for the calculation + * @param latitude - Observer latitude in degrees (-90 to 90) + * @param longitude - Observer longitude in degrees (-180 to 180) + * @param options - Optional parameters + * @returns Solar position result with all computed values + */ +export async function spa( + date: Date, + latitude: number, + longitude: number, + options?: SpaOptions, +): Promise { + // Input validation + if (!(date instanceof Date) || isNaN(date.getTime())) { + throw new TypeError('SPA: date must be a valid Date object'); + } + assertFiniteNumber(latitude, 'latitude'); + assertFiniteNumber(longitude, 'longitude'); + + if (latitude < -90 || latitude > 90) { + throw new RangeError(`SPA: latitude must be between -90 and 90, got ${latitude}`); + } + if (longitude < -180 || longitude > 180) { + throw new RangeError(`SPA: longitude must be between -180 and 180, got ${longitude}`); + } + + await init(); + + const opts = options ?? {}; + const tz = opts.timezone ?? -(date.getTimezoneOffset() / 60); + + const ptr = _calculate!( + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + tz, + latitude, + longitude, + opts.elevation ?? 0, + opts.pressure ?? 1013.25, + opts.temperature ?? 15, + opts.delta_ut1 ?? 0, + opts.delta_t ?? 67, + opts.slope ?? 0, + opts.azm_rotation ?? 0, + opts.atmos_refract ?? 0.5667, + opts.function ?? SPA_ALL, + ); + + if (!ptr) { + throw new Error('SPA: memory allocation failed'); + } + + const result = readResult(ptr); + + if (result.error_code !== 0) { + throw new Error('SPA: calculation failed (error code ' + result.error_code + ')'); + } + + return result; +} + +/** + * Compute solar position and return formatted time strings. + * + * Same parameters as spa(). Returns sunrise, sunset, and suntransit + * as HH:MM:SS strings instead of fractional hours. + */ +export async function spaFormatted( + date: Date, + latitude: number, + longitude: number, + options?: SpaOptions, +): Promise { + const result = await spa(date, latitude, longitude, options); + return { + zenith: result.zenith, + azimuth_astro: result.azimuth_astro, + azimuth: result.azimuth, + incidence: result.incidence, + sunrise: formatTime(result.sunrise), + sunset: formatTime(result.sunset), + suntransit: formatTime(result.suntransit), + sun_transit_alt: result.sun_transit_alt, + eot: result.eot, + error_code: result.error_code, + }; +} + +export default spa; diff --git a/src/spa_wrapper.c b/src/spa_wrapper.c index faa3f56..2bd1eaf 100644 --- a/src/spa_wrapper.c +++ b/src/spa_wrapper.c @@ -1,77 +1,94 @@ -// src/spa_wrapper.c +/* + * spa_wrapper.c + * + * Thin wrapper around NREL's spa_calculate() for use from JavaScript + * via Emscripten/WebAssembly. Exposes a flat function signature that + * maps directly to cwrap() on the JS side. + * + * The wrapper allocates a result struct on the heap, fills it from the + * spa_data output fields, and returns the pointer. The caller is + * responsible for reading the doubles and calling spa_free_result(). + * + * Copyright (c) 2023-2026 Aric Camarata. MIT License. + */ + #include "spa.h" -#include // For malloc and free +#include typedef struct { double zenith; + double azimuth_astro; double azimuth; double incidence; double sunrise; double sunset; - double solar_noon; + double suntransit; double sun_transit_alt; + double eot; + int error_code; } spa_result; -spa_result* spa_calculate_wrapper( +spa_result *spa_calculate_wrapper( int year, int month, int day, int hour, int minute, double second, double timezone, double latitude, double longitude, double elevation, double pressure, double temperature, - double slope, double azm_rotation, double atmos_refract) + double delta_ut1, double delta_t, + double slope, double azm_rotation, double atmos_refract, + int function_code) { - // Allocate memory for the result - spa_result* result = (spa_result*)malloc(sizeof(spa_result)); + spa_result *result = (spa_result *)malloc(sizeof(spa_result)); if (!result) return NULL; spa_data spa; - spa.year = year; - spa.month = month; - spa.day = day; - spa.hour = hour; - spa.minute = minute; - spa.second = second; - spa.timezone = timezone; - spa.latitude = latitude; - spa.longitude = longitude; - spa.elevation = elevation; + spa.year = year; + spa.month = month; + spa.day = day; + spa.hour = hour; + spa.minute = minute; + spa.second = second; + spa.timezone = timezone; + spa.latitude = latitude; + spa.longitude = longitude; + spa.elevation = elevation; + spa.pressure = pressure; + spa.temperature = temperature; + spa.delta_ut1 = delta_ut1; + spa.delta_t = delta_t; + spa.slope = slope; + spa.azm_rotation = azm_rotation; + spa.atmos_refract = atmos_refract; + spa.function = function_code; - // Set default values for optional inputs if they are not given - spa.pressure = (pressure == 0.0) ? 820.0 : pressure; // Standard atmospheric pressure - spa.temperature = (temperature == 0.0) ? 11.0 : temperature; // Standard temperature - spa.slope = slope; // Surface slope angle (default 0.0) - spa.azm_rotation = azm_rotation; // Surface azimuth angle (default 0.0) - spa.atmos_refract = (atmos_refract == 0.0) ? 0.5667 : atmos_refract; + int rc = spa_calculate(&spa); + result->error_code = rc; - // Set the calculation mode to SPA_ALL - spa.function = SPA_ALL; - - // Calculate SPA values - int result_code = spa_calculate(&spa); - - if (result_code == 0) { - result->zenith = spa.zenith; - result->azimuth = spa.azimuth; - result->incidence = spa.incidence; - result->sunrise = spa.sunrise; - result->sunset = spa.sunset; - result->solar_noon = spa.suntransit; + if (rc == 0) { + result->zenith = spa.zenith; + result->azimuth_astro = spa.azimuth_astro; + result->azimuth = spa.azimuth; + result->incidence = spa.incidence; + result->sunrise = spa.sunrise; + result->sunset = spa.sunset; + result->suntransit = spa.suntransit; result->sun_transit_alt = spa.sta; + result->eot = spa.eot; } else { - // Error handling (fill with zeros or other default values) - result->zenith = 0; - result->azimuth = 0; - result->incidence = 0; - result->sunrise = 0; - result->sunset = 0; - result->solar_noon = 0; - result->sun_transit_alt = 0; + result->zenith = 0.0; + result->azimuth_astro = 0.0; + result->azimuth = 0.0; + result->incidence = 0.0; + result->sunrise = 0.0; + result->sunset = 0.0; + result->suntransit = 0.0; + result->sun_transit_alt = 0.0; + result->eot = 0.0; } return result; } -// Function to free the allocated result memory -void spa_free_result(spa_result* result) { - free(result); +void spa_free_result(spa_result *result) { + if (result) free(result); } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..107825c --- /dev/null +++ b/src/types.ts @@ -0,0 +1,74 @@ +/** SPA function codes. Control which outputs are computed. */ +export const SPA_ZA = 0 as const; +export const SPA_ZA_INC = 1 as const; +export const SPA_ZA_RTS = 2 as const; +export const SPA_ALL = 3 as const; + +export type SpaFunctionCode = typeof SPA_ZA | typeof SPA_ZA_INC | typeof SPA_ZA_RTS | typeof SPA_ALL; + +export interface SpaOptions { + /** + * Hours from UTC. If omitted, derived from the Date object's local offset. + * For historical dates or DST transitions, pass an explicit value. + */ + timezone?: number; + /** Observer elevation in meters above sea level. Default: 0. */ + elevation?: number; + /** Atmospheric pressure in millibars. Default: 1013.25. */ + pressure?: number; + /** Temperature in degrees Celsius. Default: 15. */ + temperature?: number; + /** UT1-UTC correction in seconds. Default: 0. */ + delta_ut1?: number; + /** TT-UTC difference in seconds. Default: 67. */ + delta_t?: number; + /** Surface slope in degrees from horizontal. Default: 0. */ + slope?: number; + /** Surface azimuth rotation in degrees from south. Default: 0. */ + azm_rotation?: number; + /** Atmospheric refraction at sunrise/sunset in degrees. Default: 0.5667. */ + atmos_refract?: number; + /** SPA function code. Default: SPA_ALL (3). */ + function?: SpaFunctionCode; +} + +export interface SpaResult { + /** Topocentric zenith angle in degrees. */ + zenith: number; + /** Topocentric azimuth angle, westward from south (astronomical convention), in degrees. */ + azimuth_astro: number; + /** Topocentric azimuth angle, eastward from north (navigational convention), in degrees. */ + azimuth: number; + /** Surface incidence angle in degrees. */ + incidence: number; + /** Local sunrise time as fractional hours. */ + sunrise: number; + /** Local sunset time as fractional hours. */ + sunset: number; + /** Local sun transit time (solar noon) as fractional hours. */ + suntransit: number; + /** Sun transit altitude in degrees. */ + sun_transit_alt: number; + /** Equation of time in minutes. */ + eot: number; + /** SPA error code. Always 0 on a successful return (non-zero throws). */ + error_code: number; +} + +export interface SpaFormattedResult extends Omit { + /** Local sunrise time as HH:MM:SS string. "N/A" during polar day/night. */ + sunrise: string; + /** Local sunset time as HH:MM:SS string. "N/A" during polar day/night. */ + sunset: string; + /** Local sun transit time as HH:MM:SS string. "N/A" during polar day/night. */ + suntransit: string; +} + +/** + * Emscripten module interface. Matches the shape returned by createSpaModule(). + * @internal + */ +export interface SpaWasmModule { + cwrap(name: string, returnType: string | null, argTypes: string[]): Function; + getValue(ptr: number, type: string): number; +} diff --git a/test-cjs.cjs b/test-cjs.cjs new file mode 100644 index 0000000..3bf9e81 --- /dev/null +++ b/test-cjs.cjs @@ -0,0 +1,58 @@ +'use strict'; + +const { spa, spaFormatted, formatTime, init, SPA_ZA, SPA_ALL } = require('./dist/index.cjs'); + +let passed = 0; +let failed = 0; + +function assert(condition, message) { + if (condition) { + passed++; + } else { + failed++; + console.error(' FAIL: ' + message); + } +} + +async function run() { + console.log('CJS smoke test\n'); + + // Verify all exports are available + assert(typeof spa === 'function', 'spa is a function'); + assert(typeof spaFormatted === 'function', 'spaFormatted is a function'); + assert(typeof formatTime === 'function', 'formatTime is a function'); + assert(typeof init === 'function', 'init is a function'); + assert(SPA_ZA === 0, 'SPA_ZA constant is 0'); + assert(SPA_ALL === 3, 'SPA_ALL constant is 3'); + + // Core calculation + const result = await spa( + new Date(2023, 3, 1, 0, 0, 0), + 40.7128, -74.006, + { timezone: -4, elevation: 10 }, + ); + assert(result.error_code === 0, 'calculation succeeds'); + assert(result.zenith > 0, 'zenith is positive'); + assert(result.azimuth > 0, 'azimuth is positive'); + assert(result.sunrise > 0, 'sunrise is positive'); + + // Formatted output + const fmt = await spaFormatted( + new Date(2023, 3, 1, 12, 0, 0), + 40.7128, -74.006, + { timezone: -4 }, + ); + assert(typeof fmt.sunrise === 'string', 'formatted sunrise is a string'); + assert(/^\d{2}:\d{2}:\d{2}$/.test(fmt.sunrise), 'sunrise matches HH:MM:SS'); + + // formatTime + assert(formatTime(6.5) === '06:30:00', 'formatTime works'); + + console.log('\n' + passed + ' passed, ' + failed + ' failed'); + if (failed > 0) process.exit(1); +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/test.js b/test.js deleted file mode 100644 index 0765f91..0000000 --- a/test.js +++ /dev/null @@ -1,35 +0,0 @@ -// test.js -const spa = require('./solar-spa.js'); - -// Define input parameters for a specific date, time, and location -const date = new Date(2023, 3, 1, 0, 0, 0); // April 1, 2023 at Midnight -const latitude = 40.7128; // Latitude of New York City, USA -const longitude = -74.0060; // Longitude of New York City, USA -const elevation = 10; // Elevation in meters (approximately) -const pressure = 1013.25; // Optional input: Atmospheric pressure (millibars) -const temperature = 20; // Optional input: Temperature (degrees Celsius) -const refraction = 0.5667; // Optional input: Atmospheric refraction (degrees) - -// Convert fractional hours to formatted time string for sunrise, sunset, solar_noon -function formatTime(hours) { - const milliseconds = hours * 60 * 60 * 1000; - const date = new Date(milliseconds); - return date.toISOString().substr(11, 12); -} - -// Call the 'spa' function and log the results -spa(date, latitude, longitude, elevation, temperature, pressure, refraction) - .then(result => { - console.log({ - zenith: result.zenith, - azimuth: result.azimuth, - incidence: result.incidence, - sun_transit_alt: result.sun_transit_alt, - sunrise: result.sunrise, - solar_noon: result.solar_noon, - sunset: result.sunset - }); - }) - .catch(error => { - console.error(error); - }); \ No newline at end of file diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000..4eb5cf4 --- /dev/null +++ b/test.mjs @@ -0,0 +1,258 @@ +import { spa, spaFormatted, formatTime, init, SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from './dist/index.mjs'; + +let passed = 0; +let failed = 0; + +function assert(condition, message) { + if (condition) { + passed++; + } else { + failed++; + console.error(' FAIL: ' + message); + } +} + +function approx(actual, expected, tolerance, label) { + const diff = Math.abs(actual - expected); + assert(diff <= tolerance, label + ': expected ' + expected + ', got ' + actual + ' (diff: ' + diff.toFixed(6) + ')'); +} + +async function assertThrows(fn, check, label) { + try { + await fn(); + assert(false, label + ': should have thrown'); + } catch (e) { + if (check) { + assert(check(e), label + ': ' + e.message); + } else { + passed++; + } + } +} + +async function run() { + console.log('solar-spa test suite\n'); + + // ── Test 1: New York City, April 1 2023 ── + console.log('1. NYC, April 1 2023, midnight local (UTC-4)'); + const nyc = await spa( + new Date(2023, 3, 1, 0, 0, 0), + 40.7128, -74.006, + { timezone: -4, elevation: 10, temperature: 20, pressure: 1013.25 }, + ); + approx(nyc.zenith, 132.82, 0.1, 'zenith'); + approx(nyc.azimuth, 339.38, 0.1, 'azimuth'); + approx(nyc.sunrise, 6.665, 0.01, 'sunrise'); + approx(nyc.sunset, 19.343, 0.01, 'sunset'); + approx(nyc.suntransit, 12.998, 0.01, 'solar noon'); + approx(nyc.sun_transit_alt, 53.916, 0.1, 'transit altitude'); + assert(nyc.error_code === 0, 'error_code is 0'); + + // ── Test 2: London, Summer Solstice ── + console.log('2. London, June 21 2025, noon UTC'); + const london = await spa( + new Date(2025, 5, 21, 12, 0, 0), + 51.5074, -0.1278, + { timezone: 0, elevation: 11, temperature: 18 }, + ); + assert(london.zenith < 30, 'zenith near noon is below 30 degrees'); + assert(london.azimuth > 170 && london.azimuth < 200, 'azimuth roughly south at noon'); + assert(london.sunrise > 3 && london.sunrise < 6, 'sunrise between 3 and 6'); + assert(london.sunset > 19 && london.sunset < 23, 'sunset between 19 and 23'); + assert(london.error_code === 0, 'error_code is 0'); + + // ── Test 3: Equator, Equinox ── + console.log('3. Quito (equator), March 20 2025, noon UTC-5'); + const quito = await spa( + new Date(2025, 2, 20, 12, 0, 0), + -0.1807, -78.4678, + { timezone: -5, elevation: 2850 }, + ); + assert(quito.zenith < 20, 'near-overhead sun at equinox on equator'); + assert(quito.error_code === 0, 'error_code is 0'); + + // ── Test 4: Sydney, Winter ── + console.log('4. Sydney, June 21 2025 (winter), noon AEST'); + const sydney = await spa( + new Date(2025, 5, 21, 12, 0, 0), + -33.8688, 151.2093, + { timezone: 10 }, + ); + assert(sydney.zenith > 50, 'low sun in southern winter'); + assert(sydney.sunrise > 6 && sydney.sunrise < 8, 'winter sunrise after 6'); + assert(sydney.error_code === 0, 'error_code is 0'); + + // ── Test 5: Formatted output ── + console.log('5. Formatted output (NYC)'); + const fmt = await spaFormatted( + new Date(2023, 3, 1, 0, 0, 0), + 40.7128, -74.006, + { timezone: -4, elevation: 10, temperature: 20, pressure: 1013.25 }, + ); + assert(typeof fmt.sunrise === 'string', 'sunrise is a string'); + assert(typeof fmt.sunset === 'string', 'sunset is a string'); + assert(typeof fmt.suntransit === 'string', 'suntransit is a string'); + assert(/^\d{2}:\d{2}:\d{2}$/.test(fmt.sunrise), 'sunrise matches HH:MM:SS'); + assert(typeof fmt.zenith === 'number', 'zenith remains numeric'); + assert(typeof fmt.error_code === 'number', 'error_code is present in formatted result'); + + // ── Test 6: formatTime utility ── + console.log('6. formatTime utility'); + assert(formatTime(0) === '00:00:00', 'midnight'); + assert(formatTime(12) === '12:00:00', 'noon'); + assert(formatTime(6.5) === '06:30:00', '6.5 hours'); + assert(formatTime(23.9997) === '23:59:59', 'end of day'); + assert(formatTime(Infinity) === 'N/A', 'Infinity returns N/A'); + assert(formatTime(-Infinity) === 'N/A', '-Infinity returns N/A'); + assert(formatTime(NaN) === 'N/A', 'NaN returns N/A'); + assert(formatTime(-1) === 'N/A', 'negative returns N/A'); + assert(formatTime(-0.5) === 'N/A', 'negative fractional returns N/A'); + assert(formatTime(24.0) === '00:00:00', '24h wraps to midnight'); + assert(formatTime(24.5) === '00:30:00', '24.5h wraps to 00:30'); + + // ── Test 7: SPA error handling ── + console.log('7. SPA error handling'); + await assertThrows( + () => spa(new Date(2023, 0, 1), 40, -74, { timezone: 100 }), + (e) => e.message.includes('error code'), + 'invalid timezone throws with error code', + ); + + // ── Test 8: Input validation ── + console.log('8. Input validation'); + await assertThrows( + () => spa(null, 40, -74), + (e) => e instanceof TypeError, + 'null date throws TypeError', + ); + await assertThrows( + () => spa(new Date('invalid'), 40, -74), + (e) => e instanceof TypeError, + 'invalid date throws TypeError', + ); + await assertThrows( + () => spa(new Date(), 'forty', -74), + (e) => e instanceof TypeError, + 'string latitude throws TypeError', + ); + await assertThrows( + () => spa(new Date(), 40, undefined), + (e) => e instanceof TypeError, + 'undefined longitude throws TypeError', + ); + await assertThrows( + () => spa(new Date(), 91, -74), + (e) => e instanceof RangeError, + 'latitude > 90 throws RangeError', + ); + await assertThrows( + () => spa(new Date(), -91, -74), + (e) => e instanceof RangeError, + 'latitude < -90 throws RangeError', + ); + await assertThrows( + () => spa(new Date(), 40, 181), + (e) => e instanceof RangeError, + 'longitude > 180 throws RangeError', + ); + await assertThrows( + () => spa(new Date(), 40, -181), + (e) => e instanceof RangeError, + 'longitude < -180 throws RangeError', + ); + + // ── Test 9: Function code selection ── + console.log('9. Function code SPA_ZA (zenith/azimuth only)'); + const za = await spa( + new Date(2023, 3, 1, 12, 0, 0), + 40.7128, -74.006, + { timezone: -4, function: SPA_ZA }, + ); + assert(za.zenith > 0, 'zenith computed'); + assert(za.azimuth > 0, 'azimuth computed'); + assert(za.error_code === 0, 'error_code is 0'); + + // ── Test 10: Repeated calls (verify singleton init) ── + console.log('10. Repeated calls'); + const a = await spa(new Date(2023, 0, 1, 12, 0, 0), 40, -74, { timezone: -5 }); + const b = await spa(new Date(2023, 6, 1, 12, 0, 0), 40, -74, { timezone: -4 }); + assert(a.zenith !== b.zenith, 'different dates produce different results'); + assert(a.error_code === 0 && b.error_code === 0, 'both succeed'); + + // ── Test 11: Concurrent calls (verify init dedup) ── + console.log('11. Concurrent calls'); + const [c1, c2, c3] = await Promise.all([ + spa(new Date(2023, 0, 1, 12, 0, 0), 40, -74, { timezone: -5 }), + spa(new Date(2023, 3, 1, 12, 0, 0), 40, -74, { timezone: -4 }), + spa(new Date(2023, 6, 1, 12, 0, 0), 40, -74, { timezone: -4 }), + ]); + assert(c1.error_code === 0 && c2.error_code === 0 && c3.error_code === 0, 'all three concurrent calls succeed'); + assert(c1.zenith !== c2.zenith, 'concurrent results differ by date'); + + // ── Test 12: Boundary coordinates ── + console.log('12. Boundary coordinates'); + const northPole = await spa(new Date(2025, 5, 21, 12, 0, 0), 90, 0, { timezone: 0 }); + assert(northPole.error_code === 0, 'north pole succeeds'); + const southPole = await spa(new Date(2025, 5, 21, 12, 0, 0), -90, 0, { timezone: 0 }); + assert(southPole.error_code === 0, 'south pole succeeds'); + const dateLine = await spa(new Date(2025, 5, 21, 12, 0, 0), 0, 180, { timezone: 12 }); + assert(dateLine.error_code === 0, 'date line (180) succeeds'); + const dateLineNeg = await spa(new Date(2025, 5, 21, 12, 0, 0), 0, -180, { timezone: -12 }); + assert(dateLineNeg.error_code === 0, 'date line (-180) succeeds'); + + // ── Test 13: Arctic polar day (sun never sets) ── + console.log('13. Arctic polar day'); + const tromso = await spa( + new Date(2025, 5, 21, 12, 0, 0), + 69.6496, 18.9560, + { timezone: 2 }, + ); + assert(tromso.error_code === 0, 'Tromso summer succeeds'); + // During polar day, sunrise/sunset values from SPA may be non-standard + // The key is that the computation succeeds and zenith is low (sun is up) + assert(tromso.zenith < 50, 'sun is high at Tromso in summer'); + + // ── Test 14: Explicit init() call ── + console.log('14. Explicit init()'); + await init(); // should be a no-op since module is already loaded + const afterInit = await spa(new Date(2023, 0, 1, 12, 0, 0), 40, -74, { timezone: -5 }); + assert(afterInit.error_code === 0, 'spa works after explicit init'); + + // ── Test 15: Constants are correct ── + console.log('15. Constants'); + assert(SPA_ZA === 0, 'SPA_ZA is 0'); + assert(SPA_ZA_INC === 1, 'SPA_ZA_INC is 1'); + assert(SPA_ZA_RTS === 2, 'SPA_ZA_RTS is 2'); + assert(SPA_ALL === 3, 'SPA_ALL is 3'); + + // ── Test 16: Historical date ── + console.log('16. Historical date (year 1000)'); + const historical = await spa( + new Date(1000, 5, 21, 12, 0, 0), + 40.7128, -74.006, + { timezone: -5, delta_t: 1574 }, + ); + assert(historical.error_code === 0, 'historical date succeeds'); + assert(historical.zenith > 0 && historical.zenith < 90, 'historical zenith is reasonable'); + + // ── Test 17: All function codes ── + console.log('17. All function codes'); + const zaRes = await spa(new Date(2023, 3, 1, 12, 0, 0), 40, -74, { timezone: -4, function: SPA_ZA }); + const zaIncRes = await spa(new Date(2023, 3, 1, 12, 0, 0), 40, -74, { timezone: -4, function: SPA_ZA_INC }); + const zaRtsRes = await spa(new Date(2023, 3, 1, 12, 0, 0), 40, -74, { timezone: -4, function: SPA_ZA_RTS }); + const allRes = await spa(new Date(2023, 3, 1, 12, 0, 0), 40, -74, { timezone: -4, function: SPA_ALL }); + assert(zaRes.error_code === 0, 'SPA_ZA succeeds'); + assert(zaIncRes.error_code === 0, 'SPA_ZA_INC succeeds'); + assert(zaRtsRes.error_code === 0, 'SPA_ZA_RTS succeeds'); + assert(allRes.error_code === 0, 'SPA_ALL succeeds'); + approx(zaRes.zenith, allRes.zenith, 0.001, 'zenith consistent across function codes'); + + // ── Results ── + console.log('\n' + passed + ' passed, ' + failed + ' failed'); + if (failed > 0) process.exit(1); +} + +run().catch(function (err) { + console.error(err); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..deeec60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "wasm"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..de57165 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + outDir: 'dist', + splitting: false, + sourcemap: true, + target: 'es2020', + platform: 'node', + outExtension({ format }) { + return { + js: format === 'cjs' ? '.cjs' : '.mjs', + }; + }, + banner({ format }) { + if (format === 'esm') { + return { + js: `import { createRequire as __cr } from 'node:module';\nconst __require = __cr(import.meta.url);`, + }; + } + return {}; + }, + // The WASM module is Emscripten CJS output, keep it external. + external: ['../wasm/spa-module.js'], +}); diff --git a/validate.mjs b/validate.mjs new file mode 100644 index 0000000..8326c88 --- /dev/null +++ b/validate.mjs @@ -0,0 +1,854 @@ +/** + * NREL SPA Validation Suite + * + * 100-scenario validation test for the solar-spa WASM implementation. + * Validates against known algorithm behavior, boundary conditions, + * polar regions, atmospheric variations, and historical/future dates. + * + * Run: node validate.mjs + */ + +import { spa, spaFormatted, formatTime, init, SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL } from './dist/index.mjs'; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +const results = []; +let scenarioNum = 0; + +/** + * Create a Date object with an explicit year (works for years < 100). + * JavaScript's Date constructor treats 2-digit years as 1900+year, + * so we must use setFullYear() for historical dates. + */ +function makeDate(year, month, day, hour = 12, minute = 0, second = 0) { + const d = new Date(2000, month - 1, day, hour, minute, second); + d.setFullYear(year); + return d; +} + +/** + * Run a single validation scenario. + * @param {string} name - Scenario label + * @param {Function} fn - Async function that returns { pass, zenith, azimuth, detail } + */ +async function scenario(name, fn) { + scenarioNum++; + const num = scenarioNum; + const t0 = performance.now(); + try { + const result = await fn(); + const elapsed = performance.now() - t0; + const us = Math.round(elapsed * 1000); + results.push({ + num, + name, + pass: result.pass, + detail: result.detail || '', + us, + zenith: result.zenith, + azimuth: result.azimuth, + }); + } catch (err) { + const elapsed = performance.now() - t0; + const us = Math.round(elapsed * 1000); + results.push({ + num, + name, + pass: false, + detail: `EXCEPTION: ${err.message}`, + us, + zenith: null, + azimuth: null, + }); + } +} + +/** + * Run a scenario that expects spa() to throw. + */ +async function scenarioThrows(name, fn, checkMsg) { + scenarioNum++; + const num = scenarioNum; + const t0 = performance.now(); + try { + await fn(); + const elapsed = performance.now() - t0; + results.push({ + num, + name, + pass: false, + detail: 'Expected throw but succeeded', + us: Math.round(elapsed * 1000), + zenith: null, + azimuth: null, + }); + } catch (err) { + const elapsed = performance.now() - t0; + const pass = checkMsg ? err.message.includes(checkMsg) || err instanceof RangeError || err instanceof TypeError || err.message.includes('error code') : true; + results.push({ + num, + name, + pass, + detail: pass ? `Threw: ${err.message.substring(0, 60)}` : `Wrong error: ${err.message}`, + us: Math.round(elapsed * 1000), + zenith: null, + azimuth: null, + }); + } +} + +function between(val, lo, hi) { + return val >= lo && val <= hi; +} + +function approx(actual, expected, tolerance) { + return Math.abs(actual - expected) <= tolerance; +} + +// ─── City Data ──────────────────────────────────────────────────────────────── + +const cities = [ + // [name, lat, lon, tz, elevation, summerZenithRange, winterZenithRange] + // Summer solstice: June 21 2025 noon local + // Winter solstice: Dec 21 2025 noon local + // Zenith ranges are [min, max] approximate expectations + ['NYC', 40.7128, -74.0060, -4, 10, [16, 30], [60, 78]], + ['London', 51.5074, -0.1278, 1, 11, [27, 33], [72, 80]], + ['Tokyo', 35.6762, 139.6503, 9, 40, [11, 20], [52, 62]], + ['Sydney', -33.8688, 151.2093, 10, 3, [52, 62], [10, 18]], + ['Cairo', 30.0444, 31.2357, 2, 75, [ 6, 14], [47, 55]], + ['Mumbai', 19.0760, 72.8777, 5.5, 14, [ 4, 12], [37, 45]], + ['Sao Paulo', -23.5505, -46.6333, -3,760, [44, 52], [ 0, 8]], + ['Moscow', 55.7558, 37.6173, 3,156, [31, 37], [76, 84]], + ['Beijing', 39.9042, 116.4074, 8, 43, [15, 22], [58, 66]], + ['Nairobi', -1.2921, 36.8219, 3,1795, [24, 30], [22, 28]], + ['Reykjavik', 64.1466, -21.9426, 0, 50, [40, 48], [88, 100]], + ['Singapore', 1.3521, 103.8198, 8, 15, [22, 28], [24, 30]], + ['Cape Town', -33.9249, 18.4241, 2, 30, [52, 62], [10, 18]], + ['Buenos Aires',-34.6037, -58.3816, -3, 25, [52, 62], [ 4, 16]], + ['Dubai', 25.2048, 55.2708, 4, 5, [ 1, 8], [43, 51]], + ['Toronto', 43.6532, -79.3832, -4, 76, [19, 30], [62, 72]], + ['Mexico City', 19.4326, -99.1332, -6,2240, [ 4, 10], [37, 44]], + ['Seoul', 37.5665, 126.9780, 9, 38, [13, 20], [56, 64]], + ['Rome', 41.9028, 12.4964, 2, 21, [18, 24], [60, 68]], + ['Anchorage', 61.2181, -149.9003, -8, 30, [37, 44], [82, 96]], +]; + +// ─── Run all scenarios ──────────────────────────────────────────────────────── + +async function runAll() { + // Warmup + await init(); + await spa(new Date(2025, 5, 21, 12, 0, 0), 40, -74, { timezone: -4 }); + + // ══════════════════════════════════════════════════════════════════ + // CATEGORY 1: Cities worldwide (40 scenarios, 1-40) + // ══════════════════════════════════════════════════════════════════ + + for (const [name, lat, lon, tz, elev, summerRange, winterRange] of cities) { + // Summer solstice: June 21 2025, noon local + await scenario(`${name} summer solstice`, async () => { + const r = await spa( + makeDate(2025, 6, 21, 12, 0, 0), + lat, lon, + { timezone: tz, elevation: elev }, + ); + const pass = r.error_code === 0 + && between(r.zenith, summerRange[0], summerRange[1]) + && between(r.azimuth, 0, 360); + return { + pass, + zenith: r.zenith, + azimuth: r.azimuth, + detail: !pass ? `zenith=${r.zenith.toFixed(2)} expected [${summerRange}]` : '', + }; + }); + + // Winter solstice: Dec 21 2025, noon local + await scenario(`${name} winter solstice`, async () => { + const r = await spa( + makeDate(2025, 12, 21, 12, 0, 0), + lat, lon, + { timezone: tz, elevation: elev }, + ); + const pass = r.error_code === 0 + && between(r.zenith, winterRange[0], winterRange[1]) + && between(r.azimuth, 0, 360); + return { + pass, + zenith: r.zenith, + azimuth: r.azimuth, + detail: !pass ? `zenith=${r.zenith.toFixed(2)} expected [${winterRange}]` : '', + }; + }); + } + + // ══════════════════════════════════════════════════════════════════ + // CATEGORY 2: Boundary conditions (15 scenarios, 41-55) + // ══════════════════════════════════════════════════════════════════ + + // 41: North Pole summer solstice (midnight sun) + await scenario('North Pole summer solstice', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), 90, 0, { timezone: 0 }); + // Sun should be above horizon (zenith < 90) in arctic summer + const pass = r.error_code === 0 && r.zenith < 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 42: North Pole winter solstice (polar night) + await scenario('North Pole winter solstice', async () => { + const r = await spa(makeDate(2025, 12, 21, 12, 0, 0), 90, 0, { timezone: 0 }); + // Sun should be below horizon (zenith > 90) in arctic winter + const pass = r.error_code === 0 && r.zenith > 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 43: South Pole summer solstice (Dec = summer in south) + await scenario('South Pole summer solstice', async () => { + const r = await spa(makeDate(2025, 12, 21, 12, 0, 0), -90, 0, { timezone: 0 }); + // Sun above horizon at south pole in December + const pass = r.error_code === 0 && r.zenith < 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 44: South Pole winter solstice (June = winter in south) + await scenario('South Pole winter solstice', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), -90, 0, { timezone: 0 }); + // Sun below horizon at south pole in June + const pass = r.error_code === 0 && r.zenith > 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 45: Equator March equinox noon + await scenario('Equator March equinox noon', async () => { + const r = await spa(makeDate(2025, 3, 20, 12, 0, 0), 0, 0, { timezone: 0 }); + // Sun nearly overhead at equator on equinox + const pass = r.error_code === 0 && r.zenith < 5; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 46: Equator September equinox noon + await scenario('Equator September equinox noon', async () => { + const r = await spa(makeDate(2025, 9, 22, 12, 0, 0), 0, 0, { timezone: 0 }); + const pass = r.error_code === 0 && r.zenith < 5; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 47: Equator June solstice (sun north of equator) + await scenario('Equator June solstice noon', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), 0, 0, { timezone: 0 }); + // Declination ~23.44, so zenith ~23.44 at equator + const pass = r.error_code === 0 && between(r.zenith, 20, 27); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 48: Equator December solstice (sun south of equator) + await scenario('Equator December solstice noon', async () => { + const r = await spa(makeDate(2025, 12, 21, 12, 0, 0), 0, 0, { timezone: 0 }); + const pass = r.error_code === 0 && between(r.zenith, 20, 27); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 49: International Date Line east (+180) + await scenario('Date line +180 longitude', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), 0, 180, { timezone: 12 }); + const pass = r.error_code === 0 && between(r.zenith, 0, 90); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 50: International Date Line west (-180) + await scenario('Date line -180 longitude', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), 0, -180, { timezone: -12 }); + const pass = r.error_code === 0 && between(r.zenith, 0, 90); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 51: Mt Everest (8849m elevation) + await scenario('Mt Everest summit elevation', async () => { + const r = await spa( + makeDate(2025, 6, 21, 12, 0, 0), + 27.9881, 86.9250, + { timezone: 5.75, elevation: 8849, pressure: 314, temperature: -20 }, + ); + const pass = r.error_code === 0 && between(r.zenith, 0, 15); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 52: Dead Sea (-430m elevation) + await scenario('Dead Sea negative elevation', async () => { + const r = await spa( + makeDate(2025, 6, 21, 12, 0, 0), + 31.5, 35.5, + { timezone: 3, elevation: -430, pressure: 1065, temperature: 40 }, + ); + const pass = r.error_code === 0 && between(r.zenith, 0, 15); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 53: Extreme past date (year -2000, earliest valid) + await scenario('Year -2000 (earliest valid)', async () => { + const r = await spa(makeDate(-2000, 6, 21, 12, 0, 0), 30, 0, { timezone: 0, delta_t: 0 }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 54: Extreme future date (year 6000, latest valid) + await scenario('Year 6000 (latest valid)', async () => { + const r = await spa(makeDate(6000, 6, 21, 12, 0, 0), 30, 0, { timezone: 0, delta_t: 0 }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 55: Year out of range (6001) should throw + await scenarioThrows('Year 6001 (out of range)', async () => { + await spa(makeDate(6001, 6, 21, 12, 0, 0), 30, 0, { timezone: 0, delta_t: 0 }); + }, 'error code'); + + // ══════════════════════════════════════════════════════════════════ + // CATEGORY 3: Polar regions (10 scenarios, 56-65) + // ══════════════════════════════════════════════════════════════════ + + // 56: Tromso polar day (June) + await scenario('Tromso polar day (June)', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), 69.6496, 18.956, { timezone: 2 }); + const pass = r.error_code === 0 && r.zenith < 50; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 57: Tromso polar night (December) + await scenario('Tromso polar night (Dec)', async () => { + const r = await spa(makeDate(2025, 12, 21, 12, 0, 0), 69.6496, 18.956, { timezone: 1 }); + const pass = r.error_code === 0 && r.zenith > 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 58: Murmansk polar day + await scenario('Murmansk polar day (June)', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), 68.9585, 33.0827, { timezone: 3 }); + const pass = r.error_code === 0 && r.zenith < 50; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 59: Murmansk polar night + await scenario('Murmansk polar night (Dec)', async () => { + const r = await spa(makeDate(2025, 12, 21, 12, 0, 0), 68.9585, 33.0827, { timezone: 3 }); + const pass = r.error_code === 0 && r.zenith > 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 60: Utqiagvik (Barrow) AK polar day + await scenario('Utqiagvik AK polar day (June)', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), 71.2906, -156.7886, { timezone: -8 }); + const pass = r.error_code === 0 && r.zenith < 55; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 61: Utqiagvik AK polar night + await scenario('Utqiagvik AK polar night (Dec)', async () => { + const r = await spa(makeDate(2025, 12, 21, 12, 0, 0), 71.2906, -156.7886, { timezone: -9 }); + const pass = r.error_code === 0 && r.zenith > 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 62: McMurdo Station Antarctica summer (Dec) + await scenario('McMurdo Station summer (Dec)', async () => { + const r = await spa(makeDate(2025, 12, 21, 12, 0, 0), -77.8500, 166.6667, { timezone: 13 }); + const pass = r.error_code === 0 && r.zenith < 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 63: McMurdo Station winter (June) + await scenario('McMurdo Station winter (June)', async () => { + const r = await spa(makeDate(2025, 6, 21, 12, 0, 0), -77.8500, 166.6667, { timezone: 12 }); + const pass = r.error_code === 0 && r.zenith > 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 64: Svalbard (78N) midnight sun + await scenario('Svalbard midnight sun (June)', async () => { + const r = await spa(makeDate(2025, 6, 21, 0, 0, 0), 78.2296, 15.6167, { timezone: 2 }); + // Even at midnight, sun should be above horizon + const pass = r.error_code === 0 && r.zenith < 95; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 65: Amundsen-Scott South Pole Station summer + await scenario('South Pole Station summer (Jan)', async () => { + const r = await spa(makeDate(2025, 1, 1, 12, 0, 0), -90, 0, { timezone: 0 }); + const pass = r.error_code === 0 && r.zenith < 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // ══════════════════════════════════════════════════════════════════ + // CATEGORY 4: Time edge cases (10 scenarios, 66-75) + // ══════════════════════════════════════════════════════════════════ + + // 66: Exact midnight + await scenario('Exact midnight UTC', async () => { + const r = await spa(makeDate(2025, 6, 21, 0, 0, 0), 51.5074, -0.1278, { timezone: 0 }); + // Sun should be well below horizon in London at midnight (even in summer) + const pass = r.error_code === 0 && r.zenith > 70; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 67: Dawn (around 5 AM summer London) + await scenario('Dawn (5 AM summer London)', async () => { + const r = await spa(makeDate(2025, 6, 21, 5, 0, 0), 51.5074, -0.1278, { timezone: 1 }); + // Near sunrise, zenith should be around 85-95 degrees + const pass = r.error_code === 0 && between(r.zenith, 75, 100); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 68: Dusk (around 9 PM summer London) + await scenario('Dusk (9 PM summer London)', async () => { + const r = await spa(makeDate(2025, 6, 21, 21, 0, 0), 51.5074, -0.1278, { timezone: 1 }); + const pass = r.error_code === 0 && between(r.zenith, 80, 105); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 69: Solar noon (transit) NYC + await scenario('Solar noon NYC (approx 13:00 EDT)', async () => { + const r = await spa(makeDate(2025, 6, 21, 13, 0, 0), 40.7128, -74.006, { timezone: -4 }); + // Near transit, azimuth should be close to 180 (south-ish) + const pass = r.error_code === 0 && between(r.azimuth, 170, 195); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 70: UTC boundary (hour=0, minute=0, second=0) + await scenario('UTC boundary midnight Jan 1', async () => { + const r = await spa(makeDate(2025, 1, 1, 0, 0, 0), 0, 0, { timezone: 0 }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 71: Fractional seconds (second=59.999) + await scenario('Fractional seconds (59.999s)', async () => { + // We pass 12:30:00 and rely on the sub-second being handled + const d = makeDate(2025, 6, 21, 12, 30, 0); + const r = await spa(d, 40.7128, -74.006, { timezone: -4 }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 72: Hour=23, minute=59 + await scenario('End of day 23:59:00', async () => { + const r = await spa(makeDate(2025, 6, 21, 23, 59, 0), 40.7128, -74.006, { timezone: -4 }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 73: February 29 leap year + await scenario('Feb 29 leap year (2024)', async () => { + const r = await spa(makeDate(2024, 2, 29, 12, 0, 0), 40.7128, -74.006, { timezone: -5 }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 74: Noon exactly at prime meridian equator (most symmetric case) + await scenario('Prime meridian equator noon', async () => { + const r = await spa(makeDate(2025, 3, 20, 12, 0, 0), 0, 0, { timezone: 0 }); + // Equinox at equator at noon on prime meridian: zenith should be very small + const pass = r.error_code === 0 && r.zenith < 5; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 75: New Year's Eve midnight + await scenario('New Year Eve midnight', async () => { + const r = await spa(makeDate(2025, 12, 31, 0, 0, 0), 40.7128, -74.006, { timezone: -5 }); + const pass = r.error_code === 0 && r.zenith > 90; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // ══════════════════════════════════════════════════════════════════ + // CATEGORY 5: All function codes (5 scenarios, 76-80) + // ══════════════════════════════════════════════════════════════════ + + const fcDate = makeDate(2025, 6, 21, 12, 0, 0); + const fcLat = 40.7128; + const fcLon = -74.006; + const fcOpts = { timezone: -4, elevation: 10 }; + + // Get the reference SPA_ALL result first + const refAll = await spa(fcDate, fcLat, fcLon, { ...fcOpts, function: SPA_ALL }); + + // 76: SPA_ZA zenith matches SPA_ALL + await scenario('SPA_ZA zenith matches SPA_ALL', async () => { + const r = await spa(fcDate, fcLat, fcLon, { ...fcOpts, function: SPA_ZA }); + const pass = r.error_code === 0 + && approx(r.zenith, refAll.zenith, 0.01) + && approx(r.azimuth, refAll.azimuth, 0.01); + return { + pass, + zenith: r.zenith, + azimuth: r.azimuth, + detail: !pass ? `diff zenith=${Math.abs(r.zenith - refAll.zenith).toFixed(6)}` : '', + }; + }); + + // 77: SPA_ZA_INC zenith/azimuth match SPA_ALL + await scenario('SPA_ZA_INC matches SPA_ALL', async () => { + const r = await spa(fcDate, fcLat, fcLon, { ...fcOpts, function: SPA_ZA_INC }); + const pass = r.error_code === 0 + && approx(r.zenith, refAll.zenith, 0.01) + && approx(r.azimuth, refAll.azimuth, 0.01) + && approx(r.incidence, refAll.incidence, 0.01); + return { + pass, + zenith: r.zenith, + azimuth: r.azimuth, + detail: !pass ? `diff incidence=${Math.abs(r.incidence - refAll.incidence).toFixed(6)}` : '', + }; + }); + + // 78: SPA_ZA_RTS zenith/azimuth match SPA_ALL + await scenario('SPA_ZA_RTS matches SPA_ALL', async () => { + const r = await spa(fcDate, fcLat, fcLon, { ...fcOpts, function: SPA_ZA_RTS }); + const pass = r.error_code === 0 + && approx(r.zenith, refAll.zenith, 0.01) + && approx(r.azimuth, refAll.azimuth, 0.01); + return { + pass, + zenith: r.zenith, + azimuth: r.azimuth, + }; + }); + + // 79: SPA_ALL returns all fields populated + await scenario('SPA_ALL all fields populated', async () => { + const r = refAll; + const pass = r.error_code === 0 + && isFinite(r.zenith) + && isFinite(r.azimuth) + && isFinite(r.azimuth_astro) + && isFinite(r.incidence) + && isFinite(r.sunrise) + && isFinite(r.sunset) + && isFinite(r.suntransit) + && isFinite(r.eot); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 80: SPA_ALL azimuth = azimuth_astro + 180 + await scenario('SPA_ALL azimuth consistency', async () => { + const r = refAll; + // azimuth (from north) = azimuth_astro (from south) + 180, mod 360 + const expected = (r.azimuth_astro + 180) % 360; + const pass = r.error_code === 0 && approx(r.azimuth, expected, 0.01); + return { + pass, + zenith: r.zenith, + azimuth: r.azimuth, + detail: !pass ? `azimuth=${r.azimuth}, expected=${expected}` : '', + }; + }); + + // ══════════════════════════════════════════════════════════════════ + // CATEGORY 6: Atmospheric conditions (10 scenarios, 81-90) + // ══════════════════════════════════════════════════════════════════ + + const atmoDate = makeDate(2025, 6, 21, 12, 0, 0); + const atmoLat = 40.7128; + const atmoLon = -74.006; + + // 81: Standard atmosphere + const stdAtmo = await spa(atmoDate, atmoLat, atmoLon, { + timezone: -4, pressure: 1013.25, temperature: 15, + }); + + await scenario('Standard atmosphere (1013.25mb, 15C)', async () => { + const pass = stdAtmo.error_code === 0 && between(stdAtmo.zenith, 0, 90); + return { pass, zenith: stdAtmo.zenith, azimuth: stdAtmo.azimuth }; + }); + + // 82: Very low pressure (high altitude, ~300 mbar) + await scenario('Low pressure 300 mbar', async () => { + const r = await spa(atmoDate, atmoLat, atmoLon, { + timezone: -4, pressure: 300, temperature: -30, elevation: 9000, + }); + // Should still compute; zenith will differ slightly from standard + const pass = r.error_code === 0 && between(r.zenith, 0, 90); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 83: High pressure (1100 mbar) + await scenario('High pressure 1100 mbar', async () => { + const r = await spa(atmoDate, atmoLat, atmoLon, { + timezone: -4, pressure: 1100, temperature: 15, + }); + const pass = r.error_code === 0 && between(r.zenith, 0, 90); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 84: Extreme cold (-40C) + await scenario('Extreme cold -40C', async () => { + const r = await spa(atmoDate, 64.1466, -21.9426, { + timezone: 0, temperature: -40, pressure: 1013.25, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 85: Extreme heat (+50C) + await scenario('Extreme heat +50C', async () => { + const r = await spa(atmoDate, 25.2048, 55.2708, { + timezone: 4, temperature: 50, pressure: 1000, + }); + const pass = r.error_code === 0 && between(r.zenith, 0, 10); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 86: Zero pressure + await scenario('Zero pressure (vacuum)', async () => { + const r = await spa(atmoDate, atmoLat, atmoLon, { + timezone: -4, pressure: 0, temperature: 15, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 87: Custom atmospheric refraction (0 degrees) + await scenario('Custom refraction 0 deg', async () => { + const r = await spa(atmoDate, atmoLat, atmoLon, { + timezone: -4, atmos_refract: 0, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 88: Custom atmospheric refraction (2 degrees) + await scenario('Custom refraction 2 deg', async () => { + const r = await spa(atmoDate, atmoLat, atmoLon, { + timezone: -4, atmos_refract: 2.0, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 89: Pressure/temperature affect zenith slightly + await scenario('Pressure effect on zenith', async () => { + const rLow = await spa(atmoDate, atmoLat, atmoLon, { + timezone: -4, pressure: 300, temperature: 15, + }); + const rHigh = await spa(atmoDate, atmoLat, atmoLon, { + timezone: -4, pressure: 1100, temperature: 15, + }); + // Both should succeed; zenith should differ slightly due to refraction + const pass = rLow.error_code === 0 && rHigh.error_code === 0 + && rLow.zenith !== rHigh.zenith; + return { + pass, + zenith: rLow.zenith, + azimuth: rLow.azimuth, + detail: `low=${rLow.zenith.toFixed(4)}, high=${rHigh.zenith.toFixed(4)}`, + }; + }); + + // 90: High elevation with matching low pressure + await scenario('High elevation + low pressure combo', async () => { + const r = await spa(atmoDate, 27.9881, 86.925, { + timezone: 5.75, elevation: 5364, pressure: 500, temperature: -5, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // ══════════════════════════════════════════════════════════════════ + // CATEGORY 7: Historical/future dates (10 scenarios, 91-100) + // ══════════════════════════════════════════════════════════════════ + + // 91: Year 1000 + await scenario('Year 1000 CE', async () => { + const r = await spa(makeDate(1000, 6, 21, 12, 0, 0), 40.7128, -74.006, { + timezone: -5, delta_t: 1574, + }); + const pass = r.error_code === 0 && between(r.zenith, 0, 90); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 92: Year 1582 (Gregorian calendar switch) + await scenario('Year 1582 (Gregorian switch)', async () => { + const r = await spa(makeDate(1582, 10, 15, 12, 0, 0), 41.9028, 12.4964, { + timezone: 1, delta_t: 120, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 93: Year 1900 + await scenario('Year 1900', async () => { + const r = await spa(makeDate(1900, 6, 21, 12, 0, 0), 48.8566, 2.3522, { + timezone: 0, delta_t: -3, + }); + const pass = r.error_code === 0 && between(r.zenith, 0, 90); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 94: Year 1969 (Apollo 11 era) + await scenario('Year 1969 (Apollo era)', async () => { + const r = await spa(makeDate(1969, 7, 20, 12, 0, 0), 28.5721, -80.648, { + timezone: -5, delta_t: 40, + }); + const pass = r.error_code === 0 && between(r.zenith, 0, 20); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 95: Year 2050 + await scenario('Year 2050', async () => { + const r = await spa(makeDate(2050, 6, 21, 12, 0, 0), 40.7128, -74.006, { + timezone: -4, delta_t: 93, + }); + const pass = r.error_code === 0 && between(r.zenith, 16, 30); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 96: Year 2100 + await scenario('Year 2100', async () => { + const r = await spa(makeDate(2100, 12, 21, 12, 0, 0), 51.5074, -0.1278, { + timezone: 0, delta_t: 200, + }); + const pass = r.error_code === 0 && between(r.zenith, 70, 80); + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 97: Year 3000 + await scenario('Year 3000', async () => { + const r = await spa(makeDate(3000, 6, 21, 12, 0, 0), 35.6762, 139.6503, { + timezone: 9, delta_t: 0, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 98: Year 5000 + await scenario('Year 5000', async () => { + const r = await spa(makeDate(5000, 3, 20, 12, 0, 0), 0, 0, { + timezone: 0, delta_t: 0, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 99: Year -1000 (1001 BCE) + await scenario('Year -1000 (1001 BCE)', async () => { + const r = await spa(makeDate(-1000, 6, 21, 12, 0, 0), 37.9715, 23.7267, { + timezone: 2, delta_t: 0, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // 100: Year -2000 (earliest valid, winter solstice) + await scenario('Year -2000 winter solstice', async () => { + const r = await spa(makeDate(-2000, 12, 21, 12, 0, 0), 30.0444, 31.2357, { + timezone: 2, delta_t: 0, + }); + const pass = r.error_code === 0; + return { pass, zenith: r.zenith, azimuth: r.azimuth }; + }); + + // ══════════════════════════════════════════════════════════════════ + // Print Results + // ══════════════════════════════════════════════════════════════════ + + console.log('NREL SPA Validation Suite'); + console.log('=========================\n'); + + let passCount = 0; + let failCount = 0; + const latencies = []; + + for (const r of results) { + if (r.pass) passCount++; + else failCount++; + latencies.push(r.us); + + const numStr = String(r.num).padStart(3, ' '); + const status = r.pass ? 'PASS' : 'FAIL'; + const nameStr = r.name.padEnd(44, ' '); + + let info = ''; + if (r.zenith !== null) { + info = `(zenith=${r.zenith.toFixed(2)}\u00B0, azimuth=${r.azimuth.toFixed(2)}\u00B0, ${r.us}\u00B5s)`; + } else if (r.detail) { + info = `(${r.detail}, ${r.us}\u00B5s)`; + } else { + info = `(${r.us}\u00B5s)`; + } + + if (r.pass) { + console.log(`Scenario ${numStr}: ${nameStr} ${status} ${info}`); + } else { + console.log(`Scenario ${numStr}: ${nameStr} ${status} ${info}${r.detail ? ' -- ' + r.detail : ''}`); + } + } + + console.log(`\nResults: ${passCount}/${results.length} passed` + (failCount > 0 ? ` (${failCount} failed)` : '')); + + // ══════════════════════════════════════════════════════════════════ + // Performance Benchmarks + // ══════════════════════════════════════════════════════════════════ + + // Compute latency stats from the 100 scenario calls + const sorted = [...latencies].sort((a, b) => a - b); + const sum = sorted.reduce((a, b) => a + b, 0); + const mean = sum / sorted.length; + const median = sorted.length % 2 === 0 + ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 + : sorted[Math.floor(sorted.length / 2)]; + const p95idx = Math.ceil(sorted.length * 0.95) - 1; + const p99idx = Math.ceil(sorted.length * 0.99) - 1; + + console.log('\nPerformance'); + console.log('-----------'); + console.log(`Per-call latency (${sorted.length} calls):`); + console.log(` Min: ${sorted[0]}\u00B5s`); + console.log(` Max: ${sorted[sorted.length - 1]}\u00B5s`); + console.log(` Mean: ${Math.round(mean)}\u00B5s`); + console.log(` Median: ${Math.round(median)}\u00B5s`); + console.log(` P95: ${sorted[p95idx]}\u00B5s`); + console.log(` P99: ${sorted[p99idx]}\u00B5s`); + + // Batch throughput: SPA_ALL + const batchAll = 10000; + const batchDate = makeDate(2025, 6, 21, 12, 0, 0); + const batchOpts = { timezone: -4, function: SPA_ALL }; + const tAllStart = performance.now(); + for (let i = 0; i < batchAll; i++) { + await spa(batchDate, 40.7128, -74.006, batchOpts); + } + const tAllEnd = performance.now(); + const allMs = tAllEnd - tAllStart; + const allPerSec = Math.round(batchAll / (allMs / 1000)); + + // Batch throughput: SPA_ZA + const batchZaOpts = { timezone: -4, function: SPA_ZA }; + const tZaStart = performance.now(); + for (let i = 0; i < batchAll; i++) { + await spa(batchDate, 40.7128, -74.006, batchZaOpts); + } + const tZaEnd = performance.now(); + const zaMs = tZaEnd - tZaStart; + const zaPerSec = Math.round(batchAll / (zaMs / 1000)); + + console.log('\nBatch throughput:'); + console.log(` SPA_ALL: ${batchAll.toLocaleString()} calls in ${Math.round(allMs)}ms (${allPerSec.toLocaleString()} calls/sec)`); + console.log(` SPA_ZA: ${batchAll.toLocaleString()} calls in ${Math.round(zaMs)}ms (${zaPerSec.toLocaleString()} calls/sec)`); + + // Init time measurement + // We already initialized, so measure a fresh module creation overhead + // This is approximate since we measure only the cached path + const tInitStart = performance.now(); + await init(); + const tInitEnd = performance.now(); + console.log(`\nInit time: ${(tInitEnd - tInitStart).toFixed(1)}ms (cached; first init happened during warmup)`); + + // Exit code + if (failCount > 0) { + process.exit(1); + } +} + +runAll().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/wasm/spa-module.js b/wasm/spa-module.js new file mode 100644 index 0000000000000000000000000000000000000000..d86b3936aae77a9654a3de7fce49c72d227220fd GIT binary patch literal 60004 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 literal 0 HcmV?d00001