@neilberkman/sidereon 0.29.1 → 0.30.0
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 +20 -1
- package/README.md +39 -2
- package/exact-cache.js +242 -0
- package/package.json +8 -1
- package/pkg/sidereon.d.ts +1759 -1713
- package/pkg/sidereon.js +167 -0
- package/pkg/sidereon_bg.wasm +0 -0
- package/pkg/sidereon_bg.wasm.d.ts +1724 -1713
- package/pkg-node/sidereon.d.ts +35 -0
- package/pkg-node/sidereon.js +170 -0
- package/pkg-node/sidereon_bg.wasm +0 -0
- package/pkg-node/sidereon_bg.wasm.d.ts +1724 -1713
- package/types/exact-cache.d.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.30.0 - 2026-07-16
|
|
4
|
+
|
|
5
|
+
- Exposes analysis center, parsed format version, and the canonical all-field
|
|
6
|
+
cache key on exact product identities.
|
|
7
|
+
- Adds the shared schema-v3 commit builder and verifier, binding the full
|
|
8
|
+
identity, explicit source, and all immutable byte objects.
|
|
9
|
+
- Adds `@neilberkman/sidereon/exact-cache`, using Web Locks for bounded
|
|
10
|
+
same-origin tab/worker coordination and one strict-durability IndexedDB
|
|
11
|
+
transaction for atomic immutable-entry publication.
|
|
12
|
+
- Builds against `sidereon` and `sidereon-core` 0.30.0.
|
|
13
|
+
|
|
14
|
+
## 0.29.2 - 2026-07-16
|
|
15
|
+
|
|
16
|
+
- Adds `GnssExactProductSet`, a fail-closed gate for a declared exact identity
|
|
17
|
+
inventory. Empty declarations, duplicates, missing products, and undeclared
|
|
18
|
+
products are rejected.
|
|
19
|
+
- Preserves prediction-tier identity during exact-set comparison. SP3
|
|
20
|
+
observed/predicted timing remains available from the parser's authoritative
|
|
21
|
+
record-flag summary.
|
|
22
|
+
- Builds against `sidereon` and `sidereon-core` 0.29.2.
|
|
4
23
|
|
|
5
24
|
## 0.29.1 - 2026-07-15
|
|
6
25
|
|
package/README.md
CHANGED
|
@@ -108,7 +108,11 @@ Product identity and distribution are separate. The pure derivation functions
|
|
|
108
108
|
perform no network or file IO:
|
|
109
109
|
|
|
110
110
|
```js
|
|
111
|
-
import {
|
|
111
|
+
import {
|
|
112
|
+
distributionLocation,
|
|
113
|
+
GnssExactProductSet,
|
|
114
|
+
productIdentity,
|
|
115
|
+
} from "@neilberkman/sidereon";
|
|
112
116
|
|
|
113
117
|
const identity = productIdentity("cod", "sp3", 2026, 7, 12);
|
|
114
118
|
console.log(identity.officialFilename);
|
|
@@ -126,6 +130,11 @@ const cddis = distributionLocation(
|
|
|
126
130
|
);
|
|
127
131
|
console.log(cddis.originalUrl);
|
|
128
132
|
console.log(cddis.compression); // gzip
|
|
133
|
+
|
|
134
|
+
const exactSet = new GnssExactProductSet();
|
|
135
|
+
exactSet.addExpected(identity);
|
|
136
|
+
exactSet.addAvailable(identity); // add only after content validation
|
|
137
|
+
exactSet.validate();
|
|
129
138
|
```
|
|
130
139
|
|
|
131
140
|
`productIdentity` returns publisher, solution class, campaign, date, issue,
|
|
@@ -136,8 +145,36 @@ Changing the source cannot change the exact product. Unsupported combinations
|
|
|
136
145
|
throw instead of selecting another center, tier, issue, date, cadence, or
|
|
137
146
|
family.
|
|
138
147
|
|
|
148
|
+
`GnssExactProductSet.validate()` rejects an empty declaration, duplicates,
|
|
149
|
+
missing products, undeclared products, and same-filename identities with
|
|
150
|
+
different prediction metadata. Start dependent processing only after it
|
|
151
|
+
returns successfully. For SP3 observed/predicted timing, use
|
|
152
|
+
`sp3.predictionSummary()`; do not infer the boundary from issue times or catalog
|
|
153
|
+
fields.
|
|
154
|
+
|
|
139
155
|
Browser and Node callers retain control of `fetch`, Earthdata credentials,
|
|
140
|
-
retries, and
|
|
156
|
+
retries, and product-format validation. The browser cache uses the same WASM
|
|
157
|
+
commit builder/verifier as the other interfaces:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
import { BrowserExactProductCache } from "@neilberkman/sidereon/exact-cache";
|
|
161
|
+
|
|
162
|
+
const cache = await BrowserExactProductCache.open();
|
|
163
|
+
const entry = await cache.withLock(identity, "nasa_cddis", async (locked) => {
|
|
164
|
+
const hit = await locked.read();
|
|
165
|
+
if (hit) return hit;
|
|
166
|
+
// Fetch and validate first; these are the resulting immutable bytes.
|
|
167
|
+
return locked.publish(productBytes, archiveBytes, provenanceBytes);
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Web Locks coordinate same-origin tabs and workers with a bounded wait.
|
|
172
|
+
IndexedDB publishes the immutable entry and marker in one strict-durability
|
|
173
|
+
transaction; reads return bytes only after shared schema-v3 verification. Node
|
|
174
|
+
hosts can call `buildExactCacheCommit` and `verifyExactCacheCommit` with their
|
|
175
|
+
own storage transaction.
|
|
176
|
+
|
|
177
|
+
Send credentials only to NASA's documented hosts;
|
|
141
178
|
remove URL queries from diagnostics; reject HTML success bodies; validate
|
|
142
179
|
content length, gzip completion, and hashes; then parse with `loadSp3` or
|
|
143
180
|
`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.30.0",
|
|
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"
|