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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "4.1.1",
"version": "4.1.2",
"private": true,
"engines": {
"node": ">=20.0.0"
Expand All @@ -17,6 +17,9 @@
],
"scripts": {
"test": "vitest run",
"review:agent": "coderabbit review --agent",
"review:cli": "coderabbit review",
"review:findings": "coderabbit review findings",
"build:tempo": "npm run build:library && npm run build:std && npm run build --workspace=@magmacomputing/tempo",
"build:library": "npm run build --workspace=@magmacomputing/library",
"build:std": "npm run build --workspace=@magmacomputing/tempo-std",
Expand Down Expand Up @@ -77,4 +80,4 @@
"magic-string": "^1.1.1",
"typescript-7": "npm:typescript@^7.0.2"
}
}
}
6 changes: 5 additions & 1 deletion packages/functions/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-09-02
## [0.2.0] - 2026-09-09

### Added
- **Elevation Horizon Dip**: Integrated observer elevation (meters above sea level) into `getSunriseSunset()` apparent solar timing calculations:
- Added optional `elevation` parameter to `SolarOptions` and included resolved `elevation` in `SunriseSunsetResult`.
- Factors atmospheric horizon dip ($\Delta\theta \approx 0.0347^\circ \times \sqrt{\max(0, \text{elevation})}$) into the solar zenith angle ($90.833^\circ + \Delta\theta$).
- Correctly shifts sunrise earlier, sunset later, and expands daylight duration for elevated observers while keeping true solar noon transit invariant.
- **Celestial Utilities**: Introduced new pure astronomical, celestial, solar, lunar, and zodiac utility module (`@magmacomputing/tempo-fns/celestial`):
- `getLunarPhase`: Calculates lunar phase name, 1-based index (1..8), illumination 0.0–1.0 fraction, age in days, waxing status, and hemisphere-aware emojis.
- `getLunarPhaseRange`: Resolves start/end boundaries for active lunar phase cycles.
Expand Down
44 changes: 21 additions & 23 deletions packages/functions/src/celestial/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,13 @@ export interface SolarOptions {
long?: number;
lng?: number;
lon?: number;
elevation?: number;
geo?: {
latitude?: number;
longitude?: number;
elevation?: number;
[key: string]: any;
};
}

export interface SolarTwilightWindow {
Expand All @@ -437,6 +444,7 @@ export interface SolarTwilightWindow {
export interface SunriseSunsetResult {
latitude: number;
longitude: number;
elevation?: number;
sunriseMs: number;
sunsetMs: number;
solarNoonMs: number;
Expand All @@ -451,39 +459,27 @@ export interface SunriseSunsetResult {
}

/**
* Resolves latitude and longitude from positional arguments or a coordinate options object.
* Resolves latitude, longitude, and elevation from positional arguments or a coordinate options object.
*
* @param latOrOptions - A latitude value or options containing coordinate fields
* @param lngInput - The longitude used when `latOrOptions` is a numeric latitude
* @returns An object containing the resolved `lat` and `lng` values
* @returns An object containing the resolved `lat`, `lng`, and `elevation` values
*/
/**
* Resolves latitude and longitude from positional arguments or a coordinate options object.
*
* @param latOrOptions - A latitude value or options containing coordinate fields
* @param lngInput - The longitude used when `latOrOptions` is a numeric latitude
* @returns An object containing the resolved `lat` and `lng` values
*/
function resolveCoordinates(latOrOptions: number | SolarOptions = 0, lngInput = 0): { lat: number; lng: number } {
function resolveCoordinates(latOrOptions: number | SolarOptions = 0, lngInput = 0): { lat: number; lng: number; elevation: number } {
if (typeof latOrOptions === 'number')
return { lat: latOrOptions, lng: lngInput };
return { lat: latOrOptions, lng: lngInput, elevation: 0 };

if (latOrOptions && typeof latOrOptions === 'object') {
const geo = (latOrOptions as any).geo ?? latOrOptions;
const lat = geo.latitude ?? geo.lat ?? (latOrOptions as any).latitude ?? (latOrOptions as any).lat ?? 0;
const lng = geo.longitude ?? geo.lng ?? geo.lon ?? geo.long ?? (latOrOptions as any).longitude ?? (latOrOptions as any).lng ?? (latOrOptions as any).lon ?? (latOrOptions as any).long ?? 0;
return { lat, lng };
const rawElevation = geo.elevation ?? (latOrOptions as any).elevation;
const elevation = typeof rawElevation === 'number' && Number.isFinite(rawElevation) ? rawElevation : 0;
return { lat, lng, elevation };
}
return { lat: 0, lng: 0 };
return { lat: 0, lng: 0, elevation: 0 };
}

/**
* Determines the UTC start of the calendar day at a specified longitude.
*
* @param epochMs - The input timestamp in milliseconds since the Unix epoch
* @param lng - The longitude in degrees used to determine the local date
* @returns The UTC start timestamp, local date, and longitude-adjusted timestamp
*/
/**
* Determines the UTC start of the calendar day at a specified longitude.
*
Expand Down Expand Up @@ -517,10 +513,11 @@ export function getSunriseSunset(
? new Date(dateInput).getTime()
: dateInput.getTime();

const { lat, lng } = resolveCoordinates(latOrOptions, lonInput);
const { lat, lng, elevation } = resolveCoordinates(latOrOptions, lonInput);
const { startOfDayMs, localDate, localMs } = getStartOfLocalDayMs(epochMs, lng);

// Solar calculations using standard zenith (90.833°)
// Solar calculations using standard zenith (90.833°) adjusted for atmospheric horizon dip
const dipDeg = elevation > 0 ? 0.0347 * Math.sqrt(elevation) : 0;
const dayOfYear = Math.floor((localMs - Date.UTC(localDate.getUTCFullYear(), 0, 0)) / 86400000);
const gamma = (2 * Math.PI / 365) * (dayOfYear - 1);

Expand All @@ -543,7 +540,7 @@ export function getSunriseSunset(
return Math.acos(cosHA) * (180 / Math.PI);
};

const haDeg = calcHaDeg(90.833);
const haDeg = calcHaDeg(90.833 + dipDeg);
const haMin = haDeg * 4;
const sunriseMs = startOfDayMs + ((solarNoonMin - haMin) * 60000);
const sunsetMs = startOfDayMs + ((solarNoonMin + haMin) * 60000);
Expand Down Expand Up @@ -589,6 +586,7 @@ export function getSunriseSunset(
return {
latitude: lat,
longitude: lng,
elevation,
sunriseMs,
sunsetMs,
solarNoonMs,
Comment thread
magmacomputing marked this conversation as resolved.
Expand Down
39 changes: 39 additions & 0 deletions packages/functions/test/celestial.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,45 @@ describe('Astro Pure Functions (tempo-fns)', () => {
expect(nightRes.index).toBe(1); // 1-based (1 = night)
});

it('factors elevation into apparent sunrise/sunset and daylight duration via horizon dip', () => {
const date = new Date('2026-06-21T12:00:00Z');
// Sea level in Denver (lat 39.7392, lng -104.9903, elevation 0m)
const seaLevel = getSunriseSunset(date, { lat: 39.7392, lng: -104.9903, elevation: 0 });
// Actual Mile High City elevation (1600m above sea level)
const highAltitude = getSunriseSunset(date, { lat: 39.7392, lng: -104.9903, elevation: 1600 });

// Higher elevation causes horizon dip: sunrise is earlier, sunset is later
expect(highAltitude.sunriseMs).toBeLessThan(seaLevel.sunriseMs);
expect(highAltitude.sunsetMs).toBeGreaterThan(seaLevel.sunsetMs);
expect(highAltitude.daylightDurationMs).toBeGreaterThan(seaLevel.daylightDurationMs);
// Solar noon remains unchanged by elevation
expect(highAltitude.solarNoonMs).toBe(seaLevel.solarNoonMs);

// Difference in sunrise/sunset is approx 8.5 minutes for 1600m at latitude ~40°N
const diffMin = (seaLevel.sunriseMs - highAltitude.sunriseMs) / 60000;
expect(diffMin).toBeGreaterThan(7);
expect(diffMin).toBeLessThan(10);
expect(highAltitude.elevation).toBe(1600);
});

it('handles non-finite elevation gracefully and produces valid sunrise/sunset timings', () => {
const date = '2026-06-21T12:00:00Z';
const baseline = getSunriseSunset(date, { lat: 39.7392, lng: -104.9903, elevation: 0 });
const infResult = getSunriseSunset(date, { lat: 39.7392, lng: -104.9903, elevation: Infinity });
const negInfResult = getSunriseSunset(date, { lat: 39.7392, lng: -104.9903, elevation: -Infinity });
const nanResult = getSunriseSunset(date, { lat: 39.7392, lng: -104.9903, elevation: NaN });

expect(infResult.elevation).toBe(0);
expect(Number.isFinite(infResult.sunriseMs)).toBe(true);
expect(Number.isFinite(infResult.sunsetMs)).toBe(true);
expect(Number.isFinite(infResult.daylightDurationMs)).toBe(true);
expect(infResult.sunriseMs).toBe(baseline.sunriseMs);
expect(infResult.sunsetMs).toBe(baseline.sunsetMs);

expect(negInfResult.elevation).toBe(0);
expect(nanResult.elevation).toBe(0);
});

it('calculates Western Tropical Zodiac sign', () => {
expect(getZodiacSign('2026-03-25')).toBe('Aries');
expect(getZodiacSign('2026-07-25')).toBe('Leo');
Expand Down
18 changes: 18 additions & 0 deletions packages/library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.2.0] - 2026-09-09

### Added
- **Generic Bounded LRU & TTL Cache Engine (`BoundedCache`)**:
- Implemented high-performance `BoundedCache<K, V>` in `#library/cache.class.js` supporting configurable capacity constraints (`maxSize`, default 1000) and time-to-live expiration (`ttl`, default 24 hours / `86,400,000 ms`).
- Added per-entry TTL override support in `set(key, val, ttl?)` with precomputed absolute expiration deadlines (`expiresAt = Date.now() + ttl`).
- Implemented $O(1)$ fast-path expiration checks: eliminates clock reads (`Date.now()`) when keys have no entry-level expiration deadline recorded in `#expires`.
- Added bulk clear (`clear()`), lazy eviction (`evictExpired()`), iteration (`keys()`, `values()`, `entries()`, `forEach()`, `[Symbol.iterator]()`), and size inspection (`size`).
- **Bounded In-Memory Server Storage (`storage.library`)**:
- Replaced unbounded `Map` backing `nodeStorage` with `BoundedCache<string, string | undefined>(1000, Infinity)` via `#library/cache.class.js`.
- Added `ServerStorageOptions` interface and updated `setStorage(key, value, options?)` overload to support optional custom `ttl`.
- Added `clearStorage()` utility to purge in-memory storage entries across test environments and lifecycle boundaries while preserving tombstone deletion semantics (`undefined` value).
- **Geolocation Caching & Multi-Tenant Partitioning (`mapper.library`)**:
- Wired `geoLookup()` to automatically cache resolved geographic coordinates in ambient storage with a 24-hour TTL (`86,400,000 ms`).
- Added `{ refresh: true }` option to `geoLookup()` to bypass cached results and force fresh network resolution.
- Added `stashGeo(coords, ttl?, keyOrOpts?)`, `clearStashedGeo(keyOrOpts?)`, and `getStashedGeo(keyOrOpts?)` helpers.
- Implemented multi-tenant and IP cache key partitioning (`resolveCacheKey` scoping to `_magma_geo_:<key>` or `_magma_geo_:<ip>`), preventing tenants from trampling shared geolocation coordinates.

## [4.1.0] - 2026-09-06

### Added
Expand Down
6 changes: 5 additions & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/library",
"version": "4.1.1",
"version": "4.2.0",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down Expand Up @@ -185,6 +185,10 @@
"development": "./src/common/scheduling/schedule.library.ts",
"default": "./dist/common/scheduling/schedule.library.js"
},
"#library/cache.class.js": {
"development": "./src/common/runtime/cache.class.ts",
"default": "./dist/common/runtime/cache.class.js"
},
"#library/decorator.library.js": {
"development": "./src/common/runtime/decorator.library.ts",
"default": "./dist/common/runtime/decorator.library.js"
Expand Down
6 changes: 3 additions & 3 deletions packages/library/src/browser/mapper.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface MapStore { // a localStorage object
const defaults = { catch: true, debug: 0 } as MapOpts; // default Options
const context = getContext(); // browser / nodejs / google-apps
const mapStore = {} as MapStore; // static object to hold last position
const MAP_KEY = '_map_'; // localStorage key
const MAP_KEY = '_magma_browser_map_'; // distinct browser MapStore localStorage key
const log = new Logger('[Mapper]');

let storePromise: Promise<void | WebStore> | null = null;
Expand All @@ -38,7 +38,7 @@ const getStore = () => {
import('#browser/webstore.class.js')
.then(({ WebStore }) => {
const local = new WebStore('local');
Object.assign(mapStore, local.get(MAP_KEY, {}));// fetch the previous MAP_KEY coordinates
Object.assign(mapStore, local.get(MAP_KEY, {}));// fetch previous coordinates
resolve(local); // localStorage wrapper
})
.catch(reject);
Expand Down Expand Up @@ -146,7 +146,7 @@ export const mapQuery = (coords?: google.maps.GeocoderRequest, opts = {} as MapO
geoCoords(coords) // get a Location object
.then((loc) => {
switch (true) {
case (!(typeof window !== 'undefined' && 'google' in window && 'maps' in window['google'])):
case (context.type !== CONTEXT.Browser || !window['google']?.maps):
throw new Error('Google Maps API not configured');

case isNullish(loc): // unsuccessful geoLocation
Expand Down
Loading
Loading