@realitycollective/service-framework-three 1.0.2-preview.0 → 1.0.2-preview.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/CHANGELOG.md CHANGED
@@ -10,13 +10,25 @@ A native platform, beside IWSDK, three.js and Babylon.js, and byte I/O that work
10
10
 
11
11
  ### Added
12
12
 
13
+ - `FrameInfo.frame`, the binding's frame count from 1: the same number the `renderTick` context carries for that frame. The native host's frames, the IWSDK bridge and the three.js and Babylon.js owned loops all set it from the one counter they already kept for `renderTick`, and `emitFrame` takes it as an optional third argument, so a client that needs a frame number reads one clock instead of counting frames again. Additive: a frame an app emits itself without one carries none.
14
+ - `@realitycollective/service-framework-native` - `NativeSessionInfo.features`, the WebXR feature names the session enabled (`"hand-tracking"`, `"plane-detection"` and the rest), which the capabilities read as the IWSDK adapter reads `enabledFeatures`; `NativeHost.input`, the `input` slice's `onSourcesChanged` signal and source kinds; `NativeHost.onSessionRefused`, the app's way to say it will not start a requested session; and the `manager` option, the service manager whose focus and pause signals follow the session.
13
15
  - `@realitycollective/service-framework-native`, the binding for a native XR app that embeds a JavaScript engine such as Hermes. The app owns the session, the frame loop, rendering and physics, and installs one host object, `globalThis.__rcHost`, before it evaluates the bundle. Everything crosses that object as plain values. `NativeRuntimeAdapter` publishes the app's frames and session through the same `RuntimeAdapter` seam as the other bindings, with the session facet, and passes `runtimeAdapterContractCases()`. It derives capabilities from the OpenXR session state, the enabled extensions, system hand tracking support and the blend mode. It keeps the same sticky `setCapabilities` override, `clearCapabilityOverrides()` and `refreshCapabilities()` the other adapters have. Given a `scheduler`, it emits `renderTick` with `source: "native"`. `createNativeHostIO()` returns a `HostIO` over the host's `io` slice. The host object's shape was proven on a Quest 3 and in the visionOS Simulator by the team that built the first native host. The package README documents it, including the slices the other families' native packages read.
14
16
  - `HostIO` in the core, with `createWebHostIO()` over `fetch` and `DecompressionStream`. A service that reads assets takes a `HostIO` and never reaches for a global, so the same code runs on the web and on a native host. The globals are read when a method is called, so a host without them fails only if it uses them. Helpers that work on both: `fetchText`, `fetchJson`, `fetchMaybeGzipped` (gunzips only bytes that carry the gzip magic number, since hosts differ on whether they undo `Content-Encoding`), `decodeUtf8` (no `TextDecoder`, which Hermes lacks), `isGzip` and `concatBytes`. This is transport only. Decoding images, audio and models stays with the engine.
15
17
  - A "Host requirements" section in the core package README. It names every global the core expects from its host beyond the ECMAScript language: `setTimeout` and `clearTimeout` for `resolveAsync`, `waitUntilInitialized` and the mock adapter's session request, and `setInterval` and `clearInterval` for a `TimerScheduler` built without injected timers. It names the two globals used only when present, `AbortController` (with a built-in fallback) and the `fetch` and `DecompressionStream` behind `createWebHostIO()`, and states that nothing else is assumed. A host that embeds a bare engine, such as Hermes in a native app, can now supply exactly that list. `test/host-requirements.test.ts` checks the table against the source, so a new host global fails the suite until it is listed.
16
18
  - The capability override rule, stated in the README and on the `RuntimeAdapter` doc comment. Every adapter exposes `setCapabilities(partial)` as a sticky override over what it derives, and an adapter that derives also exposes `clearCapabilityOverrides()`. Every in-repo adapter already did, and every conformance driver already wired its `capabilities` hook to it. The rule was never written down, so an adapter written elsewhere that only derived from its host failed the capability-change case. The method stays off the `RuntimeAdapter` interface, because services must not call it. The same test checks that every in-repo adapter still exposes it.
17
-
18
19
  - `hostIOContractCases()` and `renderTickContractCases()`, shared conformance suites shipped as data, like `runtimeAdapterContractCases()`. Clients only ever use the core: a service reads assets through `HostIO` and subscribes to `renderTick` on the core scheduler, and never asks which platform is underneath. That only holds if every platform behaves the same way, so every platform now runs the same checks. `hostIOContractCases()` runs against `createWebHostIO()` and `createNativeHostIO()`: served bytes come back exactly, a missing resource rejects, gunzip restores and rejects non-gzip, and the core helpers read plain and gzipped JSON alike. It needs no `CompressionStream` or `TextEncoder`, so a native app can run it on device. `renderTickContractCases()` runs against `ThreeRenderLoopBridge`, `WebXRRuntimeAdapter`, `BabylonRenderLoopBridge`, `BabylonRuntimeAdapter`, the IWSDK service bridge system and `NativeRuntimeAdapter`: one tick per frame, frames counted from 1, a stable source, and `timestamp` and `deltaTime` in milliseconds.
19
20
  - `LifecycleContext` now documents its units: `timestamp` and `deltaTime` in milliseconds, `frame` counted from 1, and `source` naming the binding.
21
+ - `@realitycollective/service-framework-three` and `@realitycollective/service-framework-babylon` - the `manager` option on `WebXRRuntimeAdapter` and `BabylonRuntimeAdapter`, the service manager whose focus and pause signals follow a live session's visibility, as the `manager` option already does on the native adapter. `WebXRFocusSink` and `BabylonFocusSink` name the two methods it needs.
22
+ - `WebXRRuntimeAdapter.tick(timestampMs)` and `BabylonRuntimeAdapter.tick()`, now public. Each is the whole frame step `start()` already bound to the owned loop - the visibility gate, the focus/pause signals, the one frame count, `emitFrame` and `renderTick` - pulled out from under `start()` so a host that owns its OWN loop and will never call `start()` (an XR Blocks app, whose `Core` calls `setAnimationLoop` itself) can drive the exact same step by hand from whatever per-frame hook that host provides, rather than reimplementing the gate. The owned loop and a hand-driven app now share one code path; before this, a hand-driven three.js app could only reach the ungated `emitFrame`, so it had no way to reach the focus gate at all.
23
+
24
+ ### Changed
25
+
26
+ - **Behaviour change for native apps:** `@realitycollective/service-framework-native` now bridges the native frame loop as IWSDK's `ServiceBridgeSystem` bridges IWSDK's. Services tick only while the session is FOCUSED: a host frame outside focus reaches no frame listener and no `renderTick`, and does not advance the frame count. Given the new `manager` option, the adapter emits `emitFocusChange(focused)` and `emitPauseChange({ paused: !focused })` on every change of focus. Before this a native app ticked its services while the headset was off the face.
27
+ - **Behaviour change for three.js and Babylon.js apps with a live XR session:** `@realitycollective/service-framework-three` and `@realitycollective/service-framework-babylon` now gate their owned loop on the session's own visibility while one is live: a tick that is not `"visible"` reaches no frame listener and no `renderTick`, and does not advance the frame count, exactly as the IWSDK bridge and the native adapter gate on focus. Given the new `manager` option, the adapter emits `emitFocusChange(focused)` and `emitPauseChange({ paused: !focused })` on every change. Unlike IWSDK and native, both packages also serve a desktop page with no session at all, and gating never applies there: with no session the loop ticks exactly as it did before this existed, and a session ending while paused restores focus at once. Before this, a three.js or Babylon app kept rendering into a hidden headset.
28
+ - `@realitycollective/service-framework-native` - a session in the OpenXR `ready` state is live: `immersive` is true there, as the session state already read `active`. For one phase of every session start the two used to disagree.
29
+ - `@realitycollective/service-framework-native` - capabilities re-derive on the `input` slice's source-change signal as well as on session changes, as the IWSDK adapter re-derives on `inputsourceschange`. `handTracking` is true for the `"hand-tracking"` feature, for `XR_EXT_hand_tracking` with system support, or while a hand is among the sources; `planeDetection` is true only for the `"plane-detection"` feature, where it was always false.
30
+ - `@realitycollective/service-framework-native` - a refused session request resolves with the app's reason, `"denied"`, `"unsupported"` or `"error"`, at once. Before this only `"timeout"` and `"error"` could be reached, so a dismissed permission prompt waited ten seconds and read as a timeout.
31
+ - `@realitycollective/service-framework-native` - every member of `NativeHost` and `NativeSessionInfo` states its units and meaning: milliseconds and seconds on `onFrame`, what counts as live, what the blend mode must reflect, and what each signal triggers.
20
32
 
21
33
  ### Fixed
22
34
 
package/README.md CHANGED
@@ -66,11 +66,17 @@ Pass the adapter to your services (through their configuration, or a token you r
66
66
 
67
67
  ### Owning the loop
68
68
 
69
- Given a `host`, the adapter owns the animation loop: `start()` binds it, `stop()` releases it, and each callback becomes one `FrameInfo` - `timestamp` in milliseconds, `delta` in seconds. Given a `scheduler` as well, the same callback also emits `renderTick` with `source: "three"` and `deltaTime` in milliseconds, exactly as `ThreeRenderLoopBridge` does, so an app needs one loop owner rather than two. Use the adapter or the bridge, not both.
69
+ Given a `host`, the adapter owns the animation loop: `start()` binds it, `stop()` releases it, and each callback runs `tick(timestamp)` - the whole frame step: the visibility gate described below, one `FrameInfo` (`timestamp` in milliseconds, `delta` in seconds), and, given a `scheduler` too, `renderTick` with `source: "three"` and `deltaTime` in milliseconds, exactly as `ThreeRenderLoopBridge` emits it, so an app needs one loop owner rather than two. Use the adapter or the bridge, not both.
70
70
 
71
71
  three.js routes `setAnimationLoop` through the session's own `requestAnimationFrame` while presenting, so one call covers both the 2D page and the headset.
72
72
 
73
- Omit `host` to keep the loop yourself and call `adapter.emitFrame(timestamp, delta)` per frame.
73
+ Omit `host` where something else already owns the loop - an XR Blocks app, whose `Core` calls `renderer.setAnimationLoop` itself, so passing it here as `host` would fight `Core` for the one callback and silently stop every XR Blocks system. Call `adapter.tick(timestamp)` by hand instead, from whatever per-frame hook that host provides (XR Blocks' `Script.update`), to get the identical gate, frame count and `renderTick` `start()` would have produced. Call `adapter.emitFrame(timestamp, delta)` instead only where you want the raw frame fan-out with no gate and no `renderTick` at all. See `test/xrblocks-contract.test.ts` for the XR Blocks wiring in full, including why `host` must never be XR Blocks' renderer.
74
+
75
+ #### Ticking only while focused
76
+
77
+ While a session is live, `tick()` gates on its own visibility, exactly as IWSDK's `ServiceBridgeSystem` and the native adapter gate on focus: a call that is not `"visible"` reaches no frame listener and no `renderTick`, and does not advance the frame count. Given a `manager` - a `WebXRFocusSink`, which `ServiceManager` already satisfies - the adapter calls `emitFocusChange(focused)` and `emitPauseChange({ paused: !focused })` on every change. This applies equally whether `tick()` runs from the owned loop or is called by hand, because both paths are the same method.
78
+
79
+ This differs from IWSDK and native, which serve nothing but a live XR session: this adapter also serves a desktop page with no session at all, and gating never applies there. With no session `tick()` runs unrestricted exactly as it did before this existed, and if a session that had paused ticking ends, focus is restored at once so the desktop page resumes unrestricted.
74
80
 
75
81
  ### Sessions
76
82
 
@@ -117,11 +123,12 @@ A desktop build with no headset needs no special case. `request` returns `{ ok:
117
123
  | `ThreeRenderLoopBridgeOptions` | interface | `{ scheduler, host }`. |
118
124
  | `AnimationLoopHostLike` | interface | Anything with `setAnimationLoop`. |
119
125
  | `FIRST_FRAME_DELTA_MS` | const | 16 - the delta reported for the first frame. |
120
- | `WebXRRuntimeAdapter` | class | `RuntimeAdapter` over WebXR; `start`, `stop`, `emitFrame`, `getSession`, `refreshCapabilities`, `setCapabilities`, `clearCapabilityOverrides`, `session`, `dispose`. |
121
- | `WebXRRuntimeAdapterOptions` | interface | `{ xr, xrSystem?, host?, scheduler?, sessionInit? }`. |
126
+ | `WebXRRuntimeAdapter` | class | `RuntimeAdapter` over WebXR; `start`, `stop`, `tick`, `emitFrame`, `getSession`, `refreshCapabilities`, `setCapabilities`, `clearCapabilityOverrides`, `session`, `dispose`. |
127
+ | `WebXRRuntimeAdapterOptions` | interface | `{ xr, xrSystem?, host?, scheduler?, sessionInit?, manager? }`. |
122
128
  | `WebXRManagerLike` | interface | The slice of `renderer.xr` the adapter drives. |
123
129
  | `WebXRSystemLike` | interface | The slice of `navigator.xr` it negotiates through. |
124
130
  | `WebXRSessionLike` | interface | The slice of `XRSession` it reads. |
131
+ | `WebXRFocusSink` | interface | `{ emitFocusChange, emitPauseChange }` - what `manager` must implement; `ServiceManager` already does. |
125
132
  | `WebXRManagerEventType` / `WebXRSessionEventType` / `WebXREventListener` | types | The host events it subscribes to. |
126
133
 
127
134
  ## Live examples
package/dist/index.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  export { FIRST_FRAME_DELTA_MS, ThreeRenderLoopBridge } from "./three-render-loop-bridge.js";
7
7
  export type { AnimationLoopHostLike, ThreeRenderLoopBridgeOptions } from "./three-render-loop-bridge.js";
8
8
  export { WebXRRuntimeAdapter } from "./webxr-runtime-adapter.js";
9
- export type { WebXREventListener, WebXRManagerEventType, WebXRManagerLike, WebXRRuntimeAdapterOptions, WebXRSessionEventType, WebXRSessionLike, WebXRSystemLike } from "./webxr-runtime-adapter.js";
9
+ export type { WebXREventListener, WebXRFocusSink, WebXRManagerEventType, WebXRManagerLike, WebXRRuntimeAdapterOptions, WebXRSessionEventType, WebXRSessionLike, WebXRSystemLike } from "./webxr-runtime-adapter.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAM5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * Public entry point of the three.js binding: the render-loop bridge, and the\n * WebXR runtime adapter that gives a three.js or desktop app the same\n * `RuntimeAdapter` surface the IWSDK binding has.\n */\nexport { FIRST_FRAME_DELTA_MS, ThreeRenderLoopBridge } from \"./three-render-loop-bridge.js\";\nexport type {\n AnimationLoopHostLike,\n ThreeRenderLoopBridgeOptions\n} from \"./three-render-loop-bridge.js\";\n\nexport { WebXRRuntimeAdapter } from \"./webxr-runtime-adapter.js\";\nexport type {\n WebXREventListener,\n WebXRManagerEventType,\n WebXRManagerLike,\n WebXRRuntimeAdapterOptions,\n WebXRSessionEventType,\n WebXRSessionLike,\n WebXRSystemLike\n} from \"./webxr-runtime-adapter.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAM5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC","sourcesContent":["/**\n * Public entry point of the three.js binding: the render-loop bridge, and the\n * WebXR runtime adapter that gives a three.js or desktop app the same\n * `RuntimeAdapter` surface the IWSDK binding has.\n */\nexport { FIRST_FRAME_DELTA_MS, ThreeRenderLoopBridge } from \"./three-render-loop-bridge.js\";\nexport type {\n AnimationLoopHostLike,\n ThreeRenderLoopBridgeOptions\n} from \"./three-render-loop-bridge.js\";\n\nexport { WebXRRuntimeAdapter } from \"./webxr-runtime-adapter.js\";\nexport type {\n WebXREventListener,\n WebXRFocusSink,\n WebXRManagerEventType,\n WebXRManagerLike,\n WebXRRuntimeAdapterOptions,\n WebXRSessionEventType,\n WebXRSessionLike,\n WebXRSystemLike\n} from \"./webxr-runtime-adapter.js\";\n"]}
@@ -13,6 +13,23 @@
13
13
  *
14
14
  * Every host type here is structural, so this package still imports neither
15
15
  * `three` nor any WebXR type, and the adapter unit-tests headless.
16
+ *
17
+ * Given a `host` and a `manager`, the owned animation loop also gates on the
18
+ * live session's visibility, as IWSDK's `ServiceBridgeSystem` and the native
19
+ * adapter do: while a session is live, frames and `renderTick` flow only on
20
+ * `"visible"`, `emitFocusChange`/`emitPauseChange` fire on every change, and
21
+ * the frame count skips a gated tick. Unlike IWSDK and native, this adapter
22
+ * also serves a desktop page with no session at all, and gating never applies
23
+ * there - a plain three.js page keeps ticking exactly as it did before this
24
+ * existed.
25
+ *
26
+ * That whole step - the gate, the focus/pause signals, the frame count,
27
+ * `emitFrame` and `renderTick` - lives in one place, {@link
28
+ * WebXRRuntimeAdapter.tick}, which `start()` binds to the owned loop. A host
29
+ * that owns its OWN animation loop and will never call `start()` - an XR
30
+ * Blocks app, whose `Core` already calls `setAnimationLoop` itself - calls
31
+ * `tick(timestampMs)` by hand from its own per-frame hook instead, and gets
32
+ * the identical gate rather than reimplementing it.
16
33
  */
17
34
  import { type AdapterCapabilities, type CapabilitiesListener, type CapabilitySessionLike, type FrameListener, type IScheduler, type RuntimeAdapter, type SessionFacet, type SessionMode, type Unsubscribe } from "@realitycollective/service-framework";
18
35
  import { type AnimationLoopHostLike } from "./three-render-loop-bridge.js";
@@ -22,6 +39,18 @@ export type WebXRSessionEventType = "end" | "visibilitychange" | "inputsourcesch
22
39
  export type WebXRManagerEventType = "sessionstart" | "sessionend";
23
40
  /** Host event callback. The adapter reads the host, not the event object. */
24
41
  export type WebXREventListener = (event?: unknown) => void;
42
+ /**
43
+ * The two focus signals the adapter drives while a session is live: a
44
+ * `ServiceManager` is one. Declared locally, as `NativeFocusSink` is in the
45
+ * native package, rather than shared, because the two packages do not depend
46
+ * on each other.
47
+ */
48
+ export interface WebXRFocusSink {
49
+ emitFocusChange(focused: boolean): void;
50
+ emitPauseChange(context: {
51
+ readonly paused: boolean;
52
+ }): void;
53
+ }
25
54
  /**
26
55
  * The slice of an `XRSession` the adapter reads. It extends the core's
27
56
  * {@link CapabilitySessionLike}, so a live session goes straight to
@@ -61,8 +90,11 @@ export interface WebXRRuntimeAdapterOptions {
61
90
  /**
62
91
  * The renderer, or anything else with `setAnimationLoop`. Given one, the
63
92
  * adapter owns the loop: {@link WebXRRuntimeAdapter.start} binds it and each
64
- * callback becomes a frame. Omit it to drive frames yourself with
65
- * {@link WebXRRuntimeAdapter.emitFrame}.
93
+ * callback runs {@link WebXRRuntimeAdapter.tick}. Omit it where something
94
+ * else already owns the loop: call {@link WebXRRuntimeAdapter.tick} by hand
95
+ * from whatever per-frame hook that host provides for the same gate, frame
96
+ * count and `renderTick`, or {@link WebXRRuntimeAdapter.emitFrame} directly
97
+ * for the frame fan-out alone.
66
98
  */
67
99
  readonly host?: AnimationLoopHostLike;
68
100
  /**
@@ -76,6 +108,17 @@ export interface WebXRRuntimeAdapterOptions {
76
108
  * Called once per request; the default sends no init at all.
77
109
  */
78
110
  readonly sessionInit?: (mode: SessionMode) => unknown;
111
+ /**
112
+ * The service manager whose focus and pause signals follow the session's
113
+ * visibility, as `ServiceBridgeSystem`'s `manager` option does on IWSDK and
114
+ * the native adapter's `manager` option does on a native host: both fire on
115
+ * every change of focus, `paused` being `!focused`. Gated ONLY while a
116
+ * session is live - `XRSession.visibilityState` via `visibilitychange` -
117
+ * because unlike IWSDK and native, this adapter also serves a desktop page
118
+ * with no session at all, and that page keeps ticking exactly as it did
119
+ * before this option existed.
120
+ */
121
+ readonly manager?: WebXRFocusSink;
79
122
  }
80
123
  export declare class WebXRRuntimeAdapter implements RuntimeAdapter {
81
124
  private readonly frameListeners;
@@ -88,6 +131,7 @@ export declare class WebXRRuntimeAdapter implements RuntimeAdapter {
88
131
  private readonly host;
89
132
  private readonly scheduler;
90
133
  private readonly sessionInit;
134
+ private readonly manager;
91
135
  private derived;
92
136
  private overrides;
93
137
  private capabilities;
@@ -97,6 +141,13 @@ export declare class WebXRRuntimeAdapter implements RuntimeAdapter {
97
141
  private animationLoopBound;
98
142
  private frame;
99
143
  private lastTimestamp;
144
+ /**
145
+ * Whether the current session is visible/focused; `undefined` while there is
146
+ * no session, meaning ticking is not gated at all. Reset to `undefined` on
147
+ * session end, so a desktop page resumes exactly as before a session ever
148
+ * existed.
149
+ */
150
+ private focused;
100
151
  private readonly onManagerSessionStart;
101
152
  private readonly onManagerSessionEnd;
102
153
  /** Session lifecycle over `navigator.xr` and the renderer's XR manager. */
@@ -108,10 +159,10 @@ export declare class WebXRRuntimeAdapter implements RuntimeAdapter {
108
159
  /** The session the renderer is presenting, or null in 2D. */
109
160
  getSession(): WebXRSessionLike | null;
110
161
  /**
111
- * Bind the animation loop, if this adapter was given a host. Each callback
112
- * becomes one {@link FrameInfo} and, where a scheduler was supplied, one
113
- * `renderTick`. With no host this does nothing: the app owns the loop and
114
- * calls {@link WebXRRuntimeAdapter.emitFrame} itself.
162
+ * Bind the animation loop, if this adapter was given a host: each callback
163
+ * runs {@link WebXRRuntimeAdapter.tick}. With no host this does nothing -
164
+ * something else owns the loop, and drives {@link WebXRRuntimeAdapter.tick}
165
+ * or {@link WebXRRuntimeAdapter.emitFrame} itself.
115
166
  *
116
167
  * three.js routes `setAnimationLoop` through the session's own
117
168
  * `requestAnimationFrame` while presenting, so one call covers both the 2D
@@ -121,7 +172,7 @@ export declare class WebXRRuntimeAdapter implements RuntimeAdapter {
121
172
  /** Release the animation loop. Safe to call when it was never bound. */
122
173
  stop(): void;
123
174
  /** Push one frame to every subscriber. Call this when you own the loop. */
124
- emitFrame(timestamp: number, delta: number): void;
175
+ emitFrame(timestamp: number, delta: number, frame?: number): void;
125
176
  /**
126
177
  * Re-read the renderer's session and publish any capability change. The
127
178
  * adapter does this itself on every session and input-source event; call it
@@ -145,7 +196,22 @@ export declare class WebXRRuntimeAdapter implements RuntimeAdapter {
145
196
  * listener. It does not end the session - the app owns that decision.
146
197
  */
147
198
  dispose(): void;
148
- private handleAnimationFrame;
199
+ /**
200
+ * Run one frame step: the visibility gate, the focus/pause signals, the one
201
+ * frame count, {@link WebXRRuntimeAdapter.emitFrame} and `renderTick`. This
202
+ * is what {@link WebXRRuntimeAdapter.start} binds to the owned animation
203
+ * loop, and it is public so a host that owns its OWN loop - one that will
204
+ * never call `start()` because it already calls `setAnimationLoop` itself,
205
+ * such as an XR Blocks app - can drive the exact same step by hand from
206
+ * whatever per-frame hook that host provides, rather than reimplementing the
207
+ * gate, the frame count or the `renderTick` shape.
208
+ *
209
+ * `timestampMs` is the loop's own clock reading, the same value `start()`
210
+ * passes from `setAnimationLoop`'s callback.
211
+ */
212
+ tick(timestampMs: number): void;
213
+ /** Emit focus and pause on a change, as `ServiceBridgeSystem` does. */
214
+ private setFocused;
149
215
  /**
150
216
  * Subscribe to one session's own events. The listeners close over the session
151
217
  * they belong to, so nothing here has to re-check which session is live.
@@ -13,6 +13,23 @@
13
13
  *
14
14
  * Every host type here is structural, so this package still imports neither
15
15
  * `three` nor any WebXR type, and the adapter unit-tests headless.
16
+ *
17
+ * Given a `host` and a `manager`, the owned animation loop also gates on the
18
+ * live session's visibility, as IWSDK's `ServiceBridgeSystem` and the native
19
+ * adapter do: while a session is live, frames and `renderTick` flow only on
20
+ * `"visible"`, `emitFocusChange`/`emitPauseChange` fire on every change, and
21
+ * the frame count skips a gated tick. Unlike IWSDK and native, this adapter
22
+ * also serves a desktop page with no session at all, and gating never applies
23
+ * there - a plain three.js page keeps ticking exactly as it did before this
24
+ * existed.
25
+ *
26
+ * That whole step - the gate, the focus/pause signals, the frame count,
27
+ * `emitFrame` and `renderTick` - lives in one place, {@link
28
+ * WebXRRuntimeAdapter.tick}, which `start()` binds to the owned loop. A host
29
+ * that owns its OWN animation loop and will never call `start()` - an XR
30
+ * Blocks app, whose `Core` already calls `setAnimationLoop` itself - calls
31
+ * `tick(timestampMs)` by hand from its own per-frame hook instead, and gets
32
+ * the identical gate rather than reimplementing it.
16
33
  */
17
34
  import { DEFAULT_CAPABILITIES, DEFAULT_SESSION_TIMEOUT_MS, deriveCapabilities, mergeSessionInit, } from "@realitycollective/service-framework";
18
35
  import { FIRST_FRAME_DELTA_MS } from "./three-render-loop-bridge.js";
@@ -60,6 +77,7 @@ export class WebXRRuntimeAdapter {
60
77
  host;
61
78
  scheduler;
62
79
  sessionInit;
80
+ manager;
63
81
  derived = DEFAULT_CAPABILITIES;
64
82
  overrides = {};
65
83
  capabilities = DEFAULT_CAPABILITIES;
@@ -69,6 +87,13 @@ export class WebXRRuntimeAdapter {
69
87
  animationLoopBound = false;
70
88
  frame = 0;
71
89
  lastTimestamp = 0;
90
+ /**
91
+ * Whether the current session is visible/focused; `undefined` while there is
92
+ * no session, meaning ticking is not gated at all. Reset to `undefined` on
93
+ * session end, so a desktop page resumes exactly as before a session ever
94
+ * existed.
95
+ */
96
+ focused;
72
97
  onManagerSessionStart = () => {
73
98
  this.handleSessionStart();
74
99
  };
@@ -99,6 +124,7 @@ export class WebXRRuntimeAdapter {
99
124
  this.host = options.host;
100
125
  this.scheduler = options.scheduler;
101
126
  this.sessionInit = options.sessionInit;
127
+ this.manager = options.manager;
102
128
  this.xr.addEventListener("sessionstart", this.onManagerSessionStart);
103
129
  this.xr.addEventListener("sessionend", this.onManagerSessionEnd);
104
130
  const session = this.xr.getSession();
@@ -129,10 +155,10 @@ export class WebXRRuntimeAdapter {
129
155
  return this.boundSession;
130
156
  }
131
157
  /**
132
- * Bind the animation loop, if this adapter was given a host. Each callback
133
- * becomes one {@link FrameInfo} and, where a scheduler was supplied, one
134
- * `renderTick`. With no host this does nothing: the app owns the loop and
135
- * calls {@link WebXRRuntimeAdapter.emitFrame} itself.
158
+ * Bind the animation loop, if this adapter was given a host: each callback
159
+ * runs {@link WebXRRuntimeAdapter.tick}. With no host this does nothing -
160
+ * something else owns the loop, and drives {@link WebXRRuntimeAdapter.tick}
161
+ * or {@link WebXRRuntimeAdapter.emitFrame} itself.
136
162
  *
137
163
  * three.js routes `setAnimationLoop` through the session's own
138
164
  * `requestAnimationFrame` while presenting, so one call covers both the 2D
@@ -143,7 +169,7 @@ export class WebXRRuntimeAdapter {
143
169
  return;
144
170
  }
145
171
  this.animationLoopBound = true;
146
- this.host?.setAnimationLoop((timestamp) => this.handleAnimationFrame(timestamp));
172
+ this.host?.setAnimationLoop((timestamp) => this.tick(timestamp));
147
173
  }
148
174
  /** Release the animation loop. Safe to call when it was never bound. */
149
175
  stop() {
@@ -154,9 +180,9 @@ export class WebXRRuntimeAdapter {
154
180
  this.host?.setAnimationLoop(null);
155
181
  }
156
182
  /** Push one frame to every subscriber. Call this when you own the loop. */
157
- emitFrame(timestamp, delta) {
158
- const frame = { timestamp, delta };
159
- this.frameListeners.forEach((listener) => listener(frame));
183
+ emitFrame(timestamp, delta, frame) {
184
+ const info = frame === undefined ? { timestamp, delta } : { timestamp, delta, frame };
185
+ this.frameListeners.forEach((listener) => listener(info));
160
186
  }
161
187
  /**
162
188
  * Re-read the renderer's session and publish any capability change. The
@@ -210,21 +236,54 @@ export class WebXRRuntimeAdapter {
210
236
  this.stateListeners.clear();
211
237
  this.visibilityListeners.clear();
212
238
  }
213
- handleAnimationFrame(timestamp) {
214
- const deltaMs = this.lastTimestamp === 0 ? FIRST_FRAME_DELTA_MS : timestamp - this.lastTimestamp;
215
- this.lastTimestamp = timestamp;
239
+ /**
240
+ * Run one frame step: the visibility gate, the focus/pause signals, the one
241
+ * frame count, {@link WebXRRuntimeAdapter.emitFrame} and `renderTick`. This
242
+ * is what {@link WebXRRuntimeAdapter.start} binds to the owned animation
243
+ * loop, and it is public so a host that owns its OWN loop - one that will
244
+ * never call `start()` because it already calls `setAnimationLoop` itself,
245
+ * such as an XR Blocks app - can drive the exact same step by hand from
246
+ * whatever per-frame hook that host provides, rather than reimplementing the
247
+ * gate, the frame count or the `renderTick` shape.
248
+ *
249
+ * `timestampMs` is the loop's own clock reading, the same value `start()`
250
+ * passes from `setAnimationLoop`'s callback.
251
+ */
252
+ tick(timestampMs) {
253
+ const deltaMs = this.lastTimestamp === 0 ? FIRST_FRAME_DELTA_MS : timestampMs - this.lastTimestamp;
254
+ this.lastTimestamp = timestampMs;
255
+ const session = this.boundSession;
256
+ if (session) {
257
+ this.setFocused(toSessionVisibility(session.visibilityState) === "visible");
258
+ if (!this.focused) {
259
+ // Gated: a live session that is not visible. Skip this tick entirely -
260
+ // no frame, no renderTick, no advance of the frame count - as IWSDK's
261
+ // bridge skips an unfocused frame. A page with no session at all never
262
+ // reaches this branch, so it is never gated.
263
+ return;
264
+ }
265
+ }
216
266
  this.frame += 1;
217
267
  // `FrameInfo.delta` is seconds; the scheduler's `LifecycleContext` is in
218
268
  // milliseconds, which is the unit `ThreeRenderLoopBridge` already emits.
219
- this.emitFrame(timestamp, deltaMs / 1000);
269
+ this.emitFrame(timestampMs, deltaMs / 1000, this.frame);
220
270
  const context = {
221
- timestamp,
271
+ timestamp: timestampMs,
222
272
  deltaTime: deltaMs,
223
273
  frame: this.frame,
224
274
  source: "three",
225
275
  };
226
276
  this.scheduler?.emit("renderTick", context);
227
277
  }
278
+ /** Emit focus and pause on a change, as `ServiceBridgeSystem` does. */
279
+ setFocused(focused) {
280
+ if (focused === this.focused) {
281
+ return;
282
+ }
283
+ this.focused = focused;
284
+ this.manager?.emitFocusChange(focused);
285
+ this.manager?.emitPauseChange({ paused: !focused });
286
+ }
228
287
  /**
229
288
  * Subscribe to one session's own events. The listeners close over the session
230
289
  * they belong to, so nothing here has to re-check which session is live.
@@ -282,6 +341,14 @@ export class WebXRRuntimeAdapter {
282
341
  this.updateDerived(null);
283
342
  this.notifyVisibility("non-immersive");
284
343
  this.settleEndWaiters();
344
+ // The session that was gating ticks is gone. A page with no session is
345
+ // never gated, so if it had been paused, restore focus now rather than
346
+ // leaving the manager believing it still is.
347
+ if (this.focused === false) {
348
+ this.manager?.emitFocusChange(true);
349
+ this.manager?.emitPauseChange({ paused: false });
350
+ }
351
+ this.focused = undefined;
285
352
  }
286
353
  updateDerived(session) {
287
354
  this.derived = deriveCapabilities(session);
@@ -1 +1 @@
1
- {"version":3,"file":"webxr-runtime-adapter.js","sourceRoot":"","sources":["../src/webxr-runtime-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WebXR implementation of {@link RuntimeAdapter}, for a three.js app or any\n * other page that owns its own renderer.\n *\n * The IWSDK binding gets its adapter from IWSDK. Everything else - a plain\n * three.js app, a desktop build with no headset - had nothing, so a service\n * written against `RuntimeAdapter` could not be hosted there. This adapter\n * closes that gap: it orchestrates the entry points the platform already\n * provides (`navigator.xr` for session negotiation, `renderer.xr` for the\n * session the renderer presents, `setAnimationLoop` for frames) and publishes\n * them through the same seam. It renders nothing, plays nothing and owns no\n * scene state.\n *\n * Every host type here is structural, so this package still imports neither\n * `three` nor any WebXR type, and the adapter unit-tests headless.\n */\nimport {\n DEFAULT_CAPABILITIES,\n DEFAULT_SESSION_TIMEOUT_MS,\n deriveCapabilities,\n mergeSessionInit,\n type AdapterCapabilities,\n type CapabilitiesListener,\n type CapabilitySessionLike,\n type FrameInfo,\n type FrameListener,\n type IScheduler,\n type LifecycleContext,\n type RuntimeAdapter,\n type SessionFacet,\n type SessionFailureReason,\n type SessionMode,\n type SessionRequestOptions,\n type SessionResult,\n type SessionState,\n type SessionVisibility,\n type Unsubscribe,\n} from \"@realitycollective/service-framework\";\nimport { FIRST_FRAME_DELTA_MS, type AnimationLoopHostLike } from \"./three-render-loop-bridge.js\";\n\n/** The events an `XRSession` raises that this adapter listens for. */\nexport type WebXRSessionEventType = \"end\" | \"visibilitychange\" | \"inputsourceschange\";\n\n/** The events three.js's `WebXRManager` raises that this adapter listens for. */\nexport type WebXRManagerEventType = \"sessionstart\" | \"sessionend\";\n\n/** Host event callback. The adapter reads the host, not the event object. */\nexport type WebXREventListener = (event?: unknown) => void;\n\n/**\n * The slice of an `XRSession` the adapter reads. It extends the core's\n * {@link CapabilitySessionLike}, so a live session goes straight to\n * `deriveCapabilities` with no mapping.\n */\nexport interface WebXRSessionLike extends CapabilitySessionLike {\n /** Ends the session. The `end` event is what the adapter acts on. */\n end(): Promise<void>;\n /** `\"visible\"`, `\"visible-blurred\"` or `\"hidden\"` on a live session. */\n readonly visibilityState?: string;\n addEventListener(type: WebXRSessionEventType, listener: WebXREventListener): void;\n removeEventListener(type: WebXRSessionEventType, listener: WebXREventListener): void;\n}\n\n/** The slice of `navigator.xr` the session facet negotiates through. */\nexport interface WebXRSystemLike {\n isSessionSupported(mode: string): Promise<boolean>;\n requestSession(mode: string, init?: unknown): Promise<WebXRSessionLike>;\n}\n\n/** The slice of a three.js `renderer.xr` (`WebXRManager`) the adapter drives. */\nexport interface WebXRManagerLike {\n /** Hands the renderer the session it should present. */\n setSession(session: WebXRSessionLike): Promise<void> | void;\n /** The session the renderer is presenting, or null in 2D. */\n getSession(): WebXRSessionLike | null;\n addEventListener(type: WebXRManagerEventType, listener: WebXREventListener): void;\n removeEventListener(type: WebXRManagerEventType, listener: WebXREventListener): void;\n}\n\nexport interface WebXRRuntimeAdapterOptions {\n /** `renderer.xr` - the renderer's WebXR manager. */\n readonly xr: WebXRManagerLike;\n /**\n * `navigator.xr`. Defaults to the global when omitted, and to `null` where\n * there is no global, which is what a Node test sees. A null system reports\n * every session request as `\"unsupported\"`.\n */\n readonly xrSystem?: WebXRSystemLike | null;\n /**\n * The renderer, or anything else with `setAnimationLoop`. Given one, the\n * adapter owns the loop: {@link WebXRRuntimeAdapter.start} binds it and each\n * callback becomes a frame. Omit it to drive frames yourself with\n * {@link WebXRRuntimeAdapter.emitFrame}.\n */\n readonly host?: AnimationLoopHostLike;\n /**\n * Given a scheduler, each owned frame also emits the `renderTick` channel\n * with `source: \"three\"`, exactly as `ThreeRenderLoopBridge` does, so an app\n * needs one loop owner rather than two.\n */\n readonly scheduler?: IScheduler;\n /**\n * Supplies the `XRSessionInit` for a mode - required and optional features.\n * Called once per request; the default sends no init at all.\n */\n readonly sessionInit?: (mode: SessionMode) => unknown;\n}\n\ntype SessionStateListener = (state: SessionState) => void;\ntype SessionVisibilityListener = (visibility: SessionVisibility) => void;\n\nconst SESSION_VISIBILITY_VALUES: ReadonlySet<string> = new Set([\n \"visible\",\n \"visible-blurred\",\n \"hidden\",\n]);\n\n/**\n * Map `XRSession.visibilityState` onto the facet's vocabulary. An unrecognised\n * value is reported as `\"hidden\"`, because treating an unknown state as visible\n * would keep game logic running when it should not.\n */\nfunction toSessionVisibility(value: unknown): SessionVisibility {\n if (typeof value === \"string\" && SESSION_VISIBILITY_VALUES.has(value)) {\n return value as SessionVisibility;\n }\n\n return \"hidden\";\n}\n\n/**\n * WebXR reports a blocked request through the error name: `NotAllowedError`\n * when the user or the permission prompt refused, `SecurityError` when the\n * permissions policy did. Both are a refusal rather than a fault.\n */\nfunction toFailureReason(error: unknown): SessionFailureReason {\n const name = (error as { readonly name?: unknown } | null | undefined)?.name;\n\n if (name === \"NotAllowedError\" || name === \"SecurityError\") {\n return \"denied\";\n }\n\n return \"error\";\n}\n\n/** `navigator.xr`, read defensively: there is no navigator in a Node test. */\nfunction defaultXRSystem(): WebXRSystemLike | null {\n const globalNavigator = (globalThis as { navigator?: { xr?: WebXRSystemLike } }).navigator;\n\n return globalNavigator?.xr ?? null;\n}\n\nexport class WebXRRuntimeAdapter implements RuntimeAdapter {\n private readonly frameListeners = new Set<FrameListener>();\n private readonly capabilitiesListeners = new Set<CapabilitiesListener>();\n private readonly stateListeners = new Set<SessionStateListener>();\n private readonly visibilityListeners = new Set<SessionVisibilityListener>();\n private readonly endWaiters = new Set<() => void>();\n\n private readonly xr: WebXRManagerLike;\n private readonly xrSystem: WebXRSystemLike | null;\n private readonly host: AnimationLoopHostLike | undefined;\n private readonly scheduler: IScheduler | undefined;\n private readonly sessionInit: ((mode: SessionMode) => unknown) | undefined;\n\n private derived: AdapterCapabilities = DEFAULT_CAPABILITIES;\n private overrides: Partial<AdapterCapabilities> = {};\n private capabilities: AdapterCapabilities = DEFAULT_CAPABILITIES;\n private sessionState: SessionState = \"none\";\n private boundSession: WebXRSessionLike | null = null;\n private sessionListeners: {\n readonly type: WebXRSessionEventType;\n readonly listener: WebXREventListener;\n }[] = [];\n private animationLoopBound = false;\n private frame = 0;\n private lastTimestamp = 0;\n\n private readonly onManagerSessionStart: WebXREventListener = () => {\n this.handleSessionStart();\n };\n\n private readonly onManagerSessionEnd: WebXREventListener = () => {\n this.handleSessionEnd();\n };\n\n /** Session lifecycle over `navigator.xr` and the renderer's XR manager. */\n public readonly session: SessionFacet = {\n getState: () => this.sessionState,\n request: (mode, options) => this.requestSession(mode, options),\n end: () => this.endSession(),\n onStateChange: (listener) => {\n this.stateListeners.add(listener);\n return () => {\n this.stateListeners.delete(listener);\n };\n },\n onVisibilityChange: (listener) => {\n this.visibilityListeners.add(listener);\n return () => {\n this.visibilityListeners.delete(listener);\n };\n },\n };\n\n public constructor(options: WebXRRuntimeAdapterOptions) {\n this.xr = options.xr;\n this.xrSystem = options.xrSystem === undefined ? defaultXRSystem() : options.xrSystem;\n this.host = options.host;\n this.scheduler = options.scheduler;\n this.sessionInit = options.sessionInit;\n\n this.xr.addEventListener(\"sessionstart\", this.onManagerSessionStart);\n this.xr.addEventListener(\"sessionend\", this.onManagerSessionEnd);\n\n const session = this.xr.getSession();\n\n if (session) {\n this.attachSession(session);\n this.sessionState = \"active\";\n }\n\n this.derived = deriveCapabilities(session);\n this.capabilities = this.derived;\n }\n\n public onFrame(listener: FrameListener): Unsubscribe {\n this.frameListeners.add(listener);\n return () => {\n this.frameListeners.delete(listener);\n };\n }\n\n public getCapabilities(): AdapterCapabilities {\n return this.capabilities;\n }\n\n public onCapabilitiesChange(listener: CapabilitiesListener): Unsubscribe {\n this.capabilitiesListeners.add(listener);\n return () => {\n this.capabilitiesListeners.delete(listener);\n };\n }\n\n /** The session the renderer is presenting, or null in 2D. */\n public getSession(): WebXRSessionLike | null {\n return this.boundSession;\n }\n\n /**\n * Bind the animation loop, if this adapter was given a host. Each callback\n * becomes one {@link FrameInfo} and, where a scheduler was supplied, one\n * `renderTick`. With no host this does nothing: the app owns the loop and\n * calls {@link WebXRRuntimeAdapter.emitFrame} itself.\n *\n * three.js routes `setAnimationLoop` through the session's own\n * `requestAnimationFrame` while presenting, so one call covers both the 2D\n * page and the headset.\n */\n public start(): void {\n if (this.animationLoopBound) {\n return;\n }\n\n this.animationLoopBound = true;\n this.host?.setAnimationLoop((timestamp) => this.handleAnimationFrame(timestamp));\n }\n\n /** Release the animation loop. Safe to call when it was never bound. */\n public stop(): void {\n if (!this.animationLoopBound) {\n return;\n }\n\n this.animationLoopBound = false;\n this.host?.setAnimationLoop(null);\n }\n\n /** Push one frame to every subscriber. Call this when you own the loop. */\n public emitFrame(timestamp: number, delta: number): void {\n const frame: FrameInfo = { timestamp, delta };\n this.frameListeners.forEach((listener) => listener(frame));\n }\n\n /**\n * Re-read the renderer's session and publish any capability change. The\n * adapter does this itself on every session and input-source event; call it\n * directly after the host enables a feature mid-session, or after a host that\n * raises no events changes what it presents.\n */\n public refreshCapabilities(): void {\n const session = this.xr.getSession();\n\n if (session) {\n this.attachSession(session);\n this.updateDerived(session);\n return;\n }\n\n if (this.boundSession) {\n this.handleSessionEnd();\n return;\n }\n\n this.updateDerived(null);\n }\n\n /**\n * Force capability flags regardless of what the session reports. Overrides\n * are a layer on top of the derived values: they win for as long as they are\n * set, survive every later derivation, and are dropped only by\n * {@link WebXRRuntimeAdapter.clearCapabilityOverrides} or\n * {@link WebXRRuntimeAdapter.dispose}.\n */\n public setCapabilities(capabilities: Partial<AdapterCapabilities>): void {\n this.overrides = { ...this.overrides, ...capabilities };\n this.publishCapabilities();\n }\n\n /** Drop every manual override and fall back to the derived capabilities. */\n public clearCapabilityOverrides(): void {\n this.overrides = {};\n this.publishCapabilities();\n }\n\n /**\n * Release everything the adapter holds: the animation loop, both sets of host\n * event listeners, any in-flight `end()`, the manual overrides and every\n * listener. It does not end the session - the app owns that decision.\n */\n public dispose(): void {\n this.stop();\n this.xr.removeEventListener(\"sessionstart\", this.onManagerSessionStart);\n this.xr.removeEventListener(\"sessionend\", this.onManagerSessionEnd);\n this.detachSession();\n this.settleEndWaiters();\n\n this.overrides = {};\n this.frameListeners.clear();\n this.capabilitiesListeners.clear();\n this.stateListeners.clear();\n this.visibilityListeners.clear();\n }\n\n private handleAnimationFrame(timestamp: number): void {\n const deltaMs =\n this.lastTimestamp === 0 ? FIRST_FRAME_DELTA_MS : timestamp - this.lastTimestamp;\n\n this.lastTimestamp = timestamp;\n this.frame += 1;\n\n // `FrameInfo.delta` is seconds; the scheduler's `LifecycleContext` is in\n // milliseconds, which is the unit `ThreeRenderLoopBridge` already emits.\n this.emitFrame(timestamp, deltaMs / 1000);\n\n const context: LifecycleContext = {\n timestamp,\n deltaTime: deltaMs,\n frame: this.frame,\n source: \"three\",\n };\n\n this.scheduler?.emit(\"renderTick\", context);\n }\n\n /**\n * Subscribe to one session's own events. The listeners close over the session\n * they belong to, so nothing here has to re-check which session is live.\n */\n private attachSession(session: WebXRSessionLike): void {\n if (session === this.boundSession) {\n return;\n }\n\n this.detachSession();\n this.boundSession = session;\n this.sessionListeners = [\n { type: \"end\", listener: () => this.handleSessionEnd() },\n {\n type: \"visibilitychange\",\n listener: () => this.notifyVisibility(toSessionVisibility(session.visibilityState)),\n },\n { type: \"inputsourceschange\", listener: () => this.updateDerived(session) },\n ];\n\n for (const entry of this.sessionListeners) {\n session.addEventListener(entry.type, entry.listener);\n }\n }\n\n private detachSession(): void {\n const session = this.boundSession;\n\n if (!session) {\n return;\n }\n\n for (const entry of this.sessionListeners) {\n session.removeEventListener(entry.type, entry.listener);\n }\n\n this.sessionListeners = [];\n this.boundSession = null;\n }\n\n private handleSessionStart(): void {\n const session = this.xr.getSession();\n\n if (!session) {\n return;\n }\n\n this.attachSession(session);\n this.setSessionState(\"active\");\n this.updateDerived(session);\n this.notifyVisibility(toSessionVisibility(session.visibilityState));\n }\n\n /**\n * One session ending raises both the session's `end` event and the manager's\n * `sessionend`, so this runs twice per session and is written to be\n * idempotent.\n */\n private handleSessionEnd(): void {\n if (!this.boundSession && this.sessionState === \"none\") {\n return;\n }\n\n this.detachSession();\n this.setSessionState(\"none\");\n this.updateDerived(null);\n this.notifyVisibility(\"non-immersive\");\n this.settleEndWaiters();\n }\n\n private updateDerived(session: CapabilitySessionLike | null): void {\n this.derived = deriveCapabilities(session);\n this.publishCapabilities();\n }\n\n private publishCapabilities(): void {\n const next: AdapterCapabilities = { ...this.derived, ...this.overrides };\n const current = this.capabilities;\n\n if (\n next.immersive === current.immersive &&\n next.handTracking === current.handTracking &&\n next.planeDetection === current.planeDetection &&\n next.passthrough === current.passthrough &&\n next.environmentBlendMode === current.environmentBlendMode\n ) {\n return;\n }\n\n this.capabilities = next;\n this.capabilitiesListeners.forEach((listener) => listener(next));\n }\n\n private async requestSession(\n mode: SessionMode,\n options?: SessionRequestOptions,\n ): Promise<SessionResult> {\n if (this.sessionState === \"active\") {\n return { ok: true };\n }\n\n const system = this.xrSystem;\n\n if (!system) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const timeoutMs = options?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;\n this.setSessionState(\"requesting\");\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<SessionResult>((resolve) => {\n timer = setTimeout(() => resolve({ ok: false, reason: \"timeout\" }), timeoutMs);\n });\n\n const result = await Promise.race([this.openSession(system, mode, options), timeout]);\n clearTimeout(timer);\n\n if (!result.ok) {\n this.setSessionState(\"none\");\n return result;\n }\n\n this.setSessionState(\"active\");\n return result;\n }\n\n /**\n * Negotiate the session and hand it to the renderer. Nothing here throws at\n * the caller: a headset that is absent, refused or broken is a normal runtime\n * condition, so every outcome comes back as a {@link SessionResult}.\n *\n * The request's own features are merged over the `sessionInit` hook's result\n * by the core's `mergeSessionInit`, so they add to the app's defaults rather\n * than replacing them. A request that names none passes the hook's result\n * through untouched.\n */\n private async openSession(\n system: WebXRSystemLike,\n mode: SessionMode,\n options?: SessionRequestOptions,\n ): Promise<SessionResult> {\n try {\n const supported = await system.isSessionSupported(mode);\n\n if (!supported) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const init = mergeSessionInit(this.sessionInit?.(mode), options);\n const session = await system.requestSession(mode, init);\n await this.xr.setSession(session);\n\n // A manager that raises `sessionstart` has already attached this session;\n // one that does not is attached here, so both host styles behave alike.\n this.attachSession(session);\n this.updateDerived(session);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, reason: toFailureReason(error), error };\n }\n }\n\n private async endSession(): Promise<void> {\n const session = this.boundSession;\n\n if (!session) {\n return;\n }\n\n this.setSessionState(\"ending\");\n\n const ended = new Promise<void>((resolve) => {\n this.endWaiters.add(resolve);\n });\n\n try {\n await session.end();\n } catch {\n // Ending is best effort. The `end` event, not this promise, is what the\n // adapter acts on, and a host that refuses still has to be reported.\n }\n\n // The event has normally arrived by now and this is a no-op. A host that\n // raises none would otherwise leave the caller waiting forever, so the\n // settled `end()` call is the fallback signal.\n this.handleSessionEnd();\n\n await ended;\n }\n\n private settleEndWaiters(): void {\n const waiters = Array.from(this.endWaiters);\n this.endWaiters.clear();\n waiters.forEach((resolve) => resolve());\n }\n\n private notifyVisibility(visibility: SessionVisibility): void {\n this.visibilityListeners.forEach((listener) => listener(visibility));\n }\n\n private setSessionState(state: SessionState): void {\n if (this.sessionState === state) {\n return;\n }\n\n this.sessionState = state;\n this.stateListeners.forEach((listener) => listener(state));\n }\n}\n"]}
1
+ {"version":3,"file":"webxr-runtime-adapter.js","sourceRoot":"","sources":["../src/webxr-runtime-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WebXR implementation of {@link RuntimeAdapter}, for a three.js app or any\n * other page that owns its own renderer.\n *\n * The IWSDK binding gets its adapter from IWSDK. Everything else - a plain\n * three.js app, a desktop build with no headset - had nothing, so a service\n * written against `RuntimeAdapter` could not be hosted there. This adapter\n * closes that gap: it orchestrates the entry points the platform already\n * provides (`navigator.xr` for session negotiation, `renderer.xr` for the\n * session the renderer presents, `setAnimationLoop` for frames) and publishes\n * them through the same seam. It renders nothing, plays nothing and owns no\n * scene state.\n *\n * Every host type here is structural, so this package still imports neither\n * `three` nor any WebXR type, and the adapter unit-tests headless.\n *\n * Given a `host` and a `manager`, the owned animation loop also gates on the\n * live session's visibility, as IWSDK's `ServiceBridgeSystem` and the native\n * adapter do: while a session is live, frames and `renderTick` flow only on\n * `\"visible\"`, `emitFocusChange`/`emitPauseChange` fire on every change, and\n * the frame count skips a gated tick. Unlike IWSDK and native, this adapter\n * also serves a desktop page with no session at all, and gating never applies\n * there - a plain three.js page keeps ticking exactly as it did before this\n * existed.\n *\n * That whole step - the gate, the focus/pause signals, the frame count,\n * `emitFrame` and `renderTick` - lives in one place, {@link\n * WebXRRuntimeAdapter.tick}, which `start()` binds to the owned loop. A host\n * that owns its OWN animation loop and will never call `start()` - an XR\n * Blocks app, whose `Core` already calls `setAnimationLoop` itself - calls\n * `tick(timestampMs)` by hand from its own per-frame hook instead, and gets\n * the identical gate rather than reimplementing it.\n */\nimport {\n DEFAULT_CAPABILITIES,\n DEFAULT_SESSION_TIMEOUT_MS,\n deriveCapabilities,\n mergeSessionInit,\n type AdapterCapabilities,\n type CapabilitiesListener,\n type CapabilitySessionLike,\n type FrameInfo,\n type FrameListener,\n type IScheduler,\n type LifecycleContext,\n type RuntimeAdapter,\n type SessionFacet,\n type SessionFailureReason,\n type SessionMode,\n type SessionRequestOptions,\n type SessionResult,\n type SessionState,\n type SessionVisibility,\n type Unsubscribe,\n} from \"@realitycollective/service-framework\";\nimport { FIRST_FRAME_DELTA_MS, type AnimationLoopHostLike } from \"./three-render-loop-bridge.js\";\n\n/** The events an `XRSession` raises that this adapter listens for. */\nexport type WebXRSessionEventType = \"end\" | \"visibilitychange\" | \"inputsourceschange\";\n\n/** The events three.js's `WebXRManager` raises that this adapter listens for. */\nexport type WebXRManagerEventType = \"sessionstart\" | \"sessionend\";\n\n/** Host event callback. The adapter reads the host, not the event object. */\nexport type WebXREventListener = (event?: unknown) => void;\n\n/**\n * The two focus signals the adapter drives while a session is live: a\n * `ServiceManager` is one. Declared locally, as `NativeFocusSink` is in the\n * native package, rather than shared, because the two packages do not depend\n * on each other.\n */\nexport interface WebXRFocusSink {\n emitFocusChange(focused: boolean): void;\n emitPauseChange(context: { readonly paused: boolean }): void;\n}\n\n/**\n * The slice of an `XRSession` the adapter reads. It extends the core's\n * {@link CapabilitySessionLike}, so a live session goes straight to\n * `deriveCapabilities` with no mapping.\n */\nexport interface WebXRSessionLike extends CapabilitySessionLike {\n /** Ends the session. The `end` event is what the adapter acts on. */\n end(): Promise<void>;\n /** `\"visible\"`, `\"visible-blurred\"` or `\"hidden\"` on a live session. */\n readonly visibilityState?: string;\n addEventListener(type: WebXRSessionEventType, listener: WebXREventListener): void;\n removeEventListener(type: WebXRSessionEventType, listener: WebXREventListener): void;\n}\n\n/** The slice of `navigator.xr` the session facet negotiates through. */\nexport interface WebXRSystemLike {\n isSessionSupported(mode: string): Promise<boolean>;\n requestSession(mode: string, init?: unknown): Promise<WebXRSessionLike>;\n}\n\n/** The slice of a three.js `renderer.xr` (`WebXRManager`) the adapter drives. */\nexport interface WebXRManagerLike {\n /** Hands the renderer the session it should present. */\n setSession(session: WebXRSessionLike): Promise<void> | void;\n /** The session the renderer is presenting, or null in 2D. */\n getSession(): WebXRSessionLike | null;\n addEventListener(type: WebXRManagerEventType, listener: WebXREventListener): void;\n removeEventListener(type: WebXRManagerEventType, listener: WebXREventListener): void;\n}\n\nexport interface WebXRRuntimeAdapterOptions {\n /** `renderer.xr` - the renderer's WebXR manager. */\n readonly xr: WebXRManagerLike;\n /**\n * `navigator.xr`. Defaults to the global when omitted, and to `null` where\n * there is no global, which is what a Node test sees. A null system reports\n * every session request as `\"unsupported\"`.\n */\n readonly xrSystem?: WebXRSystemLike | null;\n /**\n * The renderer, or anything else with `setAnimationLoop`. Given one, the\n * adapter owns the loop: {@link WebXRRuntimeAdapter.start} binds it and each\n * callback runs {@link WebXRRuntimeAdapter.tick}. Omit it where something\n * else already owns the loop: call {@link WebXRRuntimeAdapter.tick} by hand\n * from whatever per-frame hook that host provides for the same gate, frame\n * count and `renderTick`, or {@link WebXRRuntimeAdapter.emitFrame} directly\n * for the frame fan-out alone.\n */\n readonly host?: AnimationLoopHostLike;\n /**\n * Given a scheduler, each owned frame also emits the `renderTick` channel\n * with `source: \"three\"`, exactly as `ThreeRenderLoopBridge` does, so an app\n * needs one loop owner rather than two.\n */\n readonly scheduler?: IScheduler;\n /**\n * Supplies the `XRSessionInit` for a mode - required and optional features.\n * Called once per request; the default sends no init at all.\n */\n readonly sessionInit?: (mode: SessionMode) => unknown;\n /**\n * The service manager whose focus and pause signals follow the session's\n * visibility, as `ServiceBridgeSystem`'s `manager` option does on IWSDK and\n * the native adapter's `manager` option does on a native host: both fire on\n * every change of focus, `paused` being `!focused`. Gated ONLY while a\n * session is live - `XRSession.visibilityState` via `visibilitychange` -\n * because unlike IWSDK and native, this adapter also serves a desktop page\n * with no session at all, and that page keeps ticking exactly as it did\n * before this option existed.\n */\n readonly manager?: WebXRFocusSink;\n}\n\ntype SessionStateListener = (state: SessionState) => void;\ntype SessionVisibilityListener = (visibility: SessionVisibility) => void;\n\nconst SESSION_VISIBILITY_VALUES: ReadonlySet<string> = new Set([\n \"visible\",\n \"visible-blurred\",\n \"hidden\",\n]);\n\n/**\n * Map `XRSession.visibilityState` onto the facet's vocabulary. An unrecognised\n * value is reported as `\"hidden\"`, because treating an unknown state as visible\n * would keep game logic running when it should not.\n */\nfunction toSessionVisibility(value: unknown): SessionVisibility {\n if (typeof value === \"string\" && SESSION_VISIBILITY_VALUES.has(value)) {\n return value as SessionVisibility;\n }\n\n return \"hidden\";\n}\n\n/**\n * WebXR reports a blocked request through the error name: `NotAllowedError`\n * when the user or the permission prompt refused, `SecurityError` when the\n * permissions policy did. Both are a refusal rather than a fault.\n */\nfunction toFailureReason(error: unknown): SessionFailureReason {\n const name = (error as { readonly name?: unknown } | null | undefined)?.name;\n\n if (name === \"NotAllowedError\" || name === \"SecurityError\") {\n return \"denied\";\n }\n\n return \"error\";\n}\n\n/** `navigator.xr`, read defensively: there is no navigator in a Node test. */\nfunction defaultXRSystem(): WebXRSystemLike | null {\n const globalNavigator = (globalThis as { navigator?: { xr?: WebXRSystemLike } }).navigator;\n\n return globalNavigator?.xr ?? null;\n}\n\nexport class WebXRRuntimeAdapter implements RuntimeAdapter {\n private readonly frameListeners = new Set<FrameListener>();\n private readonly capabilitiesListeners = new Set<CapabilitiesListener>();\n private readonly stateListeners = new Set<SessionStateListener>();\n private readonly visibilityListeners = new Set<SessionVisibilityListener>();\n private readonly endWaiters = new Set<() => void>();\n\n private readonly xr: WebXRManagerLike;\n private readonly xrSystem: WebXRSystemLike | null;\n private readonly host: AnimationLoopHostLike | undefined;\n private readonly scheduler: IScheduler | undefined;\n private readonly sessionInit: ((mode: SessionMode) => unknown) | undefined;\n private readonly manager: WebXRFocusSink | undefined;\n\n private derived: AdapterCapabilities = DEFAULT_CAPABILITIES;\n private overrides: Partial<AdapterCapabilities> = {};\n private capabilities: AdapterCapabilities = DEFAULT_CAPABILITIES;\n private sessionState: SessionState = \"none\";\n private boundSession: WebXRSessionLike | null = null;\n private sessionListeners: {\n readonly type: WebXRSessionEventType;\n readonly listener: WebXREventListener;\n }[] = [];\n private animationLoopBound = false;\n private frame = 0;\n private lastTimestamp = 0;\n /**\n * Whether the current session is visible/focused; `undefined` while there is\n * no session, meaning ticking is not gated at all. Reset to `undefined` on\n * session end, so a desktop page resumes exactly as before a session ever\n * existed.\n */\n private focused: boolean | undefined;\n\n private readonly onManagerSessionStart: WebXREventListener = () => {\n this.handleSessionStart();\n };\n\n private readonly onManagerSessionEnd: WebXREventListener = () => {\n this.handleSessionEnd();\n };\n\n /** Session lifecycle over `navigator.xr` and the renderer's XR manager. */\n public readonly session: SessionFacet = {\n getState: () => this.sessionState,\n request: (mode, options) => this.requestSession(mode, options),\n end: () => this.endSession(),\n onStateChange: (listener) => {\n this.stateListeners.add(listener);\n return () => {\n this.stateListeners.delete(listener);\n };\n },\n onVisibilityChange: (listener) => {\n this.visibilityListeners.add(listener);\n return () => {\n this.visibilityListeners.delete(listener);\n };\n },\n };\n\n public constructor(options: WebXRRuntimeAdapterOptions) {\n this.xr = options.xr;\n this.xrSystem = options.xrSystem === undefined ? defaultXRSystem() : options.xrSystem;\n this.host = options.host;\n this.scheduler = options.scheduler;\n this.sessionInit = options.sessionInit;\n this.manager = options.manager;\n\n this.xr.addEventListener(\"sessionstart\", this.onManagerSessionStart);\n this.xr.addEventListener(\"sessionend\", this.onManagerSessionEnd);\n\n const session = this.xr.getSession();\n\n if (session) {\n this.attachSession(session);\n this.sessionState = \"active\";\n }\n\n this.derived = deriveCapabilities(session);\n this.capabilities = this.derived;\n }\n\n public onFrame(listener: FrameListener): Unsubscribe {\n this.frameListeners.add(listener);\n return () => {\n this.frameListeners.delete(listener);\n };\n }\n\n public getCapabilities(): AdapterCapabilities {\n return this.capabilities;\n }\n\n public onCapabilitiesChange(listener: CapabilitiesListener): Unsubscribe {\n this.capabilitiesListeners.add(listener);\n return () => {\n this.capabilitiesListeners.delete(listener);\n };\n }\n\n /** The session the renderer is presenting, or null in 2D. */\n public getSession(): WebXRSessionLike | null {\n return this.boundSession;\n }\n\n /**\n * Bind the animation loop, if this adapter was given a host: each callback\n * runs {@link WebXRRuntimeAdapter.tick}. With no host this does nothing -\n * something else owns the loop, and drives {@link WebXRRuntimeAdapter.tick}\n * or {@link WebXRRuntimeAdapter.emitFrame} itself.\n *\n * three.js routes `setAnimationLoop` through the session's own\n * `requestAnimationFrame` while presenting, so one call covers both the 2D\n * page and the headset.\n */\n public start(): void {\n if (this.animationLoopBound) {\n return;\n }\n\n this.animationLoopBound = true;\n this.host?.setAnimationLoop((timestamp) => this.tick(timestamp));\n }\n\n /** Release the animation loop. Safe to call when it was never bound. */\n public stop(): void {\n if (!this.animationLoopBound) {\n return;\n }\n\n this.animationLoopBound = false;\n this.host?.setAnimationLoop(null);\n }\n\n /** Push one frame to every subscriber. Call this when you own the loop. */\n public emitFrame(timestamp: number, delta: number, frame?: number): void {\n const info: FrameInfo = frame === undefined ? { timestamp, delta } : { timestamp, delta, frame };\n this.frameListeners.forEach((listener) => listener(info));\n }\n\n /**\n * Re-read the renderer's session and publish any capability change. The\n * adapter does this itself on every session and input-source event; call it\n * directly after the host enables a feature mid-session, or after a host that\n * raises no events changes what it presents.\n */\n public refreshCapabilities(): void {\n const session = this.xr.getSession();\n\n if (session) {\n this.attachSession(session);\n this.updateDerived(session);\n return;\n }\n\n if (this.boundSession) {\n this.handleSessionEnd();\n return;\n }\n\n this.updateDerived(null);\n }\n\n /**\n * Force capability flags regardless of what the session reports. Overrides\n * are a layer on top of the derived values: they win for as long as they are\n * set, survive every later derivation, and are dropped only by\n * {@link WebXRRuntimeAdapter.clearCapabilityOverrides} or\n * {@link WebXRRuntimeAdapter.dispose}.\n */\n public setCapabilities(capabilities: Partial<AdapterCapabilities>): void {\n this.overrides = { ...this.overrides, ...capabilities };\n this.publishCapabilities();\n }\n\n /** Drop every manual override and fall back to the derived capabilities. */\n public clearCapabilityOverrides(): void {\n this.overrides = {};\n this.publishCapabilities();\n }\n\n /**\n * Release everything the adapter holds: the animation loop, both sets of host\n * event listeners, any in-flight `end()`, the manual overrides and every\n * listener. It does not end the session - the app owns that decision.\n */\n public dispose(): void {\n this.stop();\n this.xr.removeEventListener(\"sessionstart\", this.onManagerSessionStart);\n this.xr.removeEventListener(\"sessionend\", this.onManagerSessionEnd);\n this.detachSession();\n this.settleEndWaiters();\n\n this.overrides = {};\n this.frameListeners.clear();\n this.capabilitiesListeners.clear();\n this.stateListeners.clear();\n this.visibilityListeners.clear();\n }\n\n /**\n * Run one frame step: the visibility gate, the focus/pause signals, the one\n * frame count, {@link WebXRRuntimeAdapter.emitFrame} and `renderTick`. This\n * is what {@link WebXRRuntimeAdapter.start} binds to the owned animation\n * loop, and it is public so a host that owns its OWN loop - one that will\n * never call `start()` because it already calls `setAnimationLoop` itself,\n * such as an XR Blocks app - can drive the exact same step by hand from\n * whatever per-frame hook that host provides, rather than reimplementing the\n * gate, the frame count or the `renderTick` shape.\n *\n * `timestampMs` is the loop's own clock reading, the same value `start()`\n * passes from `setAnimationLoop`'s callback.\n */\n public tick(timestampMs: number): void {\n const deltaMs =\n this.lastTimestamp === 0 ? FIRST_FRAME_DELTA_MS : timestampMs - this.lastTimestamp;\n\n this.lastTimestamp = timestampMs;\n\n const session = this.boundSession;\n\n if (session) {\n this.setFocused(toSessionVisibility(session.visibilityState) === \"visible\");\n\n if (!this.focused) {\n // Gated: a live session that is not visible. Skip this tick entirely -\n // no frame, no renderTick, no advance of the frame count - as IWSDK's\n // bridge skips an unfocused frame. A page with no session at all never\n // reaches this branch, so it is never gated.\n return;\n }\n }\n\n this.frame += 1;\n\n // `FrameInfo.delta` is seconds; the scheduler's `LifecycleContext` is in\n // milliseconds, which is the unit `ThreeRenderLoopBridge` already emits.\n this.emitFrame(timestampMs, deltaMs / 1000, this.frame);\n\n const context: LifecycleContext = {\n timestamp: timestampMs,\n deltaTime: deltaMs,\n frame: this.frame,\n source: \"three\",\n };\n\n this.scheduler?.emit(\"renderTick\", context);\n }\n\n /** Emit focus and pause on a change, as `ServiceBridgeSystem` does. */\n private setFocused(focused: boolean): void {\n if (focused === this.focused) {\n return;\n }\n\n this.focused = focused;\n this.manager?.emitFocusChange(focused);\n this.manager?.emitPauseChange({ paused: !focused });\n }\n\n /**\n * Subscribe to one session's own events. The listeners close over the session\n * they belong to, so nothing here has to re-check which session is live.\n */\n private attachSession(session: WebXRSessionLike): void {\n if (session === this.boundSession) {\n return;\n }\n\n this.detachSession();\n this.boundSession = session;\n this.sessionListeners = [\n { type: \"end\", listener: () => this.handleSessionEnd() },\n {\n type: \"visibilitychange\",\n listener: () => this.notifyVisibility(toSessionVisibility(session.visibilityState)),\n },\n { type: \"inputsourceschange\", listener: () => this.updateDerived(session) },\n ];\n\n for (const entry of this.sessionListeners) {\n session.addEventListener(entry.type, entry.listener);\n }\n }\n\n private detachSession(): void {\n const session = this.boundSession;\n\n if (!session) {\n return;\n }\n\n for (const entry of this.sessionListeners) {\n session.removeEventListener(entry.type, entry.listener);\n }\n\n this.sessionListeners = [];\n this.boundSession = null;\n }\n\n private handleSessionStart(): void {\n const session = this.xr.getSession();\n\n if (!session) {\n return;\n }\n\n this.attachSession(session);\n this.setSessionState(\"active\");\n this.updateDerived(session);\n this.notifyVisibility(toSessionVisibility(session.visibilityState));\n }\n\n /**\n * One session ending raises both the session's `end` event and the manager's\n * `sessionend`, so this runs twice per session and is written to be\n * idempotent.\n */\n private handleSessionEnd(): void {\n if (!this.boundSession && this.sessionState === \"none\") {\n return;\n }\n\n this.detachSession();\n this.setSessionState(\"none\");\n this.updateDerived(null);\n this.notifyVisibility(\"non-immersive\");\n this.settleEndWaiters();\n\n // The session that was gating ticks is gone. A page with no session is\n // never gated, so if it had been paused, restore focus now rather than\n // leaving the manager believing it still is.\n if (this.focused === false) {\n this.manager?.emitFocusChange(true);\n this.manager?.emitPauseChange({ paused: false });\n }\n\n this.focused = undefined;\n }\n\n private updateDerived(session: CapabilitySessionLike | null): void {\n this.derived = deriveCapabilities(session);\n this.publishCapabilities();\n }\n\n private publishCapabilities(): void {\n const next: AdapterCapabilities = { ...this.derived, ...this.overrides };\n const current = this.capabilities;\n\n if (\n next.immersive === current.immersive &&\n next.handTracking === current.handTracking &&\n next.planeDetection === current.planeDetection &&\n next.passthrough === current.passthrough &&\n next.environmentBlendMode === current.environmentBlendMode\n ) {\n return;\n }\n\n this.capabilities = next;\n this.capabilitiesListeners.forEach((listener) => listener(next));\n }\n\n private async requestSession(\n mode: SessionMode,\n options?: SessionRequestOptions,\n ): Promise<SessionResult> {\n if (this.sessionState === \"active\") {\n return { ok: true };\n }\n\n const system = this.xrSystem;\n\n if (!system) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const timeoutMs = options?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;\n this.setSessionState(\"requesting\");\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<SessionResult>((resolve) => {\n timer = setTimeout(() => resolve({ ok: false, reason: \"timeout\" }), timeoutMs);\n });\n\n const result = await Promise.race([this.openSession(system, mode, options), timeout]);\n clearTimeout(timer);\n\n if (!result.ok) {\n this.setSessionState(\"none\");\n return result;\n }\n\n this.setSessionState(\"active\");\n return result;\n }\n\n /**\n * Negotiate the session and hand it to the renderer. Nothing here throws at\n * the caller: a headset that is absent, refused or broken is a normal runtime\n * condition, so every outcome comes back as a {@link SessionResult}.\n *\n * The request's own features are merged over the `sessionInit` hook's result\n * by the core's `mergeSessionInit`, so they add to the app's defaults rather\n * than replacing them. A request that names none passes the hook's result\n * through untouched.\n */\n private async openSession(\n system: WebXRSystemLike,\n mode: SessionMode,\n options?: SessionRequestOptions,\n ): Promise<SessionResult> {\n try {\n const supported = await system.isSessionSupported(mode);\n\n if (!supported) {\n return { ok: false, reason: \"unsupported\" };\n }\n\n const init = mergeSessionInit(this.sessionInit?.(mode), options);\n const session = await system.requestSession(mode, init);\n await this.xr.setSession(session);\n\n // A manager that raises `sessionstart` has already attached this session;\n // one that does not is attached here, so both host styles behave alike.\n this.attachSession(session);\n this.updateDerived(session);\n\n return { ok: true };\n } catch (error) {\n return { ok: false, reason: toFailureReason(error), error };\n }\n }\n\n private async endSession(): Promise<void> {\n const session = this.boundSession;\n\n if (!session) {\n return;\n }\n\n this.setSessionState(\"ending\");\n\n const ended = new Promise<void>((resolve) => {\n this.endWaiters.add(resolve);\n });\n\n try {\n await session.end();\n } catch {\n // Ending is best effort. The `end` event, not this promise, is what the\n // adapter acts on, and a host that refuses still has to be reported.\n }\n\n // The event has normally arrived by now and this is a no-op. A host that\n // raises none would otherwise leave the caller waiting forever, so the\n // settled `end()` call is the fallback signal.\n this.handleSessionEnd();\n\n await ended;\n }\n\n private settleEndWaiters(): void {\n const waiters = Array.from(this.endWaiters);\n this.endWaiters.clear();\n waiters.forEach((resolve) => resolve());\n }\n\n private notifyVisibility(visibility: SessionVisibility): void {\n this.visibilityListeners.forEach((listener) => listener(visibility));\n }\n\n private setSessionState(state: SessionState): void {\n if (this.sessionState === state) {\n return;\n }\n\n this.sessionState = state;\n this.stateListeners.forEach((listener) => listener(state));\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realitycollective/service-framework-three",
3
- "version": "1.0.2-preview.0",
3
+ "version": "1.0.2-preview.1",
4
4
  "description": "three.js render-loop bindings for the Reality Collective TypeScript Service Framework.",
5
5
  "author": "Reality Collective",
6
6
  "license": "MIT",
@@ -35,7 +35,7 @@
35
35
  "url": "https://github.com/realitycollective/com.realitycollective.service-framework.ts/issues"
36
36
  },
37
37
  "dependencies": {
38
- "@realitycollective/service-framework": "^1.0.2-preview.0"
38
+ "@realitycollective/service-framework": "^1.0.2-preview.1"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.build.json",