@neilberkman/sidereon 0.29.2 → 0.31.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,32 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.31.2 - 2026-07-16
4
+
5
+ - Returns canonical contributors and ordered precedence contributors alongside
6
+ the merged-SP3 stable ID.
7
+ - Rejects artifact byte lengths that are not positive exact JavaScript safe
8
+ integers and enforces whole-second target epoch intervals.
9
+ - Adds the shared literal provenance fixture and builds against `sidereon` and
10
+ `sidereon-core` 0.31.2.
11
+
12
+ ## 0.31.0 - 2026-07-16
13
+
14
+ - Adds `sp3MergeInputIdentity`, which validates complete exact SP3 artifact
15
+ records plus the full merge policy and returns the shared versioned stable
16
+ identity. Incomplete, malformed, mismatched, duplicate, non-SP3, and unknown
17
+ fields fail closed.
18
+ - Builds against `sidereon` and `sidereon-core` 0.31.0.
19
+
20
+ ## 0.30.0 - 2026-07-16
21
+
22
+ - Exposes analysis center, parsed format version, and the canonical all-field
23
+ cache key on exact product identities.
24
+ - Adds the shared schema-v3 commit builder and verifier, binding the full
25
+ identity, explicit source, and all immutable byte objects.
26
+ - Adds `@neilberkman/sidereon/exact-cache`, using Web Locks for bounded
27
+ same-origin tab/worker coordination and one strict-durability IndexedDB
28
+ transaction for atomic immutable-entry publication.
29
+ - Builds against `sidereon` and `sidereon-core` 0.30.0.
4
30
 
5
31
  ## 0.29.2 - 2026-07-16
6
32
 
package/README.md CHANGED
@@ -112,12 +112,23 @@ import {
112
112
  distributionLocation,
113
113
  GnssExactProductSet,
114
114
  productIdentity,
115
+ sp3MergeInputIdentity,
115
116
  } from "@neilberkman/sidereon";
116
117
 
117
118
  const identity = productIdentity("cod", "sp3", 2026, 7, 12);
118
119
  console.log(identity.officialFilename);
119
120
  // COD0MGXFIN_20261930000_01D_05M_ORB.SP3
120
121
 
122
+ // Once every input has been acquired and validated, bind its complete exact
123
+ // artifact record and the full merge policy to one canonical stable ID.
124
+ const mergeInput = sp3MergeInputIdentity([artifactRecord], {
125
+ combine: "mean",
126
+ systems: ["G", "E"],
127
+ });
128
+ console.log(mergeInput.schemaVersion, mergeInput.stableId);
129
+ console.log(mergeInput.contributors); // distributor-independent canonical order
130
+ // precedenceContributors is defined only when combine is "precedence".
131
+
121
132
  const cddis = distributionLocation(
122
133
  "cod",
123
134
  "sp3",
@@ -137,6 +148,17 @@ exactSet.addAvailable(identity); // add only after content validation
137
148
  exactSet.validate();
138
149
  ```
139
150
 
151
+ `artifactRecord` contains the requested and parsed/resolved product identities,
152
+ explicit distributor, official filename, decompressed and archive SHA-256/length
153
+ pairs, and archive compression. Contributor enumeration does not affect mean or
154
+ median IDs. Precedence IDs bind the original order because it determines source
155
+ priority. Object-property ordering does not affect any ID; changing an artifact
156
+ or effective merge control does. Incomplete, mismatched, duplicate, non-SP3,
157
+ and unknown fields are rejected. Byte lengths must be positive integers no
158
+ greater than `Number.MAX_SAFE_INTEGER`. Retrieval timestamps, URLs, HTTP metadata,
159
+ credentials, cache paths, and retry history are intentionally not accepted as
160
+ canonical inputs.
161
+
140
162
  `productIdentity` returns publisher, solution class, campaign, date, issue,
141
163
  coverage span, cadence, official filename, format, and a validated cache key.
142
164
  `distributionLocation` accepts `direct`, `nasa_cddis`, `local_file`, or
@@ -153,7 +175,28 @@ returns successfully. For SP3 observed/predicted timing, use
153
175
  fields.
154
176
 
155
177
  Browser and Node callers retain control of `fetch`, Earthdata credentials,
156
- retries, and cache policy. Send credentials only to NASA's documented hosts;
178
+ retries, and product-format validation. The browser cache uses the same WASM
179
+ commit builder/verifier as the other interfaces:
180
+
181
+ ```js
182
+ import { BrowserExactProductCache } from "@neilberkman/sidereon/exact-cache";
183
+
184
+ const cache = await BrowserExactProductCache.open();
185
+ const entry = await cache.withLock(identity, "nasa_cddis", async (locked) => {
186
+ const hit = await locked.read();
187
+ if (hit) return hit;
188
+ // Fetch and validate first; these are the resulting immutable bytes.
189
+ return locked.publish(productBytes, archiveBytes, provenanceBytes);
190
+ });
191
+ ```
192
+
193
+ Web Locks coordinate same-origin tabs and workers with a bounded wait.
194
+ IndexedDB publishes the immutable entry and marker in one strict-durability
195
+ transaction; reads return bytes only after shared schema-v3 verification. Node
196
+ hosts can call `buildExactCacheCommit` and `verifyExactCacheCommit` with their
197
+ own storage transaction.
198
+
199
+ Send credentials only to NASA's documented hosts;
157
200
  remove URL queries from diagnostics; reject HTML success bodies; validate
158
201
  content length, gzip completion, and hashes; then parse with `loadSp3` or
159
202
  `loadIonex`. See [NASA CDDIS archive access](https://www.earthdata.nasa.gov/centers/cddis-daac/archive-access)
package/exact-cache.js ADDED
@@ -0,0 +1,242 @@
1
+ import { buildExactCacheCommit, verifyExactCacheCommit } from "./pkg/sidereon.js";
2
+
3
+ const MARKERS = "markers";
4
+ const ENTRIES = "entries";
5
+
6
+ function request(request) {
7
+ return new Promise((resolve, reject) => {
8
+ request.onsuccess = () => resolve(request.result);
9
+ request.onerror = () => reject(request.error);
10
+ });
11
+ }
12
+
13
+ function complete(transaction) {
14
+ return new Promise((resolve, reject) => {
15
+ transaction.oncomplete = () => resolve();
16
+ transaction.onerror = () => reject(transaction.error);
17
+ transaction.onabort = () => reject(transaction.error ?? new Error("exact-cache transaction aborted"));
18
+ });
19
+ }
20
+
21
+ function bytes(value) {
22
+ return value instanceof Uint8Array ? value : new Uint8Array(value);
23
+ }
24
+
25
+ function entryId() {
26
+ const random = new Uint8Array(16);
27
+ globalThis.crypto.getRandomValues(random);
28
+ return Array.from(random, (value) => value.toString(16).padStart(2, "0")).join("");
29
+ }
30
+
31
+ function baseKey(identity, source) {
32
+ return `${source}\0${identity.cacheKey}`;
33
+ }
34
+
35
+ function entryKey(base, entry) {
36
+ return `${base}\0${entry}`;
37
+ }
38
+
39
+ async function openDatabase(name) {
40
+ if (typeof globalThis.indexedDB === "undefined") {
41
+ throw new Error("BrowserExactProductCache requires IndexedDB");
42
+ }
43
+ const open = globalThis.indexedDB.open(name, 3);
44
+ open.onupgradeneeded = () => {
45
+ if (!open.result.objectStoreNames.contains(MARKERS)) {
46
+ open.result.createObjectStore(MARKERS);
47
+ }
48
+ if (!open.result.objectStoreNames.contains(ENTRIES)) {
49
+ open.result.createObjectStore(ENTRIES);
50
+ }
51
+ };
52
+ return request(open);
53
+ }
54
+
55
+ class LockedExactCache {
56
+ constructor(owner, identity, source, base) {
57
+ this.owner = owner;
58
+ this.identity = identity;
59
+ this.source = source;
60
+ this.base = base;
61
+ }
62
+
63
+ read() {
64
+ return this.owner._read(this.identity, this.source, this.base);
65
+ }
66
+
67
+ async publish(product, archive, provenance) {
68
+ product = bytes(product);
69
+ archive = bytes(archive);
70
+ provenance = bytes(provenance);
71
+ const entry = entryId();
72
+ const marker = buildExactCacheCommit(
73
+ this.identity,
74
+ this.source,
75
+ entry,
76
+ product,
77
+ archive,
78
+ provenance,
79
+ );
80
+ const transaction = this.owner.database.transaction([MARKERS, ENTRIES], "readwrite", {
81
+ durability: "strict",
82
+ });
83
+ transaction.objectStore(ENTRIES).add(
84
+ {
85
+ product: product.slice(),
86
+ archive: archive.slice(),
87
+ provenance: provenance.slice(),
88
+ },
89
+ entryKey(this.base, entry),
90
+ );
91
+ transaction.objectStore(MARKERS).put({ entry, marker: marker.slice() }, this.base);
92
+ await complete(transaction);
93
+ return { entryId: entry, product, archive, provenance, marker };
94
+ }
95
+
96
+ async cleanupAbandoned() {
97
+ const transaction = this.owner.database.transaction([MARKERS, ENTRIES], "readwrite", {
98
+ durability: "strict",
99
+ });
100
+ const marker = await request(transaction.objectStore(MARKERS).get(this.base));
101
+ const current = marker?.entry;
102
+ const store = transaction.objectStore(ENTRIES);
103
+ const range = globalThis.IDBKeyRange.bound(`${this.base}\0`, `${this.base}\0\uffff`);
104
+ await new Promise((resolve, reject) => {
105
+ const cursor = store.openCursor(range);
106
+ cursor.onerror = () => reject(cursor.error);
107
+ cursor.onsuccess = () => {
108
+ const item = cursor.result;
109
+ if (!item) {
110
+ resolve();
111
+ return;
112
+ }
113
+ if (item.key !== entryKey(this.base, current)) item.delete();
114
+ item.continue();
115
+ };
116
+ });
117
+ await complete(transaction);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Browser exact-product cache using Web Locks for cross-tab/worker acquisition
123
+ * coordination and one durable IndexedDB transaction for entry publication.
124
+ */
125
+ export class BrowserExactProductCache {
126
+ static async open({ name = "sidereon-exact-products-v3" } = {}) {
127
+ return new BrowserExactProductCache(await openDatabase(name));
128
+ }
129
+
130
+ constructor(database) {
131
+ this.database = database;
132
+ }
133
+
134
+ async withLock(identity, source, operation, { timeoutMs = 30_000 } = {}) {
135
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
136
+ throw new TypeError("timeoutMs must be finite and non-negative");
137
+ }
138
+ if (!globalThis.navigator?.locks) {
139
+ throw new Error("BrowserExactProductCache requires the Web Locks API");
140
+ }
141
+ const base = baseKey(identity, source);
142
+ const lockName = `sidereon-exact-cache:${base}`;
143
+ if (timeoutMs === 0) {
144
+ return globalThis.navigator.locks.request(
145
+ lockName,
146
+ { mode: "exclusive", ifAvailable: true },
147
+ (lock) => {
148
+ if (lock === null) throw new Error("timed out waiting for exact-cache lock");
149
+ return operation(new LockedExactCache(this, identity, source, base));
150
+ },
151
+ );
152
+ }
153
+ const controller = new globalThis.AbortController();
154
+ const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
155
+ let acquired = false;
156
+ try {
157
+ return await globalThis.navigator.locks.request(
158
+ lockName,
159
+ { mode: "exclusive", signal: controller.signal },
160
+ () => {
161
+ acquired = true;
162
+ globalThis.clearTimeout(timer);
163
+ return operation(new LockedExactCache(this, identity, source, base));
164
+ },
165
+ );
166
+ } catch (error) {
167
+ if (!acquired && controller.signal.aborted) {
168
+ throw new Error("timed out waiting for exact-cache lock", { cause: error });
169
+ }
170
+ throw error;
171
+ } finally {
172
+ globalThis.clearTimeout(timer);
173
+ }
174
+ }
175
+
176
+ read(identity, source) {
177
+ return this._read(identity, source, baseKey(identity, source));
178
+ }
179
+
180
+ _read(identity, source, base) {
181
+ return new Promise((resolve, reject) => {
182
+ const transaction = this.database.transaction([MARKERS, ENTRIES], "readonly");
183
+ const markerRequest = transaction.objectStore(MARKERS).get(base);
184
+ let result;
185
+ markerRequest.onerror = () => reject(markerRequest.error);
186
+ markerRequest.onsuccess = () => {
187
+ const committed = markerRequest.result;
188
+ if (!committed) {
189
+ result = null;
190
+ return;
191
+ }
192
+ const entryRequest = transaction
193
+ .objectStore(ENTRIES)
194
+ .get(entryKey(base, committed.entry));
195
+ entryRequest.onerror = () => reject(entryRequest.error);
196
+ entryRequest.onsuccess = () => {
197
+ const entry = entryRequest.result;
198
+ if (!entry) {
199
+ reject(new Error("committed exact-cache entry is incomplete"));
200
+ return;
201
+ }
202
+ const product = bytes(entry.product);
203
+ const archive = bytes(entry.archive);
204
+ const provenance = bytes(entry.provenance);
205
+ const marker = bytes(committed.marker);
206
+ let verified;
207
+ try {
208
+ verified = verifyExactCacheCommit(
209
+ identity,
210
+ source,
211
+ marker,
212
+ product,
213
+ archive,
214
+ provenance,
215
+ );
216
+ } catch (error) {
217
+ reject(error);
218
+ return;
219
+ }
220
+ if (verified !== committed.entry) {
221
+ reject(new Error("exact-cache entry identifier mismatch"));
222
+ return;
223
+ }
224
+ result = {
225
+ entryId: verified,
226
+ product,
227
+ archive,
228
+ provenance,
229
+ marker,
230
+ };
231
+ };
232
+ };
233
+ transaction.onerror = () => reject(transaction.error);
234
+ transaction.onabort = () => reject(transaction.error ?? new Error("exact-cache read aborted"));
235
+ transaction.oncomplete = () => resolve(result);
236
+ });
237
+ }
238
+
239
+ close() {
240
+ this.database.close();
241
+ }
242
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neilberkman/sidereon",
3
- "version": "0.29.2",
3
+ "version": "0.31.2",
4
4
  "description": "WebAssembly / JavaScript interface over the sidereon GNSS + astrodynamics engine: SP3 loading and multi-product merge, SPP / RTK (float + fixed) / PPP (float + fixed) positioning, IONEX slant delay, SGP4 and numerical orbit propagation, RINEX observation/navigation/clock parsing, CRINEX (Hatanaka) decoding, observable-domain math (carrier combinations, cycle slips, Hatch smoothing, weighting, velocity, GPS C/A signal), and the astro domain (scale-tagged time, frame transforms, Sun/Moon ephemerides, eclipse / body angles, DOP, passes / visibility, conjunction + collision probability, CDM / OMM / ANTEX parsing, RF link budget)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,6 +16,10 @@
16
16
  "import": "./pkg/sidereon.js",
17
17
  "require": "./pkg-node/sidereon.js"
18
18
  },
19
+ "./exact-cache": {
20
+ "types": "./types/exact-cache.d.ts",
21
+ "import": "./exact-cache.js"
22
+ },
19
23
  "./types": "./types/sidereon-extra.d.ts"
20
24
  },
21
25
  "types": "./pkg/sidereon.d.ts",
@@ -30,6 +34,8 @@
30
34
  "pkg-node/sidereon.d.ts",
31
35
  "pkg-node/sidereon_bg.wasm",
32
36
  "pkg-node/sidereon_bg.wasm.d.ts",
37
+ "exact-cache.js",
38
+ "types/exact-cache.d.ts",
33
39
  "types/sidereon-extra.d.ts",
34
40
  "README.md"
35
41
  ],
@@ -43,6 +49,7 @@
43
49
  "devDependencies": {
44
50
  "@eslint/js": "^10.0.1",
45
51
  "eslint": "^10.6.0",
52
+ "fake-indexeddb": "^6.2.5",
46
53
  "globals": "^17.7.0",
47
54
  "prettier": "^3.9.1",
48
55
  "typescript": "^6.0.3"