@hla4ts/spacekit 0.1.0 → 0.1.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/src/see-app.ts CHANGED
@@ -1,460 +1,460 @@
1
- import { CallbackModel, encodeHLAinteger64Time, type LogicalTime } from "@hla4ts/hla-api";
2
- import {
3
- buildDeclarativeFomFromDecorators,
4
- registerDecoratedClasses,
5
- type FomClassConstructor,
6
- } from "./decorators.ts";
7
- import { SpacekitApp as BaseSpacekitApp, type SpacekitAppConfig as BaseSpacekitAppConfig } from "./app.ts";
8
- import { parseSpacekitEnv, type LogLevel, type SpacekitEnvDefaults, type SpacekitLogger } from "./env.ts";
9
- import type { SpacekitEntity } from "./entity.ts";
10
- import { createConsoleLogger } from "./logger.ts";
11
- import { DynamicalEntity, ExecutionConfiguration, PhysicalEntity, ReferenceFrame } from "./spacefom-entities.ts";
12
- import { ModeTransitionRequest } from "./spacefom-interactions.ts";
13
- import { SpaceFomLateJoinerBootstrap, type SpaceFomBootstrapState } from "./spacefom-bootstrap.ts";
14
-
15
- export type MissingReferenceFrameMode = "error" | "continue";
16
-
17
- export interface SpacekitAppOptions {
18
- federationName?: string;
19
- federateName?: string;
20
- federateType?: string;
21
- rtiHost?: string;
22
- rtiPort?: number;
23
- rtiUseTls?: boolean;
24
- lookaheadMicros?: bigint;
25
- updatePeriodMicros?: bigint;
26
- referenceFrameTimeoutMs?: number;
27
- logLevel?: LogLevel;
28
- logEveryN?: number;
29
- debug?: boolean;
30
- logStartup?: boolean;
31
- logBootstrap?: boolean;
32
- enableGracefulShutdown?: boolean;
33
- callbackModel?: CallbackModel;
34
- logger?: SpacekitLogger;
35
- env?: Record<string, string | undefined>;
36
- envDefaults?: SpacekitEnvDefaults;
37
- bootstrap?: SpaceFomLateJoinerBootstrap;
38
- waitForReferenceFrameTree?: boolean;
39
- autoWaitForReferenceFrame?: boolean;
40
- missingReferenceFrameMode?: MissingReferenceFrameMode;
41
- declarative?: BaseSpacekitAppConfig["declarative"];
42
- fomExport?: BaseSpacekitAppConfig["fomExport"];
43
- }
44
-
45
- const BASE_OBJECT_CLASSES = new Set<FomClassConstructor>([
46
- ExecutionConfiguration,
47
- ReferenceFrame,
48
- PhysicalEntity,
49
- DynamicalEntity,
50
- ]);
51
- const BASE_INTERACTION_CLASSES = new Set<FomClassConstructor>([ModeTransitionRequest]);
52
-
53
- export class SpacekitApp extends BaseSpacekitApp {
54
- private _logicalTimeMicros: bigint | null = null;
55
- private readonly _lookaheadMicros: bigint;
56
- private readonly _updatePeriodMicros: bigint;
57
- private readonly _referenceFrameTimeoutMs: number;
58
- private readonly _autoWaitForReferenceFrame: boolean;
59
- private readonly _missingReferenceFrameMode: MissingReferenceFrameMode;
60
- private readonly _referenceFrameGraceMs = 2000;
61
- private _referenceFrameFallbackSubscribe = true;
62
- private _referenceFrameNameProbe = true;
63
- private readonly _logStartup: boolean;
64
- private readonly _logBootstrap: boolean;
65
- private readonly _logEveryN: number;
66
- private readonly _startupInfo: {
67
- federationName: string;
68
- federateName: string;
69
- federateType: string;
70
- rtiHost: string;
71
- rtiPort: number;
72
- rtiUseTls: boolean;
73
- };
74
- private _bootstrapLogged = false;
75
- private _tickCount = 0;
76
-
77
- constructor(options: SpacekitAppOptions = {}) {
78
- const env = parseSpacekitEnv({
79
- env: options.env,
80
- defaults: options.envDefaults ?? {
81
- rtiHost: "localhost",
82
- rtiPort: 15164,
83
- rtiUseTls: false,
84
- federationName: "SpaceFederation",
85
- federateName: "SpacekitFederate",
86
- federateType: "SpacekitFederate",
87
- lookaheadMicros: 1_000_000n,
88
- updatePeriodMicros: 1_000_000n,
89
- referenceFrameTimeoutMs: 30_000,
90
- logLevel: "info",
91
- logEveryN: 1,
92
- debug: false,
93
- },
94
- });
95
-
96
- const resolved = {
97
- ...env,
98
- federationName: options.federationName ?? env.federationName,
99
- federateName: options.federateName ?? env.federateName,
100
- federateType: options.federateType ?? env.federateType,
101
- rtiHost: options.rtiHost ?? env.rtiHost,
102
- rtiPort: options.rtiPort ?? env.rtiPort,
103
- rtiUseTls: options.rtiUseTls ?? env.rtiUseTls,
104
- lookaheadMicros: options.lookaheadMicros ?? env.lookaheadMicros,
105
- updatePeriodMicros: options.updatePeriodMicros ?? env.updatePeriodMicros,
106
- referenceFrameTimeoutMs: options.referenceFrameTimeoutMs ?? env.referenceFrameTimeoutMs,
107
- logLevel: options.logLevel ?? env.logLevel,
108
- logEveryN: options.logEveryN ?? env.logEveryN,
109
- debug: options.debug ?? env.debug,
110
- };
111
-
112
- const logger =
113
- options.logger ??
114
- createConsoleLogger({
115
- level: resolved.logLevel,
116
- scope: "spacekit",
117
- });
118
-
119
- const bootstrap =
120
- options.bootstrap ??
121
- new SpaceFomLateJoinerBootstrap({
122
- lookaheadMicros: resolved.lookaheadMicros,
123
- waitForReferenceFrameTree: options.waitForReferenceFrameTree ?? false,
124
- referenceFrameTimeoutMs: resolved.referenceFrameTimeoutMs,
125
- waitForInitializationCompleted: false,
126
- waitForExco: false,
127
- advanceToHltb: false,
128
- });
129
-
130
- const declarative =
131
- options.declarative ??
132
- buildDeclarativeFomFromDecorators({
133
- objectClasses: [ExecutionConfiguration, ReferenceFrame, PhysicalEntity, DynamicalEntity],
134
- interactionClasses: [ModeTransitionRequest],
135
- register: false,
136
- });
137
-
138
- if (options.declarative) {
139
- registerDecoratedClasses(declarative, {
140
- objectClasses: [ExecutionConfiguration, ReferenceFrame, PhysicalEntity, DynamicalEntity],
141
- interactionClasses: [ModeTransitionRequest],
142
- register: false,
143
- });
144
- }
145
-
146
- super({
147
- federate: {
148
- rti: { host: resolved.rtiHost, port: resolved.rtiPort, useTls: resolved.rtiUseTls },
149
- connection: { callbackModel: options.callbackModel ?? CallbackModel.IMMEDIATE },
150
- federationName: resolved.federationName,
151
- federateType: resolved.federateType,
152
- federateName: resolved.federateName,
153
- },
154
- bootstrap,
155
- declarative,
156
- logging: {
157
- logger,
158
- level: resolved.logLevel,
159
- scope: "spacekit",
160
- },
161
- fomExport: {
162
- inferParentStubs: options.fomExport?.inferParentStubs ?? true,
163
- ...options.fomExport,
164
- },
165
- });
166
-
167
- this._lookaheadMicros = resolved.lookaheadMicros;
168
- this._updatePeriodMicros = resolved.updatePeriodMicros;
169
- this._referenceFrameTimeoutMs = resolved.referenceFrameTimeoutMs;
170
- this._autoWaitForReferenceFrame = options.autoWaitForReferenceFrame ?? true;
171
- this._missingReferenceFrameMode = options.missingReferenceFrameMode ?? "error";
172
- this._logStartup = options.logStartup ?? true;
173
- this._logBootstrap = options.logBootstrap ?? true;
174
- this._logEveryN = resolved.logEveryN ?? 1;
175
- this._startupInfo = {
176
- federationName: resolved.federationName,
177
- federateName: resolved.federateName,
178
- federateType: resolved.federateType,
179
- rtiHost: resolved.rtiHost,
180
- rtiPort: resolved.rtiPort,
181
- rtiUseTls: resolved.rtiUseTls,
182
- };
183
- if (options.enableGracefulShutdown ?? true) {
184
- this.enableGracefulShutdown();
185
- }
186
- }
187
-
188
- get lookaheadMicros(): bigint {
189
- return this._lookaheadMicros;
190
- }
191
-
192
- get updatePeriodMicros(): bigint {
193
- return this._updatePeriodMicros;
194
- }
195
-
196
- get logicalTimeMicros(): bigint | null {
197
- return this._logicalTimeMicros;
198
- }
199
-
200
- get spacefomState(): SpaceFomBootstrapState | undefined {
201
- return this.bootstrapState as SpaceFomBootstrapState | undefined;
202
- }
203
-
204
- override async start(): Promise<void> {
205
- if (this._logStartup) {
206
- this.logger.info("Starting Spacekit SEE app.", {
207
- federation: this._startupInfo.federationName,
208
- federate: this._startupInfo.federateName,
209
- federateType: this._startupInfo.federateType,
210
- rti: `${this._startupInfo.rtiHost}:${this._startupInfo.rtiPort}`,
211
- tls: this._startupInfo.rtiUseTls,
212
- });
213
- }
214
- await super.start();
215
- const state = this.spacefomState;
216
- this._logicalTimeMicros = state?.lastGrantedTimeMicros ?? this.currentTimeMicros;
217
- }
218
-
219
- async tick(): Promise<bigint> {
220
- const base = this._logicalTimeMicros ?? (await this.queryLogicalTimeMicros());
221
- const next = base + this._updatePeriodMicros;
222
- this._logicalTimeMicros = await this.advanceTo(next);
223
- return this._logicalTimeMicros;
224
- }
225
-
226
- async run(options: {
227
- entities?: SpacekitEntity<object>[];
228
- onTick?: (payload: {
229
- timeMicros: bigint;
230
- sendTimeMicros: bigint | null;
231
- timeSeconds: number;
232
- deltaSeconds: number;
233
- tick: number;
234
- initial: boolean;
235
- }) => void | Promise<void>;
236
- sync?: boolean;
237
- throwOnError?: boolean;
238
- onError?: (err: unknown) => void;
239
- } = {}): Promise<void> {
240
- let runError: unknown;
241
- const entities = options.entities ?? [];
242
- try {
243
- if (!this.isStarted) {
244
- const registrations = entities.map((entity) => this.registerEntity(entity));
245
- await this.start();
246
- await Promise.all(registrations);
247
- } else if (entities.length > 0) {
248
- await this.registerEntities(entities);
249
- }
250
-
251
- let tick = 0;
252
- this._tickCount = tick;
253
- let timeMicros = this.logicalTimeMicros ?? (await this.queryLogicalTimeMicros());
254
- const deltaSeconds = Number(this._updatePeriodMicros) / 1_000_000;
255
- if (this._logBootstrap && !this._bootstrapLogged) {
256
- const state = this.spacefomState;
257
- this.logger.info("Spacekit SEE bootstrap complete.", {
258
- lastGrantedTimeMicros: state?.lastGrantedTimeMicros?.toString() ?? "unknown",
259
- lctsMicros: state?.lctsMicros?.toString() ?? "unknown",
260
- rootFrame: state?.rootFrameName ?? "unknown",
261
- });
262
- this._bootstrapLogged = true;
263
- }
264
- if (options.onTick) {
265
- await options.onTick({
266
- timeMicros,
267
- sendTimeMicros: this.getSendTimeMicros(timeMicros),
268
- timeSeconds: Number(timeMicros) / 1_000_000,
269
- deltaSeconds: 0,
270
- tick,
271
- initial: true,
272
- });
273
- }
274
- if (options.sync !== false && entities.length > 0) {
275
- await this.sync(entities);
276
- }
277
-
278
- while (!this.isShutdownRequested) {
279
- tick += 1;
280
- this._tickCount = tick;
281
- timeMicros = await this.tick();
282
- if (options.onTick) {
283
- await options.onTick({
284
- timeMicros,
285
- sendTimeMicros: this.getSendTimeMicros(timeMicros),
286
- timeSeconds: Number(timeMicros) / 1_000_000,
287
- deltaSeconds,
288
- tick,
289
- initial: false,
290
- });
291
- }
292
- if (options.sync !== false && entities.length > 0) {
293
- await this.sync(entities);
294
- }
295
- }
296
- } catch (err) {
297
- runError = err;
298
- this.logger.error("Spacekit SEE run failed.", { error: err });
299
- options.onError?.(err);
300
- this.requestShutdown("error");
301
- } finally {
302
- if (this.rtiAmbassador) {
303
- try {
304
- await this.stop();
305
- } catch (err) {
306
- this.logger.warn("Spacekit SEE shutdown error.", { error: err });
307
- }
308
- }
309
- }
310
-
311
- if (runError) {
312
- process.exitCode = 1;
313
- if (options.throwOnError) {
314
- throw runError;
315
- }
316
- }
317
- }
318
-
319
- getSendTimeMicros(baseTimeMicros?: bigint): bigint | null {
320
- const base = baseTimeMicros ?? this._logicalTimeMicros;
321
- if (base === null || base === undefined) return null;
322
- return base + this._lookaheadMicros;
323
- }
324
-
325
- shouldLog(updateCount: number): boolean {
326
- if (!this._logEveryN || this._logEveryN <= 1) return true;
327
- return updateCount % this._logEveryN === 0;
328
- }
329
-
330
- shouldLogTick(tick: number = this._tickCount): boolean {
331
- return this.shouldLog(tick);
332
- }
333
-
334
- shouldLogUpdate(record: { updateCount: number }): boolean {
335
- return this.shouldLog(record.updateCount);
336
- }
337
-
338
- override async syncEntity<T extends object>(
339
- entity: SpacekitEntity<T>,
340
- time?: LogicalTime | bigint,
341
- userSuppliedTag?: Uint8Array
342
- ): Promise<void> {
343
- if (typeof time === "bigint") {
344
- const logicalTime = encodeHLAinteger64Time(time + this._lookaheadMicros);
345
- await super.syncEntity(entity, logicalTime, userSuppliedTag);
346
- return;
347
- }
348
- if (!time) {
349
- const base = this._logicalTimeMicros ?? (await this.queryLogicalTimeMicros());
350
- const logicalTime = encodeHLAinteger64Time(base + this._lookaheadMicros);
351
- await super.syncEntity(entity, logicalTime, userSuppliedTag);
352
- return;
353
- }
354
- await super.syncEntity(entity, time, userSuppliedTag);
355
- }
356
-
357
- async waitForReferenceFrame(name: string, timeoutMs = this._referenceFrameTimeoutMs): Promise<void> {
358
- const start = Date.now();
359
- const effectiveTimeoutMs = timeoutMs === 0 ? this._referenceFrameGraceMs : timeoutMs;
360
- let lastNames = "";
361
- while (true) {
362
- const state = this.spacefomState;
363
- const frames = state?.referenceFrames ?? [];
364
- const names = frames
365
- .map((frame) => frame.name)
366
- .filter((frameName): frameName is string => !!frameName)
367
- .sort()
368
- .join(",");
369
- if (names && names !== lastNames) {
370
- lastNames = names;
371
- this.logger.debug("Reference frames discovered.", { names: names.split(",") });
372
- }
373
- if (frames.some((frame) => frame.name === name)) {
374
- return;
375
- }
376
- if (
377
- this._referenceFrameFallbackSubscribe &&
378
- effectiveTimeoutMs > 0 &&
379
- Date.now() - start > Math.min(effectiveTimeoutMs, 2000)
380
- ) {
381
- this._referenceFrameFallbackSubscribe = false;
382
- await this._subscribeReferenceFrameFallback();
383
- }
384
- if (
385
- this._referenceFrameNameProbe &&
386
- effectiveTimeoutMs > 0 &&
387
- Date.now() - start > Math.min(effectiveTimeoutMs, 1500)
388
- ) {
389
- this._referenceFrameNameProbe = false;
390
- const found = await this._probeReferenceFrameByName(name);
391
- if (found) {
392
- this.logger.warn("Reference frame confirmed via name lookup.", { name });
393
- return;
394
- }
395
- }
396
- if (effectiveTimeoutMs >= 0 && Date.now() - start > effectiveTimeoutMs) {
397
- const error = `Reference frame not discovered: ${name}. Known frames: ${names || "(none)"}`;
398
- if (this._missingReferenceFrameMode === "continue") {
399
- this.logger.warn("Proceeding without discovered reference frame.", {
400
- name,
401
- knownFrames: names ? names.split(",") : [],
402
- timeoutMs: effectiveTimeoutMs,
403
- });
404
- return;
405
- }
406
- throw new Error(error);
407
- }
408
- await new Promise((resolve) => setTimeout(resolve, 200));
409
- }
410
- }
411
-
412
- protected override shouldRegisterClassInFom(ctor: Function, kind: "object" | "interaction"): boolean {
413
- if (kind === "object" && BASE_OBJECT_CLASSES.has(ctor as FomClassConstructor)) {
414
- return false;
415
- }
416
- if (kind === "interaction" && BASE_INTERACTION_CLASSES.has(ctor as FomClassConstructor)) {
417
- return false;
418
- }
419
- return true;
420
- }
421
-
422
- protected override async beforeRegisterEntity<T extends object>(entity: SpacekitEntity<T>): Promise<void> {
423
- if (!this._autoWaitForReferenceFrame) return;
424
- const parentName =
425
- (entity as { parent_reference_frame?: string }).parent_reference_frame ??
426
- (entity as { parent_name?: string }).parent_name;
427
- if (!parentName) return;
428
- await this.waitForReferenceFrame(parentName);
429
- }
430
-
431
- private async _subscribeReferenceFrameFallback(): Promise<void> {
432
- const rti = this.rtiAmbassador;
433
- if (!rti) return;
434
- const adapter = this.declarative.getObjectAdapter<ReferenceFrame>(ReferenceFrame.className);
435
- if (!adapter) return;
436
- if (!adapter.isResolved) {
437
- await adapter.resolve(rti);
438
- }
439
- await adapter.subscribe(rti);
440
- this.logger.debug("Retrying ReferenceFrame subscribe fallback.");
441
- }
442
-
443
- private async _probeReferenceFrameByName(name: string): Promise<boolean> {
444
- const rti = this.rtiAmbassador;
445
- if (!rti) return false;
446
- try {
447
- const handle = await rti.getObjectInstanceHandle(name);
448
- if (!handle || handle.length === 0) return false;
449
- const adapter = this.declarative.getObjectAdapter<ReferenceFrame>(ReferenceFrame.className);
450
- if (!adapter) return true;
451
- if (!adapter.isResolved) {
452
- await adapter.resolve(rti);
453
- }
454
- await rti.requestInstanceAttributeValueUpdate(handle, adapter.attributeHandleSet, new Uint8Array());
455
- return true;
456
- } catch {
457
- return false;
458
- }
459
- }
460
- }
1
+ import { CallbackModel, encodeHLAinteger64Time, type LogicalTime } from "@hla4ts/hla-api";
2
+ import {
3
+ buildDeclarativeFomFromDecorators,
4
+ registerDecoratedClasses,
5
+ type FomClassConstructor,
6
+ } from "./decorators.ts";
7
+ import { SpacekitApp as BaseSpacekitApp, type SpacekitAppConfig as BaseSpacekitAppConfig } from "./app.ts";
8
+ import { parseSpacekitEnv, type LogLevel, type SpacekitEnvDefaults, type SpacekitLogger } from "./env.ts";
9
+ import type { SpacekitEntity } from "./entity.ts";
10
+ import { createConsoleLogger } from "./logger.ts";
11
+ import { DynamicalEntity, ExecutionConfiguration, PhysicalEntity, ReferenceFrame } from "./spacefom-entities.ts";
12
+ import { ModeTransitionRequest } from "./spacefom-interactions.ts";
13
+ import { SpaceFomLateJoinerBootstrap, type SpaceFomBootstrapState } from "./spacefom-bootstrap.ts";
14
+
15
+ export type MissingReferenceFrameMode = "error" | "continue";
16
+
17
+ export interface SpacekitAppOptions {
18
+ federationName?: string;
19
+ federateName?: string;
20
+ federateType?: string;
21
+ rtiHost?: string;
22
+ rtiPort?: number;
23
+ rtiUseTls?: boolean;
24
+ lookaheadMicros?: bigint;
25
+ updatePeriodMicros?: bigint;
26
+ referenceFrameTimeoutMs?: number;
27
+ logLevel?: LogLevel;
28
+ logEveryN?: number;
29
+ debug?: boolean;
30
+ logStartup?: boolean;
31
+ logBootstrap?: boolean;
32
+ enableGracefulShutdown?: boolean;
33
+ callbackModel?: CallbackModel;
34
+ logger?: SpacekitLogger;
35
+ env?: Record<string, string | undefined>;
36
+ envDefaults?: SpacekitEnvDefaults;
37
+ bootstrap?: SpaceFomLateJoinerBootstrap;
38
+ waitForReferenceFrameTree?: boolean;
39
+ autoWaitForReferenceFrame?: boolean;
40
+ missingReferenceFrameMode?: MissingReferenceFrameMode;
41
+ declarative?: BaseSpacekitAppConfig["declarative"];
42
+ fomExport?: BaseSpacekitAppConfig["fomExport"];
43
+ }
44
+
45
+ const BASE_OBJECT_CLASSES = new Set<FomClassConstructor>([
46
+ ExecutionConfiguration,
47
+ ReferenceFrame,
48
+ PhysicalEntity,
49
+ DynamicalEntity,
50
+ ]);
51
+ const BASE_INTERACTION_CLASSES = new Set<FomClassConstructor>([ModeTransitionRequest]);
52
+
53
+ export class SpacekitApp extends BaseSpacekitApp {
54
+ private _logicalTimeMicros: bigint | null = null;
55
+ private readonly _lookaheadMicros: bigint;
56
+ private readonly _updatePeriodMicros: bigint;
57
+ private readonly _referenceFrameTimeoutMs: number;
58
+ private readonly _autoWaitForReferenceFrame: boolean;
59
+ private readonly _missingReferenceFrameMode: MissingReferenceFrameMode;
60
+ private readonly _referenceFrameGraceMs = 2000;
61
+ private _referenceFrameFallbackSubscribe = true;
62
+ private _referenceFrameNameProbe = true;
63
+ private readonly _logStartup: boolean;
64
+ private readonly _logBootstrap: boolean;
65
+ private readonly _logEveryN: number;
66
+ private readonly _startupInfo: {
67
+ federationName: string;
68
+ federateName: string;
69
+ federateType: string;
70
+ rtiHost: string;
71
+ rtiPort: number;
72
+ rtiUseTls: boolean;
73
+ };
74
+ private _bootstrapLogged = false;
75
+ private _tickCount = 0;
76
+
77
+ constructor(options: SpacekitAppOptions = {}) {
78
+ const env = parseSpacekitEnv({
79
+ env: options.env,
80
+ defaults: options.envDefaults ?? {
81
+ rtiHost: "localhost",
82
+ rtiPort: 15164,
83
+ rtiUseTls: false,
84
+ federationName: "SpaceFederation",
85
+ federateName: "SpacekitFederate",
86
+ federateType: "SpacekitFederate",
87
+ lookaheadMicros: 1_000_000n,
88
+ updatePeriodMicros: 1_000_000n,
89
+ referenceFrameTimeoutMs: 30_000,
90
+ logLevel: "info",
91
+ logEveryN: 1,
92
+ debug: false,
93
+ },
94
+ });
95
+
96
+ const resolved = {
97
+ ...env,
98
+ federationName: options.federationName ?? env.federationName,
99
+ federateName: options.federateName ?? env.federateName,
100
+ federateType: options.federateType ?? env.federateType,
101
+ rtiHost: options.rtiHost ?? env.rtiHost,
102
+ rtiPort: options.rtiPort ?? env.rtiPort,
103
+ rtiUseTls: options.rtiUseTls ?? env.rtiUseTls,
104
+ lookaheadMicros: options.lookaheadMicros ?? env.lookaheadMicros,
105
+ updatePeriodMicros: options.updatePeriodMicros ?? env.updatePeriodMicros,
106
+ referenceFrameTimeoutMs: options.referenceFrameTimeoutMs ?? env.referenceFrameTimeoutMs,
107
+ logLevel: options.logLevel ?? env.logLevel,
108
+ logEveryN: options.logEveryN ?? env.logEveryN,
109
+ debug: options.debug ?? env.debug,
110
+ };
111
+
112
+ const logger =
113
+ options.logger ??
114
+ createConsoleLogger({
115
+ level: resolved.logLevel,
116
+ scope: "spacekit",
117
+ });
118
+
119
+ const bootstrap =
120
+ options.bootstrap ??
121
+ new SpaceFomLateJoinerBootstrap({
122
+ lookaheadMicros: resolved.lookaheadMicros,
123
+ waitForReferenceFrameTree: options.waitForReferenceFrameTree ?? false,
124
+ referenceFrameTimeoutMs: resolved.referenceFrameTimeoutMs,
125
+ waitForInitializationCompleted: false,
126
+ waitForExco: false,
127
+ advanceToHltb: false,
128
+ });
129
+
130
+ const declarative =
131
+ options.declarative ??
132
+ buildDeclarativeFomFromDecorators({
133
+ objectClasses: [ExecutionConfiguration, ReferenceFrame, PhysicalEntity, DynamicalEntity],
134
+ interactionClasses: [ModeTransitionRequest],
135
+ register: false,
136
+ });
137
+
138
+ if (options.declarative) {
139
+ registerDecoratedClasses(declarative, {
140
+ objectClasses: [ExecutionConfiguration, ReferenceFrame, PhysicalEntity, DynamicalEntity],
141
+ interactionClasses: [ModeTransitionRequest],
142
+ register: false,
143
+ });
144
+ }
145
+
146
+ super({
147
+ federate: {
148
+ rti: { host: resolved.rtiHost, port: resolved.rtiPort, useTls: resolved.rtiUseTls },
149
+ connection: { callbackModel: options.callbackModel ?? CallbackModel.IMMEDIATE },
150
+ federationName: resolved.federationName,
151
+ federateType: resolved.federateType,
152
+ federateName: resolved.federateName,
153
+ },
154
+ bootstrap,
155
+ declarative,
156
+ logging: {
157
+ logger,
158
+ level: resolved.logLevel,
159
+ scope: "spacekit",
160
+ },
161
+ fomExport: {
162
+ inferParentStubs: options.fomExport?.inferParentStubs ?? true,
163
+ ...options.fomExport,
164
+ },
165
+ });
166
+
167
+ this._lookaheadMicros = resolved.lookaheadMicros;
168
+ this._updatePeriodMicros = resolved.updatePeriodMicros;
169
+ this._referenceFrameTimeoutMs = resolved.referenceFrameTimeoutMs;
170
+ this._autoWaitForReferenceFrame = options.autoWaitForReferenceFrame ?? true;
171
+ this._missingReferenceFrameMode = options.missingReferenceFrameMode ?? "error";
172
+ this._logStartup = options.logStartup ?? true;
173
+ this._logBootstrap = options.logBootstrap ?? true;
174
+ this._logEveryN = resolved.logEveryN ?? 1;
175
+ this._startupInfo = {
176
+ federationName: resolved.federationName,
177
+ federateName: resolved.federateName,
178
+ federateType: resolved.federateType,
179
+ rtiHost: resolved.rtiHost,
180
+ rtiPort: resolved.rtiPort,
181
+ rtiUseTls: resolved.rtiUseTls,
182
+ };
183
+ if (options.enableGracefulShutdown ?? true) {
184
+ this.enableGracefulShutdown();
185
+ }
186
+ }
187
+
188
+ get lookaheadMicros(): bigint {
189
+ return this._lookaheadMicros;
190
+ }
191
+
192
+ get updatePeriodMicros(): bigint {
193
+ return this._updatePeriodMicros;
194
+ }
195
+
196
+ get logicalTimeMicros(): bigint | null {
197
+ return this._logicalTimeMicros;
198
+ }
199
+
200
+ get spacefomState(): SpaceFomBootstrapState | undefined {
201
+ return this.bootstrapState as SpaceFomBootstrapState | undefined;
202
+ }
203
+
204
+ override async start(): Promise<void> {
205
+ if (this._logStartup) {
206
+ this.logger.info("Starting Spacekit SEE app.", {
207
+ federation: this._startupInfo.federationName,
208
+ federate: this._startupInfo.federateName,
209
+ federateType: this._startupInfo.federateType,
210
+ rti: `${this._startupInfo.rtiHost}:${this._startupInfo.rtiPort}`,
211
+ tls: this._startupInfo.rtiUseTls,
212
+ });
213
+ }
214
+ await super.start();
215
+ const state = this.spacefomState;
216
+ this._logicalTimeMicros = state?.lastGrantedTimeMicros ?? this.currentTimeMicros;
217
+ }
218
+
219
+ async tick(): Promise<bigint> {
220
+ const base = this._logicalTimeMicros ?? (await this.queryLogicalTimeMicros());
221
+ const next = base + this._updatePeriodMicros;
222
+ this._logicalTimeMicros = await this.advanceTo(next);
223
+ return this._logicalTimeMicros;
224
+ }
225
+
226
+ async run(options: {
227
+ entities?: SpacekitEntity<object>[];
228
+ onTick?: (payload: {
229
+ timeMicros: bigint;
230
+ sendTimeMicros: bigint | null;
231
+ timeSeconds: number;
232
+ deltaSeconds: number;
233
+ tick: number;
234
+ initial: boolean;
235
+ }) => void | Promise<void>;
236
+ sync?: boolean;
237
+ throwOnError?: boolean;
238
+ onError?: (err: unknown) => void;
239
+ } = {}): Promise<void> {
240
+ let runError: unknown;
241
+ const entities = options.entities ?? [];
242
+ try {
243
+ if (!this.isStarted) {
244
+ const registrations = entities.map((entity) => this.registerEntity(entity));
245
+ await this.start();
246
+ await Promise.all(registrations);
247
+ } else if (entities.length > 0) {
248
+ await this.registerEntities(entities);
249
+ }
250
+
251
+ let tick = 0;
252
+ this._tickCount = tick;
253
+ let timeMicros = this.logicalTimeMicros ?? (await this.queryLogicalTimeMicros());
254
+ const deltaSeconds = Number(this._updatePeriodMicros) / 1_000_000;
255
+ if (this._logBootstrap && !this._bootstrapLogged) {
256
+ const state = this.spacefomState;
257
+ this.logger.info("Spacekit SEE bootstrap complete.", {
258
+ lastGrantedTimeMicros: state?.lastGrantedTimeMicros?.toString() ?? "unknown",
259
+ lctsMicros: state?.lctsMicros?.toString() ?? "unknown",
260
+ rootFrame: state?.rootFrameName ?? "unknown",
261
+ });
262
+ this._bootstrapLogged = true;
263
+ }
264
+ if (options.onTick) {
265
+ await options.onTick({
266
+ timeMicros,
267
+ sendTimeMicros: this.getSendTimeMicros(timeMicros),
268
+ timeSeconds: Number(timeMicros) / 1_000_000,
269
+ deltaSeconds: 0,
270
+ tick,
271
+ initial: true,
272
+ });
273
+ }
274
+ if (options.sync !== false && entities.length > 0) {
275
+ await this.sync(entities);
276
+ }
277
+
278
+ while (!this.isShutdownRequested) {
279
+ tick += 1;
280
+ this._tickCount = tick;
281
+ timeMicros = await this.tick();
282
+ if (options.onTick) {
283
+ await options.onTick({
284
+ timeMicros,
285
+ sendTimeMicros: this.getSendTimeMicros(timeMicros),
286
+ timeSeconds: Number(timeMicros) / 1_000_000,
287
+ deltaSeconds,
288
+ tick,
289
+ initial: false,
290
+ });
291
+ }
292
+ if (options.sync !== false && entities.length > 0) {
293
+ await this.sync(entities);
294
+ }
295
+ }
296
+ } catch (err) {
297
+ runError = err;
298
+ this.logger.error("Spacekit SEE run failed.", { error: err });
299
+ options.onError?.(err);
300
+ this.requestShutdown("error");
301
+ } finally {
302
+ if (this.rtiAmbassador) {
303
+ try {
304
+ await this.stop();
305
+ } catch (err) {
306
+ this.logger.warn("Spacekit SEE shutdown error.", { error: err });
307
+ }
308
+ }
309
+ }
310
+
311
+ if (runError) {
312
+ process.exitCode = 1;
313
+ if (options.throwOnError) {
314
+ throw runError;
315
+ }
316
+ }
317
+ }
318
+
319
+ getSendTimeMicros(baseTimeMicros?: bigint): bigint | null {
320
+ const base = baseTimeMicros ?? this._logicalTimeMicros;
321
+ if (base === null || base === undefined) return null;
322
+ return base + this._lookaheadMicros;
323
+ }
324
+
325
+ shouldLog(updateCount: number): boolean {
326
+ if (!this._logEveryN || this._logEveryN <= 1) return true;
327
+ return updateCount % this._logEveryN === 0;
328
+ }
329
+
330
+ shouldLogTick(tick: number = this._tickCount): boolean {
331
+ return this.shouldLog(tick);
332
+ }
333
+
334
+ shouldLogUpdate(record: { updateCount: number }): boolean {
335
+ return this.shouldLog(record.updateCount);
336
+ }
337
+
338
+ override async syncEntity<T extends object>(
339
+ entity: SpacekitEntity<T>,
340
+ time?: LogicalTime | bigint,
341
+ userSuppliedTag?: Uint8Array
342
+ ): Promise<void> {
343
+ if (typeof time === "bigint") {
344
+ const logicalTime = encodeHLAinteger64Time(time + this._lookaheadMicros);
345
+ await super.syncEntity(entity, logicalTime, userSuppliedTag);
346
+ return;
347
+ }
348
+ if (!time) {
349
+ const base = this._logicalTimeMicros ?? (await this.queryLogicalTimeMicros());
350
+ const logicalTime = encodeHLAinteger64Time(base + this._lookaheadMicros);
351
+ await super.syncEntity(entity, logicalTime, userSuppliedTag);
352
+ return;
353
+ }
354
+ await super.syncEntity(entity, time, userSuppliedTag);
355
+ }
356
+
357
+ async waitForReferenceFrame(name: string, timeoutMs = this._referenceFrameTimeoutMs): Promise<void> {
358
+ const start = Date.now();
359
+ const effectiveTimeoutMs = timeoutMs === 0 ? this._referenceFrameGraceMs : timeoutMs;
360
+ let lastNames = "";
361
+ while (true) {
362
+ const state = this.spacefomState;
363
+ const frames = state?.referenceFrames ?? [];
364
+ const names = frames
365
+ .map((frame) => frame.name)
366
+ .filter((frameName): frameName is string => !!frameName)
367
+ .sort()
368
+ .join(",");
369
+ if (names && names !== lastNames) {
370
+ lastNames = names;
371
+ this.logger.debug("Reference frames discovered.", { names: names.split(",") });
372
+ }
373
+ if (frames.some((frame) => frame.name === name)) {
374
+ return;
375
+ }
376
+ if (
377
+ this._referenceFrameFallbackSubscribe &&
378
+ effectiveTimeoutMs > 0 &&
379
+ Date.now() - start > Math.min(effectiveTimeoutMs, 2000)
380
+ ) {
381
+ this._referenceFrameFallbackSubscribe = false;
382
+ await this._subscribeReferenceFrameFallback();
383
+ }
384
+ if (
385
+ this._referenceFrameNameProbe &&
386
+ effectiveTimeoutMs > 0 &&
387
+ Date.now() - start > Math.min(effectiveTimeoutMs, 1500)
388
+ ) {
389
+ this._referenceFrameNameProbe = false;
390
+ const found = await this._probeReferenceFrameByName(name);
391
+ if (found) {
392
+ this.logger.warn("Reference frame confirmed via name lookup.", { name });
393
+ return;
394
+ }
395
+ }
396
+ if (effectiveTimeoutMs >= 0 && Date.now() - start > effectiveTimeoutMs) {
397
+ const error = `Reference frame not discovered: ${name}. Known frames: ${names || "(none)"}`;
398
+ if (this._missingReferenceFrameMode === "continue") {
399
+ this.logger.warn("Proceeding without discovered reference frame.", {
400
+ name,
401
+ knownFrames: names ? names.split(",") : [],
402
+ timeoutMs: effectiveTimeoutMs,
403
+ });
404
+ return;
405
+ }
406
+ throw new Error(error);
407
+ }
408
+ await new Promise((resolve) => setTimeout(resolve, 200));
409
+ }
410
+ }
411
+
412
+ protected override shouldRegisterClassInFom(ctor: Function, kind: "object" | "interaction"): boolean {
413
+ if (kind === "object" && BASE_OBJECT_CLASSES.has(ctor as FomClassConstructor)) {
414
+ return false;
415
+ }
416
+ if (kind === "interaction" && BASE_INTERACTION_CLASSES.has(ctor as FomClassConstructor)) {
417
+ return false;
418
+ }
419
+ return true;
420
+ }
421
+
422
+ protected override async beforeRegisterEntity<T extends object>(entity: SpacekitEntity<T>): Promise<void> {
423
+ if (!this._autoWaitForReferenceFrame) return;
424
+ const parentName =
425
+ (entity as { parent_reference_frame?: string }).parent_reference_frame ??
426
+ (entity as { parent_name?: string }).parent_name;
427
+ if (!parentName) return;
428
+ await this.waitForReferenceFrame(parentName);
429
+ }
430
+
431
+ private async _subscribeReferenceFrameFallback(): Promise<void> {
432
+ const rti = this.rtiAmbassador;
433
+ if (!rti) return;
434
+ const adapter = this.declarative.getObjectAdapter<ReferenceFrame>(ReferenceFrame.className);
435
+ if (!adapter) return;
436
+ if (!adapter.isResolved) {
437
+ await adapter.resolve(rti);
438
+ }
439
+ await adapter.subscribe(rti);
440
+ this.logger.debug("Retrying ReferenceFrame subscribe fallback.");
441
+ }
442
+
443
+ private async _probeReferenceFrameByName(name: string): Promise<boolean> {
444
+ const rti = this.rtiAmbassador;
445
+ if (!rti) return false;
446
+ try {
447
+ const handle = await rti.getObjectInstanceHandle(name);
448
+ if (!handle || handle.length === 0) return false;
449
+ const adapter = this.declarative.getObjectAdapter<ReferenceFrame>(ReferenceFrame.className);
450
+ if (!adapter) return true;
451
+ if (!adapter.isResolved) {
452
+ await adapter.resolve(rti);
453
+ }
454
+ await rti.requestInstanceAttributeValueUpdate(handle, adapter.attributeHandleSet, new Uint8Array());
455
+ return true;
456
+ } catch {
457
+ return false;
458
+ }
459
+ }
460
+ }