@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 +27 -1
- package/README.md +44 -1
- package/exact-cache.js +242 -0
- package/package.json +8 -1
- package/pkg/sidereon.d.ts +1855 -1737
- package/pkg/sidereon.js +206 -0
- package/pkg/sidereon_bg.wasm +0 -0
- package/pkg/sidereon_bg.wasm.d.ts +1552 -1542
- package/pkg-node/sidereon.d.ts +108 -0
- package/pkg-node/sidereon.js +210 -0
- package/pkg-node/sidereon_bg.wasm +0 -0
- package/pkg-node/sidereon_bg.wasm.d.ts +1552 -1542
- package/types/exact-cache.d.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|
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.
|
|
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"
|