@3dsource/angular-unreal-module 0.0.146-dev.4 → 0.0.146-dev.6

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.
@@ -1670,6 +1670,10 @@ class RegionsPingService {
1670
1670
  * entry is well-formed and still within {@link REGION_PING_CACHE_TTL_MS}.
1671
1671
  * All access is guarded — `localStorage` can throw (private mode, disabled
1672
1672
  * storage) or be absent (non-browser).
1673
+ *
1674
+ * Public so adopted connections (which skip `getFastest`) can replay the
1675
+ * region-resolving telemetry phase from the same cached winner — see
1676
+ * `UnrealEffects.regionTelemetryOnAdopt$`.
1673
1677
  */
1674
1678
  readCachedWinner() {
1675
1679
  try {
@@ -3598,26 +3602,41 @@ class StreamAdoptionService {
3598
3602
  *
3599
3603
  * - nothing parked → emit `false` at once (caller connects normally).
3600
3604
  * - already settled (`ready`/`failed`/`adopted`) → adopt-if-ready at once.
3601
- * - still `connecting` → poll until it settles, then adopt-if-ready. Bounded
3602
- * by `timeoutMs` so a stuck prefetch can't block the fallback forever.
3605
+ * - still `connecting` → poll until the script settles it, then adopt-if-ready.
3606
+ *
3607
+ * Deliberately NO time budget: for DIRECT orchestration the prefetch holds
3608
+ * the socket open while the orchestrator provisions a machine, which can take
3609
+ * minutes (150–200s+). A fixed deadline would make Angular give up and open a
3610
+ * second connection mid-provisioning — exactly the race/orphan this avoids.
3611
+ * The wait is bounded by liveness instead: the script flips the parked state
3612
+ * to `failed` on a dead socket (`ws.onclose`/`onerror`), and the caller wraps
3613
+ * this in `takeUntil(abortEstablishingConnection)`, so a route change / teardown
3614
+ * cancels it. Emits exactly once. Cancelling runs no adoption — which is what
3615
+ * stops a fast route-switch from starting overlapping connections.
3603
3616
  *
3604
- * Emits exactly once. Cancel by unsubscribing (the effect does this on
3605
- * teardown via `takeUntil`) — no adoption runs after cancellation, which is
3606
- * what stops a fast route-switch from starting overlapping connections.
3617
+ * Wrapped in `defer` so the `tryAdopt()` side effect (which dispatches
3618
+ * `streamAdopted`) runs at SUBSCRIPTION time, not when the observable is
3619
+ * built. The effect prepends `startWith(setEstablishingConnection(true))`,
3620
+ * which starts the telemetry session and subscribes its phase catalog; that
3621
+ * must happen BEFORE `streamAdopted` → `regionTelemetryOnAdopt$` replays
3622
+ * `regionResolved`, or the region phase fires into a session that doesn't
3623
+ * exist yet and is lost. `defer` guarantees that ordering.
3607
3624
  */
3608
- awaitAndAdopt$(timeoutMs) {
3609
- const parked = window.__stream;
3610
- if (!parked) {
3611
- return of(false);
3612
- }
3613
- if (parked.state !== 'connecting') {
3614
- return of(this.tryAdopt());
3615
- }
3616
- Logger.info('[Adoption] Prefetch in progress — waiting before connecting');
3617
- return timer(0, ADOPT_POLL_MS).pipe(map$1(() => window.__stream?.state ?? 'failed'),
3618
- // Keep polling while still connecting AND within budget; the `true`
3619
- // (inclusive) emits the settling value (or the last one at the deadline).
3620
- takeWhile((state, i) => state === 'connecting' && i * ADOPT_POLL_MS < timeoutMs, true), last(), map$1(() => this.tryAdopt()));
3625
+ awaitAndAdopt$() {
3626
+ return defer(() => {
3627
+ const parked = window.__stream;
3628
+ if (!parked) {
3629
+ return of(false);
3630
+ }
3631
+ if (parked.state !== 'connecting') {
3632
+ return of(this.tryAdopt());
3633
+ }
3634
+ Logger.info('[Adoption] Prefetch in progress — waiting before connecting');
3635
+ return timer(0, ADOPT_POLL_MS).pipe(map$1(() => window.__stream?.state ?? 'failed'),
3636
+ // Keep polling while still connecting; the `true` (inclusive) emits the
3637
+ // settling value (`ready`/`failed`) that ends the wait.
3638
+ takeWhile((state) => state === 'connecting', true), last(), map$1(() => this.tryAdopt()));
3639
+ });
3621
3640
  }
3622
3641
  /**
3623
3642
  * @returns `true` if a ready parked stream was adopted (caller must NOT
@@ -3638,7 +3657,7 @@ class StreamAdoptionService {
3638
3657
  return false;
3639
3658
  }
3640
3659
  Logger.info('[Adoption] Adopting prefetched stream', parked.timings);
3641
- // Stop the script's watchdog and mark the handoff complete.
3660
+ // Clear the script's timers and mark the handoff complete.
3642
3661
  parked.adopt();
3643
3662
  // Flag cirrus connected (so a later `initSignalling` won't open a 2nd
3644
3663
  // connection), record the adopted machine, and hydrate the streamer info
@@ -4197,9 +4216,7 @@ class UnrealEffects {
4197
4216
  // connection; otherwise fall back to a fresh signalling connection.
4198
4217
  // `takeUntil(abort)` cancels the wait on teardown / route change so a
4199
4218
  // fast switch can't leave overlapping connections.
4200
- this.streamAdoptionService
4201
- .awaitAndAdopt$(this.dataChannelConnectionTimeout)
4202
- .pipe(switchMap((adopted) => adopted ? EMPTY : this.signallingService.connectToSignaling(urls)), takeUntil(this.actions$.pipe(ofType(abortEstablishingConnection))),
4219
+ this.streamAdoptionService.awaitAndAdopt$().pipe(switchMap((adopted) => adopted ? EMPTY : this.signallingService.connectToSignaling(urls)), takeUntil(this.actions$.pipe(ofType(abortEstablishingConnection))),
4203
4220
  // The adopt/connect stream is side-effect-only (its values aren't
4204
4221
  // actions) — swallow them, and emit `setEstablishingConnection`
4205
4222
  // first so the reconnect/resume popup stays hidden during the
@@ -4207,6 +4224,24 @@ class UnrealEffects {
4207
4224
  // defers `connectToSignaling`, which is what normally sets this.
4208
4225
  ignoreElements(), startWith(setEstablishingConnection({ value: true })))));
4209
4226
  });
4227
+ // Adoption skips `connectToSignaling → getRegion`, so the region-resolving
4228
+ // telemetry phase never fires. Replay the region waterfall (start → end →
4229
+ // resolved) from the cached winner the prefetch reused — a near-zero-length
4230
+ // phase that still carries the ping samples. No live ping sweep; if there is
4231
+ // no cache the resolved event just carries empty values.
4232
+ this.regionTelemetryOnAdopt$ = createEffect(() => {
4233
+ return this.actions$.pipe(ofType(streamAdopted), mergeMap(() => {
4234
+ const cached = this.regionsPingService.readCachedWinner();
4235
+ return [
4236
+ regionPingFetchStart(),
4237
+ regionPingFetchEnd(),
4238
+ regionResolved({
4239
+ region: cached?.region_code ?? '',
4240
+ pingsMs: cached?.result ?? [],
4241
+ }),
4242
+ ];
4243
+ }));
4244
+ });
4210
4245
  }
4211
4246
  canPeerReconnect() {
4212
4247
  return RECONNECT_CONFIG.enabled && this.signallingService.isConnected();
@@ -5615,17 +5650,13 @@ class StreamStatusTelemetryService2 {
5615
5650
  // CLOSER — the `regionResolved` action carries the raw ping
5616
5651
  // samples (one per probe) from `RegionsPingService`; the
5617
5652
  // catalog flattens them to a comma-separated string since
5618
- // `toPhaseParams` only accepts primitives.
5619
- //
5620
- // Adopted connections skip `connectToSignaling` → `getRegion`, so
5621
- // `regionResolved` never fires. We synthesise it from `streamAdopted`
5622
- // using the cached ping result the prefetch reused, so the region
5623
- // phase still appears (with its ping samples). Because this fires at
5624
- // adoption time it also re-anchors `lastMilestonePerf`, which keeps the
5625
- // following signaling / webrtc phases at ~0 for adopted sessions.
5653
+ // `toPhaseParams` only accepts primitives. Adopted connections never
5654
+ // run `getRegion`, but `UnrealEffects.regionTelemetryOnAdopt$` replays
5655
+ // the region waterfall (start → end → resolved) from the cached winner,
5656
+ // so this phase still appears with its ping samples.
5626
5657
  {
5627
5658
  id: 'region-resolved',
5628
- event: merge(this.actions$.pipe(ofType(regionResolved)), this.actions$.pipe(ofType(streamAdopted), map(() => this.cachedRegionResult()))).pipe(take(1), map(({ region, pingsMs }) => ({
5659
+ event: this.actions$.pipe(ofType(regionResolved), take(1), map(({ region, pingsMs }) => ({
5629
5660
  region,
5630
5661
  pingsMs: pingsMs.length ? `${pingsMs.join(', ')} ms` : null,
5631
5662
  }))),
@@ -5786,29 +5817,6 @@ class StreamStatusTelemetryService2 {
5786
5817
  return v4();
5787
5818
  }
5788
5819
  }
5789
- /**
5790
- * Region winner + ping samples cached in `localStorage` by
5791
- * `region-ping-prefetch.js` / `RegionsPingService` (`REGION_PING_CACHE_KEY`).
5792
- * Used to fill the region phase for ADOPTED connections, which never run
5793
- * `getRegion()` and so never dispatch `regionResolved`. Empty when there is
5794
- * no usable cache.
5795
- */
5796
- cachedRegionResult() {
5797
- try {
5798
- const raw = localStorage.getItem(REGION_PING_CACHE_KEY);
5799
- const parsed = raw ? JSON.parse(raw) : null;
5800
- if (parsed && typeof parsed.region_code === 'string') {
5801
- return {
5802
- region: parsed.region_code,
5803
- pingsMs: Array.isArray(parsed.result) ? parsed.result : [],
5804
- };
5805
- }
5806
- }
5807
- catch {
5808
- /* ignore — best-effort */
5809
- }
5810
- return { region: '', pingsMs: [] };
5811
- }
5812
5820
  init() {
5813
5821
  // Start a new session when a real connection attempt BEGINS, not on
5814
5822
  // every `initSignalling` dispatch. `initSignalling` is fired multiple