@saber-usa/node-common 1.7.17 → 1.7.18-alpha.1
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/README.md +20 -0
- package/package.json +6 -2
- package/src/astro.js +400 -277
- package/src/ballisticPropagator.js +1 -1
- package/src/index.js +3 -0
- package/src/wasmProp/index.js +10 -0
- package/src/wasmProp/primitives.js +295 -0
- package/src/wasmProp/runtime.js +147 -0
- package/src/wasmProp/wasmAstro.js +251 -0
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./transform.js";
|
|
|
4
4
|
export * from "./checkNetwork.cjs";
|
|
5
5
|
export * from "./fixDate.js";
|
|
6
6
|
export * from "./astro.js";
|
|
7
|
+
export * from "./wasmProp/index.js";
|
|
7
8
|
export * from "./launchNominal.js";
|
|
8
9
|
export * from "./LaunchNominalClass.js";
|
|
9
10
|
export * from "./OrbitUtils.js";
|
|
@@ -22,6 +23,7 @@ import * as transformNS from "./transform.js";
|
|
|
22
23
|
import * as checkNetworkNS from "./checkNetwork.cjs";
|
|
23
24
|
import * as fixDateNS from "./fixDate.js";
|
|
24
25
|
import * as astroNS from "./astro.js";
|
|
26
|
+
import * as wasmPropNS from "./wasmProp/index.js";
|
|
25
27
|
import * as launchNominalNS from "./launchNominal.js";
|
|
26
28
|
import * as LaunchNominalClassNS from "./LaunchNominalClass.js";
|
|
27
29
|
import * as OrbitUtilsNS from "./OrbitUtils.js";
|
|
@@ -35,6 +37,7 @@ const aggregate = {
|
|
|
35
37
|
...checkNetworkNS,
|
|
36
38
|
...fixDateNS,
|
|
37
39
|
...astroNS,
|
|
40
|
+
...wasmPropNS,
|
|
38
41
|
...launchNominalNS,
|
|
39
42
|
...LaunchNominalClassNS,
|
|
40
43
|
...OrbitUtilsNS,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Barrel for all WASM-backed bulk propagation exports. Kept behind a
|
|
2
|
+
// single entry point so the rest of the codebase (and external consumers
|
|
3
|
+
// via `@saber-usa/node-common`) can discover the bulk API without having
|
|
4
|
+
// to reach into per-feature files.
|
|
5
|
+
//
|
|
6
|
+
// See `docs/BULK_PROPAGATION.md` for the design rationale, lifecycle, and
|
|
7
|
+
// the measured performance characteristics.
|
|
8
|
+
export * from "./runtime.js";
|
|
9
|
+
export * from "./primitives.js";
|
|
10
|
+
export * from "./wasmAstro.js";
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
twoline2satrec,
|
|
3
|
+
EciBaseCalculator,
|
|
4
|
+
GmstCalculator,
|
|
5
|
+
EcfPositionCalculator,
|
|
6
|
+
GeodeticPositionCalculator,
|
|
7
|
+
LookAnglesCalculator,
|
|
8
|
+
} from "satellite.js";
|
|
9
|
+
import {getBulkRuntime, getOrCreateBulkPropagator} from "./runtime.js";
|
|
10
|
+
import {RAD2DEG} from "../constants.js";
|
|
11
|
+
|
|
12
|
+
// ----------------------------------------------------------------------------
|
|
13
|
+
// WASM-backed bulk primitives
|
|
14
|
+
//
|
|
15
|
+
// Sibling functions to `prop` / `propGeodetic` / the manual LookAngles chain
|
|
16
|
+
// that live in `../astro.js`. The originals stay synchronous; these are
|
|
17
|
+
// async because the per-thread `BulkPropagator` runtime is async-bootstrapped
|
|
18
|
+
// (see `docs/BULK_PROPAGATION.md`).
|
|
19
|
+
//
|
|
20
|
+
// Time-grid semantics are matched exactly to the JS-path siblings to keep
|
|
21
|
+
// parity tests trivial:
|
|
22
|
+
// - `propBulk` mirrors `prop` -> half-open [start, end)
|
|
23
|
+
// - `propGeodeticBulk` mirrors `propGeodetic` -> closed [start, end]
|
|
24
|
+
//
|
|
25
|
+
// Return shapes mirror the JS-path siblings element-wise so callers can swap
|
|
26
|
+
// transparently. `meanElements` is intentionally absent from `propBulk`
|
|
27
|
+
// records: the WASM `EciBaseCalculator` exposes only position/velocity/error,
|
|
28
|
+
// it does not surface SGP4 mean elements. Callers that need them must either
|
|
29
|
+
// stay on `prop` or recompute osculating elements from p/v.
|
|
30
|
+
// ----------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds the closed list of evaluation timestamps for a half-open or closed
|
|
34
|
+
* interval, matching the loop semantics of `prop` / `propGeodetic`.
|
|
35
|
+
*
|
|
36
|
+
* @param {number} start
|
|
37
|
+
* @param {number} end
|
|
38
|
+
* @param {number} stepMs
|
|
39
|
+
* @param {boolean} inclusive when true, end is included (propGeodetic).
|
|
40
|
+
* @return {number[]}
|
|
41
|
+
*/
|
|
42
|
+
const buildTimeGrid = (start, end, stepMs, inclusive) => {
|
|
43
|
+
const grid = [];
|
|
44
|
+
if (inclusive) {
|
|
45
|
+
for (let t = start; t <= end; t = t + stepMs) {grid.push(t);}
|
|
46
|
+
} else {
|
|
47
|
+
for (let t = start; t < end; t = t + stepMs) {grid.push(t);}
|
|
48
|
+
}
|
|
49
|
+
return grid;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Bulk-converts an array of `{Line1, Line2}` elsets into satrecs, returning
|
|
54
|
+
* `null` if any single elset fails to convert. Mirrors the silent-skip
|
|
55
|
+
* behavior of `prop` (which returns `[]` on the first invalid pv).
|
|
56
|
+
*
|
|
57
|
+
* @param {Array<{Line1: string, Line2: string}>} elsets
|
|
58
|
+
* @return {Array|null}
|
|
59
|
+
*/
|
|
60
|
+
const elsetsToSatrecs = (elsets) => {
|
|
61
|
+
const satrecs = elsets.map((e) => twoline2satrec(e.Line1, e.Line2));
|
|
62
|
+
return satrecs;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* WASM-backed sibling of `prop`. Computes ECI position and velocity for
|
|
67
|
+
* `elsets.length` satellites at every step in the half-open interval
|
|
68
|
+
* `[start, end)` with step `stepMs`.
|
|
69
|
+
*
|
|
70
|
+
* Returns `Array<Array<{p, v, t}>>` indexed `[satIdx][dateIdx]`. Each inner
|
|
71
|
+
* array matches the shape returned by `prop` minus the `meanElements` field
|
|
72
|
+
* (see module-level note). If the WASM SGP4 produces a non-zero error code
|
|
73
|
+
* for any (sat, date) pair, that satellite's entire ephemeris is returned as
|
|
74
|
+
* `[]` — same all-or-nothing semantic as `prop` on `propTo` failure.
|
|
75
|
+
*
|
|
76
|
+
* @param {Array<{Line1: string, Line2: string}>} elsets
|
|
77
|
+
* @param {number} start UNIX milliseconds
|
|
78
|
+
* @param {number} end UNIX milliseconds (exclusive)
|
|
79
|
+
* @param {number} stepMs default 1000
|
|
80
|
+
* @return {Promise<Array<Array<{p: object, v: object, t: number}>>>}
|
|
81
|
+
*/
|
|
82
|
+
const propBulk = async (elsets, start, end, stepMs = 1000) => {
|
|
83
|
+
const grid = buildTimeGrid(start, end, stepMs, false);
|
|
84
|
+
if (elsets.length === 0 || grid.length === 0) {
|
|
85
|
+
return elsets.map(() => []);
|
|
86
|
+
}
|
|
87
|
+
const satrecs = elsetsToSatrecs(elsets);
|
|
88
|
+
const dates = grid.map((t) => new Date(t));
|
|
89
|
+
|
|
90
|
+
const runtime = await getBulkRuntime();
|
|
91
|
+
const propagator = getOrCreateBulkPropagator({
|
|
92
|
+
runtime,
|
|
93
|
+
key: "eci",
|
|
94
|
+
makeCalculators: () => [new EciBaseCalculator()],
|
|
95
|
+
satRecsCount: satrecs.length,
|
|
96
|
+
datesCount: dates.length,
|
|
97
|
+
});
|
|
98
|
+
propagator.setSatRecs(satrecs);
|
|
99
|
+
propagator.setDates(dates);
|
|
100
|
+
propagator.run();
|
|
101
|
+
|
|
102
|
+
const {eci} = propagator.getRawOutput();
|
|
103
|
+
// raw layout: [sat0 date0 (x,y,z), sat0 date1 (x,y,z), ..., sat1 date0, ...]
|
|
104
|
+
// error layout: [sat0 date0, sat0 date1, ..., sat1 date0, ...]
|
|
105
|
+
const out = new Array(satrecs.length);
|
|
106
|
+
const m = dates.length;
|
|
107
|
+
for (let s = 0; s < satrecs.length; s++) {
|
|
108
|
+
const ephem = new Array(m);
|
|
109
|
+
let satFailed = false;
|
|
110
|
+
for (let d = 0; d < m; d++) {
|
|
111
|
+
const errIdx = s * m + d;
|
|
112
|
+
if (eci.error[errIdx] !== 0) {
|
|
113
|
+
satFailed = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const vec = (s * m + d) * 3;
|
|
117
|
+
ephem[d] = {
|
|
118
|
+
p: {
|
|
119
|
+
x: eci.position[vec],
|
|
120
|
+
y: eci.position[vec + 1],
|
|
121
|
+
z: eci.position[vec + 2],
|
|
122
|
+
},
|
|
123
|
+
v: {
|
|
124
|
+
x: eci.velocity[vec],
|
|
125
|
+
y: eci.velocity[vec + 1],
|
|
126
|
+
z: eci.velocity[vec + 2],
|
|
127
|
+
},
|
|
128
|
+
t: grid[d],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
out[s] = satFailed ? [] : ephem;
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* WASM-backed sibling of `propGeodetic`. Computes geodetic
|
|
138
|
+
* (lat, lon, t) for every satellite at every step in the closed interval
|
|
139
|
+
* `[start, end]`.
|
|
140
|
+
*
|
|
141
|
+
* Lat/Lon are returned in **degrees** to match `propGeodetic`. Returns
|
|
142
|
+
* `Array<Array<{lat, lon, t}>>` indexed `[satIdx][dateIdx]`. On any SGP4
|
|
143
|
+
* error for a satellite the whole ephemeris for that satellite is `[]`,
|
|
144
|
+
* matching `propGeodetic`'s short-circuit behavior.
|
|
145
|
+
*
|
|
146
|
+
* @param {Array<{Line1: string, Line2: string}>} elsets
|
|
147
|
+
* @param {number} start UNIX milliseconds
|
|
148
|
+
* @param {number} end UNIX milliseconds (inclusive)
|
|
149
|
+
* @param {number} stepMs default 60000
|
|
150
|
+
* @return {Promise<Array<Array<{lat: number, lon: number, t: number}>>>}
|
|
151
|
+
*/
|
|
152
|
+
const propGeodeticBulk = async (elsets, start, end, stepMs = 60000) => {
|
|
153
|
+
const grid = buildTimeGrid(start, end, stepMs, true);
|
|
154
|
+
if (elsets.length === 0 || grid.length === 0) {
|
|
155
|
+
return elsets.map(() => []);
|
|
156
|
+
}
|
|
157
|
+
const satrecs = elsetsToSatrecs(elsets);
|
|
158
|
+
const dates = grid.map((t) => new Date(t));
|
|
159
|
+
|
|
160
|
+
const runtime = await getBulkRuntime();
|
|
161
|
+
const propagator = getOrCreateBulkPropagator({
|
|
162
|
+
runtime,
|
|
163
|
+
key: "eci+gmst+geo",
|
|
164
|
+
makeCalculators: () => [
|
|
165
|
+
new EciBaseCalculator(),
|
|
166
|
+
new GmstCalculator(),
|
|
167
|
+
new GeodeticPositionCalculator(),
|
|
168
|
+
],
|
|
169
|
+
satRecsCount: satrecs.length,
|
|
170
|
+
datesCount: dates.length,
|
|
171
|
+
});
|
|
172
|
+
propagator.setSatRecs(satrecs);
|
|
173
|
+
propagator.setDates(dates);
|
|
174
|
+
propagator.run();
|
|
175
|
+
|
|
176
|
+
const {eci, geodeticPosition} = propagator.getRawOutput();
|
|
177
|
+
// KNOWN satellite.js@7.0.0 BUG / docs error: the
|
|
178
|
+
// `GeodeticPositionCalculator` raw layout is documented as
|
|
179
|
+
// `[lat, lon, height, ...]`, and `getFormattedOutput()` exposes fields
|
|
180
|
+
// `{latitude: raw[0], longitude: raw[1], height: raw[2]}` — but the
|
|
181
|
+
// physical values are SWAPPED. Empirically, against the canonical JS
|
|
182
|
+
// `eciToGeodetic` path:
|
|
183
|
+
// raw[3*k + 0] -> physical LONGITUDE (in radians)
|
|
184
|
+
// raw[3*k + 1] -> physical LATITUDE (in radians)
|
|
185
|
+
// raw[3*k + 2] -> height (km)
|
|
186
|
+
// We map raw -> {lat, lon} accordingly so `propGeodeticBulk` is exactly
|
|
187
|
+
// value-equivalent to `propGeodetic`. If a future satellite.js release
|
|
188
|
+
// fixes this upstream, swap the indices back here.
|
|
189
|
+
const out = new Array(satrecs.length);
|
|
190
|
+
const m = dates.length;
|
|
191
|
+
for (let s = 0; s < satrecs.length; s++) {
|
|
192
|
+
const positions = new Array(m);
|
|
193
|
+
let satFailed = false;
|
|
194
|
+
for (let d = 0; d < m; d++) {
|
|
195
|
+
const errIdx = s * m + d;
|
|
196
|
+
if (eci.error[errIdx] !== 0) {
|
|
197
|
+
satFailed = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
const vec = (s * m + d) * 3;
|
|
201
|
+
positions[d] = {
|
|
202
|
+
lat: geodeticPosition[vec + 1] * RAD2DEG,
|
|
203
|
+
lon: geodeticPosition[vec] * RAD2DEG,
|
|
204
|
+
t: grid[d],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
out[s] = satFailed ? [] : positions;
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* WASM-backed bulk look-angles primitive. Computes (azimuth, elevation,
|
|
214
|
+
* rangeSat) for every satellite at every supplied date relative to a single
|
|
215
|
+
* observer (Geodetic location, **radians + km** as required by satellite.js
|
|
216
|
+
* v7 — `{latitude, longitude, height}`).
|
|
217
|
+
*
|
|
218
|
+
* Unlike `propBulk` and `propGeodeticBulk` this primitive takes an explicit
|
|
219
|
+
* `dates` array (rather than start/end/step) because look-angles consumers
|
|
220
|
+
* typically already hold the date list (e.g. observation timestamps in
|
|
221
|
+
* `GetResiduals`) and synthesizing a regular grid would re-impose JS-side
|
|
222
|
+
* step math the WASM pipeline is meant to avoid.
|
|
223
|
+
*
|
|
224
|
+
* Returns `Array<Array<{azimuth, elevation, rangeSat, t}>>` indexed
|
|
225
|
+
* `[satIdx][dateIdx]`, with angles in **radians** and `rangeSat` in km. On
|
|
226
|
+
* any SGP4 error for a satellite that satellite's entire output is `[]`.
|
|
227
|
+
*
|
|
228
|
+
* @param {Array<{Line1: string, Line2: string}>} elsets
|
|
229
|
+
* @param {Date[]|number[]} dates
|
|
230
|
+
* @param {{latitude: number, longitude: number, height: number}} observerGd
|
|
231
|
+
* Observer position in radians (lat/lon) and km (height).
|
|
232
|
+
* @return {Promise<Array<Array<{azimuth: number, elevation: number, rangeSat: number, t: number}>>>}
|
|
233
|
+
*/
|
|
234
|
+
const propLookAnglesBulk = async (elsets, dates, observerGd) => {
|
|
235
|
+
if (elsets.length === 0 || dates.length === 0) {
|
|
236
|
+
return elsets.map(() => []);
|
|
237
|
+
}
|
|
238
|
+
const satrecs = elsetsToSatrecs(elsets);
|
|
239
|
+
const dateObjs = dates.map((d) => (d instanceof Date ? d : new Date(d)));
|
|
240
|
+
const ts = dateObjs.map((d) => d.getTime());
|
|
241
|
+
|
|
242
|
+
const runtime = await getBulkRuntime();
|
|
243
|
+
const propagator = getOrCreateBulkPropagator({
|
|
244
|
+
runtime,
|
|
245
|
+
key: "eci+gmst+ecf+lookAngles",
|
|
246
|
+
makeCalculators: () => [
|
|
247
|
+
new EciBaseCalculator(),
|
|
248
|
+
new GmstCalculator(),
|
|
249
|
+
new EcfPositionCalculator(),
|
|
250
|
+
new LookAnglesCalculator(),
|
|
251
|
+
],
|
|
252
|
+
satRecsCount: satrecs.length,
|
|
253
|
+
datesCount: dateObjs.length,
|
|
254
|
+
});
|
|
255
|
+
propagator.setSatRecs(satrecs);
|
|
256
|
+
propagator.setDates(dateObjs);
|
|
257
|
+
propagator.run({lookAngles: {observer: observerGd}});
|
|
258
|
+
|
|
259
|
+
const {eci, lookAngles} = propagator.getRawOutput();
|
|
260
|
+
// lookAngles layout: [sat0 date0 (az, el, range), sat0 date1, ...]
|
|
261
|
+
const out = new Array(satrecs.length);
|
|
262
|
+
const m = dateObjs.length;
|
|
263
|
+
for (let s = 0; s < satrecs.length; s++) {
|
|
264
|
+
const samples = new Array(m);
|
|
265
|
+
let satFailed = false;
|
|
266
|
+
for (let d = 0; d < m; d++) {
|
|
267
|
+
const errIdx = s * m + d;
|
|
268
|
+
if (eci.error[errIdx] !== 0) {
|
|
269
|
+
satFailed = true;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
const vec = (s * m + d) * 3;
|
|
273
|
+
samples[d] = {
|
|
274
|
+
azimuth: lookAngles[vec],
|
|
275
|
+
elevation: lookAngles[vec + 1],
|
|
276
|
+
rangeSat: lookAngles[vec + 2],
|
|
277
|
+
t: ts[d],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
out[s] = satFailed ? [] : samples;
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export {
|
|
286
|
+
// `buildTimeGrid` is intentionally NOT exported: it is consumed only by
|
|
287
|
+
// `propBulk` / `propGeodeticBulk` inside this module. Keeping it private
|
|
288
|
+
// avoids polluting the public `@saber-usa/node-common` surface with a
|
|
289
|
+
// helper that exists purely to bridge the JS-path loop semantics to the
|
|
290
|
+
// WASM `setDates(Date[])` API.
|
|
291
|
+
elsetsToSatrecs, // used by `./wasmAstro.js#getGeoShadowZonesBulk`
|
|
292
|
+
propBulk,
|
|
293
|
+
propGeodeticBulk,
|
|
294
|
+
propLookAnglesBulk,
|
|
295
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {createSingleThreadRuntime, BulkPropagator} from "satellite.js";
|
|
2
|
+
|
|
3
|
+
// Module-local state. In Node, ES module instances are per Realm/per worker
|
|
4
|
+
// thread, so this state is naturally per-thread when consumed inside
|
|
5
|
+
// `worker_threads` (e.g. node-pub-sub's workerpool workers).
|
|
6
|
+
//
|
|
7
|
+
// See docs/BULK_PROPAGATION.md sections 6, 7, and 8 for the consumer model,
|
|
8
|
+
// lifecycle, and registry design.
|
|
9
|
+
let runtimePromise = null;
|
|
10
|
+
let registry = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lazily creates (or returns) the per-thread WASM runtime.
|
|
14
|
+
*
|
|
15
|
+
* On first call within a thread, triggers `createSingleThreadRuntime()`
|
|
16
|
+
* (the v7 successor to the blog's `createWasmModule()`) and caches the
|
|
17
|
+
* resulting promise. All subsequent callers in that thread reuse it.
|
|
18
|
+
*
|
|
19
|
+
* Safe to await concurrently: every caller awaits the same in-flight
|
|
20
|
+
* promise, so only one WASM compile + instantiate ever happens.
|
|
21
|
+
*
|
|
22
|
+
* @return {Promise<import("satellite.js").SingleThreadRuntime>}
|
|
23
|
+
*/
|
|
24
|
+
const getBulkRuntime = () => {
|
|
25
|
+
if (runtimePromise === null) {
|
|
26
|
+
runtimePromise = createSingleThreadRuntime();
|
|
27
|
+
}
|
|
28
|
+
return runtimePromise;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Eager bootstrap. Equivalent to `await getBulkRuntime()` but named for the
|
|
33
|
+
* explicit warm-up call site (e.g. top of `dedicated_worker.js`).
|
|
34
|
+
*
|
|
35
|
+
* @return {Promise<import("satellite.js").SingleThreadRuntime>}
|
|
36
|
+
*/
|
|
37
|
+
const initBulkRuntime = () => getBulkRuntime();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if the runtime has been requested in this thread (regardless
|
|
41
|
+
* of whether it has finished initializing). Used by tests and by the
|
|
42
|
+
* disposal path to decide whether dispose work is necessary.
|
|
43
|
+
*
|
|
44
|
+
* @return {boolean}
|
|
45
|
+
*/
|
|
46
|
+
const isBulkRuntimeInitialized = () => runtimePromise !== null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Acquires (or lazily constructs) the per-thread, per-pipeline singleton
|
|
50
|
+
* `BulkPropagator` for the given pipeline key.
|
|
51
|
+
*
|
|
52
|
+
* Reuse rules:
|
|
53
|
+
* - The same instance is returned on every call with the same `key`.
|
|
54
|
+
* - If the caller's `satRecsCount` or `datesCount` exceed the current
|
|
55
|
+
* allocation, the propagator's own `setSatRecs` / `setDates` will perform
|
|
56
|
+
* a single `free` + `malloc` grow-realloc internally on the next set.
|
|
57
|
+
* The initial allocation here is sized to the first caller's request, so
|
|
58
|
+
* "warm" subsequent calls of the same shape pay zero allocation cost.
|
|
59
|
+
*
|
|
60
|
+
* Caller responsibilities:
|
|
61
|
+
* - `setSatRecs`, `setDates`, then `run()` between `acquire` and using
|
|
62
|
+
* results. Within a single thread these are synchronous so no other
|
|
63
|
+
* pipeline consumer can interleave (per docs/BULK_PROPAGATION.md §9).
|
|
64
|
+
* - **Do not call `dispose()` on the returned propagator.** Lifecycle is
|
|
65
|
+
* owned by the registry; `disposeBulkRuntime()` is the only sanctioned
|
|
66
|
+
* teardown path.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} params
|
|
69
|
+
* @param {import("satellite.js").SingleThreadRuntime} params.runtime
|
|
70
|
+
* @param {string} params.key Pipeline signature, e.g. `"eci"`.
|
|
71
|
+
* @param {() => readonly any[]} params.makeCalculators Factory invoked once
|
|
72
|
+
* per (key, thread) to build fresh calculator instances. A factory is
|
|
73
|
+
* required because calculator instances are stateful and bound to a
|
|
74
|
+
* propagator at construction time; they cannot be shared across keys.
|
|
75
|
+
* @param {number} params.satRecsCount Initial satellite-record allocation
|
|
76
|
+
* hint. Used only on first construction for this key in this thread.
|
|
77
|
+
* @param {number} params.datesCount Initial dates allocation hint. Same
|
|
78
|
+
* first-call-only semantics.
|
|
79
|
+
* @return {import("satellite.js").BulkPropagator<any, any>}
|
|
80
|
+
*/
|
|
81
|
+
const getOrCreateBulkPropagator = ({
|
|
82
|
+
runtime,
|
|
83
|
+
key,
|
|
84
|
+
makeCalculators,
|
|
85
|
+
satRecsCount,
|
|
86
|
+
datesCount,
|
|
87
|
+
}) => {
|
|
88
|
+
const cached = registry.get(key);
|
|
89
|
+
if (cached) {
|
|
90
|
+
return cached;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const propagator = new BulkPropagator({
|
|
94
|
+
runtime,
|
|
95
|
+
calculators: makeCalculators(),
|
|
96
|
+
satRecsCount: Math.max(1, satRecsCount),
|
|
97
|
+
datesCount: Math.max(1, datesCount),
|
|
98
|
+
});
|
|
99
|
+
registry.set(key, propagator);
|
|
100
|
+
return propagator;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Releases all WASM memory owned by this thread: every registry-held
|
|
105
|
+
* `BulkPropagator` is disposed and the runtime itself is torn down.
|
|
106
|
+
*
|
|
107
|
+
* Idempotent: safe to call when nothing was ever initialized. After this
|
|
108
|
+
* returns, the next `getBulkRuntime()` will lazily re-initialize from
|
|
109
|
+
* scratch.
|
|
110
|
+
*/
|
|
111
|
+
const disposeBulkRuntime = async () => {
|
|
112
|
+
for (const propagator of registry.values()) {
|
|
113
|
+
try {
|
|
114
|
+
propagator.dispose();
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// Disposal must never throw out of teardown; we surface the
|
|
117
|
+
// failure to stderr but continue cleaning up the rest of the
|
|
118
|
+
// registry to minimize the leak surface.
|
|
119
|
+
console.error("disposeBulkRuntime: failed to dispose propagator", e);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
registry = new Map();
|
|
123
|
+
|
|
124
|
+
if (runtimePromise !== null) {
|
|
125
|
+
const pending = runtimePromise;
|
|
126
|
+
runtimePromise = null;
|
|
127
|
+
try {
|
|
128
|
+
const runtime = await pending;
|
|
129
|
+
runtime.dispose();
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Emscripten's `_exit_runtime` (with EXIT_RUNTIME=1) signals clean
|
|
132
|
+
// shutdown by throwing an `ExitStatus` object with `status === 0`.
|
|
133
|
+
// That is the documented success path, not a failure to log.
|
|
134
|
+
if (e?.name !== "ExitStatus" || e?.status !== 0) {
|
|
135
|
+
console.error("disposeBulkRuntime: failed to dispose runtime", e);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
getBulkRuntime,
|
|
143
|
+
initBulkRuntime,
|
|
144
|
+
isBulkRuntimeInitialized,
|
|
145
|
+
getOrCreateBulkPropagator,
|
|
146
|
+
disposeBulkRuntime,
|
|
147
|
+
};
|