@3dsource/angular-unreal-module 0.0.161 → 0.0.162

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 CHANGED
@@ -114,6 +114,15 @@ The `loadChildren()` boundary keeps the streaming engine out of the initial
114
114
  bundle. Route-scoping `provideUnrealModule()` tears down its effects when the
115
115
  route is left.
116
116
 
117
+ > `UNREAL_CONFIG` on a route's `providers` does **not** work: the engine
118
+ > services are `providedIn: 'root'`, so they are created by the root injector
119
+ > and read the token from there. A route-level value is invisible to them and
120
+ > `inject(UNREAL_CONFIG, { optional: true })` resolves to `null` — e.g. region
121
+ > pinging is skipped entirely (empty `regionsPingUrl`), the orchestration
122
+ > `requestStream` goes out without a region and the post-connection re-ping
123
+ > never runs. Importing only the token at the root does not pull the module into
124
+ > the initial bundle (the package is `sideEffects: false`).
125
+
117
126
  ### 3. Render the scene
118
127
 
119
128
  ```typescript
@@ -1538,6 +1538,16 @@ class RegionsPingService {
1538
1538
  pingsMs: data.result,
1539
1539
  }))), map$1((data) => data.region_code));
1540
1540
  }
1541
+ // No regions URL and no cache: there is nothing to ping. Bail out loudly
1542
+ // instead of letting `HttpClient.get('')` request the current document
1543
+ // (which yields HTML, fails to parse and silently resolves an empty
1544
+ // region). The usual cause is `UNREAL_CONFIG` provided on a lazy route
1545
+ // rather than at the application root — this service is
1546
+ // `providedIn: 'root'`, so a route-level value never reaches it.
1547
+ if (!regionListUrl) {
1548
+ Logger.warn('RegionsPingService: regionsPingUrl is empty — skipping region discovery. Provide UNREAL_CONFIG at the APPLICATION ROOT (a route-level provider is invisible to root-scoped services).');
1549
+ return of(undefined).pipe(tap(() => this.store.dispatch(regionPingFetchEnd())), tap(() => this.store.dispatch(regionResolved({ region: '', pingsMs: [] }))));
1550
+ }
1541
1551
  return this.getProviders(regionListUrl).pipe(tap(() => this.store.dispatch(regionPingFetchEnd())), switchMap((providers) => this.getPingResult(providers)), catchError(() => of(null)), tapLog('PingResult'),
1542
1552
  // Warm the cache with the live result so the next connection (and any
1543
1553
  // reload within the TTL) can skip pinging even if the prefetch script
@@ -1558,9 +1568,11 @@ class RegionsPingService {
1558
1568
  * All access is guarded — `localStorage` can throw (private mode, disabled
1559
1569
  * storage) or be absent (non-browser).
1560
1570
  *
1561
- * Public so adopted connections (which skip `getFastest`) can replay the
1571
+ * Public so adopted connections (which skip `getFastest`) can still report a
1562
1572
  * region-resolving telemetry phase from the same cached winner — see
1563
- * `UnrealEffects.regionTelemetryOnAdopt$`.
1573
+ * `StreamStatusTelemetryService2.emitAdoptedConnection`. Returns `null` when
1574
+ * the region-ping prefetch never ran or its cache expired; that phase then
1575
+ * carries no region and no ping samples.
1564
1576
  */
1565
1577
  readCachedWinner() {
1566
1578
  try {
@@ -1606,6 +1618,10 @@ class RegionsPingService {
1606
1618
  * connection handshake. Aborts with the injector via `destroyController`.
1607
1619
  */
1608
1620
  refreshCacheInBackground(regionListUrl = this.unrealConfig?.regionsPingUrl || '') {
1621
+ if (!regionListUrl) {
1622
+ Logger.warn('RegionsPingService: regionsPingUrl is empty — skipping the background region re-ping.');
1623
+ return of(null);
1624
+ }
1609
1625
  return defer(() => this.getProviders(regionListUrl)).pipe(switchMap((providers) => from(this.measureAccurate(providers.regions, providers.timeout ?? this.config.ping_timeout))), tap((winner) => {
1610
1626
  if (winner) {
1611
1627
  this.writeCachedWinner(winner.region_code, winner.result);
@@ -2198,9 +2214,18 @@ class SignallingService extends SubService {
2198
2214
  },
2199
2215
  }));
2200
2216
  }
2217
+ /**
2218
+ * Status message for the connect branch. It does NOT dispatch
2219
+ * `setEstablishingConnection` — `UnrealEffects.connectToSignaling$` already
2220
+ * emits that once per logical attempt via `startWith(...)`, covering BOTH
2221
+ * branches (prefetch adoption and a fresh connect). Dispatching it here as
2222
+ * well fired the boundary twice on every cold start, and since
2223
+ * `StreamStatusTelemetryService2` starts a session on that boundary, the
2224
+ * first session was killed 2 ms in as `restart` — losing the only honest
2225
+ * page-load measurement and the prefetch adoption with it.
2226
+ */
2201
2227
  startEstablishingConnection() {
2202
2228
  this.showStatusMessage(UnrealStatusMessage.STARTING_YOUR_SESSION);
2203
- this.store.dispatch(setEstablishingConnection({ value: true }));
2204
2229
  }
2205
2230
  adaptUrlsToRegion(urlList, region) {
2206
2231
  return urlList.map((url) => {
@@ -3688,11 +3713,12 @@ class StreamAdoptionService {
3688
3713
  *
3689
3714
  * Wrapped in `defer` so the `tryAdopt()` side effect (which dispatches
3690
3715
  * `streamAdopted`) runs at SUBSCRIPTION time, not when the observable is
3691
- * built. The effect prepends `startWith(setEstablishingConnection(true))`,
3692
- * which starts the telemetry session and subscribes its phase catalog; that
3693
- * must happen BEFORE `streamAdopted` → `regionTelemetryOnAdopt$` replays
3694
- * `regionResolved`, or the region phase fires into a session that doesn't
3695
- * exist yet and is lost. `defer` guarantees that ordering.
3716
+ * built. The effect prepends `startWith(setEstablishingConnection(true))` —
3717
+ * the single dispatch site for that boundary — which starts the telemetry
3718
+ * session and subscribes its phase catalog; that must happen BEFORE
3719
+ * `streamAdopted`, or `emitAdoptedConnection` fires into a session that does
3720
+ * not exist yet and the whole connection waterfall is lost. `defer`
3721
+ * guarantees that ordering.
3696
3722
  */
3697
3723
  awaitAndAdopt$() {
3698
3724
  return defer(() => {
@@ -5880,10 +5906,14 @@ class StreamStatusTelemetryService2 {
5880
5906
  // CLOSER — the `regionResolved` action carries the raw ping
5881
5907
  // samples (one per probe) from `RegionsPingService`; the
5882
5908
  // catalog flattens them to a comma-separated string since
5883
- // `toPhaseParams` only accepts primitives. Adopted connections never
5884
- // run `getRegion`, but `UnrealEffects.regionTelemetryOnAdopt$` replays
5885
- // the region waterfall (start → end → resolved) from the cached winner,
5886
- // so this phase still appears with its ping samples.
5909
+ // `toPhaseParams` only accepts primitives.
5910
+ //
5911
+ // Adopted connections never run `getRegion`, so this closer never
5912
+ // fires for them (and is suppressed anyway via PREFETCHED_PHASE_IDS).
5913
+ // Their region phase comes from {@link emitAdoptedConnection}, which
5914
+ // reports `duration: 0`, no milestones, and reads the winner + samples
5915
+ // out of the ping cache — so it is EMPTY when the region-ping prefetch
5916
+ // never ran or its cache expired.
5887
5917
  {
5888
5918
  id: 'region-resolved',
5889
5919
  event: this.actions$.pipe(ofType(regionResolved), take(1), map(({ region, pingsMs }) => ({
@@ -6053,12 +6083,17 @@ class StreamStatusTelemetryService2 {
6053
6083
  // times per page load (the unreal-scene effect, `startStream`, the
6054
6084
  // reconnect effect), but the connection layer collapses them into one
6055
6085
  // attempt via `connectToSignaling$` (`exhaustMap` + `!cirrusConnected`
6056
- // filter). `setEstablishingConnection(true)` is dispatched exactly
6057
- // once per real attempt from `SignallingService.startEstablishingConnection()`,
6058
- // downstream of that guard — so binding here yields one session per
6059
- // attempt instead of a burst of `restart` churn sessions. Genuine
6086
+ // filter). `setEstablishingConnection(true)` is dispatched exactly once
6087
+ // per real attempt, by that effect's `startWith(...)` — the SINGLE
6088
+ // dispatch site, downstream of the guard and ahead of both branches
6089
+ // (prefetch adoption / fresh connect). So binding here yields one session
6090
+ // per attempt instead of a burst of `restart` churn sessions. Genuine
6060
6091
  // reconnects (e.g. DataChannelTimeout) re-enter the same path and
6061
6092
  // correctly end the prior session as `restart` + start a new one.
6093
+ //
6094
+ // Do NOT add a second dispatch site: this binding turns every extra
6095
+ // `true` into `endSession('restart')` + a new session, which throws away
6096
+ // the first session's page-load measurement and its prefetch adoption.
6062
6097
  this.actions$
6063
6098
  .pipe(ofType(setEstablishingConnection), filter$1(({ value }) => value === true), takeUntil$1(this.destroy$))
6064
6099
  .subscribe(() => this.startSession());